diff --git a/src/main/java/com/sjmresorts/opsapp/admin/controllers/AdminDevicesController.java b/src/main/java/com/sjmresorts/opsapp/admin/controllers/AdminDevicesController.java deleted file mode 100644 index 93e5ec3..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/controllers/AdminDevicesController.java +++ /dev/null @@ -1,94 +0,0 @@ -package com.sjmresorts.opsapp.admin.controllers; - -import com.sjmresorts.opsapp.admin.dtos.appsetting.AdminDevicesInfoPageRequest; -import com.sjmresorts.opsapp.admin.dtos.appsetting.AdminDevicesInfoPageResponse; -import com.sjmresorts.opsapp.admin.dtos.appsetting.AdminGetClientInfosResponse; -import com.sjmresorts.opsapp.admin.dtos.appsetting.ClientInfosDto; -import com.sjmresorts.opsapp.admin.services.AdminDevicesService; -import com.sjmresorts.opsapp.authentication.dtos.LoginUserInfo; -import com.sjmresorts.opsapp.common.dtos.BaseApiResponse; -import com.sjmresorts.opsapp.common.dtos.DataTableResult; -import com.sjmresorts.opsapp.common.utils.JwtTokenUtil; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.annotation.Resource; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.validation.constraints.NotNull; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -@Tag(name = "CMS", description = "APIs for Admin Devices functions") -@RestController -@RequestMapping("/api/v1/admin") -public class AdminDevicesController { - - @Resource - private AdminDevicesService adminDeviceService; - - @Operation( - summary = "Query CMS devices info page", - description = "API for query CMS devices info page" - ) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "CMS devices info page queried successful"), - @ApiResponse(responseCode = "400", description = "Invalid request data") - }) - @PostMapping(value = "/devices/queryPage", produces = "application/json;charset=UTF-8") - public ResponseEntity getAppDevicesByPage(HttpServletRequest request, - - @RequestBody(required = true) AdminDevicesInfoPageRequest pageRequest) { - - DataTableResult devicesInfoList = adminDeviceService.getDevicesInfoListByPage(pageRequest); - AdminDevicesInfoPageResponse devicesInfoResponse = new AdminDevicesInfoPageResponse(); - devicesInfoResponse.setCode("200"); - devicesInfoResponse.setSuccess(true); - devicesInfoResponse.setMessage("Query Successfully"); - devicesInfoResponse.setData(devicesInfoList); - return ResponseEntity.status(HttpStatus.OK).body(devicesInfoResponse); - } - - @Operation( - summary = "Update CMS devices info", - description = "API for CMS devices info update" - ) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "CMS devices info updated successful"), - @ApiResponse(responseCode = "400", description = "Invalid request data") - }) - @PostMapping(value = "/devices/update", produces = "application/json;charset=UTF-8") - public ResponseEntity updateDevice( - HttpServletRequest request, - @RequestBody ClientInfosDto requestData) { - LoginUserInfo loginUserInfo = JwtTokenUtil.parseUserFromRequest(request); - if (loginUserInfo == null) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(null); - } else { - BaseApiResponse responseData = adminDeviceService.updateDevice(requestData, loginUserInfo); - return ResponseEntity.status(HttpStatus.OK).body(responseData); - } - } - - @Operation( - summary = "Get one CMS devices info", - description = "API for get one CMS devices info" - ) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "CMS devices info got successful"), - @ApiResponse(responseCode = "400", description = "Invalid request data") - }) - @GetMapping(value = "/devices/{deviceId}", produces = "application/json;charset=UTF-8") - public ResponseEntity getAppDeviceById( - @PathVariable(name = "deviceId", required = true) @NotNull Long deviceId) { - - AdminGetClientInfosResponse responseData = new AdminGetClientInfosResponse(); - ClientInfosDto deviceDto = adminDeviceService.queryDeviceById(deviceId); - responseData.setCode("200"); - responseData.setSuccess(deviceDto != null); - responseData.setMessage(deviceDto != null ? "Query Successfully" : "Device does not exists"); - responseData.setData(deviceDto); - return ResponseEntity.status(HttpStatus.OK).body(responseData); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/admin/controllers/AdminLoginController.java b/src/main/java/com/sjmresorts/opsapp/admin/controllers/AdminLoginController.java deleted file mode 100644 index 3e3f26e..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/controllers/AdminLoginController.java +++ /dev/null @@ -1,158 +0,0 @@ -package com.sjmresorts.opsapp.admin.controllers; - -import com.sjmresorts.opsapp.admin.dtos.system.login.AdminLoginRequest; -import com.sjmresorts.opsapp.admin.dtos.system.login.AdminLoginResponse; -import com.sjmresorts.opsapp.admin.dtos.system.login.AdminLogoutResponse; -import com.sjmresorts.opsapp.admin.dtos.system.login.CmsLoginInfo; -import com.sjmresorts.opsapp.admin.services.AdminLoginService; -import com.sjmresorts.opsapp.common.constants.AppConstants; -import com.sjmresorts.opsapp.authentication.dtos.LoginUserInfo; -import com.sjmresorts.opsapp.authentication.dtos.RefreshTokenResponse; -import com.sjmresorts.opsapp.authentication.models.UserToken; -import com.sjmresorts.opsapp.authentication.services.UserTokenService; -import com.sjmresorts.opsapp.common.utils.JwtTokenUtil; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.annotation.Resource; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import jakarta.validation.Valid; -import org.apache.commons.lang3.StringUtils; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.Base64; - -@Tag(name = "CMS", description = "APIs for CMS administration") -@RestController -@RequestMapping("/api/v1/admin") -@CrossOrigin(origins = "*") -public class AdminLoginController { - - @Resource - private AdminLoginService adminLoginService; - @Resource - private UserTokenService userTokenService; - - @Operation( - summary = "CMS Login", - description = "API for CMS login with input ad account and password" - ) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "Check ad login success") - }) - @PostMapping(value="/login",produces = "application/json;charset=UTF-8") - public ResponseEntity cmsLogin( - HttpServletRequest request, - @Parameter(description = "login request info", required = true) @Valid @RequestBody AdminLoginRequest requestData){ - AdminLoginResponse result = new AdminLoginResponse(); - String passwordVal = requestData.getPassword(); - if(StringUtils.isNotBlank(passwordVal)) { - passwordVal =new String(Base64.getDecoder().decode(passwordVal.getBytes())); - } - int isCheckLogin; - try{ - isCheckLogin = adminLoginService.checkLoginByLoginIdAndPassword(requestData.getUserCode(), passwordVal); - } catch (Exception e) { - result.setSuccess(false); - result.setCode("500"); - result.setMessage(e.getCause().toString()); - return ResponseEntity.status(HttpStatus.OK).body(result); - } - - if (isCheckLogin >= 1) { - CmsLoginInfo loginInfo; - try{ - loginInfo = adminLoginService.getLoginUserInfoByEmployeeId(requestData.getUserCode()); - } catch (Exception e) { - result.setSuccess(false); - result.setCode("500"); - result.setMessage(e.getCause().toString()); - return ResponseEntity.status(HttpStatus.OK).body(result); - } - if (loginInfo != null && loginInfo.getLoginUser()!=null) { - userTokenService.clearUserTokenByType(AppConstants.CMS_API_ACCESS_TOKEN_TYPE,requestData.getUserCode()); - userTokenService.clearUserTokenByType(AppConstants.CMS_REFRESH_TOKEN_TYPE,requestData.getUserCode()); - LoginUserInfo loginUser = loginInfo.getLoginUser(); - loginUser.setLoginSource("cms_login"); - UserToken accessToken = JwtTokenUtil.generateTokenByLoginUser(AppConstants.CMS_API_ACCESS_TOKEN_TYPE,loginUser); - userTokenService.saveUserToken(accessToken); - UserToken refreshToken = JwtTokenUtil.generateTokenByLoginUser(AppConstants.CMS_REFRESH_TOKEN_TYPE,loginUser); - userTokenService.saveUserToken(refreshToken); - loginInfo.setAccessToken(accessToken.getTokenValue()); - loginInfo.setRefreshToken(refreshToken.getTokenValue()); - result.setSuccess(true); - result.setCode("200"); - result.setMessage("User "+requestData.getUserCode() + " login CMS successful."); - result.setLoginInfo(loginInfo); - } else { - result.setSuccess(false); - result.setMessage("User "+requestData.getUserCode()+" not found."); - } - } else if (isCheckLogin == -2) { - result.setSuccess(false); - result.setMessage(requestData.getUserCode() + " has been locked."); - } else if (isCheckLogin == -3) { - result.setSuccess(false); - result.setMessage(requestData.getUserCode() + " has been disabled."); - } else { - result.setSuccess(false); - result.setMessage("Incorrect login ID or password."); - } - return ResponseEntity.status(HttpStatus.OK).body(result); - } - - @PostMapping(value="/verifyToken",produces = "application/json;charset=UTF-8") - public ResponseEntity verifyToken( - HttpServletRequest request, - HttpServletResponse response) { - return ResponseEntity.status(HttpStatus.OK).body("Token Verified"); - } - - @PostMapping(value="/logout",produces = "application/json;charset=UTF-8") - public ResponseEntity processLogout( - HttpServletRequest request, - HttpServletResponse response) { - AdminLogoutResponse result = new AdminLogoutResponse(); - LoginUserInfo loginUser = JwtTokenUtil.parseUserFromRequest(request); - if(loginUser!=null){ - adminLoginService.processLogout(loginUser); - result.setSuccess(true); - result.setCode("200"); - result.setMessage("Logout successful."); - }else{ - result.setSuccess(false); - result.setCode("401"); - result.setMessage("Unauthorized logout"); - } - return ResponseEntity.status(HttpStatus.OK).body(result); - } - - @Operation( - summary = "Renew API token by refresh token", - description = "API to renew access token" - ) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "Access Token refreshed successful") - }) - @PostMapping(value="/refreshToken",produces = "application/json;charset=UTF-8") - public ResponseEntity refreshToken( - HttpServletRequest request, - HttpServletResponse response) { - RefreshTokenResponse responseData = new RefreshTokenResponse(); - LoginUserInfo loginUser = JwtTokenUtil.parseUserFromRequest(request); - if(loginUser!=null){ - responseData = adminLoginService.refreshToken(loginUser); - return ResponseEntity.status(HttpStatus.OK).body(responseData); - }else{ - responseData.setSuccess(false); - responseData.setCode("401"); - responseData.setMessage("Unauthorized operation"); - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(responseData); - } - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/admin/controllers/AdminMenusController.java b/src/main/java/com/sjmresorts/opsapp/admin/controllers/AdminMenusController.java deleted file mode 100644 index 7b94ccf..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/controllers/AdminMenusController.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.sjmresorts.opsapp.admin.controllers; - -import com.sjmresorts.opsapp.admin.dtos.menus.AdminMenuStatusRequest; -import com.sjmresorts.opsapp.admin.dtos.menus.AdminMenuStatusResponse; -import com.sjmresorts.opsapp.admin.dtos.menus.AdminMenusListResponse; -import com.sjmresorts.opsapp.admin.services.AdminMenusService; -import com.sjmresorts.opsapp.authentication.dtos.LoginUserInfo; -import com.sjmresorts.opsapp.common.utils.JwtTokenUtil; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.annotation.Resource; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.validation.Valid; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -@Tag(name = "CMS", description = "APIs for CMS sidebar menus") -@RestController -@RequestMapping("/api/v1/admin") -@CrossOrigin(origins = "*") -public class AdminMenusController { - - @Resource - private AdminMenusService adminMenusService; - - @Operation(summary = "List CMS sidebar menus with visibility status") - @PostMapping(value = "/menu/list", produces = "application/json;charset=UTF-8") - public ResponseEntity listMenus(HttpServletRequest request) { - LoginUserInfo loginUserInfo = JwtTokenUtil.parseUserFromRequest(request); - if (loginUserInfo == null) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(null); - } - return ResponseEntity.ok(adminMenusService.listMenus()); - } - - @Operation(summary = "Update CMS sidebar menu visibility (sys_routers.status)") - @PostMapping(value = "/menu/update_status", produces = "application/json;charset=UTF-8") - public ResponseEntity updateStatus( - HttpServletRequest request, - @Valid @RequestBody AdminMenuStatusRequest body) { - LoginUserInfo loginUserInfo = JwtTokenUtil.parseUserFromRequest(request); - if (loginUserInfo == null) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(null); - } - return ResponseEntity.ok(adminMenusService.updateStatus(body, loginUserInfo)); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/admin/controllers/AdminRoleUsersController.java b/src/main/java/com/sjmresorts/opsapp/admin/controllers/AdminRoleUsersController.java deleted file mode 100644 index 81cd5b9..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/controllers/AdminRoleUsersController.java +++ /dev/null @@ -1,101 +0,0 @@ -package com.sjmresorts.opsapp.admin.controllers; - -import com.sjmresorts.opsapp.admin.dtos.permissions.AdminAssignGroupPermissionRequest; -import com.sjmresorts.opsapp.admin.dtos.permissions.AdminAssignGroupPermissionResponse; -import com.sjmresorts.opsapp.admin.dtos.roleUsers.AdminGetRoleUsersRequest; -import com.sjmresorts.opsapp.admin.dtos.roleUsers.AdminGetRoleUsersResponse; -import com.sjmresorts.opsapp.admin.services.AdminPermissionService; -import com.sjmresorts.opsapp.admin.services.AdminUserService; -import com.sjmresorts.opsapp.authentication.dtos.LoginUserInfo; -import com.sjmresorts.opsapp.authentication.models.User; -import com.sjmresorts.opsapp.common.utils.JwtTokenUtil; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.annotation.Resource; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.validation.Valid; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -@Tag(name = "CMS", description = "APIs for CMS RoleUser") -@RestController -@RequestMapping("/api/v1/admin") -@CrossOrigin(origins = "*") -public class AdminRoleUsersController { - - @Resource - private AdminUserService adminUserService; - - @Resource - private AdminPermissionService adminPermissionService; - - @Operation( - summary = "CMS Get Role And User Status", - description = "API for CMS Get Role And User Status" - ) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "Check CMS Get Role And User Status Success") - }) - @PostMapping(value = "/role/get_role_user_status", produces = "application/json;charset=UTF-8") - public ResponseEntity cmsGetRoleUserStatus( - HttpServletRequest request, - @RequestBody(required = true) AdminGetRoleUsersRequest pageRequest) { - AdminGetRoleUsersResponse Requestresult = new AdminGetRoleUsersResponse(); - Map> serviceResult = new HashMap<>(); - List assignUsers = adminUserService.listUser(pageRequest,"USER_ROLE"); - List unassignUsers = adminUserService.listUser(pageRequest,"UNASSIGN_USER"); - serviceResult.put("assignUser", assignUsers); - serviceResult.put("unassignUser", unassignUsers); - - if (serviceResult != null) { - Requestresult.setSuccess(true); - Requestresult.setMessage("CMS Get Roles Success"); - Requestresult.setData(serviceResult); - Requestresult.setCode("200"); - }else{ - Requestresult.setCode("401"); - Requestresult.setMessage("CMS Get Roles Failed"); - Requestresult.setSuccess(false); - } - return ResponseEntity.status(HttpStatus.OK).body(Requestresult); - } - - @Operation( - summary = "CMS Assign Role Permissions", - description = "API for CMS Assign Role Permissions" - ) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "CMS Assign Role Permissions") - }) - @PostMapping(value = "/role/assign_permission", produces = "application/json;charset=UTF-8") - public ResponseEntity cmsAssignRolePermissions( - HttpServletRequest request, - @Parameter(description = "CMS Assign Role Permissions", required = true) @Valid @RequestBody AdminAssignGroupPermissionRequest requestData) { - LoginUserInfo loginUserInfo = JwtTokenUtil.parseUserFromRequest(request); - if(loginUserInfo==null) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(null); - }else{ - boolean success = adminPermissionService.assginPermission(requestData); - AdminAssignGroupPermissionResponse responseData = new AdminAssignGroupPermissionResponse(); - if(success) { - responseData.setCode("200"); - responseData.setSuccess(true); - responseData.setMessage("CMS Assign Role Permissions Success"); - }else{ - responseData.setSuccess(false); - responseData.setMessage("CMS Assign Role Permissions Failed"); - } - return ResponseEntity.status(HttpStatus.OK).body(responseData); - } - } -} - - diff --git a/src/main/java/com/sjmresorts/opsapp/admin/controllers/AdminRolesController.java b/src/main/java/com/sjmresorts/opsapp/admin/controllers/AdminRolesController.java deleted file mode 100644 index e4a6503..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/controllers/AdminRolesController.java +++ /dev/null @@ -1,203 +0,0 @@ -package com.sjmresorts.opsapp.admin.controllers; - -import com.sjmresorts.opsapp.admin.dtos.permissions.AdminListPermissionSettingRequest; -import com.sjmresorts.opsapp.admin.dtos.permissions.AdminListPermissionSettingResponse; -import com.sjmresorts.opsapp.admin.dtos.roles.*; -import com.sjmresorts.opsapp.admin.services.AdminPermissionService; -import com.sjmresorts.opsapp.admin.services.AdminRolesService; -import com.sjmresorts.opsapp.authentication.dtos.LoginUserInfo; -import com.sjmresorts.opsapp.common.dtos.BaseApiResponse; -import com.sjmresorts.opsapp.common.dtos.DataTableResult; -import com.sjmresorts.opsapp.common.utils.JwtTokenUtil; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.annotation.Resource; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.validation.Valid; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -@Tag(name = "CMS", description = "APIs for CMS Roles") -@RestController -@RequestMapping("/api/v1/admin") -@CrossOrigin(origins = "*") -public class AdminRolesController { - @Resource - private AdminRolesService adminRolesService; - @Resource - private AdminPermissionService adminPermissionService; - - @Operation( - summary = "CMS Get Roles", - description = "API for CMS Get Roles" - ) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "Check CMS Get Roles success") - }) - @PostMapping(value = "/role/get_roles", produces = "application/json;charset=UTF-8") - public ResponseEntity cmsGetRoles( - HttpServletRequest request, - @RequestBody(required = true) AdminGetRolesRequest pageRequest) { - AdminGetRolesResponse Requestresult = new AdminGetRolesResponse(); - DataTableResult serviceResult = adminRolesService.getRoleInfoListByPage(pageRequest); - - if (serviceResult != null) { - Requestresult.setSuccess(true); - Requestresult.setMessage("CMS Get Roles Success"); - Requestresult.setData(serviceResult); - Requestresult.setCode("200"); - }else{ - Requestresult.setCode("401"); - Requestresult.setMessage("CMS Get Roles Failed"); - Requestresult.setSuccess(false); - } - return ResponseEntity.status(HttpStatus.OK).body(Requestresult); - } - - - @Operation( - summary = "CMS Create Role", - description = "API for CMS Create Role" - ) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "Check CMS Create Role success") - }) - @PostMapping(value = "/role/create_role", produces = "application/json;charset=UTF-8") - public ResponseEntity cmsCreateRole( - HttpServletRequest request, - @Parameter(description = "CMS Create Role info", required = true) @Valid @RequestBody AdminCreateRoleRequest requestData) { - LoginUserInfo loginUserInfo = JwtTokenUtil.parseUserFromRequest(request); - if(loginUserInfo==null) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(null); - }else{ - BaseApiResponse responseData = adminRolesService.createRole(requestData, loginUserInfo); - return ResponseEntity.status(HttpStatus.OK).body(responseData); - } - } - - - @Operation( - summary = "CMS Delete Role", - description = "API for CMS Delete Role" - ) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "Check CMS Delete Role success") - }) - @PostMapping(value = "/role/delete_role", produces = "application/json;charset=UTF-8") - public ResponseEntity cmsDeleteRole( - HttpServletRequest request, - @Parameter(description = "CMS Delete Role info", required = true) @Valid @RequestBody AdminDeleteRoleRequest requestData) { - LoginUserInfo loginUserInfo = JwtTokenUtil.parseUserFromRequest(request); - if(loginUserInfo==null) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(null); - }else{ - BaseApiResponse responseData = adminRolesService.deleteRole(requestData, loginUserInfo); - return ResponseEntity.status(HttpStatus.OK).body(responseData); - } - } - - - @Operation( - summary = "CMS Modify Role", - description = "API for CMS Modify Role" - ) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "Check CMS Modify Role success") - }) - @PostMapping(value = "/role/modify_role", produces = "application/json;charset=UTF-8") - public ResponseEntity cmsmodifyRole( - HttpServletRequest request, - @Parameter(description = "CMS modify Role info", required = true) @Valid @RequestBody AdminModifyRoleRequest requestData) { - LoginUserInfo loginUserInfo = JwtTokenUtil.parseUserFromRequest(request); - if(loginUserInfo==null) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(null); - }else{ - BaseApiResponse responseData = adminRolesService.modifyRole(requestData, loginUserInfo); - return ResponseEntity.status(HttpStatus.OK).body(responseData); - } - } - - @Operation( - summary = "CMS Get Role By Id", - description = "API for CMS Get Role By Id" - ) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "Check CMS Get Role success") - }) - @PostMapping(value = "/role/get_role_by_id", produces = "application/json;charset=UTF-8") - public ResponseEntity cmsGetRoleById( - HttpServletRequest request, - @Parameter(description = "CMS modify Role info", required = true) @Valid @RequestBody AdminGetRoleByIdRequest requestData) { - LoginUserInfo loginUserInfo = JwtTokenUtil.parseUserFromRequest(request); - if(loginUserInfo==null) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(null); - }else{ - AdminGetRoleByIdResponse responseData = adminRolesService.getRoleById(requestData); - return ResponseEntity.status(HttpStatus.OK).body(responseData); - } - } - - - @Operation( - summary = "CMS Set Assign Users", - description = "API for CMS Set Assign Users" - ) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "CMS Set Assign Users") - }) - @PostMapping(value = "/role/assign_users", produces = "application/json;charset=UTF-8") - public ResponseEntity cmsSetAssignUsers( - HttpServletRequest request, - @Parameter(description = "CMS modify Role info", required = true) @Valid @RequestBody AdminRoleAssignUsersRequest requestData) { - LoginUserInfo loginUserInfo = JwtTokenUtil.parseUserFromRequest(request); - if(loginUserInfo==null) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(null); - }else{ - AdminRoleAssignUsersResponse responseData = new AdminRoleAssignUsersResponse(); - Boolean serviceResult = adminRolesService.updateGroupAssignUser(requestData,loginUserInfo); - if(serviceResult) { - responseData.setCode("200"); - responseData.setSuccess(true); - responseData.setMessage("CMS Set Assign Users Success"); - }else{ - responseData.setSuccess(false); - responseData.setMessage("CMS Set Assign Users Failed"); - } - return ResponseEntity.status(HttpStatus.OK).body(responseData); - } - } - - @Operation( - summary = "CMS Get Permission", - description = "API for CMS Get Permission" - ) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "CMS Get Permission") - }) - @PostMapping(value = "/role/list_permission_setting", produces = "application/json;charset=UTF-8") - public ResponseEntity cmsGetListPermission( - HttpServletRequest request, - @Parameter(description = "CMS Get Permission", required = true) @Valid @RequestBody AdminListPermissionSettingRequest requestData) { - LoginUserInfo loginUserInfo = JwtTokenUtil.parseUserFromRequest(request); - if(loginUserInfo==null) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(null); - }else{ - AdminListPermissionSettingResponse responseData = adminPermissionService.getPermissionById(requestData); - if(responseData!=null) { - responseData.setCode("200"); - responseData.setSuccess(true); - responseData.setMessage("CMS Get Permission Success"); - }else{ - responseData.setSuccess(false); - responseData.setMessage("CMS Get Permission Failed"); - } - return ResponseEntity.status(HttpStatus.OK).body(responseData); - } - } -} - - diff --git a/src/main/java/com/sjmresorts/opsapp/admin/controllers/AdminUserController.java b/src/main/java/com/sjmresorts/opsapp/admin/controllers/AdminUserController.java deleted file mode 100644 index 3d16df5..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/controllers/AdminUserController.java +++ /dev/null @@ -1,137 +0,0 @@ -package com.sjmresorts.opsapp.admin.controllers; - -import com.sjmresorts.opsapp.admin.dtos.system.user.CmsAppUserPageRequest; -import com.sjmresorts.opsapp.admin.dtos.system.user.CmsUserDto; -import com.sjmresorts.opsapp.admin.dtos.system.user.CmsUserInfoResponse; -import com.sjmresorts.opsapp.admin.dtos.system.user.GetCmsUserResponse; -import com.sjmresorts.opsapp.admin.services.AdminUserService; -import com.sjmresorts.opsapp.authentication.dtos.LoginUserInfo; -import com.sjmresorts.opsapp.common.dtos.BaseApiResponse; -import com.sjmresorts.opsapp.common.dtos.DataTableResult; -import com.sjmresorts.opsapp.common.utils.JwtTokenUtil; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.annotation.Resource; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.validation.constraints.NotNull; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -@Tag(name = "CMS", description = "APIs for Admin User functions") -@RestController -@RequestMapping("/api/v1/admin") -public class AdminUserController { - - @Resource - private AdminUserService adminUserService; - - @Operation( - summary = "Query CMS user info page", - description = "API for query CMS user info page" - ) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "CMS user info page queried successful"), - @ApiResponse(responseCode = "400", description = "Invalid request data") - }) - @PostMapping(value = "/user/queryPage", produces = "application/json;charset=UTF-8") - public ResponseEntity getAppUserByPage( - HttpServletRequest request, - @RequestBody(required = true) CmsAppUserPageRequest pageRequest) { - - DataTableResult userInfoList = adminUserService.getUserInfoListByPage(pageRequest); - CmsUserInfoResponse cmsUserInfoResponse = new CmsUserInfoResponse(); - cmsUserInfoResponse.setCode("200"); - cmsUserInfoResponse.setSuccess(true); - cmsUserInfoResponse.setMessage("Query Successfully"); - cmsUserInfoResponse.setData(userInfoList); - return ResponseEntity.status(HttpStatus.OK).body(cmsUserInfoResponse); - } - - @Operation( - summary = "Create CMS user", - description = "API for CMS user creation" - ) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "CMS user created successful"), - @ApiResponse(responseCode = "400", description = "Invalid request data") - }) - @PostMapping(value = "/user/create", produces = "application/json;charset=UTF-8") - public ResponseEntity createUser( - HttpServletRequest request, - @RequestBody CmsUserDto requestData) { - LoginUserInfo loginUserInfo = JwtTokenUtil.parseUserFromRequest(request); - if (loginUserInfo == null) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(null); - } else { - BaseApiResponse responseData = adminUserService.createUser(requestData, loginUserInfo); - return ResponseEntity.status(HttpStatus.OK).body(responseData); - } - } - - @Operation( - summary = "Update CMS user status", - description = "API for CMS user status update" - ) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "CMS user status updated successful"), - @ApiResponse(responseCode = "400", description = "Invalid request data") - }) - @PostMapping(value = "/user/updateStatus", produces = "application/json;charset=UTF-8") - public ResponseEntity updateStatus( - HttpServletRequest request, - @RequestBody CmsUserDto requestData) { - - LoginUserInfo loginUserInfo = JwtTokenUtil.parseUserFromRequest(request); - if (loginUserInfo == null) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(null); - } else { - BaseApiResponse responseData = adminUserService.updateStatus(requestData, loginUserInfo); - return ResponseEntity.status(HttpStatus.OK).body(responseData); - } - } - - @Operation( - summary = "Update CMS user info", - description = "API for CMS user info update" - ) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "CMS user info updated successful"), - @ApiResponse(responseCode = "400", description = "Invalid request data") - }) - @PostMapping(value = "/user/update", produces = "application/json;charset=UTF-8") - public ResponseEntity updateUser( - HttpServletRequest request, - @RequestBody CmsUserDto requestData) { - LoginUserInfo loginUserInfo = JwtTokenUtil.parseUserFromRequest(request); - if (loginUserInfo == null) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(null); - } else { - BaseApiResponse responseData = adminUserService.updateUser(requestData, loginUserInfo); - return ResponseEntity.status(HttpStatus.OK).body(responseData); - } - } - - @Operation( - summary = "Get one CMS user info", - description = "API for get one CMS user info" - ) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "CMS user info got successful"), - @ApiResponse(responseCode = "400", description = "Invalid request data") - }) - @GetMapping(value = "/user/{userId}", produces = "application/json;charset=UTF-8") - public ResponseEntity getUserById( - @PathVariable(name = "userId", required = true) @NotNull Long userId) { - - GetCmsUserResponse responseData = new GetCmsUserResponse(); - CmsUserDto userDto = adminUserService.queryCmsUserById(userId); - responseData.setCode("200"); - responseData.setSuccess(userDto != null); - responseData.setMessage(userDto != null ? "Query Successfully" : "User does not exists"); - responseData.setData(userDto); - return ResponseEntity.status(HttpStatus.OK).body(responseData); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/admin/dtos/appsetting/AdminDevicesInfoPageRequest.java b/src/main/java/com/sjmresorts/opsapp/admin/dtos/appsetting/AdminDevicesInfoPageRequest.java deleted file mode 100644 index 021764b..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/dtos/appsetting/AdminDevicesInfoPageRequest.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.sjmresorts.opsapp.admin.dtos.appsetting; - -import com.sjmresorts.opsapp.common.dtos.BasePageRequest; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Data -@Getter -@Setter -@NoArgsConstructor -@Schema(description = "Get Admin Devices Info Page Request") -public class AdminDevicesInfoPageRequest extends BasePageRequest { - -} diff --git a/src/main/java/com/sjmresorts/opsapp/admin/dtos/appsetting/AdminDevicesInfoPageResponse.java b/src/main/java/com/sjmresorts/opsapp/admin/dtos/appsetting/AdminDevicesInfoPageResponse.java deleted file mode 100644 index c7d1b98..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/dtos/appsetting/AdminDevicesInfoPageResponse.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.sjmresorts.opsapp.admin.dtos.appsetting; - -import com.sjmresorts.opsapp.common.dtos.BaseApiResponse; -import com.sjmresorts.opsapp.common.dtos.DataTableResult; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Getter -@Setter -@NoArgsConstructor -@Schema(description = "Get Admin Devices Info Page Response") -public class AdminDevicesInfoPageResponse extends BaseApiResponse { - - @Schema(description = "Match Devices Info items") - private DataTableResult data; -} diff --git a/src/main/java/com/sjmresorts/opsapp/admin/dtos/appsetting/AdminGetClientInfosRequest.java b/src/main/java/com/sjmresorts/opsapp/admin/dtos/appsetting/AdminGetClientInfosRequest.java deleted file mode 100644 index 22f08dd..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/dtos/appsetting/AdminGetClientInfosRequest.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.sjmresorts.opsapp.admin.dtos.appsetting; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; -import lombok.Data; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Data -@Getter -@Setter -@NoArgsConstructor -@Schema(description = "CMS get device Request") -public class AdminGetClientInfosRequest { - @Schema(description = "Device ID of Clients Info") - @NotBlank(message = "Device Id must not be blank") - private String deviceId; -} diff --git a/src/main/java/com/sjmresorts/opsapp/admin/dtos/appsetting/AdminGetClientInfosResponse.java b/src/main/java/com/sjmresorts/opsapp/admin/dtos/appsetting/AdminGetClientInfosResponse.java deleted file mode 100644 index c2803a3..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/dtos/appsetting/AdminGetClientInfosResponse.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.sjmresorts.opsapp.admin.dtos.appsetting; - -import com.sjmresorts.opsapp.common.dtos.BaseApiResponse; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Getter -@Setter -@NoArgsConstructor -@Schema(description = "CMS get device Response") -public class AdminGetClientInfosResponse extends BaseApiResponse { - - @Schema(description = "Match Device items") - private ClientInfosDto data; -} diff --git a/src/main/java/com/sjmresorts/opsapp/admin/dtos/appsetting/ClientInfosDto.java b/src/main/java/com/sjmresorts/opsapp/admin/dtos/appsetting/ClientInfosDto.java deleted file mode 100644 index 107dd81..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/dtos/appsetting/ClientInfosDto.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.sjmresorts.opsapp.admin.dtos.appsetting; - -import lombok.Data; - -import java.time.Instant; - -@Data -public class ClientInfosDto { - - private Long id; - - private Boolean isLatest; - - private Boolean isActive; - - private String clientName; - - private String clientVersion; - - private String deviceFuid; - - private String casinoCode; - - private Instant updatedAt; -} - diff --git a/src/main/java/com/sjmresorts/opsapp/admin/dtos/appsetting/GetAppSettingRequest.java b/src/main/java/com/sjmresorts/opsapp/admin/dtos/appsetting/GetAppSettingRequest.java deleted file mode 100644 index 849cdee..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/dtos/appsetting/GetAppSettingRequest.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.sjmresorts.opsapp.admin.dtos.appsetting; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotNull; -import lombok.Data; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -import java.util.List; - -@Data -@Getter -@Setter -@NoArgsConstructor -@Schema(description = "CMS get App Setting Request") -public class GetAppSettingRequest { - @Schema(description = "App setting keys to retrieve") - @NotNull(message = "App setting keys must not be empty") - private List appSettingKeys; -} diff --git a/src/main/java/com/sjmresorts/opsapp/admin/dtos/menus/AdminMenuItemDto.java b/src/main/java/com/sjmresorts/opsapp/admin/dtos/menus/AdminMenuItemDto.java deleted file mode 100644 index 6d851b8..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/dtos/menus/AdminMenuItemDto.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.sjmresorts.opsapp.admin.dtos.menus; - -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -public class AdminMenuItemDto { - private String id; - private String parentId; - private String heading; - private String sectionTitle; - private String route; - private String fontIcon; - private Integer sortNo; - /** 1=显示,0=隐藏 */ - private Integer status; - /** 不可关闭(避免锁死后台) */ - private boolean locked; -} diff --git a/src/main/java/com/sjmresorts/opsapp/admin/dtos/menus/AdminMenuStatusRequest.java b/src/main/java/com/sjmresorts/opsapp/admin/dtos/menus/AdminMenuStatusRequest.java deleted file mode 100644 index 8674569..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/dtos/menus/AdminMenuStatusRequest.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.sjmresorts.opsapp.admin.dtos.menus; - -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -public class AdminMenuStatusRequest { - @NotBlank - private String routerId; - /** 1=显示,0=隐藏 */ - @NotNull - private Integer status; -} diff --git a/src/main/java/com/sjmresorts/opsapp/admin/dtos/menus/AdminMenuStatusResponse.java b/src/main/java/com/sjmresorts/opsapp/admin/dtos/menus/AdminMenuStatusResponse.java deleted file mode 100644 index daa24b9..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/dtos/menus/AdminMenuStatusResponse.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.sjmresorts.opsapp.admin.dtos.menus; - -import com.sjmresorts.opsapp.admin.dtos.system.router.UserRouterDto; -import com.sjmresorts.opsapp.common.dtos.BaseApiResponse; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; - -import java.util.ArrayList; -import java.util.List; - -@Data -@EqualsAndHashCode(callSuper = true) -@NoArgsConstructor -public class AdminMenuStatusResponse extends BaseApiResponse { - /** 更新后当前登录用户侧栏菜单(可立刻刷新) */ - private List userRouters = new ArrayList<>(); -} diff --git a/src/main/java/com/sjmresorts/opsapp/admin/dtos/menus/AdminMenusListResponse.java b/src/main/java/com/sjmresorts/opsapp/admin/dtos/menus/AdminMenusListResponse.java deleted file mode 100644 index ebb4112..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/dtos/menus/AdminMenusListResponse.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.sjmresorts.opsapp.admin.dtos.menus; - -import com.sjmresorts.opsapp.common.dtos.BaseApiResponse; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; - -import java.util.ArrayList; -import java.util.List; - -@Data -@EqualsAndHashCode(callSuper = true) -@NoArgsConstructor -public class AdminMenusListResponse extends BaseApiResponse { - private List data = new ArrayList<>(); -} diff --git a/src/main/java/com/sjmresorts/opsapp/admin/dtos/permissions/AdminAssignGroupPermissionRequest.java b/src/main/java/com/sjmresorts/opsapp/admin/dtos/permissions/AdminAssignGroupPermissionRequest.java deleted file mode 100644 index cbbd5b9..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/dtos/permissions/AdminAssignGroupPermissionRequest.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.sjmresorts.opsapp.admin.dtos.permissions; - -import com.sjmresorts.opsapp.authentication.models.RouterPermission; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -import java.util.List; - -@Data -@Getter -@Setter -@NoArgsConstructor -@Schema(description = " Assign Role Permission Request") -public class AdminAssignGroupPermissionRequest { - @Schema(description = "Role Id",example="200") - private Long roleId; - @Schema(description = "Router Operation",example="200") - private List assignRouterOperation; -} diff --git a/src/main/java/com/sjmresorts/opsapp/admin/dtos/permissions/AdminAssignGroupPermissionResponse.java b/src/main/java/com/sjmresorts/opsapp/admin/dtos/permissions/AdminAssignGroupPermissionResponse.java deleted file mode 100644 index d3b23ed..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/dtos/permissions/AdminAssignGroupPermissionResponse.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.sjmresorts.opsapp.admin.dtos.permissions; - -import com.sjmresorts.opsapp.admin.models.GroupPermissionObject; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Data -@Getter -@Setter -@NoArgsConstructor -@Schema(description = "Response info of CMS Delete Role") -public class AdminAssignGroupPermissionResponse { - @Schema(description = "Indicate CMS delete roles success status") - private boolean success=false; - - @Schema(description = "Status Code of response",example="200") - private String code; - - @Schema(description = "Description of result",example="Get roles successfully") - private String message; - - @Schema(description = "data of result") - private GroupPermissionObject data; -} diff --git a/src/main/java/com/sjmresorts/opsapp/admin/dtos/permissions/AdminListPermissionSettingRequest.java b/src/main/java/com/sjmresorts/opsapp/admin/dtos/permissions/AdminListPermissionSettingRequest.java deleted file mode 100644 index e1d3f34..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/dtos/permissions/AdminListPermissionSettingRequest.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.sjmresorts.opsapp.admin.dtos.permissions; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Data -@Getter -@Setter -@NoArgsConstructor -@Schema(description = " Delete Role Request") -public class AdminListPermissionSettingRequest { - @Schema(description = "Role Id",example="200") - private Long roleId; -} diff --git a/src/main/java/com/sjmresorts/opsapp/admin/dtos/permissions/AdminListPermissionSettingResponse.java b/src/main/java/com/sjmresorts/opsapp/admin/dtos/permissions/AdminListPermissionSettingResponse.java deleted file mode 100644 index abf6d06..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/dtos/permissions/AdminListPermissionSettingResponse.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.sjmresorts.opsapp.admin.dtos.permissions; - -import com.sjmresorts.opsapp.admin.models.GroupPermissionObject; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Data -@Getter -@Setter -@NoArgsConstructor -@Schema(description = "Response info of CMS Delete Role") -public class AdminListPermissionSettingResponse { - @Schema(description = "Indicate CMS delete roles success status") - private boolean success=false; - - @Schema(description = "Status Code of response",example="200") - private String code; - - @Schema(description = "Description of result",example="Get roles successfully") - private String message; - - @Schema(description = "data of result") - private GroupPermissionObject data; -} diff --git a/src/main/java/com/sjmresorts/opsapp/admin/dtos/roleUsers/AdminGetRoleUsersRequest.java b/src/main/java/com/sjmresorts/opsapp/admin/dtos/roleUsers/AdminGetRoleUsersRequest.java deleted file mode 100644 index add51a6..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/dtos/roleUsers/AdminGetRoleUsersRequest.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.sjmresorts.opsapp.admin.dtos.roleUsers; - -import com.sjmresorts.opsapp.common.dtos.BasePageRequest; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Data -@Getter -@Setter -@NoArgsConstructor -@Schema(description = "CMS Get Role Uers Status Request") -public class AdminGetRoleUsersRequest extends BasePageRequest { - - private Long searchRoleId; - - private String searchRoleName; - -} diff --git a/src/main/java/com/sjmresorts/opsapp/admin/dtos/roleUsers/AdminGetRoleUsersResponse.java b/src/main/java/com/sjmresorts/opsapp/admin/dtos/roleUsers/AdminGetRoleUsersResponse.java deleted file mode 100644 index ee3441f..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/dtos/roleUsers/AdminGetRoleUsersResponse.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.sjmresorts.opsapp.admin.dtos.roleUsers; - -import com.sjmresorts.opsapp.authentication.models.User; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -import java.util.List; -import java.util.Map; - -@Data -@Getter -@Setter -@NoArgsConstructor -@Schema(description = "Response info of CMS Role Users Status") -public class AdminGetRoleUsersResponse { - @Schema(description = "Indicate CMS get roles success status") - private boolean success=false; - - @Schema(description = "Status Code of response",example="200") - private String code; - - @Schema(description = "Description of result",example="Get roles successfully") - private String message; - - @Schema(description = "data of result") - private Map> data; -} diff --git a/src/main/java/com/sjmresorts/opsapp/admin/dtos/roleUsers/AdminRoleUsersDto.java b/src/main/java/com/sjmresorts/opsapp/admin/dtos/roleUsers/AdminRoleUsersDto.java deleted file mode 100644 index 69ea795..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/dtos/roleUsers/AdminRoleUsersDto.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.sjmresorts.opsapp.admin.dtos.roleUsers; - -import lombok.Data; -import org.mapstruct.Mapper; - -@Data -@Mapper(componentModel = "spring") -public class AdminRoleUsersDto { - - private Long id; - private Long roleId; - private Long userId; -} diff --git a/src/main/java/com/sjmresorts/opsapp/admin/dtos/roles/AdminCreateRoleRequest.java b/src/main/java/com/sjmresorts/opsapp/admin/dtos/roles/AdminCreateRoleRequest.java deleted file mode 100644 index 7ade959..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/dtos/roles/AdminCreateRoleRequest.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.sjmresorts.opsapp.admin.dtos.roles; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Data -@Getter -@Setter -@NoArgsConstructor -@Schema(description = " Create Role Request") -public class AdminCreateRoleRequest { - @Schema(description = "Role Name",example="200") - private String roleName; -} diff --git a/src/main/java/com/sjmresorts/opsapp/admin/dtos/roles/AdminCreateRoleResponse.java b/src/main/java/com/sjmresorts/opsapp/admin/dtos/roles/AdminCreateRoleResponse.java deleted file mode 100644 index 9fc9f8d..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/dtos/roles/AdminCreateRoleResponse.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.sjmresorts.opsapp.admin.dtos.roles; - -import com.sjmresorts.opsapp.authentication.models.Role; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Data -@Getter -@Setter -@NoArgsConstructor -@Schema(description = "Response info of CMS Create Role") -public class AdminCreateRoleResponse { - @Schema(description = "Indicate CMS create roles success status") - private boolean success=false; - - @Schema(description = "Status Code of response",example="200") - private String code; - - @Schema(description = "Description of result",example="Get roles successfully") - private String message; - - @Schema(description = "data of result") - private Role data; -} diff --git a/src/main/java/com/sjmresorts/opsapp/admin/dtos/roles/AdminDeleteRoleRequest.java b/src/main/java/com/sjmresorts/opsapp/admin/dtos/roles/AdminDeleteRoleRequest.java deleted file mode 100644 index b479efe..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/dtos/roles/AdminDeleteRoleRequest.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.sjmresorts.opsapp.admin.dtos.roles; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Data -@Getter -@Setter -@NoArgsConstructor -@Schema(description = " Delete Role Request") -public class AdminDeleteRoleRequest { - @Schema(description = "Role Id",example="200") - private Long roleId; - @Schema(description = "Role Name",example="200") - private String roleName; -} diff --git a/src/main/java/com/sjmresorts/opsapp/admin/dtos/roles/AdminDeleteRoleResponse.java b/src/main/java/com/sjmresorts/opsapp/admin/dtos/roles/AdminDeleteRoleResponse.java deleted file mode 100644 index 3d16497..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/dtos/roles/AdminDeleteRoleResponse.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.sjmresorts.opsapp.admin.dtos.roles; - -import com.sjmresorts.opsapp.authentication.models.Role; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Data -@Getter -@Setter -@NoArgsConstructor -@Schema(description = "Response info of CMS Delete Role") -public class AdminDeleteRoleResponse { - @Schema(description = "Indicate CMS delete roles success status") - private boolean success=false; - - @Schema(description = "Status Code of response",example="200") - private String code; - - @Schema(description = "Description of result",example="Get roles successfully") - private String message; - - @Schema(description = "data of result") - private Role data; -} diff --git a/src/main/java/com/sjmresorts/opsapp/admin/dtos/roles/AdminGetRoleByIdRequest.java b/src/main/java/com/sjmresorts/opsapp/admin/dtos/roles/AdminGetRoleByIdRequest.java deleted file mode 100644 index bd0667e..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/dtos/roles/AdminGetRoleByIdRequest.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.sjmresorts.opsapp.admin.dtos.roles; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Data -@Getter -@Setter -@NoArgsConstructor -@Schema(description = " Modify Role Request") -public class AdminGetRoleByIdRequest { - @Schema(description = "Role Id",example="200") - private Long roleId; -} diff --git a/src/main/java/com/sjmresorts/opsapp/admin/dtos/roles/AdminGetRoleByIdResponse.java b/src/main/java/com/sjmresorts/opsapp/admin/dtos/roles/AdminGetRoleByIdResponse.java deleted file mode 100644 index c902a7a..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/dtos/roles/AdminGetRoleByIdResponse.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.sjmresorts.opsapp.admin.dtos.roles; - -import com.sjmresorts.opsapp.authentication.models.Role; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Data -@Getter -@Setter -@NoArgsConstructor -@Schema(description = "Response info of CMS Get Role By Id") -public class AdminGetRoleByIdResponse { - @Schema(description = "Indicate CMS get roles by id success status") - private boolean success=false; - - @Schema(description = "Status Code of response",example="200") - private String code; - - @Schema(description = "Description of result",example="Get roles successfully") - private String message; - - @Schema(description = "data of result") - private Role data; -} diff --git a/src/main/java/com/sjmresorts/opsapp/admin/dtos/roles/AdminGetRolesRequest.java b/src/main/java/com/sjmresorts/opsapp/admin/dtos/roles/AdminGetRolesRequest.java deleted file mode 100644 index 920a26d..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/dtos/roles/AdminGetRolesRequest.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.sjmresorts.opsapp.admin.dtos.roles; - -import com.sjmresorts.opsapp.common.dtos.BasePageRequest; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Data -@Getter -@Setter -@NoArgsConstructor -@Schema(description = "CMS Roles Request") -public class AdminGetRolesRequest extends BasePageRequest { - - private String searchRoleName; - -} diff --git a/src/main/java/com/sjmresorts/opsapp/admin/dtos/roles/AdminGetRolesResponse.java b/src/main/java/com/sjmresorts/opsapp/admin/dtos/roles/AdminGetRolesResponse.java deleted file mode 100644 index 8f279e3..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/dtos/roles/AdminGetRolesResponse.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.sjmresorts.opsapp.admin.dtos.roles; - -import com.sjmresorts.opsapp.common.dtos.DataTableResult; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Data -@Getter -@Setter -@NoArgsConstructor -@Schema(description = "Response info of CMS Roles") -public class AdminGetRolesResponse { - @Schema(description = "Indicate CMS get roles success status") - private boolean success=false; - - @Schema(description = "Status Code of response",example="200") - private String code; - - @Schema(description = "Description of result",example="Get roles successfully") - private String message; - - @Schema(description = "data of result") - private DataTableResult data; -} diff --git a/src/main/java/com/sjmresorts/opsapp/admin/dtos/roles/AdminModifyRoleRequest.java b/src/main/java/com/sjmresorts/opsapp/admin/dtos/roles/AdminModifyRoleRequest.java deleted file mode 100644 index 5a15131..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/dtos/roles/AdminModifyRoleRequest.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.sjmresorts.opsapp.admin.dtos.roles; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Data -@Getter -@Setter -@NoArgsConstructor -@Schema(description = " Modify Role Request") -public class AdminModifyRoleRequest { - @Schema(description = "Role Id",example="200") - private Long roleId; - @Schema(description = "Role Name",example="200") - private String roleName; -} diff --git a/src/main/java/com/sjmresorts/opsapp/admin/dtos/roles/AdminModifyRoleResponse.java b/src/main/java/com/sjmresorts/opsapp/admin/dtos/roles/AdminModifyRoleResponse.java deleted file mode 100644 index 146427d..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/dtos/roles/AdminModifyRoleResponse.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.sjmresorts.opsapp.admin.dtos.roles; - -import com.sjmresorts.opsapp.authentication.models.Role; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Data -@Getter -@Setter -@NoArgsConstructor -@Schema(description = "Response info of CMS Modify Role") -public class AdminModifyRoleResponse { - @Schema(description = "Indicate CMS modify roles success status") - private boolean success=false; - - @Schema(description = "Status Code of response",example="200") - private String code; - - @Schema(description = "Description of result",example="Get roles successfully") - private String message; - - @Schema(description = "data of result") - private Role data; -} diff --git a/src/main/java/com/sjmresorts/opsapp/admin/dtos/roles/AdminRoleAssignUsersRequest.java b/src/main/java/com/sjmresorts/opsapp/admin/dtos/roles/AdminRoleAssignUsersRequest.java deleted file mode 100644 index c6360cc..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/dtos/roles/AdminRoleAssignUsersRequest.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.sjmresorts.opsapp.admin.dtos.roles; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -import java.util.List; - -@Data -@Getter -@Setter -@NoArgsConstructor -@Schema(description = " Delete Role Request") -public class AdminRoleAssignUsersRequest { - @Schema(description = "Role Id",example="200") - private Long roleId; - @Schema(description = "Role Name",example="200") - private List employeeIds; -} diff --git a/src/main/java/com/sjmresorts/opsapp/admin/dtos/roles/AdminRoleAssignUsersResponse.java b/src/main/java/com/sjmresorts/opsapp/admin/dtos/roles/AdminRoleAssignUsersResponse.java deleted file mode 100644 index cfa3719..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/dtos/roles/AdminRoleAssignUsersResponse.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.sjmresorts.opsapp.admin.dtos.roles; - -import com.sjmresorts.opsapp.authentication.models.Role; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Data -@Getter -@Setter -@NoArgsConstructor -@Schema(description = "Response info of CMS Delete Role") -public class AdminRoleAssignUsersResponse { - @Schema(description = "Indicate CMS delete roles success status") - private boolean success=false; - - @Schema(description = "Status Code of response",example="200") - private String code; - - @Schema(description = "Description of result",example="Get roles successfully") - private String message; - - @Schema(description = "data of result") - private Role data; -} diff --git a/src/main/java/com/sjmresorts/opsapp/admin/dtos/roles/AdminRoleDto.java b/src/main/java/com/sjmresorts/opsapp/admin/dtos/roles/AdminRoleDto.java deleted file mode 100644 index 9dfb8e2..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/dtos/roles/AdminRoleDto.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.sjmresorts.opsapp.admin.dtos.roles; - -import lombok.Data; -import org.mapstruct.Mapper; - -@Data -@Mapper(componentModel = "spring") -public class AdminRoleDto { - private Long roleId; - private String roleName; -} diff --git a/src/main/java/com/sjmresorts/opsapp/admin/dtos/system/login/AdminLoginRequest.java b/src/main/java/com/sjmresorts/opsapp/admin/dtos/system/login/AdminLoginRequest.java deleted file mode 100644 index 8eb994f..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/dtos/system/login/AdminLoginRequest.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.sjmresorts.opsapp.admin.dtos.system.login; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; -import lombok.Data; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Data -@Getter -@Setter -@NoArgsConstructor -@Schema(description = "CMS LoginRequest") -public class AdminLoginRequest { - - @Schema(description = "AD Account of Employee") - @NotBlank(message = "Employee Id must not be blank") - private String userCode; - - @Schema(description = "Password of AD Account") - @NotBlank(message = "Password must not be blank") - private String password; - -} diff --git a/src/main/java/com/sjmresorts/opsapp/admin/dtos/system/login/AdminLoginResponse.java b/src/main/java/com/sjmresorts/opsapp/admin/dtos/system/login/AdminLoginResponse.java deleted file mode 100644 index cc8f9c1..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/dtos/system/login/AdminLoginResponse.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.sjmresorts.opsapp.admin.dtos.system.login; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Data -@Getter -@Setter -@NoArgsConstructor -@Schema(description = "Response info of CMS login with AD") -public class AdminLoginResponse { - - @Schema(description = "Indicate CMS login authorization success status") - private boolean success=false; - - @Schema(description = "Status Code of response",example="200") - private String code; - - @Schema(description = "Description of result",example="Login successfully") - private String message; - - @Schema(description = "Login Info of response") - private CmsLoginInfo loginInfo; - -} diff --git a/src/main/java/com/sjmresorts/opsapp/admin/dtos/system/login/AdminLogoutResponse.java b/src/main/java/com/sjmresorts/opsapp/admin/dtos/system/login/AdminLogoutResponse.java deleted file mode 100644 index ca1add9..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/dtos/system/login/AdminLogoutResponse.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.sjmresorts.opsapp.admin.dtos.system.login; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Data -@Getter -@Setter -@NoArgsConstructor -@Schema(description = "Response info of CMS logout") -public class AdminLogoutResponse { - - @Schema(description = "Indicate CMS logout success status") - private boolean success=false; - - @Schema(description = "Status Code of response",example="200") - private String code; - - @Schema(description = "Description of result",example="Logout successfully") - private String message; - -} diff --git a/src/main/java/com/sjmresorts/opsapp/admin/dtos/system/login/CmsLoginInfo.java b/src/main/java/com/sjmresorts/opsapp/admin/dtos/system/login/CmsLoginInfo.java deleted file mode 100644 index 25df173..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/dtos/system/login/CmsLoginInfo.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.sjmresorts.opsapp.admin.dtos.system.login; - -import com.sjmresorts.opsapp.admin.dtos.system.router.RouterPermissionDto; -import com.sjmresorts.opsapp.admin.dtos.system.router.UserRouterDto; -import com.sjmresorts.opsapp.authentication.dtos.LoginUserInfo; -import lombok.Data; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -import java.util.List; -import java.util.Map; - -@Data -@Getter -@Setter -@NoArgsConstructor -public class CmsLoginInfo { - private String accessToken; - private String refreshToken; - private String defaultRouter; - private LoginUserInfo loginUser; - private List userRouters; - private Map> userPermissions; -} diff --git a/src/main/java/com/sjmresorts/opsapp/admin/dtos/system/router/RouterPermissionDto.java b/src/main/java/com/sjmresorts/opsapp/admin/dtos/system/router/RouterPermissionDto.java deleted file mode 100644 index cf3dba8..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/dtos/system/router/RouterPermissionDto.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.sjmresorts.opsapp.admin.dtos.system.router; - -import lombok.Data; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Data -@Getter -@Setter -@NoArgsConstructor -public class RouterPermissionDto { - private String id; - private String routerId; - private String permissionName; -} diff --git a/src/main/java/com/sjmresorts/opsapp/admin/dtos/system/router/UserRouterDto.java b/src/main/java/com/sjmresorts/opsapp/admin/dtos/system/router/UserRouterDto.java deleted file mode 100644 index 8c2f0c5..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/dtos/system/router/UserRouterDto.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.sjmresorts.opsapp.admin.dtos.system.router; - -import lombok.Data; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -import java.util.List; - -@Data -@Getter -@Setter -@NoArgsConstructor -public class UserRouterDto { - private String parentId; - private String heading; - private String sectionTitle; - private String route; - private String svgIcon; - private String fontIcon; - private Integer sortNo; - private Integer status; - private List sub; -} diff --git a/src/main/java/com/sjmresorts/opsapp/admin/dtos/system/user/CmsAppUserPageRequest.java b/src/main/java/com/sjmresorts/opsapp/admin/dtos/system/user/CmsAppUserPageRequest.java deleted file mode 100644 index 637fb1d..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/dtos/system/user/CmsAppUserPageRequest.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.sjmresorts.opsapp.admin.dtos.system.user; - -import com.sjmresorts.opsapp.common.dtos.BasePageRequest; -import lombok.Getter; -import lombok.Setter; - -@Setter -@Getter -public class CmsAppUserPageRequest extends BasePageRequest { - - private String searchAccount; - private String searchUserName; - -} diff --git a/src/main/java/com/sjmresorts/opsapp/admin/dtos/system/user/CmsUserDto.java b/src/main/java/com/sjmresorts/opsapp/admin/dtos/system/user/CmsUserDto.java deleted file mode 100644 index 54d090f..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/dtos/system/user/CmsUserDto.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.sjmresorts.opsapp.admin.dtos.system.user; - -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.time.Instant; - -@Data -@NoArgsConstructor -public class CmsUserDto { - - private Long userId; - private String userCode; - private String userName; - private String email; - private Integer status; - private Instant updatedAt; - private Instant createdAt; -} diff --git a/src/main/java/com/sjmresorts/opsapp/admin/dtos/system/user/CmsUserInfoResponse.java b/src/main/java/com/sjmresorts/opsapp/admin/dtos/system/user/CmsUserInfoResponse.java deleted file mode 100644 index 7d55719..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/dtos/system/user/CmsUserInfoResponse.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.sjmresorts.opsapp.admin.dtos.system.user; - -import com.sjmresorts.opsapp.common.dtos.BaseApiResponse; -import com.sjmresorts.opsapp.common.dtos.DataTableResult; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - - -@Getter -@Setter -@NoArgsConstructor -@Schema(description = "User Info List") -public class CmsUserInfoResponse extends BaseApiResponse { - private DataTableResult data; -} diff --git a/src/main/java/com/sjmresorts/opsapp/admin/dtos/system/user/GetCmsUserResponse.java b/src/main/java/com/sjmresorts/opsapp/admin/dtos/system/user/GetCmsUserResponse.java deleted file mode 100644 index ba55aa9..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/dtos/system/user/GetCmsUserResponse.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.sjmresorts.opsapp.admin.dtos.system.user; - -import com.sjmresorts.opsapp.common.dtos.BaseApiResponse; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Getter -@Setter -@NoArgsConstructor -@Schema(description = "Response info of getting CMS User") -public class GetCmsUserResponse extends BaseApiResponse { - private CmsUserDto data; -} diff --git a/src/main/java/com/sjmresorts/opsapp/admin/mappers/ClientInfosMapper.java b/src/main/java/com/sjmresorts/opsapp/admin/mappers/ClientInfosMapper.java deleted file mode 100644 index a6633b7..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/mappers/ClientInfosMapper.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.sjmresorts.opsapp.admin.mappers; - -import com.sjmresorts.opsapp.admin.dtos.appsetting.ClientInfosDto; -import com.sjmresorts.opsapp.authentication.models.ClientInfo; -import org.mapstruct.Mapper; - -import java.util.List; - -@Mapper(componentModel = "spring") -public interface ClientInfosMapper { - - ClientInfosDto toClientInfosDto(ClientInfo device); - - List toClientInfosDtoList(List content); - - -} diff --git a/src/main/java/com/sjmresorts/opsapp/admin/mappers/CmsLoginRouterMapper.java b/src/main/java/com/sjmresorts/opsapp/admin/mappers/CmsLoginRouterMapper.java deleted file mode 100644 index 92648ac..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/mappers/CmsLoginRouterMapper.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.sjmresorts.opsapp.admin.mappers; - -import com.sjmresorts.opsapp.admin.dtos.system.router.RouterPermissionDto; -import com.sjmresorts.opsapp.admin.dtos.system.router.UserRouterDto; -import com.sjmresorts.opsapp.authentication.models.RouterPermission; -import com.sjmresorts.opsapp.authentication.models.SysRouter; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -@Component -@RequiredArgsConstructor -public class CmsLoginRouterMapper { - private final SysRouterMapper routerMapper; - private final RouterPermissionMapper routerPermissionMapper; - - public List toRouterDtoListWithChildren(List sysRouters) { - List routerDtos = new ArrayList(); - routerMapper.toUserRouterDtoList(sysRouters); - for(SysRouter sysRouter:sysRouters){ - UserRouterDto routerDto = routerMapper.toUserRouterDto(sysRouter); - if(sysRouter.getSub()!=null && !sysRouter.getSub().isEmpty()){ - routerDto.setSub(toRouterDtoListWithChildren(sysRouter.getSub())); - } - routerDtos.add(routerDto); - } - return routerDtos; - } - - public Map> toRouterPermissionMap(Map> userPermissions) { - Map> permissionMap = new HashMap>(); - userPermissions.forEach((key, value) -> { - permissionMap.put(key, routerPermissionMapper.toRouterPermissionDtoList(value)); - }); - return permissionMap; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/admin/mappers/RouterPermissionMapper.java b/src/main/java/com/sjmresorts/opsapp/admin/mappers/RouterPermissionMapper.java deleted file mode 100644 index fc3df8c..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/mappers/RouterPermissionMapper.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.sjmresorts.opsapp.admin.mappers; - -import com.sjmresorts.opsapp.admin.dtos.system.router.RouterPermissionDto; -import com.sjmresorts.opsapp.authentication.models.RouterPermission; -import org.mapstruct.Mapper; - -import java.util.List; - -@Mapper(componentModel = "spring") -public interface RouterPermissionMapper { - - RouterPermissionDto toRouterPermissionDto(RouterPermission entity); - - List toRouterPermissionDtoList(List routerPermissions); -} diff --git a/src/main/java/com/sjmresorts/opsapp/admin/mappers/SysRouterMapper.java b/src/main/java/com/sjmresorts/opsapp/admin/mappers/SysRouterMapper.java deleted file mode 100644 index 2cede1e..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/mappers/SysRouterMapper.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.sjmresorts.opsapp.admin.mappers; - -import com.sjmresorts.opsapp.admin.dtos.system.router.UserRouterDto; -import com.sjmresorts.opsapp.authentication.models.SysRouter; -import org.mapstruct.Mapper; -import org.mapstruct.Mapping; - -import java.util.List; - -@Mapper(componentModel = "spring") -public interface SysRouterMapper { - - @Mapping(target = "sub", ignore = true) - UserRouterDto toUserRouterDto(SysRouter sysRouter); - - List toUserRouterDtoList(List sysRouters); -} diff --git a/src/main/java/com/sjmresorts/opsapp/admin/models/GroupPermissionObject.java b/src/main/java/com/sjmresorts/opsapp/admin/models/GroupPermissionObject.java deleted file mode 100644 index 1f6d0fc..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/models/GroupPermissionObject.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.sjmresorts.opsapp.admin.models; - -import com.sjmresorts.opsapp.authentication.models.SysRouter; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -import java.util.List; - -@Getter -@Setter -@NoArgsConstructor -public class GroupPermissionObject { - private Long groupId; - private String groupName; - private List groupRouterSetting; -} diff --git a/src/main/java/com/sjmresorts/opsapp/admin/repositories/RoleUsersRepository.java b/src/main/java/com/sjmresorts/opsapp/admin/repositories/RoleUsersRepository.java deleted file mode 100644 index dc39aee..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/repositories/RoleUsersRepository.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.sjmresorts.opsapp.admin.repositories; - -import com.sjmresorts.opsapp.authentication.models.RoleUsers; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.stereotype.Repository; - -import java.util.List; - -@Repository -public interface RoleUsersRepository extends JpaRepository { - - - List findAllByRoleId(Long roleId); - - @Query(value = "select roleUser.roleId from RoleUsers roleUser where employeeId=?1 ") - List findRoleIdListByEmployeeId(String employeeId); -} \ No newline at end of file diff --git a/src/main/java/com/sjmresorts/opsapp/admin/repositories/RolesRepository.java b/src/main/java/com/sjmresorts/opsapp/admin/repositories/RolesRepository.java deleted file mode 100644 index 14e4a71..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/repositories/RolesRepository.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.sjmresorts.opsapp.admin.repositories; - -import com.sjmresorts.opsapp.authentication.models.Role; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.domain.Specification; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.stereotype.Repository; - -import java.util.List; - -@Repository -public interface RolesRepository extends JpaRepository { - - Page findAll(Specification listSpec, Pageable pageable); - - Role getRoleByRoleId(Long roleId); - - @Query(value="select r.roleId from Role r where r.roleName = ?1") - List queryRoleIdByRoleName(String roleName); - -} \ No newline at end of file diff --git a/src/main/java/com/sjmresorts/opsapp/admin/services/AdminDevicesService.java b/src/main/java/com/sjmresorts/opsapp/admin/services/AdminDevicesService.java deleted file mode 100644 index 02c7719..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/services/AdminDevicesService.java +++ /dev/null @@ -1,108 +0,0 @@ -package com.sjmresorts.opsapp.admin.services; - -import com.sjmresorts.opsapp.admin.dtos.appsetting.AdminDevicesInfoPageRequest; -import com.sjmresorts.opsapp.admin.dtos.appsetting.ClientInfosDto; -import com.sjmresorts.opsapp.admin.mappers.ClientInfosMapper; -import com.sjmresorts.opsapp.authentication.dtos.LoginUserInfo; -import com.sjmresorts.opsapp.authentication.models.ClientInfo; -import com.sjmresorts.opsapp.authentication.repositories.DeviceInfoRepository; -import com.sjmresorts.opsapp.common.dtos.BaseApiResponse; -import com.sjmresorts.opsapp.common.dtos.BasePageRequest; -import com.sjmresorts.opsapp.common.dtos.DataTableResult; -import com.sjmresorts.opsapp.common.utils.DaoUtil; -import com.sjmresorts.opsapp.logging.models.AuditLog; -import com.sjmresorts.opsapp.logging.services.AuditLogService; -import jakarta.persistence.criteria.Predicate; -import jakarta.validation.constraints.NotBlank; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.data.jpa.domain.Specification; -import org.springframework.stereotype.Service; - -import java.time.Instant; -import java.util.ArrayList; -import java.util.List; - -@Service -@RequiredArgsConstructor -public class AdminDevicesService { - - private final DeviceInfoRepository deviceInfoRepository; - private final AuditLogService auditLogService; - private final ClientInfosMapper clientInfosMapper; - private final DeviceInfoRepository deviceRepository; - - public DataTableResult getDevicesInfoListByPage(AdminDevicesInfoPageRequest pageRequest) { - - Specification listSpec = buildSpecificationByPageRequest(pageRequest); - int pageSize = pageRequest.getPageSize(); - int pageStart = pageRequest.getStartRow() / pageSize; - Sort sort = DaoUtil.buildSortByString(pageRequest.getSortOrder(), "id"); - Pageable pageable = PageRequest.of(pageStart, pageSize, sort); - Page resultPage = deviceInfoRepository.findAll(listSpec, pageable); - List resultList = clientInfosMapper.toClientInfosDtoList(resultPage.getContent()); - DataTableResult dataResult = new DataTableResult<>(); - dataResult.setTotalRecords(resultPage.getTotalElements()); - dataResult.setPageSize(pageSize); - if (resultList == null) { - resultList = new ArrayList<>(); - } - dataResult.setRowData(resultList); - return dataResult; - } - - private Specification buildSpecificationByPageRequest(BasePageRequest pageRequest) { - - return ((root, criteriaQuery, cb) -> { - List list = new ArrayList<>(); -// if (StringUtils.isNotBlank(pageRequest.getSearchUserName())) { -// list.add(cb.like(cb.upper(root.get("userName")), -// "%" + pageRequest.getSearchUserName().toUpperCase() + "%")); -// } -// if (StringUtils.isNotBlank(pageRequest.getSearchAccount())) { -// list.add(cb.like(cb.upper(root.get("employeeId")), -// "%" + pageRequest.getSearchAccount().toUpperCase() + "%")); -// } - Predicate[] predicates = new Predicate[list.size()]; - criteriaQuery.where(list.toArray(predicates)); - return cb.and(list.toArray(predicates)); - }); - } - - public BaseApiResponse updateDevice(ClientInfosDto deviceDto, LoginUserInfo loginUserInfo) { - BaseApiResponse result = new BaseApiResponse(); - AuditLog auditLog = auditLogService.createAuditLog("Update Device", "Update Device from AdminDevicesService.updateDevice()", loginUserInfo); - ClientInfo device = deviceRepository.findById(deviceDto.getId()).orElse(null); - Instant currentTime = Instant.now(); - if (device != null) { - auditLogService.setLogOldValue(auditLog, device); - device.setCasinoCode(deviceDto.getCasinoCode()); - device.setUpdatedAt(currentTime); - deviceRepository.save(device); - auditLogService.setLogNewValue(auditLog, device); - auditLogService.saveAuditLog(auditLog); - result.setSuccess(true); - result.setCode("200"); - result.setMessage("Updated Successfully"); - } else { - result.setSuccess(false); - result.setCode("400"); - result.setMessage("User does not exists"); - } - return result; - } - - - public ClientInfosDto queryDeviceById(@NotBlank long deviceId) { - ClientInfosDto deviceDto = null; - ClientInfo device = deviceRepository.findById(deviceId).orElse(null); - - if (device != null) { - deviceDto = clientInfosMapper.toClientInfosDto(device); - } - return deviceDto; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/admin/services/AdminLoginService.java b/src/main/java/com/sjmresorts/opsapp/admin/services/AdminLoginService.java deleted file mode 100644 index 98199d9..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/services/AdminLoginService.java +++ /dev/null @@ -1,163 +0,0 @@ -package com.sjmresorts.opsapp.admin.services; - -import com.sjmresorts.opsapp.admin.dtos.system.login.CmsLoginInfo; -import com.sjmresorts.opsapp.admin.mappers.CmsLoginRouterMapper; -import com.sjmresorts.opsapp.admin.repositories.RoleUsersRepository; -import com.sjmresorts.opsapp.admin.repositories.RolesRepository; -import com.sjmresorts.opsapp.common.constants.AppConstants; -import com.sjmresorts.opsapp.authentication.dtos.LoginUserInfo; -import com.sjmresorts.opsapp.authentication.dtos.RefreshTokenResponse; -import com.sjmresorts.opsapp.authentication.mappers.UserMapper; -import com.sjmresorts.opsapp.authentication.models.RouterPermission; -import com.sjmresorts.opsapp.authentication.models.SysRouter; -import com.sjmresorts.opsapp.authentication.models.User; -import com.sjmresorts.opsapp.authentication.models.UserToken; -import com.sjmresorts.opsapp.authentication.repositories.RouterPermissionRepository; -import com.sjmresorts.opsapp.authentication.repositories.UserRepository; -import com.sjmresorts.opsapp.authentication.services.RolePermissionService; -import com.sjmresorts.opsapp.authentication.services.SysRouterService; -import com.sjmresorts.opsapp.authentication.services.UserTokenService; -import com.sjmresorts.opsapp.common.dtos.LDAP.ADAccountInfo; -import com.sjmresorts.opsapp.common.utils.AESUtil; -import com.sjmresorts.opsapp.common.utils.JwtTokenUtil; -import com.sjmresorts.opsapp.common.utils.LDAPUtil; -import jakarta.transaction.Transactional; -import jakarta.validation.constraints.NotBlank; -import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.StringUtils; -import org.springframework.dao.OptimisticLockingFailureException; -import org.springframework.retry.annotation.Backoff; -import org.springframework.retry.annotation.Retryable; -import org.springframework.stereotype.Service; - -import java.time.Instant; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -@Service -@RequiredArgsConstructor -public class AdminLoginService { - - private final UserRepository userRepository; - private final RoleUsersRepository roleUsersRepository; - private final UserMapper userMapper; - private final CmsLoginRouterMapper cmsLoginRouterMapper; - private final SysRouterService sysRouterService; - private final RolePermissionService rolePermissionService; - private final UserTokenService userTokenService; - private final LDAPUtil ldapUtil; - private final RolesRepository rolesRepository; - private final RouterPermissionRepository routerPermissionRepository; - - // -3=disabled, -2=locked, -1 or 0=incorrect loginId or password, 1=success - public int checkLoginByLoginIdAndPassword(String employeeId, String password) { - int checkPassed = -1; - if(AppConstants.SUPER_ADMIN_ID.equalsIgnoreCase(employeeId)) { - User user = userRepository.findByEmployeeId(employeeId).orElse(null); - if (user != null) { - if(user.getStatus()==null || user.getStatus()==0){ - return -3; // user disabled by app - }else if(user.getStatus()==2) { - return -2; // user locked - } - String encryptPassword = AESUtil.sha256(password); - checkPassed = userRepository.countByEmployeeIdAndPassword(employeeId.toUpperCase(), encryptPassword); - } - }else{ - //Check AD - boolean adResult = ldapUtil.authenticate(employeeId, password); - if(adResult) checkPassed = 1; - } - return checkPassed; - } - - @Retryable(retryFor = OptimisticLockingFailureException.class, - maxAttempts = 3, - backoff = @Backoff(delay = 100)) - @Transactional - public CmsLoginInfo getLoginUserInfoByEmployeeId(@NotBlank(message = "Employee Id must not be blank") String employeeId) { - CmsLoginInfo result = null; - if(AppConstants.SUPER_ADMIN_ID.equalsIgnoreCase(employeeId)) { - User user = userRepository.findByEmployeeId(employeeId).orElse(null); - if(user!=null){ - user.setCmsLastLogin(Instant.now()); - userRepository.save(user); - result = new CmsLoginInfo(); - LoginUserInfo loginUser = userMapper.toLoginUser(user); - result.setLoginUser(loginUser); - Map> userPermissions = new HashMap>(); - userPermissions = rolePermissionService.getFullRouterPermissions(); - List routers = sysRouterService.getRoutersByPermission("0",userPermissions); - result.setUserRouters(cmsLoginRouterMapper.toRouterDtoListWithChildren(routers)); - result.setUserPermissions(cmsLoginRouterMapper.toRouterPermissionMap(userPermissions)); - } - }else{ - ADAccountInfo adResult = ldapUtil.getAccountInfo(employeeId); - if(adResult!=null){ - result = new CmsLoginInfo(); - LoginUserInfo loginUser = new LoginUserInfo(); - loginUser.setUserName(adResult.getDisplayName()); - loginUser.setEmployeeId(employeeId); - loginUser.setEmail(adResult.getMail()); - loginUser.setUserCode(employeeId); - result.setLoginUser(loginUser); - Map> userPermissions = new HashMap>(); - List userRoleIds = getUserRoleIds(adResult.getDirectGroups()); - userPermissions = rolePermissionService.getRouterPermissionsByGroupIds(userRoleIds); - List routers = sysRouterService.getRoutersByPermission("0",userPermissions); - result.setUserRouters(cmsLoginRouterMapper.toRouterDtoListWithChildren(routers)); - result.setUserPermissions(cmsLoginRouterMapper.toRouterPermissionMap(userPermissions)); - } - } - - return result; - } - - public void processLogout(LoginUserInfo loginUser) { - if(loginUser!=null){ - userTokenService.clearUserTokenByType(AppConstants.CMS_API_ACCESS_TOKEN_TYPE,loginUser.getEmployeeId()); - userTokenService.clearUserTokenByType(AppConstants.CMS_REFRESH_TOKEN_TYPE,loginUser.getEmployeeId()); - } - } - - public RefreshTokenResponse refreshToken(LoginUserInfo loginUser) { - RefreshTokenResponse result = new RefreshTokenResponse(); - UserToken accessToken = null; - UserToken refreshToken = null; - if(loginUser!=null){ - accessToken= JwtTokenUtil.generateTokenByLoginUser(AppConstants.CMS_API_ACCESS_TOKEN_TYPE,loginUser); - userTokenService.saveUserToken(accessToken); - refreshToken=JwtTokenUtil.generateTokenByLoginUser(AppConstants.CMS_REFRESH_TOKEN_TYPE,loginUser); - userTokenService.saveUserToken(refreshToken); - if(StringUtils.isNotBlank(loginUser.getTokenUuid())){ - userTokenService.removeTokenByUniqueId(loginUser.getTokenUuid()); - } - } - - if(accessToken!=null){ - result.setSuccess(true); - result.setCode("200"); - result.setAccessToken(accessToken.getTokenValue()); - result.setRefreshToken(refreshToken.getTokenValue()); - result.setMessage("Token renew successful."); - }else{ - result.setSuccess(false); - result.setCode("200"); - result.setMessage("Token failed."); - } - return result; - } - - private List getUserRoleIds(List groupName) { - List roleIdList = new ArrayList<>(); - if(groupName!=null&&!groupName.isEmpty()){ - for(String roleName : groupName){ - List roleIds = rolesRepository.queryRoleIdByRoleName(roleName); - roleIdList.addAll(roleIds); - } - } - return roleIdList; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/admin/services/AdminMenusService.java b/src/main/java/com/sjmresorts/opsapp/admin/services/AdminMenusService.java deleted file mode 100644 index 7498e7c..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/services/AdminMenusService.java +++ /dev/null @@ -1,113 +0,0 @@ -package com.sjmresorts.opsapp.admin.services; - -import com.sjmresorts.opsapp.admin.dtos.menus.AdminMenuItemDto; -import com.sjmresorts.opsapp.admin.dtos.menus.AdminMenuStatusRequest; -import com.sjmresorts.opsapp.admin.dtos.menus.AdminMenuStatusResponse; -import com.sjmresorts.opsapp.admin.dtos.menus.AdminMenusListResponse; -import com.sjmresorts.opsapp.admin.dtos.system.router.UserRouterDto; -import com.sjmresorts.opsapp.admin.mappers.CmsLoginRouterMapper; -import com.sjmresorts.opsapp.admin.repositories.RoleUsersRepository; -import com.sjmresorts.opsapp.authentication.dtos.LoginUserInfo; -import com.sjmresorts.opsapp.authentication.models.RouterPermission; -import com.sjmresorts.opsapp.authentication.models.SysRouter; -import com.sjmresorts.opsapp.authentication.repositories.SysRouterRepository; -import com.sjmresorts.opsapp.authentication.services.RolePermissionService; -import com.sjmresorts.opsapp.authentication.services.SysRouterService; -import com.sjmresorts.opsapp.common.constants.AppConstants; -import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.StringUtils; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; -import java.util.Map; -import java.util.Set; - -@Service -@RequiredArgsConstructor -public class AdminMenusService { - - private static final Set LOCKED_ROUTER_IDS = Set.of( - "ESIGN_DASHBOARD", - "ESIGN_SYSTEM", - "ESIGN_SYS_USERS", - "ESIGN_SYS_ROLES", - "ESIGN_SYS_MENUS"); - - private final SysRouterRepository sysRouterRepository; - private final SysRouterService sysRouterService; - private final RolePermissionService rolePermissionService; - private final RoleUsersRepository roleUsersRepository; - private final CmsLoginRouterMapper cmsLoginRouterMapper; - - public AdminMenusListResponse listMenus() { - AdminMenusListResponse response = new AdminMenusListResponse(); - List routers = sysRouterRepository.queryAllRoutersOrderBySort(); - for (SysRouter router : routers) { - AdminMenuItemDto item = new AdminMenuItemDto(); - item.setId(router.getId()); - item.setParentId(router.getParentId()); - item.setHeading(router.getHeading()); - item.setSectionTitle(router.getSectionTitle()); - item.setRoute(router.getRoute()); - item.setFontIcon(router.getFontIcon()); - item.setSortNo(router.getSortNo()); - item.setStatus(router.getStatus() == null ? 1 : router.getStatus()); - item.setLocked(LOCKED_ROUTER_IDS.contains(router.getId())); - response.getData().add(item); - } - response.setSuccess(true); - response.setCode("200"); - response.setMessage("ok"); - return response; - } - - @Transactional - public AdminMenuStatusResponse updateStatus(AdminMenuStatusRequest request, LoginUserInfo loginUser) { - AdminMenuStatusResponse response = new AdminMenuStatusResponse(); - if (request == null || StringUtils.isBlank(request.getRouterId()) || request.getStatus() == null) { - response.setSuccess(false); - response.setCode("400"); - response.setMessage("routerId and status required"); - return response; - } - int status = request.getStatus() == 0 ? 0 : 1; - if (LOCKED_ROUTER_IDS.contains(request.getRouterId()) && status == 0) { - response.setSuccess(false); - response.setCode("400"); - response.setMessage("This menu cannot be hidden"); - return response; - } - SysRouter router = sysRouterRepository.findById(request.getRouterId()).orElse(null); - if (router == null) { - response.setSuccess(false); - response.setCode("404"); - response.setMessage("menu not found"); - return response; - } - router.setStatus(status); - sysRouterRepository.save(router); - - response.setUserRouters(buildUserRouters(loginUser)); - response.setSuccess(true); - response.setCode("200"); - response.setMessage("saved"); - return response; - } - - private List buildUserRouters(LoginUserInfo loginUser) { - if (loginUser == null || StringUtils.isBlank(loginUser.getEmployeeId())) { - return List.of(); - } - Map> userPermissions; - if (AppConstants.SUPER_ADMIN_ID.equalsIgnoreCase(loginUser.getEmployeeId())) { - userPermissions = rolePermissionService.getFullRouterPermissions(); - } else { - List roleIds = roleUsersRepository.findRoleIdListByEmployeeId(loginUser.getEmployeeId()); - userPermissions = rolePermissionService.getRouterPermissionsByGroupIds( - roleIds != null ? roleIds : List.of()); - } - List routers = sysRouterService.getRoutersByPermission("0", userPermissions); - return cmsLoginRouterMapper.toRouterDtoListWithChildren(routers); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/admin/services/AdminPermissionService.java b/src/main/java/com/sjmresorts/opsapp/admin/services/AdminPermissionService.java deleted file mode 100644 index cca62d0..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/services/AdminPermissionService.java +++ /dev/null @@ -1,109 +0,0 @@ -package com.sjmresorts.opsapp.admin.services; - - -import com.sjmresorts.opsapp.admin.dtos.permissions.AdminAssignGroupPermissionRequest; -import com.sjmresorts.opsapp.admin.dtos.permissions.AdminAssignGroupPermissionResponse; -import com.sjmresorts.opsapp.admin.dtos.permissions.AdminListPermissionSettingRequest; -import com.sjmresorts.opsapp.admin.dtos.permissions.AdminListPermissionSettingResponse; -import com.sjmresorts.opsapp.admin.models.GroupPermissionObject; -import com.sjmresorts.opsapp.authentication.models.*; -import com.sjmresorts.opsapp.authentication.repositories.RolePermissionRepository; -import com.sjmresorts.opsapp.authentication.repositories.RouterPermissionRepository; -import com.sjmresorts.opsapp.authentication.repositories.SysRouterRepository; -import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.StringUtils; -import org.springframework.stereotype.Service; - -import java.util.ArrayList; -import java.util.List; - - -@Service -@RequiredArgsConstructor -public class AdminPermissionService { - private final SysRouterRepository sysRouterRepository; - private final RouterPermissionRepository routerPermissionRepository; - private final RolePermissionRepository rolePermissionRepository; - - - public AdminListPermissionSettingResponse getPermissionById(AdminListPermissionSettingRequest request) { - AdminListPermissionSettingResponse result = new AdminListPermissionSettingResponse(); - GroupPermissionObject newObject = new GroupPermissionObject(); - -// 获取路由信息,路由权限,权限组权限 - List listRouter = sysRouterRepository.getAllChildrenRoutes(); - List listPermission = routerPermissionRepository.findAll(); - List listRolePermission = rolePermissionRepository.getAllPermissionByRoleId(request.getRoleId()); -// 判断权限组是否有权限,并将selectAble属性加入返回数据 - if (listRolePermission.size() > 0) { - for(int i=0; i permissionToDelete = rolePermissionRepository.getAllPermissionByRoleId(request.getRoleId()); - if(request.getAssignRouterOperation()!=null &&request.getAssignRouterOperation().size()>0){ - List assignRouterOperation = request.getAssignRouterOperation(); - ListsaveData = new ArrayList<>(); - for(int i =0;i getAllRoles() { - return rolesRepository.findAll(); - } - - public DataTableResult getRoleInfoListByPage(AdminGetRolesRequest pageRequest) { - - Specification listSpec = buildSpecificationByPageRequest(pageRequest); - int pageSize = pageRequest.getPageSize(); - int pageStart = pageRequest.getStartRow() / pageSize; - Sort sort = DaoUtil.buildSortByString(pageRequest.getSortOrder(), "roleId"); - Pageable pageable = PageRequest.of(pageStart, pageSize, sort); - Page resultPage = rolesRepository.findAll(listSpec,pageable); - List resultList = roleMapper.toAdminRoleDto(resultPage.getContent()); - DataTableResult dataResult = new DataTableResult<>(); - dataResult.setTotalRecords(resultPage.getTotalElements()); - dataResult.setPageSize(pageSize); - if (resultList == null) { - resultList = new ArrayList<>(); - } - dataResult.setRowData(resultList); - return dataResult; - } - - - public BaseApiResponse createRole(AdminCreateRoleRequest request, LoginUserInfo loginUserInfo) { - BaseApiResponse result = new BaseApiResponse(); - AuditLog auditLog = auditLogService.createAuditLog("Create Role", "Create a new Role from AdminRolesService.createRole()", loginUserInfo); - Role newRole = new Role(); - newRole.setRoleName(request.getRoleName()); - auditLogService.setLogOldValue(auditLog,null); - rolesRepository.save(newRole); - auditLogService.setLogNewValue(auditLog,newRole); - auditLogService.saveAuditLog(auditLog); - result.setSuccess(true); - result.setCode("200"); - result.setMessage("Created Successfully"); - return result; - } - - public BaseApiResponse deleteRole(AdminDeleteRoleRequest request, LoginUserInfo loginUserInfo) { - BaseApiResponse result = new BaseApiResponse(); - AuditLog auditLog = auditLogService.createAuditLog("Delete Role", "Delete a Role from AdminRolesService.deleteRole()", loginUserInfo); - auditLogService.setLogOldValue(auditLog,request.getRoleName()); - rolesRepository.deleteById(request.getRoleId()); - auditLogService.setLogNewValue(auditLog,""); - auditLogService.saveAuditLog(auditLog); - result.setSuccess(true); - result.setCode("200"); - result.setMessage("Created Successfully"); - return result; - } - - public BaseApiResponse modifyRole(AdminModifyRoleRequest request, LoginUserInfo loginUserInfo) { - BaseApiResponse result = new BaseApiResponse(); - AuditLog auditLog = auditLogService.createAuditLog("Modify Role", "Modify a Role from AdminRolesService.deleteRole()", loginUserInfo); - auditLogService.setLogOldValue(auditLog,request.getRoleName()); - Role oRole = rolesRepository.getRoleByRoleId((request.getRoleId())); - oRole.setRoleName(request.getRoleName()); - rolesRepository.save(oRole); - auditLogService.setLogNewValue(auditLog,""); - auditLogService.saveAuditLog(auditLog); - result.setSuccess(true); - result.setCode("200"); - result.setMessage("Modify Successfully"); - return result; - } - - public AdminGetRoleByIdResponse getRoleById(AdminGetRoleByIdRequest request) { - AdminGetRoleByIdResponse result = new AdminGetRoleByIdResponse(); - Role getRole = rolesRepository.findById(request.getRoleId()).orElse(null); - result.setData(getRole); - result.setSuccess(true); - result.setCode("200"); - result.setMessage("Get Role By Id Successfully"); - return result; - } - - - private Specification buildSpecificationByPageRequest(AdminGetRolesRequest pageRequest) { - - return ((root, criteriaQuery, cb) -> { - List list = new ArrayList<>(); - if (StringUtils.isNotBlank(pageRequest.getSearchRoleName())) { - list.add(cb.like(cb.upper(root.get("roleName")), - "%" + pageRequest.getSearchRoleName().toUpperCase() + "%")); - } - Predicate[] predicates = new Predicate[list.size()]; - criteriaQuery.where(list.toArray(predicates)); - return cb.and(list.toArray(predicates)); - }); - } - - public boolean updateGroupAssignUser(AdminRoleAssignUsersRequest request, LoginUserInfo loginUserInfo) { - Long roleId = request.getRoleId(); - List employeeIdList = request.getEmployeeIds(); - List dataToDelete =roleUsersRepository.findAllByRoleId(roleId); - AuditLog auditLog = auditLogService.createAuditLog("Update Group Assign Users", "Update Group Assign Users from AdminRolesService.updateGroupAssignUser()", loginUserInfo); - auditLogService.setLogOldValue(auditLog,dataToDelete); - if(employeeIdList!=null) { - List saveData = new ArrayList<>(); - for(int i=0; i0) { - roleUsersRepository.saveAll(saveData); - } - } - if(dataToDelete.size()>0) { - roleUsersRepository.deleteAll(dataToDelete); - } - auditLogService.setLogNewValue(auditLog,request); - auditLogService.saveAuditLog(auditLog); - return true; - } - -} diff --git a/src/main/java/com/sjmresorts/opsapp/admin/services/AdminUserService.java b/src/main/java/com/sjmresorts/opsapp/admin/services/AdminUserService.java deleted file mode 100644 index 3fa96f9..0000000 --- a/src/main/java/com/sjmresorts/opsapp/admin/services/AdminUserService.java +++ /dev/null @@ -1,180 +0,0 @@ -package com.sjmresorts.opsapp.admin.services; - -import com.sjmresorts.opsapp.admin.dtos.roleUsers.AdminGetRoleUsersRequest; -import com.sjmresorts.opsapp.admin.dtos.system.user.CmsAppUserPageRequest; -import com.sjmresorts.opsapp.admin.dtos.system.user.CmsUserDto; -import com.sjmresorts.opsapp.authentication.dtos.LoginUserInfo; -import com.sjmresorts.opsapp.authentication.mappers.UserMapper; -import com.sjmresorts.opsapp.authentication.models.RoleUsers; -import com.sjmresorts.opsapp.authentication.models.User; -import com.sjmresorts.opsapp.authentication.repositories.UserRepository; -import com.sjmresorts.opsapp.common.dtos.BaseApiResponse; -import com.sjmresorts.opsapp.common.dtos.DataTableResult; -import com.sjmresorts.opsapp.common.utils.DaoUtil; -import com.sjmresorts.opsapp.logging.models.AuditLog; -import com.sjmresorts.opsapp.logging.services.AuditLogService; -import jakarta.persistence.criteria.*; -import jakarta.validation.constraints.NotBlank; -import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.StringUtils; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.data.jpa.domain.Specification; -import org.springframework.stereotype.Service; - -import java.util.ArrayList; -import java.util.List; - -@Service -@RequiredArgsConstructor -public class AdminUserService { - - private final UserRepository userRepository; - private final AuditLogService auditLogService; - private final UserMapper userMapper; - - public DataTableResult getUserInfoListByPage(CmsAppUserPageRequest pageRequest) { - - Specification listSpec = buildSpecificationByPageRequest(pageRequest); - int pageSize = pageRequest.getPageSize(); - int pageStart = pageRequest.getStartRow() / pageSize; - Sort sort = DaoUtil.buildSortByString(pageRequest.getSortOrder(), "userId"); - Pageable pageable = PageRequest.of(pageStart, pageSize, sort); - Page resultPage = userRepository.findAll(listSpec, pageable); - List resultList = userMapper.toCmsUserDtoList(resultPage.getContent()); - DataTableResult dataResult = new DataTableResult<>(); - dataResult.setTotalRecords(resultPage.getTotalElements()); - dataResult.setPageSize(pageSize); - if (resultList == null) { - resultList = new ArrayList<>(); - } - dataResult.setRowData(resultList); - return dataResult; - } - - private Specification buildSpecificationByPageRequest(CmsAppUserPageRequest pageRequest) { - - return ((root, criteriaQuery, cb) -> { - List list = new ArrayList<>(); - if (StringUtils.isNotBlank(pageRequest.getSearchUserName())) { - list.add(cb.like(cb.upper(root.get("userName")), - "%" + pageRequest.getSearchUserName().toUpperCase() + "%")); - } - if (StringUtils.isNotBlank(pageRequest.getSearchAccount())) { - list.add(cb.like(cb.upper(root.get("employeeId")), - "%" + pageRequest.getSearchAccount().toUpperCase() + "%")); - } - Predicate[] predicates = new Predicate[list.size()]; - criteriaQuery.where(list.toArray(predicates)); - return cb.and(list.toArray(predicates)); - }); - } - - public List listUser(AdminGetRoleUsersRequest request, String searchType) { - Specification listSpec = buildSpecificationByRequest(request,searchType); - Sort sort = DaoUtil.buildSortByString(request.getSortOrder(), "employeeId"); - List resultList = userRepository.findAll(listSpec,sort); - return resultList; - } - - private Specification buildSpecificationByRequest(AdminGetRoleUsersRequest request, String searchType) { - Specification SysUserpec = new Specification() { - @Override - public Predicate toPredicate(Root root, CriteriaQuery criteriaQuery, CriteriaBuilder cb) { - List list = new ArrayList<>(); - if (StringUtils.equals(searchType, "USER_ROLE") && request.getSearchRoleId() != null) { - Subquery groupSubQuery = criteriaQuery.subquery(String.class); - Root subQueryRoot = groupSubQuery.from(RoleUsers.class); - groupSubQuery.select(subQueryRoot.get("employeeId")); - List subQueryPredicates = new ArrayList<>(); - subQueryPredicates.add(cb.equal(subQueryRoot.get("roleId"), request.getSearchRoleId())); - groupSubQuery.where(subQueryPredicates.toArray(new Predicate[] {})); - list.add(cb.in(root.get("employeeId")).value(groupSubQuery)); - } else if (StringUtils.equals(searchType, "UNASSIGN_USER")) { - Subquery groupSubQuery = criteriaQuery.subquery(Long.class); - Root subQueryRoot = groupSubQuery.from(RoleUsers.class); - groupSubQuery.select(cb.literal(1L)); - List subQueryPredicates = new ArrayList<>(); - subQueryPredicates.add(cb.equal(subQueryRoot.get("employeeId"), root.get("employeeId"))); - groupSubQuery.where(subQueryPredicates.toArray(new Predicate[] {})); - list.add(cb.not(cb.exists(groupSubQuery))); - } - Predicate[] predicates = new Predicate[list.size()]; - criteriaQuery.where(list.toArray(predicates)); - return cb.and(list.toArray(predicates)); - } - }; - return SysUserpec; - } - - - - public BaseApiResponse createUser(CmsUserDto userDto, LoginUserInfo loginUserInfo) { - BaseApiResponse result = new BaseApiResponse(); - AuditLog auditLog = auditLogService.createAuditLog("Create User", "Create a new User from AdminUserService.createUser()", loginUserInfo); - User user = userMapper.toUserEntity(userDto); - user.setCasinoCode(""); - auditLogService.setLogOldValue(auditLog, null); - userRepository.save(user); - auditLogService.setLogNewValue(auditLog, user); - auditLogService.saveAuditLog(auditLog); - result.setSuccess(true); - result.setCode("200"); - result.setMessage("Created Successfully"); - return result; - } - - public BaseApiResponse updateUser(CmsUserDto userDto, LoginUserInfo loginUserInfo) { - BaseApiResponse result = new BaseApiResponse(); - AuditLog auditLog = auditLogService.createAuditLog("Update User", "Update User from AdminUserService.updateUser()", loginUserInfo); - User user = userRepository.findById(userDto.getUserId()).orElse(null); - if (user != null) { - auditLogService.setLogOldValue(auditLog, user); - userMapper.updateUserFromCmsUserDto(userDto, user); - userRepository.save(user); - auditLogService.setLogNewValue(auditLog, user); - auditLogService.saveAuditLog(auditLog); - result.setSuccess(true); - result.setCode("200"); - result.setMessage("Updated Successfully"); - } else { - result.setSuccess(false); - result.setCode("400"); - result.setMessage("User does not exists"); - } - return result; - } - - public BaseApiResponse updateStatus(CmsUserDto userDto, LoginUserInfo loginUserInfo) { - BaseApiResponse result = new BaseApiResponse(); - AuditLog auditLog = auditLogService.createAuditLog("Update User Status", "Update User status from AdminUserService.updateStatus()", loginUserInfo); - User user = userRepository.findById(userDto.getUserId()).orElse(null); - if (user != null) { - auditLogService.setLogOldValue(auditLog, user); - user.setStatus(userDto.getStatus()); - userRepository.save(user); - auditLogService.setLogNewValue(auditLog, user); - auditLogService.saveAuditLog(auditLog); - result.setSuccess(true); - result.setCode("200"); - result.setMessage("Status Updated Successfully"); - } else { - result.setSuccess(false); - result.setCode("400"); - result.setMessage("User does not exists"); - } - return result; - } - - public CmsUserDto queryCmsUserById(@NotBlank long userId) { - CmsUserDto userDto = null; - User user = userRepository.findById(userId).orElse(null); - - if (user != null) { - userDto = userMapper.toCmsUserDto(user); - } - return userDto; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/authentication/controllers/MobileAuthorizationController.java b/src/main/java/com/sjmresorts/opsapp/authentication/controllers/MobileAuthorizationController.java deleted file mode 100644 index b73b420..0000000 --- a/src/main/java/com/sjmresorts/opsapp/authentication/controllers/MobileAuthorizationController.java +++ /dev/null @@ -1,147 +0,0 @@ -package com.sjmresorts.opsapp.authentication.controllers; - -import com.sjmresorts.opsapp.authentication.dtos.*; -import com.sjmresorts.opsapp.authentication.services.AdAuthorizeService; -import com.sjmresorts.opsapp.authentication.services.LoginService; -import com.sjmresorts.opsapp.common.constants.AppConstants; -import com.sjmresorts.opsapp.common.dtos.BaseApiResponse; -import com.sjmresorts.opsapp.common.utils.JwtTokenUtil; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.annotation.Resource; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import jakarta.validation.Valid; -import org.apache.commons.lang3.StringUtils; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -@Tag(name = "Authorization", description = "APIs relate with ad login authorization") -@RestController -@RequestMapping("/api/v1/authorization") -@CrossOrigin(origins = "*") -public class MobileAuthorizationController { - @Resource - private LoginService loginService; - @Resource - private AdAuthorizeService adAuthService; - - @Operation( - summary = "appLogout", - description = "API for app logout process" - ) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "App logout successful") - }) - @PostMapping(value="/logout",produces = "application/json;charset=UTF-8") - public ResponseEntity deviceLogin( - HttpServletRequest request) { - - AppLogoutResponse result = new AppLogoutResponse(); - LoginUserInfo loginUser = JwtTokenUtil.parseUserFromRequest(request); - if(loginUser!=null){ - loginService.processLogout(loginUser); - result.setSuccess(true); - result.setCode("200"); - result.setMessage("Logout successful."); - return ResponseEntity.status(HttpStatus.OK).body(result); - }else{ - result.setSuccess(false); - result.setCode("401"); - result.setMessage("Unauthorized logout"); - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(result); - } - } - - - @Operation( - summary = "Register Pits info for tracking", - description = "API for location tracking registration" - ) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "Register Pits successful"), - @ApiResponse(responseCode = "400", description = "Invalid request data") - }) - @PostMapping(value="/register_track_pits",produces = "application/json;charset=UTF-8") - public ResponseEntity registerTrackScope( - HttpServletRequest request, - @Parameter(description = "Register request for pits tracking", required = true) @Valid @RequestBody RegisterTrackInfoRequest requestData) { - RegisterTrackInfoResponse responseData = loginService.registerTrackPits(requestData.getData()); - return ResponseEntity.status(HttpStatus.OK).body(responseData); - } - - @Operation( - summary = "Third Party Authorization", - description = "API to retrieve access token for third party caller, such as Service Button" - ) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "Access Token generated successful"), - @ApiResponse(responseCode = "401", description = "Authorization key not math") - }) - @PostMapping(value="/getAccessToken",produces = "application/json;charset=UTF-8") - public ResponseEntity getApiAccessToken( - HttpServletRequest request, - @Parameter(description = "Third party requestor info", required = true) @Valid @RequestBody GetAuthTokenRequest requestData) { - String authorizationKey = request.getHeader("Authorization"); - if(authorizationKey!=null && authorizationKey.equals(JwtTokenUtil.getThirdPartySecretKey())){ - GetAuthTokenResponse responseData = loginService.assignAccessToken(AppConstants.THIRD_PARTY_TOKEN_TYPE,requestData.getData()); - return ResponseEntity.status(HttpStatus.OK).body(responseData); - }else{ - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(null); - } - } - - @Operation( - summary = "Renew API token by non-expired token", - description = "API to renew access token" - ) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "Access Token refreshed successful") - }) - @PostMapping(value="/refreshToken",produces = "application/json;charset=UTF-8") - public ResponseEntity refreshToken( - HttpServletRequest request, - HttpServletResponse response) { - RefreshTokenResponse responseData = new RefreshTokenResponse(); - LoginUserInfo loginUser = JwtTokenUtil.parseUserFromRequest(request); - boolean isRenewRefreshToken = (!"false".equalsIgnoreCase(request.getParameter("isRenewRefreshToken"))); - if(loginUser!=null){ - responseData = loginService.refreshToken(loginUser,isRenewRefreshToken); - return ResponseEntity.status(HttpStatus.OK).body(responseData); - }else{ - responseData.setSuccess(false); - responseData.setCode("401"); - responseData.setMessage("Unauthorized operation"); - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(responseData); - } - } - - @Operation( - summary = "Heart-beat API", - description = "API to register last heart-beat time for login user" - ) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "Access Token refreshed successful") - }) - @GetMapping(value="/appHeartBeat") - public ResponseEntity sendHeartBeat( - HttpServletRequest request, - HttpServletResponse response) { - LoginUserInfo loginUser = JwtTokenUtil.parseUserFromRequest(request); - BaseApiResponse responseData = new BaseApiResponse(); - if(loginUser!=null){ - boolean registerSuccess = loginService.registerHeartBeat(loginUser); - responseData.setSuccess(registerSuccess); - return ResponseEntity.status(HttpStatus.OK).body(responseData); - }else{ - responseData.setSuccess(false); - responseData.setCode("401"); - responseData.setMessage("Unauthorized operation"); - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(responseData); - } - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/authentication/mappers/RoleMapper.java b/src/main/java/com/sjmresorts/opsapp/authentication/mappers/RoleMapper.java deleted file mode 100644 index cf714fb..0000000 --- a/src/main/java/com/sjmresorts/opsapp/authentication/mappers/RoleMapper.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.sjmresorts.opsapp.authentication.mappers; - -import com.sjmresorts.opsapp.admin.dtos.roles.AdminRoleDto; -import com.sjmresorts.opsapp.authentication.models.Role; -import org.mapstruct.Mapper; -import org.mapstruct.Mapping; - -import java.util.List; - -@Mapper(componentModel = "spring") -public interface RoleMapper { - @Mapping(source = "roleId", target = "roleName") - List toAdminRoleDto(List roles); -} diff --git a/src/main/java/com/sjmresorts/opsapp/authentication/mappers/UserMapper.java b/src/main/java/com/sjmresorts/opsapp/authentication/mappers/UserMapper.java deleted file mode 100644 index 7bfdb5a..0000000 --- a/src/main/java/com/sjmresorts/opsapp/authentication/mappers/UserMapper.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.sjmresorts.opsapp.authentication.mappers; - -import com.sjmresorts.opsapp.admin.dtos.system.user.CmsUserDto; -import com.sjmresorts.opsapp.authentication.dtos.LoginUserInfo; -import com.sjmresorts.opsapp.authentication.models.User; -import org.mapstruct.Mapper; -import org.mapstruct.Mapping; -import org.mapstruct.MappingTarget; - -import java.util.List; - -@Mapper(componentModel = "spring") -public interface UserMapper { - - @Mapping(source = "ldapGroup", target = "adGroup") - @Mapping(source = "employeeId", target = "userCode") - LoginUserInfo toLoginUser(User user); - - @Mapping(source = "employeeId", target = "userCode") - CmsUserDto toCmsUserDto(User user); - - @Mapping(source = "employeeId", target = "userCode") - List toCmsUserDtoList(List users); - - @Mapping(source = "userCode", target = "employeeId") - User toUserEntity(CmsUserDto userDto); - - @Mapping(target = "userId", ignore = true) - @Mapping(source = "userCode", target = "employeeId") - void updateUserFromCmsUserDto(CmsUserDto dto, @MappingTarget User entity); -} diff --git a/src/main/java/com/sjmresorts/opsapp/authentication/services/LoginService.java b/src/main/java/com/sjmresorts/opsapp/authentication/services/LoginService.java deleted file mode 100644 index 6cdcbbd..0000000 --- a/src/main/java/com/sjmresorts/opsapp/authentication/services/LoginService.java +++ /dev/null @@ -1,174 +0,0 @@ -package com.sjmresorts.opsapp.authentication.services; - -import com.sjmresorts.opsapp.admin.repositories.RolesRepository; -import com.sjmresorts.opsapp.authentication.dtos.*; -import com.sjmresorts.opsapp.authentication.mappers.UserMapper; -import com.sjmresorts.opsapp.authentication.models.ClientInfo; -import com.sjmresorts.opsapp.authentication.models.RouterPermission; -import com.sjmresorts.opsapp.authentication.models.User; -import com.sjmresorts.opsapp.authentication.models.UserToken; -import com.sjmresorts.opsapp.authentication.repositories.DeviceInfoRepository; -import com.sjmresorts.opsapp.authentication.repositories.RouterPermissionRepository; -import com.sjmresorts.opsapp.authentication.repositories.SysRouterRepository; -import com.sjmresorts.opsapp.authentication.repositories.UserRepository; -import com.sjmresorts.opsapp.common.constants.AppConstants; -import com.sjmresorts.opsapp.common.utils.JwtTokenUtil; -import jakarta.transaction.Transactional; -import jakarta.validation.Valid; -import jakarta.validation.constraints.NotNull; -import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.StringUtils; -import org.springframework.dao.OptimisticLockingFailureException; -import org.springframework.retry.annotation.Backoff; -import org.springframework.retry.annotation.Retryable; -import org.springframework.data.domain.PageRequest; -import org.springframework.stereotype.Service; - -import java.time.Instant; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -@Service -@RequiredArgsConstructor -public class LoginService { - - private final UserRepository userRepository; - private final DeviceInfoRepository deviceInfoRepository; - private final UserTokenService userTokenService; - private final RolesRepository rolesRepository; - private final RouterPermissionRepository routerPermissionRepository; - private final SysRouterRepository sysRouterRepository; - - @Retryable(retryFor = OptimisticLockingFailureException.class, - maxAttempts = 3, - backoff = @Backoff(delay = 100)) - @Transactional - public RegisterTrackInfoResponse registerTrackPits(@Valid RegisterTrackInfoData data) { - RegisterTrackInfoResponse result = new RegisterTrackInfoResponse(); - User user = userRepository.findByEmployeeId(data.getEmployeeId()).orElse(null); - if(user==null) { - result.setSuccess(false); - result.setCode("USER_NOT_LOGIN"); - result.setMessage("Current user has not login"); - }else{ - String pits = ""; - if(data.getPitCodes()!=null) { - List selectPitCodes=data.getPitCodes(); - for(String pitCode:selectPitCodes) { - pits +="["+pitCode+"]"; - } - } - user.setPitCodes(pits); - user.setLastHeartbeat(Instant.now()); - userRepository.save(user); - result.setSuccess(true); - result.setMessage("Pits registered successfully"); - } - return result; - } - - private void clearLastLoginInfo(String employeeId){ - userTokenService.clearUserTokenByType(AppConstants.APP_API_ACCESS_TOKEN_TYPE,employeeId); - userTokenService.clearUserTokenByType(AppConstants.APP_REFRESH_TOKEN_TYPE,employeeId); - deviceInfoRepository.clearLogoutDevice(employeeId,Instant.now()); - userRepository.clearAppLoginInfoByUser(employeeId); - } - public GetAuthTokenResponse assignAccessToken(String requestSource, @NotNull(message = "Data attribute must not be null") GetAuthTokenData data) { - GetAuthTokenResponse result = new GetAuthTokenResponse(); - LoginUserInfo thirdPartyUser = new LoginUserInfo(); - thirdPartyUser.setEmployeeId(data.getRequesterId()); - thirdPartyUser.setUserCode(data.getRequesterId()); - thirdPartyUser.setUserName(data.getRequesterId()); - thirdPartyUser.setLoginSource(requestSource); - UserToken accessToken = JwtTokenUtil.generateTokenByLoginUser(AppConstants.THIRD_PARTY_TOKEN_TYPE, thirdPartyUser); - userTokenService.saveUserToken(accessToken); - result.setAccessToken(accessToken.getTokenValue()); - result.setCode("200"); - result.setSuccess(true); - return result; - } - - @Retryable(retryFor = OptimisticLockingFailureException.class, - maxAttempts = 3, - backoff = @Backoff(delay = 100)) - @Transactional - public void processLogout(LoginUserInfo loginUser) { - if(loginUser!=null){ - clearLastLoginInfo(loginUser.getEmployeeId()); - } - } - - public RefreshTokenResponse refreshToken(LoginUserInfo loginUser, boolean isRenewRefreshToken) { - RefreshTokenResponse result = new RefreshTokenResponse(); - UserToken accessToken = null; - UserToken refreshToken = null; - if(loginUser!=null){ - accessToken=JwtTokenUtil.generateTokenByLoginUser(AppConstants.APP_API_ACCESS_TOKEN_TYPE,loginUser); - userTokenService.saveUserToken(accessToken); - result.setAccessToken(accessToken.getTokenValue()); - if(isRenewRefreshToken){ - refreshToken=JwtTokenUtil.generateTokenByLoginUser(AppConstants.APP_REFRESH_TOKEN_TYPE,loginUser); - userTokenService.saveUserToken(refreshToken); - result.setRefreshToken(refreshToken.getTokenValue()); - if(StringUtils.isNotBlank(loginUser.getTokenUuid())){ - userTokenService.removeTokenByUniqueId(loginUser.getTokenUuid()); - } - } - } - - if(accessToken!=null){ - result.setSuccess(true); - result.setCode("200"); - result.setMessage("Token renew successful."); - }else{ - result.setSuccess(false); - result.setCode("200"); - result.setMessage("Token failed."); - } - return result; - } - - public boolean registerHeartBeat(LoginUserInfo loginUser) { - boolean result = false; - if(loginUser!=null && StringUtils.isNotBlank(loginUser.getEmployeeId())){ - int updateCnt = userRepository.registerHeartBeat(loginUser.getEmployeeId(), Instant.now()); - result = updateCnt>0; - } - return result; - } - - public boolean logoutFirstIdleSessionByTime(Instant checkExpiryTime) { - boolean hasRecord =false; - List idleUsers = userRepository.findExpiredSessionEmployeeIds( - checkExpiryTime, PageRequest.of(0, 1)); - String idleUserCode = idleUsers == null || idleUsers.isEmpty() ? null : idleUsers.get(0); - if(StringUtils.isNotBlank(idleUserCode)) { - hasRecord=true; - userRepository.clearAppLoginInfoByUser(idleUserCode); - deviceInfoRepository.clearLogoutDevice(idleUserCode,Instant.now()); - } - return hasRecord; - } - - private Set getUserPermissionGroup(List groupName) { - Set permissionGroup = new HashSet<>(); - if(groupName!=null&&!groupName.isEmpty()){ - for(String roleName : groupName){ - List roleIds = rolesRepository.queryRoleIdByRoleName(roleName); - List appRouterIds = sysRouterRepository.queryIdsByHeading(AppConstants.APP_ROUTER_NAME); - if(!roleIds.isEmpty() && !appRouterIds.isEmpty()){ - List permissions = routerPermissionRepository.queryPermissionByRoleAndRouterIds(roleIds,appRouterIds); - if(!permissions.isEmpty()){ - Set names = permissions.stream() - .map(RouterPermission::getPermissionName) - .collect(Collectors.toSet()); - permissionGroup.addAll(names); - } - } - } - } - return permissionGroup; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/common/config/OpsAppDbConfig.java b/src/main/java/com/sjmresorts/opsapp/common/config/OpsAppDbConfig.java index 0105adf..7ae60e0 100644 --- a/src/main/java/com/sjmresorts/opsapp/common/config/OpsAppDbConfig.java +++ b/src/main/java/com/sjmresorts/opsapp/common/config/OpsAppDbConfig.java @@ -55,7 +55,6 @@ public class OpsAppDbConfig { @Primary @Bean(name = "opsAppEntityManagerFactory") - @org.springframework.context.annotation.DependsOn({"esignCaptureSchemaMigrator", "esignCmsSchemaMigrator"}) public LocalContainerEntityManagerFactoryBean opsAppEntityManagerFactory( EntityManagerFactoryBuilder builder, DbDialect dbDialect) { diff --git a/src/main/java/com/sjmresorts/opsapp/esign/audit/controllers/EsignAuditLogController.java b/src/main/java/com/sjmresorts/opsapp/esign/audit/controllers/EsignAuditLogController.java deleted file mode 100644 index 09c01d6..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/audit/controllers/EsignAuditLogController.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.sjmresorts.opsapp.esign.audit.controllers; - -import com.sjmresorts.opsapp.esign.audit.dtos.EsignAuditLogResponse; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignClientTransportLogRequest; -import com.sjmresorts.opsapp.esign.common.dtos.EsignSimpleResponse; -import com.sjmresorts.opsapp.esign.audit.services.EsignAuditLogService; -import com.sjmresorts.opsapp.esign.auth.services.EsignUserPresenceService; -import com.sjmresorts.opsapp.common.utils.NetworkUtil; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.annotation.Resource; -import jakarta.servlet.http.HttpServletRequest; -import org.apache.commons.lang3.StringUtils; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -@Tag(name = "Esign Audit Logs") -@RestController -@RequestMapping({"/api/v1/esign", "/api/v1/sinosecu"}) -public class EsignAuditLogController { - - @Resource(name = "esignAuditLogService") - private EsignAuditLogService auditLogService; - - @Resource - private EsignUserPresenceService userPresenceService; - - @Operation(summary = "審計日誌", description = "免 JWT。category=OPERATION|THIRD_PARTY|CONNECTION") - @GetMapping(value = "/audit-logs", produces = "application/json;charset=UTF-8") - public ResponseEntity listAuditLogs( - @RequestParam(name = "category", defaultValue = "OPERATION") String category, - @RequestParam(name = "eventType", required = false) String eventType, - @RequestParam(name = "page", defaultValue = "0") int page, - @RequestParam(name = "size", defaultValue = "50") int size) { - return ResponseEntity.ok(auditLogService.listLogs(category, page, size, eventType)); - } - - @Operation(summary = "App 上報連接/輪詢事件", description = "免 JWT。WebSocket 斷開、輪詢失敗等。") - @PostMapping(value = "/client-transport-log", consumes = "application/json;charset=UTF-8", produces = "application/json;charset=UTF-8") - public ResponseEntity clientTransportLog( - @RequestBody EsignClientTransportLogRequest request, - HttpServletRequest httpRequest) { - if (request == null || StringUtils.isBlank(request.getEventType())) { - return ResponseEntity.badRequest() - .body(EsignSimpleResponse.fail("400", "eventType required")); - } - String clientIp = EsignUserPresenceService.normalizeClientIp(NetworkUtil.getClientIpByRequest(httpRequest)); - Long userId = request.getUserId(); - String eventType = request.getEventType().trim().toUpperCase(); - String message = StringUtils.defaultIfBlank(request.getMessage(), eventType); - auditLogService.appendConnection( - eventType, - userId, - message, - request.getTransport(), - clientIp, - null); - return ResponseEntity.ok(EsignSimpleResponse.ok("logged")); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/auth/controllers/AppDeepLinkController.java b/src/main/java/com/sjmresorts/opsapp/esign/auth/controllers/AppDeepLinkController.java deleted file mode 100644 index 42d8781..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/auth/controllers/AppDeepLinkController.java +++ /dev/null @@ -1,177 +0,0 @@ -package com.sjmresorts.opsapp.esign.auth.controllers; - -import com.sjmresorts.opsapp.esign.printer.dtos.LocalExeLaunchResult; -import com.sjmresorts.opsapp.esign.printer.services.LocalExeLaunchService; -import com.sjmresorts.opsapp.esign.reservation.support.OperaFlexLinkPageBuilder; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.tags.Tag; -import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.CrossOrigin; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import java.net.URI; -import java.util.Map; - -@Tag(name = "App Deep Link", description = "唤起桌面 App / exe(免 JWT)") -@RestController -@RequestMapping({"/api/v1/esign/app", "/api/v1/sinosecu/app"}) -@CrossOrigin(origins = "*") -@RequiredArgsConstructor -public class AppDeepLinkController { - - private final LocalExeLaunchService localExeLaunchService; - - @Value("${ops-app.deeplink.url:myapp://open}") - private String deepLinkUrl; - - @Value("${ops-app.deeplink.opera.close-popup:true}") - private boolean operaClosePopup; - - @Value("${ops-app.deeplink.opera.parent-origin:}") - private String operaParentOrigin; - - @Value("${ops-app.deeplink.opera.ext-app-id:0}") - private long operaExtAppId; - - @Value("${ops-app.deeplink.opera.close-delay-ms:100}") - private int operaCloseDelayMs; - - @Value("${ops-app.deeplink.opera.open-delay-ms:1200}") - private int operaOpenDelayMs; - - @Value("${ops-app.deeplink.opera.debug-page:true}") - private boolean operaDebugPage; - - @Value("${ops-app.deeplink.opera.popup-width:100%}") - private String operaPopupWidth; - - @Value("${ops-app.deeplink.opera.popup-height:800px}") - private String operaPopupHeight; - - @Value("${ops-app.deeplink.opera.launch-mode:new-window}") - private String operaLaunchMode; - - @Value("${ops-app.deeplink.opera.close-modal-message:true}") - private boolean operaCloseModalMessage; - - @Value("${ops-app.deeplink.opera.flex-link-origin:http://127.0.0.1:8066}") - private String operaFlexLinkOrigin; - - @Operation( - summary = "HTML 页唤起 exe", - description = "免 JWT。Opera iframe 内只发 CLOSE_SHELL 信号;外层代理脚本负责销毁 Pop-up。独立标签走 myapp:// + window.close()。" - ) - @ApiResponse(responseCode = "200", description = "HTML 页面") - @GetMapping(value = "/launch", produces = "text/html;charset=UTF-8") - public ResponseEntity launchApp() { - return htmlPage(); - } - - @Operation(summary = "本机启动 exe(JSON,便于调试)") - @GetMapping("/launch/trigger") - public ResponseEntity> launchTrigger() { - LocalExeLaunchResult result = localExeLaunchService.launchIfEnabled(); - return ResponseEntity.ok(Map.of( - "launched", result.isLaunched(), - "skipped", result.isSkipped(), - "message", result.getMessage())); - } - - @Operation(summary = "302 重定向到自定义协议") - @GetMapping("/redirect") - public ResponseEntity redirectApp() { - return ResponseEntity.status(HttpStatus.FOUND) - .location(URI.create(deepLinkUrl)) - .header(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate") - .build(); - } - - @Operation(summary = "HTML 页(同 /launch)") - @GetMapping(value = "/open", produces = "text/html;charset=UTF-8") - public ResponseEntity openAppPage() { - return htmlPage(); - } - - @Operation( - summary = "Opera 外层代理脚本(Tampermonkey / Console 注入)", - description = "安装在 Opera 主页面。内层 /launch iframe 发 CLOSE_SHELL,此外层全权销毁 Pop-up 外壳。" - ) - @GetMapping(value = "/opera-parent-close-listener.js", produces = "application/javascript;charset=UTF-8") - public ResponseEntity operaParentCloseListener() { - String script = OperaFlexLinkPageBuilder.buildOperaParentCloseListenerScript(operaFlexLinkOrigin); - return ResponseEntity.ok() - .contentType(MediaType.parseMediaType("application/javascript;charset=UTF-8")) - .header(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate") - .body(script); - } - - @Operation(summary = "Tampermonkey 脚本(自动在 Opera 域注入 closeModal 监听器)") - @GetMapping(value = "/opera-parent-close-listener.user.js", produces = "text/plain;charset=UTF-8") - public ResponseEntity operaParentCloseListenerUserScript() { - String script = OperaFlexLinkPageBuilder.buildOperaParentCloseListenerUserScript(operaFlexLinkOrigin); - return ResponseEntity.ok() - .contentType(MediaType.parseMediaType("text/plain;charset=UTF-8")) - .header(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate") - .body(script); - } - - @Operation(summary = "外层代理安装说明页") - @GetMapping(value = "/opera-proxy-install", produces = "text/html;charset=UTF-8") - public ResponseEntity operaProxyInstallPage() { - String html = OperaFlexLinkPageBuilder.buildOperaProxyInstallPage(operaFlexLinkOrigin); - return ResponseEntity.ok() - .contentType(MediaType.parseMediaType("text/html;charset=UTF-8")) - .header(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate") - .body(html); - } - - @Operation(summary = "JavaScript 片段") - @GetMapping(value = "/open.js", produces = "application/javascript;charset=UTF-8") - public ResponseEntity openAppScript() { - String script = "window.location.href = \"" + escapeForJsString(deepLinkUrl) + "\";"; - return ResponseEntity.ok() - .contentType(MediaType.parseMediaType("application/javascript;charset=UTF-8")) - .header(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate") - .body(script); - } - - private ResponseEntity htmlPage() { - LocalExeLaunchResult launchResult = localExeLaunchService.launchIfEnabled(); - boolean useBrowserDeepLink = !launchResult.isLaunched(); - int openDelay = launchResult.isLaunched() ? 200 : operaOpenDelayMs; - - String html = OperaFlexLinkPageBuilder.buildLaunchPage( - deepLinkUrl, - operaLaunchMode, - useBrowserDeepLink, - launchResult.getMessage(), - operaClosePopup, - operaCloseModalMessage, - operaParentOrigin, - operaFlexLinkOrigin, - operaExtAppId, - openDelay, - operaCloseDelayMs, - operaDebugPage, - operaPopupWidth, - operaPopupHeight); - return ResponseEntity.ok() - .contentType(MediaType.parseMediaType("text/html;charset=UTF-8")) - .header(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate") - .body(html); - } - - private static String escapeForJsString(String value) { - return String.valueOf(value) - .replace("\\", "\\\\") - .replace("\"", "\\\""); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/auth/controllers/EsignAppUserController.java b/src/main/java/com/sjmresorts/opsapp/esign/auth/controllers/EsignAppUserController.java deleted file mode 100644 index ce80d77..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/auth/controllers/EsignAppUserController.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.sjmresorts.opsapp.esign.auth.controllers; - -import com.sjmresorts.opsapp.esign.auth.dtos.EsignAppUserResponse; -import com.sjmresorts.opsapp.esign.auth.dtos.EsignAppUserSaveRequest; -import com.sjmresorts.opsapp.esign.auth.dtos.EsignAppUserStatusRequest; -import com.sjmresorts.opsapp.esign.auth.services.EsignAppUserService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.CrossOrigin; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@Tag(name = "Esign App Users", description = "CMS 管理平板 App 登录账号(esign_user)") -@RestController -@RequestMapping({"/api/v1/esign", "/api/v1/sinosecu"}) -@CrossOrigin(origins = "*") -@RequiredArgsConstructor -public class EsignAppUserController { - - private final EsignAppUserService appUserService; - - @Operation(summary = "CMS 平板账号列表", description = "免 JWT。") - @GetMapping(value = "/app-users", produces = "application/json;charset=UTF-8") - public ResponseEntity listAppUsers() { - return ResponseEntity.ok(appUserService.listAll()); - } - - @Operation(summary = "CMS 创建/更新平板账号", description = "免 JWT。密码明文存储,与 App 登录一致。") - @PostMapping(value = "/app-users/save", produces = "application/json;charset=UTF-8") - public ResponseEntity saveAppUser(@RequestBody EsignAppUserSaveRequest body) { - return ResponseEntity.ok(appUserService.save(body)); - } - - @Operation(summary = "CMS 更新平板账号状态", description = "免 JWT。ACTIVE / INACTIVE / LOCKED。") - @PostMapping(value = "/app-users/status", produces = "application/json;charset=UTF-8") - public ResponseEntity updateAppUserStatus( - @RequestBody EsignAppUserStatusRequest body) { - return ResponseEntity.ok(appUserService.updateStatus(body)); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/auth/controllers/EsignHotelController.java b/src/main/java/com/sjmresorts/opsapp/esign/auth/controllers/EsignHotelController.java deleted file mode 100644 index fb9c911..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/auth/controllers/EsignHotelController.java +++ /dev/null @@ -1,115 +0,0 @@ -package com.sjmresorts.opsapp.esign.auth.controllers; - -import com.sjmresorts.opsapp.esign.auth.dtos.EsignHotelListResponse; -import com.sjmresorts.opsapp.esign.auth.dtos.EsignHotelLogoRequest; -import com.sjmresorts.opsapp.esign.auth.dtos.EsignHotelMutationResponse; -import com.sjmresorts.opsapp.esign.auth.dtos.EsignHotelUpsertRequest; -import com.sjmresorts.opsapp.esign.common.dtos.EsignSimpleResponse; -import com.sjmresorts.opsapp.esign.auth.services.EsignHotelService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.annotation.Resource; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.CrossOrigin; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -@Tag(name = "Esign Hotel", description = "旅宿 Hotel 主数据(Electron Hotel 页)") -@RestController -@RequestMapping({"/api/v1/esign/hotels", "/api/v1/sinosecu/hotels"}) -@CrossOrigin(origins = "*") -public class EsignHotelController { - - @Resource - private EsignHotelService esignHotelService; - - @Operation(summary = "Hotel 列表与默认选中", description = "免 JWT。首次访问自动种子化默认酒店。") - @GetMapping(produces = "application/json;charset=UTF-8") - public ResponseEntity listHotels( - @RequestParam(value = "includeLogos", defaultValue = "false") boolean includeLogos) { - return ResponseEntity.ok(esignHotelService.listHotels(includeLogos)); - } - - @Operation(summary = "新增酒店", description = "免 JWT。id 为 slug(小写字母、数字、连字符)。") - @PostMapping(consumes = "application/json;charset=UTF-8", produces = "application/json;charset=UTF-8") - public ResponseEntity createHotel(@RequestBody EsignHotelUpsertRequest request) { - EsignHotelMutationResponse body = esignHotelService.createHotel(request); - return mutationResponse(body); - } - - @Operation(summary = "更新酒店文字信息", description = "免 JWT。") - @PutMapping( - value = "/{hotelId}", - consumes = "application/json;charset=UTF-8", - produces = "application/json;charset=UTF-8") - public ResponseEntity updateHotel( - @PathVariable("hotelId") String hotelId, @RequestBody EsignHotelUpsertRequest request) { - EsignHotelMutationResponse body = esignHotelService.updateHotel(hotelId, request); - return mutationResponse(body); - } - - @Operation(summary = "删除酒店(软删除)", description = "免 JWT。至少保留一家启用酒店。") - @DeleteMapping(value = "/{hotelId}", produces = "application/json;charset=UTF-8") - public ResponseEntity deleteHotel(@PathVariable("hotelId") String hotelId) { - EsignSimpleResponse body = esignHotelService.deleteHotel(hotelId); - if (!body.isSuccess() && "404".equals(body.getCode())) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(body); - } - if (!body.isSuccess()) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(body); - } - return ResponseEntity.ok(body); - } - - @Operation(summary = "设置默认酒店", description = "免 JWT。") - @PutMapping(value = "/default/{hotelId}", produces = "application/json;charset=UTF-8") - public ResponseEntity setDefault(@PathVariable("hotelId") String hotelId) { - EsignSimpleResponse body = esignHotelService.setDefaultHotel(hotelId); - if (!body.isSuccess() && "404".equals(body.getCode())) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(body); - } - if (!body.isSuccess()) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(body); - } - return ResponseEntity.ok(body); - } - - @Operation(summary = "上传或清除 Hotel Logo", description = "免 JWT。body.logoData 为 data:image/* 或空。") - @PutMapping( - value = "/{hotelId}/logo", - consumes = "application/json;charset=UTF-8", - produces = "application/json;charset=UTF-8") - public ResponseEntity updateLogo( - @PathVariable("hotelId") String hotelId, @RequestBody(required = false) EsignHotelLogoRequest request) { - String logoData = request != null ? request.getLogoData() : null; - EsignSimpleResponse body = esignHotelService.updateLogo(hotelId, logoData); - if (!body.isSuccess() && "404".equals(body.getCode())) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(body); - } - if (!body.isSuccess()) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(body); - } - return ResponseEntity.ok(body); - } - - private static ResponseEntity mutationResponse(EsignHotelMutationResponse body) { - if (!body.isSuccess() && "404".equals(body.getCode())) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(body); - } - if (!body.isSuccess() && "409".equals(body.getCode())) { - return ResponseEntity.status(HttpStatus.CONFLICT).body(body); - } - if (!body.isSuccess()) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(body); - } - return ResponseEntity.ok(body); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/auth/controllers/EsignLoginController.java b/src/main/java/com/sjmresorts/opsapp/esign/auth/controllers/EsignLoginController.java deleted file mode 100644 index d134d06..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/auth/controllers/EsignLoginController.java +++ /dev/null @@ -1,92 +0,0 @@ -// src/main/java/com/sjmresorts/opsapp/logging/controllers/EsignLoginController.java -package com.sjmresorts.opsapp.esign.auth.controllers; - -import com.sjmresorts.opsapp.authentication.dtos.LoginUserInfo; -import com.sjmresorts.opsapp.common.utils.JwtTokenUtil; -import com.sjmresorts.opsapp.esign.auth.dtos.EsignBindDeviceTokenRequest; -import com.sjmresorts.opsapp.esign.auth.dtos.EsignLoginRequest; -import com.sjmresorts.opsapp.esign.auth.dtos.EsignLoginResponse; -import com.sjmresorts.opsapp.esign.common.dtos.EsignSimpleResponse; -import com.sjmresorts.opsapp.esign.auth.services.EsignLoginService; -import jakarta.servlet.http.HttpServletRequest; -import org.apache.commons.lang3.StringUtils; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -@Tag(name = "Esign Authentication", description = "APIs for Esign user authentication") -@RestController -@RequestMapping({"/api/v1/esign/auth", "/api/v1/sinosecu/auth"}) -@CrossOrigin(origins = "*") -@RequiredArgsConstructor -public class EsignLoginController { - - private final EsignLoginService loginService; - - @Operation( - summary = "Esign User Login", - description = "API for Esign user login with username and password" - ) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "Login successful"), - @ApiResponse(responseCode = "401", description = "Invalid username or password"), - @ApiResponse(responseCode = "403", description = "User is inactive or locked") - }) - @PostMapping(value = "/login", produces = "application/json;charset=UTF-8") - public ResponseEntity login( - HttpServletRequest httpRequest, - @Parameter(description = "Login request info", required = true) - @Valid @RequestBody EsignLoginRequest request) { - - EsignLoginResponse response = loginService.login( - request, com.sjmresorts.opsapp.common.utils.NetworkUtil.getClientIpByRequest(httpRequest)); - - if (response.isSuccess()) { - return ResponseEntity.status(HttpStatus.OK).body(response); - } else if ("401".equals(response.getCode())) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response); - } else if ("403".equals(response.getCode())) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(response); - } else { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); - } - } - - @Operation( - summary = "Bind device token for push notification", - description = "Save FCM/APNs token for current Esign user. Requires Bearer accessToken from login." - ) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "Token bound"), - @ApiResponse(responseCode = "401", description = "Unauthorized") - }) - @PostMapping(value = "/bind-device-token", produces = "application/json;charset=UTF-8") - public ResponseEntity bindDeviceToken( - HttpServletRequest request, - @Valid @RequestBody EsignBindDeviceTokenRequest body) { - LoginUserInfo loginUser = JwtTokenUtil.parseUserFromRequest(request); - Long userId = null; - if (loginUser != null && StringUtils.isNotBlank(loginUser.getEmployeeId())) { - try { - userId = Long.parseLong(loginUser.getEmployeeId()); - } catch (NumberFormatException ignored) { - userId = null; - } - } - EsignSimpleResponse response = loginService.bindDeviceToken(userId, body); - HttpStatus status = response.isSuccess() ? HttpStatus.OK : HttpStatus.UNAUTHORIZED; - if ("404".equals(response.getCode())) { - status = HttpStatus.NOT_FOUND; - } else if ("403".equals(response.getCode())) { - status = HttpStatus.FORBIDDEN; - } - return ResponseEntity.status(status).body(response); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/auth/dtos/EsignAppUserResponse.java b/src/main/java/com/sjmresorts/opsapp/esign/auth/dtos/EsignAppUserResponse.java deleted file mode 100644 index f5cd0e8..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/auth/dtos/EsignAppUserResponse.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.sjmresorts.opsapp.esign.auth.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 EsignAppUserResponse { - - private boolean success = true; - private String code = "200"; - private String message; - private EsignAppUserItem data; - private List items; - - @Data - @NoArgsConstructor - public static class EsignAppUserItem { - private Long id; - private String userName; - private String role; - private String hotelId; - private String status; - private String createdAt; - private String updatedAt; - private String lastLoginAt; - private String lastLoginIp; - private boolean hasDeviceToken; - } - - public static EsignAppUserResponse ok(String message) { - EsignAppUserResponse r = new EsignAppUserResponse(); - r.setMessage(message); - return r; - } - - public static EsignAppUserResponse fail(String code, String message) { - EsignAppUserResponse r = new EsignAppUserResponse(); - r.setSuccess(false); - r.setCode(code); - r.setMessage(message); - return r; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/auth/dtos/EsignAppUserSaveRequest.java b/src/main/java/com/sjmresorts/opsapp/esign/auth/dtos/EsignAppUserSaveRequest.java deleted file mode 100644 index 72dc345..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/auth/dtos/EsignAppUserSaveRequest.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.sjmresorts.opsapp.esign.auth.dtos; - -import com.fasterxml.jackson.annotation.JsonInclude; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@JsonInclude(JsonInclude.Include.NON_NULL) -public class EsignAppUserSaveRequest { - - private Long id; - /** 平板 App 登录账号 */ - private String userName; - /** 明文密码(新建必填;编辑时留空表示不修改) */ - private String password; - /** 角色,默认 STAFF */ - private String role; - /** 绑定酒店(推送等按 hotel_id 过滤) */ - private String hotelId; - /** ACTIVE / INACTIVE / LOCKED */ - private String status; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/auth/dtos/EsignAppUserStatusRequest.java b/src/main/java/com/sjmresorts/opsapp/esign/auth/dtos/EsignAppUserStatusRequest.java deleted file mode 100644 index dddfdbd..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/auth/dtos/EsignAppUserStatusRequest.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.sjmresorts.opsapp.esign.auth.dtos; - -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -public class EsignAppUserStatusRequest { - - private Long id; - /** ACTIVE / INACTIVE / LOCKED */ - private String status; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/auth/dtos/EsignBindDeviceTokenRequest.java b/src/main/java/com/sjmresorts/opsapp/esign/auth/dtos/EsignBindDeviceTokenRequest.java deleted file mode 100644 index 6fff663..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/auth/dtos/EsignBindDeviceTokenRequest.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.sjmresorts.opsapp.esign.auth.dtos; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@Schema(description = "Bind FCM/APNs device token for Esign user") -public class EsignBindDeviceTokenRequest { - - @NotBlank(message = "deviceToken must not be blank") - @Schema(description = "FCM or APNs device token") - private String deviceToken; - - @Schema(description = "Device unique id from client") - private String deviceId; - - @Schema(description = "Device display name") - private String deviceName; - - @Schema(description = "OS version string") - private String deviceVersion; - - @Schema(description = "Platform: android | ios") - private String platform; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/auth/dtos/EsignHotelDto.java b/src/main/java/com/sjmresorts/opsapp/esign/auth/dtos/EsignHotelDto.java deleted file mode 100644 index 0c44ecb..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/auth/dtos/EsignHotelDto.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.sjmresorts.opsapp.esign.auth.dtos; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@Schema(description = "旅宿 Hotel 配置项") -public class EsignHotelDto { - - @Schema(description = "酒店 ID(slug)") - private String id; - - private String area; - private String nameZh; - private String nameEn; - private String address; - private String website; - - @Schema(description = "可选 HTTPS Logo 地址") - private String logoUrl; - - @Schema(description = "是否已上传本机 Logo(仅存库,列表不返回像素数据)") - private boolean hasLogoData; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/auth/dtos/EsignHotelListResponse.java b/src/main/java/com/sjmresorts/opsapp/esign/auth/dtos/EsignHotelListResponse.java deleted file mode 100644 index 07ac555..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/auth/dtos/EsignHotelListResponse.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.sjmresorts.opsapp.esign.auth.dtos; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.ArrayList; -import java.util.List; - -@Data -@NoArgsConstructor -@Schema(description = "Hotel 列表与默认选中") -public class EsignHotelListResponse { - - private boolean success = true; - private String code = "200"; - private String message = "ok"; - private EsignHotelListData data = new EsignHotelListData(); - - @Data - @NoArgsConstructor - public static class EsignHotelListData { - private List hotels = new ArrayList<>(); - private String defaultHotelId; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/auth/dtos/EsignHotelLogoRequest.java b/src/main/java/com/sjmresorts/opsapp/esign/auth/dtos/EsignHotelLogoRequest.java deleted file mode 100644 index 010a49d..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/auth/dtos/EsignHotelLogoRequest.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.sjmresorts.opsapp.esign.auth.dtos; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@Schema(description = "上传或清除 Hotel Logo(data URL)") -public class EsignHotelLogoRequest { - - @Schema(description = "data:image/* Base64;传 null 或空字符串表示清除") - private String logoData; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/auth/dtos/EsignHotelMutationResponse.java b/src/main/java/com/sjmresorts/opsapp/esign/auth/dtos/EsignHotelMutationResponse.java deleted file mode 100644 index d7810c0..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/auth/dtos/EsignHotelMutationResponse.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.sjmresorts.opsapp.esign.auth.dtos; - -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -public class EsignHotelMutationResponse { - - private boolean success; - private String code; - private String message; - private EsignHotelDto data; - - public static EsignHotelMutationResponse ok(String message, EsignHotelDto data) { - EsignHotelMutationResponse r = new EsignHotelMutationResponse(); - r.setSuccess(true); - r.setCode("200"); - r.setMessage(message != null ? message : "ok"); - r.setData(data); - return r; - } - - public static EsignHotelMutationResponse fail(String code, String message) { - EsignHotelMutationResponse r = new EsignHotelMutationResponse(); - r.setSuccess(false); - r.setCode(code); - r.setMessage(message); - return r; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/auth/dtos/EsignHotelUpsertRequest.java b/src/main/java/com/sjmresorts/opsapp/esign/auth/dtos/EsignHotelUpsertRequest.java deleted file mode 100644 index 69a2a7d..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/auth/dtos/EsignHotelUpsertRequest.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.sjmresorts.opsapp.esign.auth.dtos; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@Schema(description = "创建/更新 Hotel 文字信息") -public class EsignHotelUpsertRequest { - - @Schema(description = "酒店 ID(slug,仅创建时必填),如 venetian、grand-dragon") - private String id; - - private String area; - - @Schema(description = "中文名称", requiredMode = Schema.RequiredMode.REQUIRED) - private String nameZh; - - private String nameEn; - private String address; - private String website; - - @Schema(description = "可选 HTTPS Logo 地址(与 logo 上传二选一或并存,上传优先)") - private String logoUrl; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/auth/dtos/EsignLoginRequest.java b/src/main/java/com/sjmresorts/opsapp/esign/auth/dtos/EsignLoginRequest.java deleted file mode 100644 index 63bee6a..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/auth/dtos/EsignLoginRequest.java +++ /dev/null @@ -1,31 +0,0 @@ -// src/main/java/com/sjmresorts/opsapp/logging/dtos/EsignLoginRequest.java -package com.sjmresorts.opsapp.esign.auth.dtos; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; -import lombok.Data; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Data -@Getter -@Setter -@NoArgsConstructor -@Schema(description = "Esign User Login Request") -public class EsignLoginRequest { - - @NotBlank(message = "userName is required") - @Schema(description = "用户名", example = "collector001", required = true) - private String userName; - - @NotBlank(message = "password is required") - @Schema(description = "密码", example = "password123", required = true) - private String password; - - @Schema(description = "FCM/APNs device token from mobile client") - private String deviceToken; - - @Schema(description = "Client device id") - private String deviceId; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/auth/dtos/EsignLoginResponse.java b/src/main/java/com/sjmresorts/opsapp/esign/auth/dtos/EsignLoginResponse.java deleted file mode 100644 index 1b54c10..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/auth/dtos/EsignLoginResponse.java +++ /dev/null @@ -1,61 +0,0 @@ -// src/main/java/com/sjmresorts/opsapp/logging/dtos/EsignLoginResponse.java -package com.sjmresorts.opsapp.esign.auth.dtos; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Data -@Getter -@Setter -@NoArgsConstructor -@Schema(description = "Esign User Login Response") -public class EsignLoginResponse { - - @Schema(description = "登录是否成功") - private boolean success = false; - - @Schema(description = "状态码", example = "200") - private String code; - - @Schema(description = "结果描述", example = "Login successful") - private String message; - - @Schema(description = "JWT访问令牌") - private String accessToken; - - @Schema(description = "用户ID") - private String employeeId; - - @Schema(description = "用户名") - private String userName; - - @Schema(description = "酒店ID") - private String hotelId; - - @Schema(description = "用户角色") - private String role; - - public static EsignLoginResponse success(String accessToken, Long userId, String userName, String hotelId, String role) { - EsignLoginResponse response = new EsignLoginResponse(); - response.setSuccess(true); - response.setCode("200"); - response.setMessage("Login successful"); - response.setAccessToken(accessToken); - response.setEmployeeId(String.valueOf(userId)); - response.setUserName(userName); - response.setHotelId(hotelId); - response.setRole(role); - return response; - } - - public static EsignLoginResponse fail(String code, String message) { - EsignLoginResponse response = new EsignLoginResponse(); - response.setSuccess(false); - response.setCode(code); - response.setMessage(message); - return response; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/auth/models/EsignHotel.java b/src/main/java/com/sjmresorts/opsapp/esign/auth/models/EsignHotel.java deleted file mode 100644 index 5b88c49..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/auth/models/EsignHotel.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.sjmresorts.opsapp.esign.auth.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_hotels") -public class EsignHotel { - - @Id - @Column(name = "hotel_id", nullable = false, length = 64) - private String hotelId; - - @Column(name = "area", length = 128) - private String area; - - @Column(name = "name_zh", nullable = false, length = 256) - private String nameZh; - - @Column(name = "name_en", length = 512) - private String nameEn; - - @Column(name = "address", length = 1000) - private String address; - - @Column(name = "website", length = 256) - private String website; - - @Column(name = "logo_url", length = 2000) - private String logoUrl; - - @Lob - @Column(name = "logo_data") - private String logoData; - - @Column(name = "sort_order", nullable = false) - private int sortOrder; - - @Column(name = "enabled", nullable = false) - private boolean enabled = true; - - @Column(name = "created_at", nullable = false) - private Instant createdAt; - - @Column(name = "updated_at", nullable = false) - private Instant updatedAt; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/auth/models/EsignHotelSetting.java b/src/main/java/com/sjmresorts/opsapp/esign/auth/models/EsignHotelSetting.java deleted file mode 100644 index 8a42e79..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/auth/models/EsignHotelSetting.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.sjmresorts.opsapp.esign.auth.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_hotel_settings") -public class EsignHotelSetting { - - public static final String KEY_DEFAULT_HOTEL_ID = "default_hotel_id"; - - @Id - @Column(name = "setting_key", nullable = false, length = 64) - private String settingKey; - - @Column(name = "setting_value", length = 256) - private String settingValue; - - @Column(name = "updated_at", nullable = false) - private Instant updatedAt; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/auth/models/EsignUser.java b/src/main/java/com/sjmresorts/opsapp/esign/auth/models/EsignUser.java deleted file mode 100644 index 3c44f13..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/auth/models/EsignUser.java +++ /dev/null @@ -1,63 +0,0 @@ -// src/main/java/com/sjmresorts/opsapp/logging/models/EsignUser.java -package com.sjmresorts.opsapp.esign.auth.models; - -import jakarta.persistence.*; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -import java.time.Instant; - -@Getter -@Setter -@NoArgsConstructor -@Entity -@Table(name = "esign_user") -public class EsignUser { - - @Id - @Column(name = "id", nullable = false) - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(name = "user_name", nullable = false, length = 50) - private String userName; - - @Column(name = "password", nullable = false, length = 255) - private String password; - - @Column(name = "role", nullable = false, length = 20) - private String role; - - @Column(name = "hotel_id", length = 64) - private String hotelId; - - @Column(name = "status", nullable = false, length = 20) - private String status; - - @Column(name = "device_token", length = 512) - private String deviceToken; - - @Column(name = "created_at", nullable = false) - private Instant createdAt; - - @Column(name = "updated_at", nullable = false) - private Instant updatedAt; - - @Column(name = "last_login_at") - private Instant lastLoginAt; - - @Column(name = "last_login_ip", length = 64) - private String lastLoginIp; - - @PrePersist - protected void onCreate() { - createdAt = Instant.now(); - updatedAt = Instant.now(); - } - - @PreUpdate - protected void onUpdate() { - updatedAt = Instant.now(); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/auth/repositories/EsignHotelRepository.java b/src/main/java/com/sjmresorts/opsapp/esign/auth/repositories/EsignHotelRepository.java deleted file mode 100644 index d93ccfe..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/auth/repositories/EsignHotelRepository.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.sjmresorts.opsapp.esign.auth.repositories; - -import com.sjmresorts.opsapp.esign.auth.models.EsignHotel; -import org.springframework.data.jpa.repository.JpaRepository; - -import java.util.List; - -public interface EsignHotelRepository extends JpaRepository { - - List findByEnabledTrueOrderBySortOrderAscHotelIdAsc(); - - long countByEnabledTrue(); - - java.util.Optional findFirstByEnabledTrueOrderBySortOrderDescHotelIdDesc(); -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/auth/repositories/EsignHotelSettingRepository.java b/src/main/java/com/sjmresorts/opsapp/esign/auth/repositories/EsignHotelSettingRepository.java deleted file mode 100644 index 95a0d9a..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/auth/repositories/EsignHotelSettingRepository.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.sjmresorts.opsapp.esign.auth.repositories; - -import com.sjmresorts.opsapp.esign.auth.models.EsignHotelSetting; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface EsignHotelSettingRepository extends JpaRepository {} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/auth/repositories/EsignUserRepository.java b/src/main/java/com/sjmresorts/opsapp/esign/auth/repositories/EsignUserRepository.java deleted file mode 100644 index de1996a..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/auth/repositories/EsignUserRepository.java +++ /dev/null @@ -1,56 +0,0 @@ -// src/main/java/com/sjmresorts/opsapp/logging/repositories/EsignUserRepository.java -package com.sjmresorts.opsapp.esign.auth.repositories; - -import com.sjmresorts.opsapp.esign.auth.models.EsignUser; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; -import org.springframework.stereotype.Repository; - -import java.util.Collection; -import java.util.List; -import java.util.Optional; - -@Repository -public interface EsignUserRepository extends JpaRepository { - - Optional findByUserName(String userName); - - Optional findByUserNameAndPassword(String userName, String password); - - List findByIdIn(Collection ids); - - Optional findByDeviceToken(String deviceToken); - - @Query(""" - SELECT u FROM EsignUser u - WHERE UPPER(u.status) = UPPER(:status) - AND u.deviceToken IS NOT NULL - AND TRIM(u.deviceToken) <> '' - AND u.hotelId IS NOT NULL - AND TRIM(u.hotelId) <> '' - AND UPPER(TRIM(u.hotelId)) = UPPER(TRIM(:hotelId)) - """) - List findPushRecipientsByHotel(@Param("hotelId") String hotelId, @Param("status") String status); - - @Query(""" - SELECT u FROM EsignUser u - WHERE UPPER(u.status) = UPPER(:status) - AND u.deviceToken IS NOT NULL - AND TRIM(u.deviceToken) <> '' - """) - List findActivePushRecipients(@Param("status") String status); - - @Query(""" - SELECT u FROM EsignUser u - WHERE UPPER(u.status) = UPPER(:status) - ORDER BY u.lastLoginAt DESC - """) - List findByStatusOrderByLastLoginAtDesc(@Param("status") String status); - - List findAllByOrderByUserNameAsc(); - - boolean existsByUserNameIgnoreCase(String userName); - - boolean existsByUserNameIgnoreCaseAndIdNot(String userName, Long id); -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/auth/services/EsignAppUserService.java b/src/main/java/com/sjmresorts/opsapp/esign/auth/services/EsignAppUserService.java deleted file mode 100644 index daca1d8..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/auth/services/EsignAppUserService.java +++ /dev/null @@ -1,179 +0,0 @@ -package com.sjmresorts.opsapp.esign.auth.services; - -import com.sjmresorts.opsapp.esign.auth.dtos.EsignAppUserResponse; -import com.sjmresorts.opsapp.esign.auth.dtos.EsignAppUserSaveRequest; -import com.sjmresorts.opsapp.esign.auth.dtos.EsignAppUserStatusRequest; -import com.sjmresorts.opsapp.esign.auth.models.EsignUser; -import com.sjmresorts.opsapp.esign.auth.repositories.EsignHotelRepository; -import com.sjmresorts.opsapp.esign.auth.repositories.EsignUserRepository; -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.util.ArrayList; -import java.util.List; -import java.util.Locale; -import java.util.Optional; -import java.util.Set; - -@Slf4j -@Service -public class EsignAppUserService { - - private static final String DEFAULT_ROLE = "STAFF"; - private static final String DEFAULT_STATUS = "ACTIVE"; - private static final Set ALLOWED_STATUSES = Set.of("ACTIVE", "INACTIVE", "LOCKED"); - private static final Set ALLOWED_ROLES = Set.of("STAFF", "ADMIN"); - - @Resource - private EsignUserRepository esignUserRepository; - - @Resource - private EsignHotelRepository hotelRepository; - - @Transactional(readOnly = true) - public EsignAppUserResponse listAll() { - EsignAppUserResponse resp = EsignAppUserResponse.ok("ok"); - resp.setItems(toItems(esignUserRepository.findAllByOrderByUserNameAsc())); - return resp; - } - - @Transactional - public EsignAppUserResponse save(EsignAppUserSaveRequest request) { - if (request == null) { - return EsignAppUserResponse.fail("400", "请求体不能为空"); - } - String userName = trim(request.getUserName(), 50); - if (StringUtils.isBlank(userName)) { - return EsignAppUserResponse.fail("400", "请填写登录账号"); - } - String role = normalizeRole(request.getRole()); - String status = normalizeStatus(request.getStatus()); - String hotelId = trim(request.getHotelId(), 64); - if (StringUtils.isNotBlank(hotelId) && hotelRepository.findById(hotelId).isEmpty()) { - return EsignAppUserResponse.fail("400", "酒店不存在:" + hotelId); - } - - boolean creating = request.getId() == null; - EsignUser row; - if (creating) { - if (esignUserRepository.existsByUserNameIgnoreCase(userName)) { - return EsignAppUserResponse.fail("409", "该登录账号已存在"); - } - String password = StringUtils.trimToEmpty(request.getPassword()); - if (password.length() < 4) { - return EsignAppUserResponse.fail("400", "密码至少 4 位"); - } - row = new EsignUser(); - row.setUserName(userName); - row.setPassword(password); - row.setRole(role); - row.setStatus(status); - row.setHotelId(StringUtils.isBlank(hotelId) ? null : hotelId); - } else { - Optional opt = esignUserRepository.findById(request.getId()); - if (opt.isEmpty()) { - return EsignAppUserResponse.fail("404", "账号不存在"); - } - row = opt.get(); - if (!userName.equalsIgnoreCase(row.getUserName())) { - return EsignAppUserResponse.fail("400", "登录账号创建后不可修改"); - } - row.setRole(role); - row.setStatus(status); - row.setHotelId(StringUtils.isBlank(hotelId) ? null : hotelId); - String password = StringUtils.trimToEmpty(request.getPassword()); - if (StringUtils.isNotBlank(password)) { - if (password.length() < 4) { - return EsignAppUserResponse.fail("400", "密码至少 4 位"); - } - row.setPassword(password); - } - } - - row = esignUserRepository.save(row); - log.info("Esign app user {} id={} status={}", creating ? "created" : "updated", row.getId(), row.getStatus()); - EsignAppUserResponse resp = EsignAppUserResponse.ok(creating ? "已创建平板账号" : "已更新平板账号"); - resp.setData(toItem(row)); - return resp; - } - - @Transactional - public EsignAppUserResponse updateStatus(EsignAppUserStatusRequest request) { - if (request == null || request.getId() == null) { - return EsignAppUserResponse.fail("400", "请指定账号 ID"); - } - String status = normalizeStatus(request.getStatus()); - Optional opt = esignUserRepository.findById(request.getId()); - if (opt.isEmpty()) { - return EsignAppUserResponse.fail("404", "账号不存在"); - } - EsignUser row = opt.get(); - row.setStatus(status); - row = esignUserRepository.save(row); - EsignAppUserResponse resp = EsignAppUserResponse.ok("状态已更新"); - resp.setData(toItem(row)); - return resp; - } - - private List toItems(List rows) { - List items = new ArrayList<>(); - if (rows == null) { - return items; - } - for (EsignUser row : rows) { - items.add(toItem(row)); - } - return items; - } - - private EsignAppUserResponse.EsignAppUserItem toItem(EsignUser row) { - EsignAppUserResponse.EsignAppUserItem item = new EsignAppUserResponse.EsignAppUserItem(); - item.setId(row.getId()); - item.setUserName(row.getUserName()); - item.setRole(row.getRole()); - item.setHotelId(row.getHotelId()); - item.setStatus(row.getStatus()); - item.setCreatedAt(row.getCreatedAt() != null ? row.getCreatedAt().toString() : null); - item.setUpdatedAt(row.getUpdatedAt() != null ? row.getUpdatedAt().toString() : null); - item.setLastLoginAt(row.getLastLoginAt() != null ? row.getLastLoginAt().toString() : null); - item.setLastLoginIp(row.getLastLoginIp()); - item.setHasDeviceToken(StringUtils.isNotBlank(row.getDeviceToken())); - return item; - } - - private static String normalizeRole(String raw) { - String role = StringUtils.upperCase(StringUtils.trimToEmpty(raw), Locale.ROOT); - if (StringUtils.isBlank(role)) { - return DEFAULT_ROLE; - } - if (!ALLOWED_ROLES.contains(role)) { - return DEFAULT_ROLE; - } - return role; - } - - private static String normalizeStatus(String raw) { - String status = StringUtils.upperCase(StringUtils.trimToEmpty(raw), Locale.ROOT); - if (StringUtils.isBlank(status)) { - return DEFAULT_STATUS; - } - if (!ALLOWED_STATUSES.contains(status)) { - return DEFAULT_STATUS; - } - return status; - } - - private static String trim(String raw, int maxLen) { - if (raw == null) { - return ""; - } - String s = raw.trim(); - if (s.length() > maxLen) { - return s.substring(0, maxLen); - } - return s; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/auth/services/EsignHotelService.java b/src/main/java/com/sjmresorts/opsapp/esign/auth/services/EsignHotelService.java deleted file mode 100644 index 4c1a478..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/auth/services/EsignHotelService.java +++ /dev/null @@ -1,419 +0,0 @@ -package com.sjmresorts.opsapp.esign.auth.services; - -import com.sjmresorts.opsapp.esign.auth.dtos.EsignHotelDto; -import com.sjmresorts.opsapp.esign.auth.dtos.EsignHotelListResponse; -import com.sjmresorts.opsapp.esign.auth.dtos.EsignHotelMutationResponse; -import com.sjmresorts.opsapp.esign.auth.dtos.EsignHotelUpsertRequest; -import com.sjmresorts.opsapp.esign.common.dtos.EsignSimpleResponse; -import com.sjmresorts.opsapp.esign.auth.models.EsignHotel; -import com.sjmresorts.opsapp.esign.auth.models.EsignHotelSetting; -import com.sjmresorts.opsapp.esign.auth.repositories.EsignHotelRepository; -import com.sjmresorts.opsapp.esign.auth.repositories.EsignHotelSettingRepository; -import org.apache.commons.lang3.StringUtils; -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.Locale; -import java.util.Optional; -import java.util.regex.Pattern; - -@Service -public class EsignHotelService { - - public static final int LOGO_DATA_MAX_LEN = 700_000; - - private static final Pattern HOTEL_ID_PATTERN = Pattern.compile("^[a-z0-9]+(?:-[a-z0-9]+)*$"); - - private final EsignHotelRepository hotelRepository; - private final EsignHotelSettingRepository settingRepository; - - public EsignHotelService( - EsignHotelRepository hotelRepository, - EsignHotelSettingRepository settingRepository) { - this.hotelRepository = hotelRepository; - this.settingRepository = settingRepository; - } - - private void ensureSeeded() { - if (hotelRepository.count() > 0) { - return; - } - Instant now = Instant.now(); - int order = 0; - for (SeedHotel s : defaultSeedHotels()) { - EsignHotel h = new EsignHotel(); - h.setHotelId(s.id); - h.setArea(s.area); - h.setNameZh(s.nameZh); - h.setNameEn(s.nameEn); - h.setAddress(s.address); - h.setWebsite(s.website); - h.setSortOrder(order++); - h.setEnabled(true); - h.setCreatedAt(now); - h.setUpdatedAt(now); - hotelRepository.save(h); - } - EsignHotelSetting setting = new EsignHotelSetting(); - setting.setSettingKey(EsignHotelSetting.KEY_DEFAULT_HOTEL_ID); - setting.setSettingValue("venetian"); - setting.setUpdatedAt(now); - settingRepository.save(setting); - } - - @Transactional - public EsignHotelListResponse listHotels(boolean includeLogos) { - ensureSeeded(); - EsignHotelListResponse resp = new EsignHotelListResponse(); - EsignHotelListResponse.EsignHotelListData data = resp.getData(); - List dtos = new ArrayList<>(); - for (EsignHotel row : hotelRepository.findByEnabledTrueOrderBySortOrderAscHotelIdAsc()) { - dtos.add(toDto(row, includeLogos)); - } - data.setHotels(dtos); - data.setDefaultHotelId(resolveDefaultHotelId(dtos)); - return resp; - } - - /** 详情/採集:含 logo_data 解析后的 logoUrl */ - @Transactional(readOnly = true) - public EsignHotelDto getHotelForPayload(String hotelId) { - if (StringUtils.isBlank(hotelId)) { - return null; - } - return hotelRepository.findById(hotelId.trim()).filter(EsignHotel::isEnabled).map(this::toDtoWithLogo).orElse(null); - } - - @Transactional(readOnly = true) - public EsignHotelDto getDefaultHotelForPayload() { - EsignHotelListResponse list = listHotels(true); - String id = list.getData().getDefaultHotelId(); - if (StringUtils.isBlank(id)) { - return null; - } - return getHotelForPayload(id); - } - - @Transactional - public EsignSimpleResponse setDefaultHotel(String hotelId) { - if (StringUtils.isBlank(hotelId)) { - return EsignSimpleResponse.fail("400", "hotelId required"); - } - String id = hotelId.trim(); - if (!hotelRepository.existsById(id)) { - return EsignSimpleResponse.fail("404", "Hotel not found"); - } - EsignHotel row = hotelRepository.findById(id).orElse(null); - if (row == null || !row.isEnabled()) { - return EsignSimpleResponse.fail("404", "Hotel not found"); - } - Instant now = Instant.now(); - EsignHotelSetting setting = - settingRepository - .findById(EsignHotelSetting.KEY_DEFAULT_HOTEL_ID) - .orElseGet( - () -> { - EsignHotelSetting s = new EsignHotelSetting(); - s.setSettingKey(EsignHotelSetting.KEY_DEFAULT_HOTEL_ID); - return s; - }); - setting.setSettingValue(id); - setting.setUpdatedAt(now); - settingRepository.save(setting); - return EsignSimpleResponse.ok("default hotel updated"); - } - - @Transactional - public EsignHotelMutationResponse createHotel(EsignHotelUpsertRequest request) { - if (request == null) { - return EsignHotelMutationResponse.fail("400", "request body required"); - } - String id = normalizeHotelId(request.getId()); - if (StringUtils.isBlank(id)) { - return EsignHotelMutationResponse.fail("400", "id required (slug: a-z, 0-9, hyphen)"); - } - if (!HOTEL_ID_PATTERN.matcher(id).matches()) { - return EsignHotelMutationResponse.fail("400", "invalid id format"); - } - String nameZh = trimToNull(request.getNameZh()); - if (StringUtils.isBlank(nameZh)) { - return EsignHotelMutationResponse.fail("400", "nameZh required"); - } - Instant now = Instant.now(); - Optional existing = hotelRepository.findById(id); - EsignHotel h; - if (existing.isPresent()) { - h = existing.get(); - if (h.isEnabled()) { - return EsignHotelMutationResponse.fail("409", "hotel id already exists"); - } - applyTextFields(h, request, nameZh); - h.setEnabled(true); - h.setUpdatedAt(now); - } else { - h = new EsignHotel(); - h.setHotelId(id); - applyTextFields(h, request, nameZh); - h.setSortOrder(nextSortOrder()); - h.setEnabled(true); - h.setCreatedAt(now); - h.setUpdatedAt(now); - } - hotelRepository.save(h); - return EsignHotelMutationResponse.ok("hotel created", toDto(h, false)); - } - - @Transactional - public EsignHotelMutationResponse updateHotel(String hotelId, EsignHotelUpsertRequest request) { - if (StringUtils.isBlank(hotelId)) { - return EsignHotelMutationResponse.fail("400", "hotelId required"); - } - if (request == null) { - return EsignHotelMutationResponse.fail("400", "request body required"); - } - String id = hotelId.trim(); - Optional opt = hotelRepository.findById(id); - if (opt.isEmpty() || !opt.get().isEnabled()) { - return EsignHotelMutationResponse.fail("404", "Hotel not found"); - } - String nameZh = trimToNull(request.getNameZh()); - if (StringUtils.isBlank(nameZh)) { - return EsignHotelMutationResponse.fail("400", "nameZh required"); - } - EsignHotel h = opt.get(); - applyTextFields(h, request, nameZh); - h.setUpdatedAt(Instant.now()); - hotelRepository.save(h); - return EsignHotelMutationResponse.ok("hotel updated", toDto(h, false)); - } - - @Transactional - public EsignSimpleResponse deleteHotel(String hotelId) { - if (StringUtils.isBlank(hotelId)) { - return EsignSimpleResponse.fail("400", "hotelId required"); - } - String id = hotelId.trim(); - Optional opt = hotelRepository.findById(id); - if (opt.isEmpty() || !opt.get().isEnabled()) { - return EsignSimpleResponse.fail("404", "Hotel not found"); - } - if (hotelRepository.countByEnabledTrue() <= 1) { - return EsignSimpleResponse.fail("400", "cannot delete the last hotel"); - } - EsignHotel h = opt.get(); - h.setEnabled(false); - h.setUpdatedAt(Instant.now()); - hotelRepository.save(h); - - String defaultId = - settingRepository - .findById(EsignHotelSetting.KEY_DEFAULT_HOTEL_ID) - .map(EsignHotelSetting::getSettingValue) - .orElse(null); - if (id.equals(defaultId)) { - List remaining = hotelRepository.findByEnabledTrueOrderBySortOrderAscHotelIdAsc(); - if (!remaining.isEmpty()) { - EsignHotelSetting setting = - settingRepository - .findById(EsignHotelSetting.KEY_DEFAULT_HOTEL_ID) - .orElseGet( - () -> { - EsignHotelSetting s = new EsignHotelSetting(); - s.setSettingKey(EsignHotelSetting.KEY_DEFAULT_HOTEL_ID); - return s; - }); - setting.setSettingValue(remaining.get(0).getHotelId()); - setting.setUpdatedAt(Instant.now()); - settingRepository.save(setting); - } - } - return EsignSimpleResponse.ok("hotel deleted"); - } - - @Transactional - public EsignSimpleResponse updateLogo(String hotelId, String logoData) { - if (StringUtils.isBlank(hotelId)) { - return EsignSimpleResponse.fail("400", "hotelId required"); - } - Optional opt = hotelRepository.findById(hotelId.trim()); - if (opt.isEmpty() || !opt.get().isEnabled()) { - return EsignSimpleResponse.fail("404", "Hotel not found"); - } - EsignHotel h = opt.get(); - if (logoData == null || StringUtils.isBlank(logoData)) { - h.setLogoData(null); - } else { - String s = logoData.trim(); - if (!s.regionMatches(true, 0, "data:image/", 0, 11)) { - return EsignSimpleResponse.fail("400", "logoData must be data:image/*"); - } - if (s.length() > LOGO_DATA_MAX_LEN) { - return EsignSimpleResponse.fail("400", "logoData too large"); - } - h.setLogoData(s); - } - h.setUpdatedAt(Instant.now()); - hotelRepository.save(h); - return EsignSimpleResponse.ok("logo updated"); - } - - private void applyTextFields(EsignHotel h, EsignHotelUpsertRequest request, String nameZh) { - h.setNameZh(nameZh); - h.setArea(trimToNull(request.getArea())); - h.setNameEn(trimToNull(request.getNameEn())); - h.setAddress(trimToNull(request.getAddress())); - h.setWebsite(trimToNull(request.getWebsite())); - String logoUrl = trimToNull(request.getLogoUrl()); - if (logoUrl != null && logoUrl.length() > 2000) { - logoUrl = logoUrl.substring(0, 2000); - } - h.setLogoUrl(logoUrl); - } - - private int nextSortOrder() { - return hotelRepository - .findFirstByEnabledTrueOrderBySortOrderDescHotelIdDesc() - .map(r -> r.getSortOrder() + 1) - .orElse(0); - } - - private static String normalizeHotelId(String raw) { - if (raw == null) { - return null; - } - return raw.trim().toLowerCase(Locale.ROOT); - } - - private static String trimToNull(String s) { - if (s == null) { - return null; - } - String t = s.trim(); - return t.isEmpty() ? null : t; - } - - private String resolveDefaultHotelId(List hotels) { - String configured = - settingRepository - .findById(EsignHotelSetting.KEY_DEFAULT_HOTEL_ID) - .map(EsignHotelSetting::getSettingValue) - .filter(StringUtils::isNotBlank) - .orElse(null); - if (configured != null && hotels.stream().anyMatch(h -> configured.equals(h.getId()))) { - return configured; - } - return hotels.isEmpty() ? null : hotels.get(0).getId(); - } - - private EsignHotelDto toDto(EsignHotel row, boolean withResolvedLogoUrl) { - EsignHotelDto dto = new EsignHotelDto(); - dto.setId(row.getHotelId()); - dto.setArea(row.getArea()); - dto.setNameZh(row.getNameZh()); - dto.setNameEn(row.getNameEn()); - dto.setAddress(row.getAddress()); - dto.setWebsite(row.getWebsite()); - dto.setHasLogoData(StringUtils.isNotBlank(row.getLogoData())); - if (withResolvedLogoUrl) { - if (StringUtils.isNotBlank(row.getLogoData())) { - dto.setLogoUrl(row.getLogoData().trim()); - } else if (StringUtils.isNotBlank(row.getLogoUrl())) { - dto.setLogoUrl(row.getLogoUrl().trim()); - } else { - dto.setLogoUrl(faviconFromWebsite(row.getWebsite())); - } - } else { - dto.setLogoUrl(trimToNull(row.getLogoUrl())); - } - return dto; - } - - private EsignHotelDto toDtoWithLogo(EsignHotel row) { - return toDto(row, true); - } - - private static String faviconFromWebsite(String website) { - if (StringUtils.isBlank(website)) { - return null; - } - String w = - website.trim() - .toLowerCase() - .replaceFirst("^https?://", "") - .replaceFirst("^www\\.", ""); - String host = w.split("/")[0].replaceAll(":\\d+$", ""); - if (host.isEmpty()) { - return null; - } - return "https://www.google.com/s2/favicons?domain=" - + java.net.URLEncoder.encode(host, java.nio.charset.StandardCharsets.UTF_8) - + "&sz=96"; - } - - private static List defaultSeedHotels() { - List list = new ArrayList<>(); - list.add( - new SeedHotel( - "venetian", - "氹仔 Taipa", - "澳門威尼斯人酒店", - "The Venetian Macao (五星 5-Star)", - "澳門望德聖母灣大馬路,路氹金光大道 Estrada da Baía de N. Senhora da Esperança, s/n, Taipa, Macau", - "www.venetianmacao.com")); - list.add( - new SeedHotel( - "grand-dragon", - "氹仔 Taipa", - "駿龍酒店", - "Grand Dragon Hotel (四星 4-Star)", - "澳門氹仔沙維斯街 36 號 Rua de Chaves, n.º 36, Taipa, Macau", - "www.granddragon.com.mo")); - list.add( - new SeedHotel( - "grandview", - "氹仔 Taipa", - "君怡酒店", - "Grandview Hotel (四星 4-Star)", - "氹仔柯維納馬路 142 號和佛山街 16 號 Estrada Governador Albano de Oliveira, n.º 142 e Rua de Fat San, n.º 16, Taipa", - "www.grandview-hotel.com")); - list.add( - new SeedHotel( - "parisian", - "氹仔 Taipa", - "澳門巴黎人酒店", - "The Parisian Macao (三星 3-Star)", - "澳門路氹金光大道連貫公路 Estrada do Istmo, Lote 3, Cotai Strip, Macau", - "www.parisianmacao.com")); - list.add( - new SeedHotel( - "royal", - "澳門 Macao", - "皇都酒店", - "Royal Hotel (五星 5-Star)", - "澳門得勝馬路 2 號-4 號 Estrada da Vitória, nºs. 2-4, Macau", - "www.hotelroyal.com.mo")); - list.add( - new SeedHotel( - "landmark", - "澳門 Macao", - "新東方置地酒店", - "New Orient Landmark Hotel (五星 5-Star)", - "澳門新口岸友誼大馬路 555 號 555 Avenida da Amizade, Macau", - "www.landmarkhotel.com.mo")); - list.add( - new SeedHotel( - "rio", - "澳門 Macao", - "利澳酒店", - "Rio Macau (四星 4-Star)", - "澳門高美士街 33 號及宋玉生廣場 682 號 Rua de Luís Gonzaga Gomes, n.º 33 e Alameda Dr. Carlos D'Assumpção, n.º 682, Macau", - "www.riomacau.com")); - return list; - } - - private record SeedHotel( - String id, String area, String nameZh, String nameEn, String address, String website) {} -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/auth/services/EsignLoginService.java b/src/main/java/com/sjmresorts/opsapp/esign/auth/services/EsignLoginService.java deleted file mode 100644 index fd2a830..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/auth/services/EsignLoginService.java +++ /dev/null @@ -1,118 +0,0 @@ -// src/main/java/com/sjmresorts/opsapp/logging/services/EsignLoginService.java -package com.sjmresorts.opsapp.esign.auth.services; - -import com.auth0.jwt.JWT; -import com.auth0.jwt.algorithms.Algorithm; -import com.sjmresorts.opsapp.esign.auth.dtos.EsignBindDeviceTokenRequest; -import com.sjmresorts.opsapp.esign.auth.dtos.EsignLoginRequest; -import com.sjmresorts.opsapp.esign.auth.dtos.EsignLoginResponse; -import com.sjmresorts.opsapp.esign.common.dtos.EsignSimpleResponse; -import com.sjmresorts.opsapp.esign.auth.models.EsignUser; -import com.sjmresorts.opsapp.esign.auth.repositories.EsignUserRepository; -import com.sjmresorts.opsapp.common.utils.JwtTokenUtil; -import jakarta.transaction.Transactional; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.springframework.stereotype.Service; - -import java.time.Instant; -import java.util.Date; -import java.util.UUID; - -@Slf4j -@Service -@RequiredArgsConstructor -public class EsignLoginService { - - private final EsignUserRepository userRepository; - - @Transactional - public EsignLoginResponse login(EsignLoginRequest request, String clientIp) { - // 根据用户名和密码查找用户(明文验证) - EsignUser user = userRepository.findByUserNameAndPassword( - request.getUserName(), request.getPassword()).orElse(null); - - if (user == null) { - return EsignLoginResponse.fail("401", "Invalid username or password"); - } - - // 检查用户状态 - if (!"ACTIVE".equalsIgnoreCase(user.getStatus())) { - if ("INACTIVE".equalsIgnoreCase(user.getStatus())) { - return EsignLoginResponse.fail("403", "User is inactive"); - } else if ("LOCKED".equalsIgnoreCase(user.getStatus())) { - return EsignLoginResponse.fail("403", "User is locked"); - } - return EsignLoginResponse.fail("403", "User status is invalid"); - } - - user.setLastLoginAt(Instant.now()); - String normalizedIp = EsignUserPresenceService.normalizeClientIp(clientIp); - if (normalizedIp != null) { - user.setLastLoginIp(normalizedIp); - } - boolean tokenUpdated = applyDeviceToken(user, request.getDeviceToken()); - userRepository.save(user); - log.info("Esign login user={}, deviceTokenUpdated={}", user.getUserName(), tokenUpdated); - - // 生成JWT Token,包含 user_id, username, hotel_id, role - String token = generateToken(user); - - return EsignLoginResponse.success( - token, - user.getId(), - user.getUserName(), - user.getHotelId(), - user.getRole() - ); - } - - @Transactional - public EsignSimpleResponse bindDeviceToken(Long userId, EsignBindDeviceTokenRequest request) { - if (userId == null) { - return EsignSimpleResponse.fail("401", "Unauthorized"); - } - EsignUser user = userRepository.findById(userId).orElse(null); - if (user == null) { - return EsignSimpleResponse.fail("404", "User not found"); - } - if (!"ACTIVE".equalsIgnoreCase(user.getStatus())) { - return EsignSimpleResponse.fail("403", "User status is invalid"); - } - applyDeviceToken(user, request.getDeviceToken()); - userRepository.save(user); - return EsignSimpleResponse.ok("Device token bound"); - } - - private boolean applyDeviceToken(EsignUser user, String deviceToken) { - if (StringUtils.isNotBlank(deviceToken)) { - user.setDeviceToken(deviceToken.trim()); - return true; - } - return false; - } - - private String generateToken(EsignUser user) { - // 平板 App 长连接:使用 app_session 有效期,避免 5 分钟 accessToken 导致 WS 重连失败 - long expiryMinutes = JwtTokenUtil.getAppSessionExpiration(); - long expiryMillis = expiryMinutes * 60 * 1000; - Date now = new Date(); - Date expDate = new Date(now.getTime() + expiryMillis); - String uuid = UUID.randomUUID().toString(); - - // 正确的 JWT 生成方式 - return JWT.create() - .withIssuer("esign_login") - .withIssuedAt(now) - .withNotBefore(now) - .withExpiresAt(expDate) - .withJWTId(uuid) - .withClaim("employeeId", String.valueOf(user.getId())) - .withClaim("userName", user.getUserName()) - .withClaim("hotelId", user.getHotelId() != null ? user.getHotelId() : "") - .withClaim("role", user.getRole()) - .withSubject(String.valueOf(user.getId())) - .sign(Algorithm.HMAC256(JwtTokenUtil.getSecretKey())); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/auth/services/EsignUserPresenceService.java b/src/main/java/com/sjmresorts/opsapp/esign/auth/services/EsignUserPresenceService.java deleted file mode 100644 index 5a738cd..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/auth/services/EsignUserPresenceService.java +++ /dev/null @@ -1,178 +0,0 @@ -package com.sjmresorts.opsapp.esign.auth.services; - -import com.sjmresorts.opsapp.esign.auth.models.EsignUser; -import com.sjmresorts.opsapp.esign.auth.repositories.EsignUserRepository; -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.util.ArrayList; -import java.util.Collection; -import java.util.Comparator; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.function.Function; -import java.util.stream.Collectors; - -/** - * 记录 App 客户端 IP,并按 IP 将多台账号视为同一物理设备(保留最近活跃账号)。 - */ -@Slf4j -@Service -public class EsignUserPresenceService { - - @Resource - private EsignUserRepository esignUserRepository; - - public static String normalizeClientIp(String ip) { - if (StringUtils.isBlank(ip)) { - return null; - } - String t = ip.trim(); - if ("0:0:0:0:0:0:0:1".equals(t)) { - return "127.0.0.1"; - } - int comma = t.indexOf(','); - if (comma > 0) { - t = t.substring(0, comma).trim(); - } - return t.isEmpty() ? null : t; - } - - @Transactional - public void touchClientIp(Long userId, String clientIp) { - if (userId == null) { - return; - } - String ip = normalizeClientIp(clientIp); - if (ip == null) { - return; - } - esignUserRepository.findById(userId).ifPresent(user -> { - if (!ip.equals(user.getLastLoginIp())) { - user.setLastLoginIp(ip); - esignUserRepository.save(user); - log.debug("Updated client IP userId={}, ip={}", userId, ip); - } - }); - } - - @Transactional(readOnly = true) - public LinkedHashSet dedupeUserIdsByClientIp(Collection userIds) { - if (userIds == null || userIds.isEmpty()) { - return new LinkedHashSet<>(); - } - Set ids = userIds.stream().filter(Objects::nonNull).collect(Collectors.toCollection(LinkedHashSet::new)); - if (ids.isEmpty()) { - return new LinkedHashSet<>(); - } - Map users = loadUsers(ids); - return dedupeUserIdsByClientIp(ids, users::get); - } - - @Transactional(readOnly = true) - public List dedupeUsersByClientIp(List users) { - if (users == null || users.isEmpty()) { - return List.of(); - } - Map bestByIp = new LinkedHashMap<>(); - List withoutIp = new ArrayList<>(); - for (EsignUser user : users) { - if (user == null || user.getId() == null) { - continue; - } - String ip = normalizeClientIp(user.getLastLoginIp()); - if (ip == null) { - withoutIp.add(user); - continue; - } - EsignUser existing = bestByIp.get(ip); - if (existing == null || isMoreRecentlyActive(user, existing)) { - bestByIp.put(ip, user); - } - } - List result = new ArrayList<>(bestByIp.values()); - result.addAll(withoutIp); - result.sort(Comparator - .comparing(EsignUser::getLastLoginAt, Comparator.nullsLast(Comparator.reverseOrder())) - .thenComparing(EsignUser::getId, Comparator.nullsLast(Comparator.reverseOrder()))); - return result; - } - - @Transactional(readOnly = true) - public int countUniqueClientIps(Collection userIds) { - return dedupeUserIdsByClientIp(userIds).size(); - } - - private LinkedHashSet dedupeUserIdsByClientIp( - Collection userIds, - Function userLoader) { - Map bestUserIdByIp = new LinkedHashMap<>(); - LinkedHashSet withoutIp = new LinkedHashSet<>(); - for (Long userId : userIds) { - if (userId == null) { - continue; - } - EsignUser user = userLoader.apply(userId); - String ip = user != null ? normalizeClientIp(user.getLastLoginIp()) : null; - if (ip == null) { - withoutIp.add(userId); - continue; - } - Long existingId = bestUserIdByIp.get(ip); - if (existingId == null) { - bestUserIdByIp.put(ip, userId); - continue; - } - EsignUser existing = userLoader.apply(existingId); - if (isMoreRecentlyActive(user, existing)) { - bestUserIdByIp.put(ip, userId); - } - } - LinkedHashSet result = new LinkedHashSet<>(bestUserIdByIp.values()); - result.addAll(withoutIp); - return result; - } - - private Map loadUsers(Set userIds) { - Map map = new LinkedHashMap<>(); - for (Long id : userIds) { - esignUserRepository.findById(id).ifPresent(u -> map.put(id, u)); - } - return map; - } - - private static boolean isMoreRecentlyActive(EsignUser candidate, EsignUser incumbent) { - if (candidate == null) { - return false; - } - if (incumbent == null) { - return true; - } - Instant c = candidate.getLastLoginAt(); - Instant i = incumbent.getLastLoginAt(); - if (c == null) { - return false; - } - if (i == null) { - return true; - } - int cmp = c.compareTo(i); - if (cmp != 0) { - return cmp > 0; - } - Long cId = candidate.getId(); - Long iId = incumbent.getId(); - if (cId == null || iId == null) { - return false; - } - return cId > iId; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/capture/config/EsignCaptureSchemaMigrator.java b/src/main/java/com/sjmresorts/opsapp/esign/capture/config/EsignCaptureSchemaMigrator.java deleted file mode 100644 index e8e4f77..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/capture/config/EsignCaptureSchemaMigrator.java +++ /dev/null @@ -1,153 +0,0 @@ -package com.sjmresorts.opsapp.esign.capture.config; - -import com.sjmresorts.opsapp.common.db.DbDialect; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.stereotype.Component; - -import javax.sql.DataSource; - -/** 证件采集表结构迁移(多库方言;由 EsignCmsSchemaMigrator 在 JPA 启动前调用)。 */ -@Component("esignCaptureSchemaMigrator") -public class EsignCaptureSchemaMigrator { - - private static final Logger log = LoggerFactory.getLogger(EsignCaptureSchemaMigrator.class); - - private final JdbcTemplate jdbc; - private final DbDialect dialect; - - public EsignCaptureSchemaMigrator( - @Qualifier("opsAppDataSource") DataSource dataSource, DbDialect dialect) { - this.jdbc = new JdbcTemplate(dataSource); - this.dialect = dialect; - } - - public void ensureTables() { - try { - renameLegacyCaptureTablesIfNeeded(); - ensureCaptureTasksTable(); - ensureCaptureTaskImagesTable(); - renameLegacyImageObjectNamesIfNeeded(); - } catch (RuntimeException e) { - log.error("Esign capture migrate failed", e); - throw e; - } - if (!dialect.tableExists(jdbc, "esign_capture_tasks")) { - throw new IllegalStateException( - "esign_capture_tasks 仍不存在,请检查库权限或手工执行 scripts 下对应库的建表脚本"); - } - log.info("Esign capture migrate: esign_capture_tasks ready ({})", dialect.type()); - } - - private void renameLegacyCaptureTablesIfNeeded() { - if (!dialect.tableExists(jdbc, "esign_capture_tasks") - && dialect.tableExists(jdbc, "sinosecu_capture_tasks")) { - log.info("Esign capture migrate: renaming sinosecu_capture_tasks -> esign_capture_tasks"); - dialect.renameTable(jdbc, "sinosecu_capture_tasks", "esign_capture_tasks"); - } - if (!dialect.tableExists(jdbc, "esign_capture_task_images") - && dialect.tableExists(jdbc, "sinosecu_capture_task_images")) { - log.info( - "Esign capture migrate: renaming sinosecu_capture_task_images -> esign_capture_task_images"); - dialect.renameTable(jdbc, "sinosecu_capture_task_images", "esign_capture_task_images"); - } - } - - private void renameLegacyImageObjectNamesIfNeeded() { - if (!dialect.tableExists(jdbc, "esign_capture_task_images")) { - return; - } - renameObjectIfExists("uq_sinosecu_img_task_slot", "uq_esign_img_task_slot"); - renameObjectIfExists("fk_sinosecu_img_task", "fk_esign_capture_img_task"); - renameObjectIfExists("ix_sinosecu_capture_task_images_task", "ix_esign_capture_task_images_task"); - } - - private void renameObjectIfExists(String oldName, String newName) { - if (!dialect.objectExists(jdbc, oldName) || dialect.objectExists(jdbc, newName)) { - return; - } - log.info("Esign capture migrate: renaming {} -> {}", oldName, newName); - dialect.renameObject(jdbc, oldName, newName); - } - - private void ensureCaptureTasksTable() { - if (dialect.tableExists(jdbc, "esign_capture_tasks")) { - add("esign_capture_tasks", "signed_at", dialect.datetime() + " NULL"); - add("esign_capture_tasks", "sign_remark", dialect.varchar(2000) + " NULL"); - add("esign_capture_tasks", "sign_signature", dialect.text() + " NULL"); - return; - } - log.info("Esign capture migrate: creating esign_capture_tasks"); - jdbc.execute( - "CREATE TABLE esign_capture_tasks (" - + "task_id " - + dialect.varchar(36) - + " NOT NULL PRIMARY KEY, " - + "payload_json " - + dialect.text() - + " NOT NULL, " - + "employee_id " - + dialect.varchar(128) - + " NULL, " - + "status " - + dialect.varchar(32) - + " NOT NULL, " - + "created_at " - + dialect.datetime() - + " NOT NULL, " - + "signed_at " - + dialect.datetime() - + " NULL, " - + "sign_remark " - + dialect.varchar(2000) - + " NULL, " - + "sign_signature " - + dialect.text() - + " NULL)"); - } - - private void ensureCaptureTaskImagesTable() { - if (dialect.tableExists(jdbc, "esign_capture_task_images") - || dialect.tableExists(jdbc, "sinosecu_capture_task_images")) { - return; - } - if (!dialect.tableExists(jdbc, "esign_capture_tasks")) { - return; - } - if (dialect.objectExists(jdbc, "uq_sinosecu_img_task_slot") - || dialect.objectExists(jdbc, "uq_esign_img_task_slot")) { - log.warn( - "Esign capture migrate: skip creating esign_capture_task_images; " - + "unique constraint already exists (legacy sinosecu table may need rename)"); - return; - } - log.info("Esign capture migrate: creating esign_capture_task_images"); - jdbc.execute( - "CREATE TABLE esign_capture_task_images (" - + dialect.bigintIdentityPrimaryKey("id") - + ", " - + "task_id " - + dialect.varchar(36) - + " NOT NULL, " - + "slot_key " - + dialect.varchar(64) - + " NOT NULL, " - + "image_data " - + dialect.text() - + " NOT NULL, " - + "CONSTRAINT uq_esign_img_task_slot UNIQUE (task_id, slot_key), " - + "CONSTRAINT fk_esign_capture_img_task FOREIGN KEY (task_id) " - + "REFERENCES esign_capture_tasks (task_id) ON DELETE CASCADE)"); - jdbc.execute( - "CREATE INDEX ix_esign_capture_task_images_task ON esign_capture_task_images (task_id)"); - } - - private void add(String table, String column, String typeSql) { - if (!dialect.columnExists(jdbc, table, column)) { - log.info("Esign capture migrate: adding {}.{}", table, column); - jdbc.execute(dialect.alterAddColumn(table, column, typeSql)); - } - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/capture/controllers/EsignCaptureController.java b/src/main/java/com/sjmresorts/opsapp/esign/capture/controllers/EsignCaptureController.java deleted file mode 100644 index 08441b4..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/capture/controllers/EsignCaptureController.java +++ /dev/null @@ -1,177 +0,0 @@ -package com.sjmresorts.opsapp.esign.capture.controllers; - -import com.auth0.jwt.JWTVerifier; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.sjmresorts.opsapp.authentication.dtos.LoginUserInfo; -import com.sjmresorts.opsapp.common.utils.JwtTokenUtil; -import com.sjmresorts.opsapp.common.utils.NetworkUtil; -import com.sjmresorts.opsapp.esign.capture.dtos.EsignIosCaptureResponse; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPendingListResponse; -import com.sjmresorts.opsapp.esign.reservation.dtos.EsignReservationTaskCompleteRequest; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignSignTaskRequest; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignTaskDetailResponse; -import com.sjmresorts.opsapp.esign.capture.services.EsignCaptureService; -import com.sjmresorts.opsapp.esign.reservation.services.EsignReservationTaskService; -import com.sjmresorts.opsapp.esign.auth.services.EsignUserPresenceService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.annotation.Resource; -import jakarta.servlet.http.HttpServletRequest; -import org.apache.commons.lang3.StringUtils; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.CrossOrigin; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -import java.util.Map; - -@Tag(name = "Esign", description = "证件采集与待签任务") -@RestController -@RequestMapping({"/api/v1/esign", "/api/v1/sinosecu"}) -@CrossOrigin(origins = "*") -public class EsignCaptureController { - - @Resource - private EsignCaptureService esignCaptureService; - - @Resource - private EsignReservationTaskService esignReservationTaskService; - - @Resource - private EsignUserPresenceService userPresenceService; - - private Long resolveUserIdAndTouchClientIp(HttpServletRequest request) { - Long userId = resolveUserIdIfVerifiedJwt(request); - if (userId != null) { - userPresenceService.touchClientIp(userId, NetworkUtil.getClientIpByRequest(request)); - } - return userId; - } - - @Operation(summary = "提交证件采集结果(待 iOS 签名)", description = "免 JWT;任务进入待签池,由 GET ios-pending-tasks 广播拉取(不按工号过滤)。") - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "创建成功"), - @ApiResponse(responseCode = "500", description = "服务器错误") - }) - @PostMapping(value = "/ios-capture-submit", produces = "application/json;charset=UTF-8") - public ResponseEntity submitIosCapture( - HttpServletRequest request, - @RequestBody Map body) { - String employeeId = resolveEmployeeIdIfVerifiedJwt(request); - - try { - String taskId = esignCaptureService.createTask(employeeId, body); - EsignIosCaptureResponse ok = new EsignIosCaptureResponse(); - ok.setSuccess(true); - ok.setCode("200"); - ok.setMessage("Capture task created"); - EsignIosCaptureResponse.EsignIosCaptureData data = new EsignIosCaptureResponse.EsignIosCaptureData(); - data.setTaskId(taskId); - ok.setData(data); - return ResponseEntity.status(HttpStatus.OK).body(ok); - } catch (JsonProcessingException e) { - EsignIosCaptureResponse err = new EsignIosCaptureResponse(); - err.setSuccess(false); - err.setCode("500"); - err.setMessage("Failed to serialize payload"); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(err); - } - } - - @Operation(summary = "待签任务列表", description = "带有效 JWT 时仅返回推送给当前平板账号的虚拟打印机 PDF 待签;无 JWT 时广播证件/预订待签。可通过 hotelId 筛选酒店。") - @GetMapping(value = "/ios-pending-tasks", produces = "application/json;charset=UTF-8") - public ResponseEntity listPendingTasks( - HttpServletRequest request, - @RequestParam(value = "hotelId", required = false) String hotelId) { - Long userId = resolveUserIdAndTouchClientIp(request); - String clientIp = NetworkUtil.getClientIpByRequest(request); - EsignPendingListResponse body = esignCaptureService.listTaskBucket("pending", hotelId, userId, clientIp); - return ResponseEntity.status(HttpStatus.OK).body(body); - } - - @Operation(summary = "任务列表按桶查询", description = "带有效 JWT 时仅返回当前平板账号的虚拟打印机 PDF;无 JWT 时广播证件/预订。bucket=pending|signed|voided。") - @GetMapping(value = "/ios-task-list", produces = "application/json;charset=UTF-8") - public ResponseEntity listTasksByBucket( - HttpServletRequest request, - @RequestParam(value = "bucket", required = false) String bucket, - @RequestParam(value = "hotelId", required = false) String hotelId) { - Long userId = resolveUserIdAndTouchClientIp(request); - String clientIp = NetworkUtil.getClientIpByRequest(request); - EsignPendingListResponse body = esignCaptureService.listTaskBucket(bucket, hotelId, userId, clientIp); - return ResponseEntity.status(HttpStatus.OK).body(body); - } - - @Operation(summary = "iOS 签收提交", description = "免 JWT。仅待签收且创建未超 24 小时可提交。") - @PostMapping(value = "/ios-task-sign", consumes = "application/json;charset=UTF-8", produces = "application/json;charset=UTF-8") - public ResponseEntity signTask(@RequestBody EsignSignTaskRequest request) { - String taskId = request != null ? request.getTaskId() : null; - String remark = request != null ? request.getSignRemark() : null; - String signature = request != null ? request.getSignSignature() : null; - EsignIosCaptureResponse body = esignCaptureService.signTask(taskId, remark, signature); - return ResponseEntity.status(HttpStatus.OK).body(body); - } - - @Operation(summary = "预订待处理任务完成", description = "免 JWT。用户打开登记卡后调用,从待签列表移除。") - @PostMapping(value = "/ios-reservation-task-complete", consumes = "application/json;charset=UTF-8", produces = "application/json;charset=UTF-8") - public ResponseEntity completeReservationTask( - @RequestBody EsignReservationTaskCompleteRequest request) { - String taskId = request != null ? request.getTaskId() : null; - String reservationId = request != null ? request.getReservationId() : null; - String hotelId = request != null ? request.getHotelId() : null; - EsignIosCaptureResponse body = - esignReservationTaskService.completeTask(taskId, reservationId, hotelId); - return ResponseEntity.status(HttpStatus.OK).body(body); - } - - @Operation(summary = "待签任务详情", description = "免 JWT。返回落库时的完整 JSON payload,供 App 展示表单。") - @GetMapping(value = "/ios-task/{taskId}", produces = "application/json;charset=UTF-8") - public ResponseEntity getTaskDetail(@PathVariable("taskId") String taskId) { - EsignTaskDetailResponse body = esignCaptureService.getTaskDetail(taskId); - if (!body.isSuccess() && "404".equals(body.getCode())) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(body); - } - if (!body.isSuccess() && "500".equals(body.getCode())) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(body); - } - if (!body.isSuccess()) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(body); - } - return ResponseEntity.status(HttpStatus.OK).body(body); - } - - /** 白名单接口:仅在校验通过后才从 JWT 取 employeeId,避免伪造。 */ - private static String resolveEmployeeIdIfVerifiedJwt(HttpServletRequest request) { - Long userId = resolveUserIdIfVerifiedJwt(request); - return userId != null ? String.valueOf(userId) : null; - } - - private static Long resolveUserIdIfVerifiedJwt(HttpServletRequest request) { - String auth = request.getHeader("Authorization"); - if (StringUtils.isBlank(auth) || !auth.startsWith("Bearer ")) { - return null; - } - String token = auth.substring(7).trim(); - if (StringUtils.isBlank(token)) { - return null; - } - try { - JWTVerifier jwtVerifier = JwtTokenUtil.getJwtVerifier(); - jwtVerifier.verify(token); - LoginUserInfo user = JwtTokenUtil.parseUserFromToken(token); - if (user == null || StringUtils.isBlank(user.getEmployeeId())) { - return null; - } - return Long.parseLong(user.getEmployeeId()); - } catch (Exception e) { - return null; - } - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/capture/controllers/EsignLogController.java b/src/main/java/com/sjmresorts/opsapp/esign/capture/controllers/EsignLogController.java deleted file mode 100644 index 29e559a..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/capture/controllers/EsignLogController.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.sjmresorts.opsapp.esign.capture.controllers; - -import com.sjmresorts.opsapp.authentication.dtos.LoginUserInfo; -import com.sjmresorts.opsapp.common.utils.JwtTokenUtil; -import com.sjmresorts.opsapp.esign.capture.dtos.CreateEsignLogRequest; -import com.sjmresorts.opsapp.esign.capture.dtos.CreateEsignLogResponse; -import com.sjmresorts.opsapp.esign.capture.dtos.EsignLogDetailDto; -import com.sjmresorts.opsapp.esign.capture.dtos.EsignLogListDto; -import com.sjmresorts.opsapp.esign.capture.dtos.EsignLogQueryRequest; -import com.sjmresorts.opsapp.esign.capture.models.EsignLog; -import com.sjmresorts.opsapp.esign.capture.services.EsignLogService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -@Tag(name = "Capture Logs", description = "证件采集日志(Capture Log);主路径 /api/v1/esign/capture-logs,兼容 /api/v1/logs") -@RestController -@RequestMapping({"/api/v1/esign/capture-logs", "/api/v1/logs"}) -@CrossOrigin(origins = "*") -@RequiredArgsConstructor -public class EsignLogController { - - private final EsignLogService esignLogService; - - @Operation( - summary = "写入证件采集日志", - description = "POST /api/v1/esign/capture-logs — 记录证件采集流程操作日志(/api/v1/logs 兼容至 2026-09-30)" - ) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "写入成功"), - @ApiResponse(responseCode = "400", description = "请求参数错误"), - @ApiResponse(responseCode = "500", description = "服务器内部错误") - }) - @PostMapping(produces = "application/json;charset=UTF-8") - public ResponseEntity createLog( - @Valid @RequestBody CreateEsignLogRequest request, - HttpServletRequest httpRequest) { // 添加 httpRequest 参数 - try { - // 从 token 中获取 operatorId(即 employeeId) - String operatorId = extractOperatorIdFromToken(httpRequest); - EsignLog saved = esignLogService.createLog(request, operatorId); - return ResponseEntity.ok(CreateEsignLogResponse.ok(saved.getId())); - } catch (Exception e) { - return ResponseEntity - .status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(CreateEsignLogResponse.fail(500, "日志写入失败: " + e.getMessage())); - } - } - - // ==================== 分页列表接口 ==================== - @Operation(summary = "查询submit日志列表(有效/无效批次)") - @GetMapping("/list") - public ResponseEntity> list( - @Valid EsignLogQueryRequest request, - @RequestParam(value = "hotelId", required = false) String hotelId) { - return ResponseEntity.ok(esignLogService.querySubmitLogs(request, hotelId)); - } - - // ==================== 详情接口 ==================== - @Operation(summary = "日志详情(左右展示ORC/RFID)") - @GetMapping("/detail/{id}") - public ResponseEntity detail(@PathVariable Long id) { - return ResponseEntity.ok(esignLogService.getLogDetail(id)); - } - - /** - * 从请求头的 JWT token 中提取 operatorId(即 employeeId) - */ - private String extractOperatorIdFromToken(HttpServletRequest request) { - try { - LoginUserInfo user = JwtTokenUtil.parseUserFromRequest(request); - if (user != null) { - return user.getEmployeeId(); - } - } catch (Exception e) { - // token 解析失败,返回 null - } - return null; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/capture/dtos/CreateEsignLogRequest.java b/src/main/java/com/sjmresorts/opsapp/esign/capture/dtos/CreateEsignLogRequest.java deleted file mode 100644 index f6ac51f..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/capture/dtos/CreateEsignLogRequest.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.sjmresorts.opsapp.esign.capture.dtos; - -import com.fasterxml.jackson.databind.PropertyNamingStrategies; -import com.fasterxml.jackson.databind.annotation.JsonNaming; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) -@Schema(description = "写入 esign_log 请求") -public class CreateEsignLogRequest { - - @NotBlank(message = "batch_id 不能为空") - @Schema(description = "业务批次ID (UUID v4)", example = "f47ac10b-58cc-4372-a567-0e02b2c3d479") - private String batchId; - - @NotBlank(message = "action_type 不能为空") - @Schema(description = "操作类型: scan_front / scan_back / scan / submit", example = "scan_front") - private String actionType; - - // 新字段 - @Schema(description = "OCR识别当面完整数据(JSON)") - private String orcPayload; - - @Schema(description = "RFID芯片读取数据(JSON),暂未使用") - private String rfidPayload; - - @Schema(description = "酒店ID", example = "hotel_001") - private String hotelId; - - @Schema(description = "任务ID", example = "task_123") - private String taskId; - - @NotBlank(message = "status 不能为空") - @Schema(description = "状态: success / failed", example = "success") - private String status; - - @Schema(description = "错误信息") - private String errorMsg; - - @Schema(description = "操作员ID", example = "op_001") - private String operatorId; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/capture/dtos/CreateEsignLogResponse.java b/src/main/java/com/sjmresorts/opsapp/esign/capture/dtos/CreateEsignLogResponse.java deleted file mode 100644 index 32ae15f..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/capture/dtos/CreateEsignLogResponse.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.sjmresorts.opsapp.esign.capture.dtos; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@Schema(description = "写入 esign_log 响应") -public class CreateEsignLogResponse { - - @Schema(description = "状态码", example = "200") - private int code; - - @Schema(description = "说明", example = "ok") - private String msg; - - @Schema(description = "业务数据") - private LogData data; - - @Data - @NoArgsConstructor - public static class LogData { - @Schema(description = "日志ID", example = "1") - private Long id; - } - - public static CreateEsignLogResponse ok(Long id) { - CreateEsignLogResponse resp = new CreateEsignLogResponse(); - resp.setCode(200); - resp.setMsg("ok"); - LogData d = new LogData(); - d.setId(id); - resp.setData(d); - return resp; - } - - public static CreateEsignLogResponse fail(int code, String msg) { - CreateEsignLogResponse resp = new CreateEsignLogResponse(); - resp.setCode(code); - resp.setMsg(msg); - return resp; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/capture/dtos/EsignIosCaptureResponse.java b/src/main/java/com/sjmresorts/opsapp/esign/capture/dtos/EsignIosCaptureResponse.java deleted file mode 100644 index fa1f3c9..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/capture/dtos/EsignIosCaptureResponse.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.sjmresorts.opsapp.esign.capture.dtos; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@Schema(description = "证件采集提交结果") -public class EsignIosCaptureResponse { - - @Schema(description = "是否成功") - private boolean success; - - @Schema(description = "状态码", example = "200") - private String code; - - @Schema(description = "说明") - private String message; - - @Schema(description = "业务数据") - private EsignIosCaptureData data; - - @Data - @NoArgsConstructor - @Schema(description = "提交后的任务标识") - public static class EsignIosCaptureData { - @Schema(description = "待签任务 ID,供 iOS 通知或拉取") - private String taskId; - - @Schema(description = "从 PDF 解析的 OPERA 确认号") - private String confirmationNumber; - - @Schema(description = "OHIP reservation id(linkId)") - private String reservationId; - - @Schema(description = "是否已提交至 OPERA fileAttachments") - private Boolean ohipUploaded; - - @Schema(description = "签名 PDF 是否已归档至 Java 服务") - private Boolean archived; - - @Schema(description = "PDF 签收后自动打开的问卷 ID") - private String nextSurveyId; - - @Schema(description = "PDF 签收后自动打开的问卷标题") - private String nextSurveyTitle; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/capture/dtos/EsignLogDetailDto.java b/src/main/java/com/sjmresorts/opsapp/esign/capture/dtos/EsignLogDetailDto.java deleted file mode 100644 index a4763db..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/capture/dtos/EsignLogDetailDto.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.sjmresorts.opsapp.esign.capture.dtos; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import java.time.LocalDateTime; - -@Data -@Schema(description = "日志详情返回对象") -public class EsignLogDetailDto { - - @Schema(description = "日志ID") - private Long id; - - @Schema(description = "批次号") - private String batchId; - - @Schema(description = "操作类型") - private String actionType; - - @Schema(description = "OCR识别原始数据(左侧展示)", example = "{\"name\":\"张三\",\"idNo\":\"110***...\"}") - private String orcPayload; - - @Schema(description = "RFID读卡原始数据(右侧展示)", example = "{\"cardNo\":\"CARD***...\"}") - private String rfidPayload; - - @Schema(description = "酒店ID") - private String hotelId; - - @Schema(description = "任务ID") - private String taskId; - - @Schema(description = "状态") - private String status; - - @Schema(description = "错误信息") - private String errorMsg; - - @Schema(description = "操作人ID") - private String operatorId; - - @Schema(description = "创建时间") - private LocalDateTime createdAt; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/capture/dtos/EsignLogListDto.java b/src/main/java/com/sjmresorts/opsapp/esign/capture/dtos/EsignLogListDto.java deleted file mode 100644 index fb199a3..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/capture/dtos/EsignLogListDto.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.sjmresorts.opsapp.esign.capture.dtos; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import java.time.LocalDateTime; - -@Data -@Schema(description = "日志列表返回对象") -public class EsignLogListDto { - - @Schema(description = "日志ID") - private Long id; - - @Schema(description = "批次号") - private String batchId; - - @Schema(description = "操作类型") - private String actionType; - - @Schema(description = "酒店ID") - private String hotelId; - - @Schema(description = "任务ID") - private String taskId; - - @Schema(description = "状态") - private String status; - - @Schema(description = "错误信息") - private String errorMsg; - - @Schema(description = "操作人ID") - private String operatorId; - - @Schema(description = "创建时间") - private LocalDateTime createdAt; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/capture/dtos/EsignLogQueryRequest.java b/src/main/java/com/sjmresorts/opsapp/esign/capture/dtos/EsignLogQueryRequest.java deleted file mode 100644 index eef06fd..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/capture/dtos/EsignLogQueryRequest.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.sjmresorts.opsapp.esign.capture.dtos; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import jakarta.validation.constraints.Max; -import jakarta.validation.constraints.Min; - -@Data -@Schema(description = "日志列表查询请求") -public class EsignLogQueryRequest { - - @Schema(description = "页码 从1开始", example = "1") - @Min(1) - private int page = 1; - - @Schema(description = "每页条数", example = "10") - @Min(1) @Max(100) - private int size = 10; - - @Schema(description = "类型:valid-有效批次 success / invalid-无效批次 fail", example = "valid") - private String type; // valid / invalid - - private String taskId; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/capture/mappers/EsignLogMapper.java b/src/main/java/com/sjmresorts/opsapp/esign/capture/mappers/EsignLogMapper.java deleted file mode 100644 index 36d7c66..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/capture/mappers/EsignLogMapper.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.sjmresorts.opsapp.esign.capture.mappers; - -import com.sjmresorts.opsapp.esign.capture.dtos.EsignLogDetailDto; -import com.sjmresorts.opsapp.esign.capture.dtos.EsignLogListDto; -import com.sjmresorts.opsapp.esign.capture.models.EsignLog; -import org.mapstruct.Mapper; -import org.mapstruct.ReportingPolicy; - -import java.util.List; - -@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE) -public interface EsignLogMapper { - - // 列表 DTO - EsignLogListDto toEsignLogListDto(EsignLog esignLog); - - List toEsignLogListDtoList(List esignLogList); - - // 详情 DTO - EsignLogDetailDto toEsignLogDetailDto(EsignLog esignLog); -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/capture/models/EsignCaptureTask.java b/src/main/java/com/sjmresorts/opsapp/esign/capture/models/EsignCaptureTask.java deleted file mode 100644 index 30c2364..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/capture/models/EsignCaptureTask.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.sjmresorts.opsapp.esign.capture.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_capture_tasks") -public class EsignCaptureTask { - - @Id - @Column(name = "task_id", nullable = false, length = 36) - private String taskId; - - @Lob - @Column(name = "payload_json", nullable = false) - private String payloadJson; - - @Column(name = "employee_id", length = 128) - private String employeeId; - - @Column(name = "status", nullable = false, length = 32) - private String status; - - @Column(name = "created_at", nullable = false) - private Instant createdAt; - - /** 签收完成时间;仅 status=SIGNED 时有值 */ - @Column(name = "signed_at") - private Instant signedAt; - - /** 签收备注(App 提交,可选文字) */ - @Column(name = "sign_remark", length = 2000) - private String signRemark; - - /** 客户手写签名(PNG Base64 data URL) */ - @Lob - @Column(name = "sign_signature") - private String signSignature; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/capture/models/EsignCaptureTaskImage.java b/src/main/java/com/sjmresorts/opsapp/esign/capture/models/EsignCaptureTaskImage.java deleted file mode 100644 index 398401f..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/capture/models/EsignCaptureTaskImage.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.sjmresorts.opsapp.esign.capture.models; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.Lob; -import jakarta.persistence.Table; -import jakarta.persistence.UniqueConstraint; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -/** - * 採集影像等大字段与主任务表分离,减轻列表/解析 payload 时的 IO 与内存。 - */ -@Getter -@Setter -@NoArgsConstructor -@Entity -@Table( - name = "esign_capture_task_images", - uniqueConstraints = @UniqueConstraint(name = "uq_esign_img_task_slot", columnNames = {"task_id", "slot_key"}) -) -public class EsignCaptureTaskImage { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "id", nullable = false) - private Long id; - - @Column(name = "task_id", nullable = false, length = 36) - private String taskId; - - @Column(name = "slot_key", nullable = false, length = 64) - private String slotKey; - - @Lob - @Column(name = "image_data", nullable = false) - private String imageData; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/capture/models/EsignLog.java b/src/main/java/com/sjmresorts/opsapp/esign/capture/models/EsignLog.java deleted file mode 100644 index 9be9c03..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/capture/models/EsignLog.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.sjmresorts.opsapp.esign.capture.models; - -import jakarta.persistence.*; -import lombok.Data; - -import java.time.LocalDateTime; - -@Entity -@Table(name = "esign_log") -@Data -public class EsignLog { - - @Id - @Column(name = "id", nullable = false) - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(name = "batch_id", nullable = false, length = 64) - private String batchId; - - @Column(name = "action_type", nullable = false, length = 32) - private String actionType; - - // 替换原来的 person_name / person_id_number - @Lob - @Column(name = "orc_payload") - private String orcPayload; - - @Lob - @Column(name = "rfid_payload") - private String rfidPayload; - - @Column(name = "hotel_id", length = 64) - private String hotelId; - - @Column(name = "task_id", length = 128) - private String taskId; - - @Column(name = "status", nullable = false, length = 32) - private String status; - - @Lob - @Column(name = "error_msg") - private String errorMsg; - - @Column(name = "operator_id", length = 64) - private String operatorId; - - @Column(name = "created_at", nullable = false) - private LocalDateTime createdAt; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/capture/repositories/EsignCaptureTaskImageRepository.java b/src/main/java/com/sjmresorts/opsapp/esign/capture/repositories/EsignCaptureTaskImageRepository.java deleted file mode 100644 index d7ef639..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/capture/repositories/EsignCaptureTaskImageRepository.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.sjmresorts.opsapp.esign.capture.repositories; - -import com.sjmresorts.opsapp.esign.capture.models.EsignCaptureTaskImage; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.List; -import java.util.Optional; - -@Repository -public interface EsignCaptureTaskImageRepository extends JpaRepository { - - List findByTaskIdOrderBySlotKeyAsc(String taskId); - - Optional findFirstByTaskIdAndSlotKey(String taskId, String slotKey); -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/capture/repositories/EsignCaptureTaskRepository.java b/src/main/java/com/sjmresorts/opsapp/esign/capture/repositories/EsignCaptureTaskRepository.java deleted file mode 100644 index 04145b6..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/capture/repositories/EsignCaptureTaskRepository.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.sjmresorts.opsapp.esign.capture.repositories; - -import com.sjmresorts.opsapp.esign.capture.models.EsignCaptureTask; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.time.Instant; -import java.util.List; - -@Repository -public interface EsignCaptureTaskRepository extends JpaRepository { - - /** 待签收:创建时间在最近 24 小时内(含边界) */ - List findTop100ByStatusAndCreatedAtGreaterThanEqualOrderByCreatedAtDesc( - String status, Instant createdAtMinInclusive); - - /** 已签收 */ - List findTop100ByStatusOrderBySignedAtDesc(String status); - - /** 已作废:仍为待签状态但创建已超过 24 小时 */ - List findTop100ByStatusAndCreatedAtLessThanOrderByCreatedAtDesc( - String status, Instant createdAtMaxExclusive); -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/capture/repositories/EsignLogRepository.java b/src/main/java/com/sjmresorts/opsapp/esign/capture/repositories/EsignLogRepository.java deleted file mode 100644 index 2f224f3..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/capture/repositories/EsignLogRepository.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.sjmresorts.opsapp.esign.capture.repositories; - -import com.sjmresorts.opsapp.esign.capture.models.EsignLog; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; -import org.springframework.stereotype.Repository; - -import java.util.List; - -@Repository -public interface EsignLogRepository extends JpaRepository { - - List findByActionType(String actionType); - - EsignLog findByTaskId(String taskId); - - @Query("SELECT l FROM EsignLog l WHERE l.batchId IN :batchIds") - Page findByBatchIds(@Param("batchIds") List batchIds, Pageable pageable); - - // 查询所有不重复批次 - @Query("SELECT DISTINCT l.batchId FROM EsignLog l") - List findAllBatchIds(); - - - @Query("SELECT DISTINCT l.batchId FROM EsignLog l WHERE l.hotelId = :hotelId") - List findAllBatchIdsByHotelId(@Param("hotelId") String hotelId); - - - @Query("SELECT l FROM EsignLog l WHERE l.hotelId = :hotelId AND l.actionType = :actionType") - List findByHotelIdAndActionType(@Param("hotelId") String hotelId, @Param("actionType") String actionType); - - - @Query("SELECT l FROM EsignLog l WHERE l.batchId IN :batchIds AND l.hotelId = :hotelId") - Page findByBatchIdsAndHotelId(@Param("batchIds") List batchIds, @Param("hotelId") String hotelId, Pageable pageable); -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/capture/services/EsignCaptureService.java b/src/main/java/com/sjmresorts/opsapp/esign/capture/services/EsignCaptureService.java deleted file mode 100644 index 7d43d14..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/capture/services/EsignCaptureService.java +++ /dev/null @@ -1,718 +0,0 @@ -package com.sjmresorts.opsapp.esign.capture.services; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.sjmresorts.opsapp.esign.capture.dtos.EsignIosCaptureResponse; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPendingListResponse; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignTaskDetailResponse; -import com.sjmresorts.opsapp.esign.capture.models.EsignCaptureTask; -import com.sjmresorts.opsapp.esign.capture.models.EsignCaptureTaskImage; -import com.sjmresorts.opsapp.esign.capture.repositories.EsignCaptureTaskImageRepository; -import com.sjmresorts.opsapp.esign.capture.repositories.EsignCaptureTaskRepository; -import com.sjmresorts.opsapp.esign.printer.services.EsignPrinterPdfTaskService; -import com.sjmresorts.opsapp.esign.reservation.services.EsignReservationTaskService; -import com.sjmresorts.opsapp.esign.survey.services.EsignSurveyPushService; -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.time.ZoneId; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.time.temporal.ChronoUnit; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; -import java.util.concurrent.ThreadLocalRandom; -import java.util.regex.Pattern; - -@Service -public class EsignCaptureService { - - public static final String STATUS_PENDING_SIGNATURE = "PENDING_SIGNATURE"; - public static final String STATUS_SIGNED = "SIGNED"; - - private static final int SIGN_WINDOW_HOURS = 24; - /** 待签列表 API 最多返回条数(证件 + 预订 + PC 打印合并后) */ - private static final int PENDING_LIST_MAX_ITEMS = 50; - - private static final int SIGN_SIGNATURE_MAX_LEN = 800_000; - - /** 採集 JSON 中占位符:詳情接口從 esign_capture_task_images 還原像素數據 */ - public static final String PAYLOAD_IMAGE_DB_MARKER = "__db_image__"; - - /** 客戶手寫簽名在影像表中的 slot(與 sign_signature 欄位雙寫,便於詳情還原) */ - public static final String SIGNATURE_IMAGE_SLOT = "sign.customer"; - - private static final int INLINE_IMAGE_OFFLOAD_MIN_LEN = 480; - - /** 系統檔案編號:澳門本地日期時間 + 8 位隨機數,寫入 payload.systemRefNo */ - private static final ZoneId SYSTEM_REF_ZONE = ZoneId.of("Asia/Macau"); - - @Resource - private EsignCaptureTaskRepository esignCaptureTaskRepository; - - @Resource - private EsignCaptureTaskImageRepository esignCaptureTaskImageRepository; - - @Resource - private ObjectMapper objectMapper; - - @Resource - private EsignReservationTaskService esignReservationTaskService; - - @Resource - private EsignPrinterPdfTaskService esignPrinterPdfTaskService; - - @Resource - private EsignSurveyPushService esignSurveyPushService; - - private Instant signWindowCutoff() { - return Instant.now().minus(SIGN_WINDOW_HOURS, ChronoUnit.HOURS); - } - - /** yyyyMMddHHmmss + 8 位數字(0–99999999) */ - private String newSystemRefNo() { - String dt = ZonedDateTime.now(SYSTEM_REF_ZONE).format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); - int rnd = ThreadLocalRandom.current().nextInt(100_000_000); - return dt + String.format("%08d", rnd); - } - - @Transactional - public String createTask(String employeeId, Map payload) throws JsonProcessingException { - String taskId = UUID.randomUUID().toString(); - Map stored = - objectMapper.convertValue(payload, new TypeReference>() { - }); - stored.put("systemRefNo", newSystemRefNo()); - List imageRows = extractImageRowsAndStripPayload(taskId, stored); - - EsignCaptureTask entity = new EsignCaptureTask(); - entity.setTaskId(taskId); - entity.setPayloadJson(objectMapper.writeValueAsString(stored)); - entity.setEmployeeId(employeeId); - entity.setStatus(STATUS_PENDING_SIGNATURE); - entity.setCreatedAt(Instant.now()); - esignCaptureTaskRepository.save(entity); - if (!imageRows.isEmpty()) { - esignCaptureTaskImageRepository.saveAll(imageRows); - } - return taskId; - } - - /** - * 兼容旧接口:仅返回 24 小时内待签收任务。 - */ - public EsignPendingListResponse listPendingBroadcast() { - return listTaskBucket("pending", null); - } - - /** - * @param bucket pending | signed | voided - */ - public EsignPendingListResponse listTaskBucket(String bucket) { - return listTaskBucket(bucket, null); - } - - /** - * @param bucket pending | signed | voided - * @param hotelId 酒店ID,为空时不按酒店筛选 - */ - @Transactional(readOnly = true) - public EsignPendingListResponse listTaskBucket(String bucket, String hotelId) { - return listTaskBucket(bucket, hotelId, null); - } - - public EsignPendingListResponse listTaskBucket(String bucket, String hotelId, Long userId) { - return listTaskBucket(bucket, hotelId, userId, null); - } - - @Transactional(readOnly = true) - public EsignPendingListResponse listTaskBucket( - String bucket, String hotelId, Long userId, String clientIp) { - EsignPendingListResponse out = new EsignPendingListResponse(); - out.setMessage("ok"); - EsignPendingListResponse.EsignPendingData data = new EsignPendingListResponse.EsignPendingData(); - out.setData(data); - String b = StringUtils.isBlank(bucket) ? "pending" : bucket.trim().toLowerCase(); - List items = new ArrayList<>(); - // 已登入 App:僅返回推送給當前平板賬號的虛擬打印機 PDF;證件/預訂廣播僅在未帶 JWT 時保留 - if (userId == null) { - Instant cutoff = signWindowCutoff(); - List rows; - if ("signed".equals(b)) { - rows = esignCaptureTaskRepository.findTop100ByStatusOrderBySignedAtDesc(STATUS_SIGNED); - } else if ("voided".equals(b)) { - rows = esignCaptureTaskRepository.findTop100ByStatusAndCreatedAtLessThanOrderByCreatedAtDesc( - STATUS_PENDING_SIGNATURE, cutoff); - } else { - rows = esignCaptureTaskRepository.findTop100ByStatusAndCreatedAtGreaterThanEqualOrderByCreatedAtDesc( - STATUS_PENDING_SIGNATURE, cutoff); - } - boolean filterByHotel = StringUtils.isNotBlank(hotelId); - for (EsignCaptureTask row : rows) { - String payloadJson = row.getPayloadJson(); - - if (filterByHotel) { - String taskHotelId = readSelectedHotelId(payloadJson); - if (!hotelId.equals(taskHotelId)) { - continue; - } - } - - EsignPendingListResponse.EsignPendingItem it = new EsignPendingListResponse.EsignPendingItem(); - it.setTaskId(row.getTaskId()); - it.setCreatedAt(row.getCreatedAt() != null ? row.getCreatedAt().toString() : null); - it.setDocTitle(readDocTitle(row.getPayloadJson())); - it.setGuestName(readGuestName(payloadJson)); - it.setSelectedHotelNameZh(readSelectedHotelNameZh(payloadJson)); - it.setGuestNameZh(readGuestNameZh(payloadJson)); - it.setGuestNameEn(readGuestNameEn(payloadJson)); - it.setSignedAt(row.getSignedAt() != null ? row.getSignedAt().toString() : null); - it.setTaskType("CAPTURE"); - items.add(it); - } - List reservationItems = - esignReservationTaskService.listItemsForBucket(b, hotelId); - EsignReservationTaskService.mergeSortedByCreatedAt(items, reservationItems); - } - List printerItems = - esignPrinterPdfTaskService.listItemsForBucket(b, hotelId, userId, clientIp); - EsignReservationTaskService.mergeSortedByCreatedAt(items, printerItems); - if ("pending".equals(b) && userId != null) { - List surveyItems = - esignSurveyPushService.listPendingItemsForUser(userId, clientIp, hotelId); - EsignReservationTaskService.mergeSortedByCreatedAt(items, surveyItems); - } - if ("pending".equals(b) && items.size() > PENDING_LIST_MAX_ITEMS) { - items.subList(PENDING_LIST_MAX_ITEMS, items.size()).clear(); - } - data.setTasks(items); - return out; - } - - /** - * 从 payload_json 中读取 selectedHotelId - */ - private String readSelectedHotelId(String payloadJson) { - if (StringUtils.isBlank(payloadJson)) { - return null; - } - try { - JsonNode root = objectMapper.readTree(payloadJson); - JsonNode n = root.get("selectedHotelId"); - if (n != null && !n.isNull() && n.isTextual()) { - String t = n.asText("").trim(); - return StringUtils.isNotBlank(t) ? t : null; - } - } catch (Exception e) { - return null; - } - return null; - } - - @Transactional - public EsignIosCaptureResponse signTask(String taskId, String signRemark, String signSignature) { - EsignIosCaptureResponse resp = new EsignIosCaptureResponse(); - if (StringUtils.isBlank(taskId)) { - resp.setSuccess(false); - resp.setCode("400"); - resp.setMessage("taskId required"); - return resp; - } - if (StringUtils.isBlank(signSignature)) { - resp.setSuccess(false); - resp.setCode("400"); - resp.setMessage("请提供手写签名"); - return resp; - } - String sig = signSignature.trim(); - if (!sig.regionMatches(true, 0, "data:image/", 0, 11)) { - resp.setSuccess(false); - resp.setCode("400"); - resp.setMessage("签名格式无效"); - return resp; - } - if (sig.length() > SIGN_SIGNATURE_MAX_LEN) { - resp.setSuccess(false); - resp.setCode("400"); - resp.setMessage("签名数据过大"); - return resp; - } - Optional opt = esignCaptureTaskRepository.findById(taskId.trim()); - if (opt.isEmpty()) { - resp.setSuccess(false); - resp.setCode("404"); - resp.setMessage("Task not found"); - return resp; - } - EsignCaptureTask t = opt.get(); - if (!STATUS_PENDING_SIGNATURE.equals(t.getStatus())) { - resp.setSuccess(false); - resp.setCode("409"); - resp.setMessage("任务已签收或状态不可签收"); - return resp; - } - Instant cutoff = signWindowCutoff(); - if (t.getCreatedAt() != null && t.getCreatedAt().isBefore(cutoff)) { - resp.setSuccess(false); - resp.setCode("410"); - resp.setMessage("已超过24小时未签收,任务已作废"); - return resp; - } - t.setStatus(STATUS_SIGNED); - t.setSignedAt(Instant.now()); - t.setSignSignature(sig); - if (signRemark != null) { - String trimmed = signRemark.trim(); - if (StringUtils.isNotBlank(trimmed)) { - int max = 2000; - t.setSignRemark(trimmed.length() > max ? trimmed.substring(0, max) : trimmed); - } - } - esignCaptureTaskRepository.save(t); - persistCustomerSignature(t.getTaskId(), sig); - resp.setSuccess(true); - resp.setCode("200"); - resp.setMessage("Signed"); - EsignIosCaptureResponse.EsignIosCaptureData data = new EsignIosCaptureResponse.EsignIosCaptureData(); - data.setTaskId(t.getTaskId()); - resp.setData(data); - return resp; - } - - @Transactional(readOnly = true) - public EsignTaskDetailResponse getTaskDetail(String taskId) { - EsignTaskDetailResponse r = new EsignTaskDetailResponse(); - if (StringUtils.isBlank(taskId)) { - r.setSuccess(false); - r.setCode("400"); - r.setMessage("taskId required"); - return r; - } - Optional opt = esignCaptureTaskRepository.findById(taskId.trim()); - if (opt.isEmpty()) { - r.setSuccess(false); - r.setCode("404"); - r.setMessage("Task not found"); - return r; - } - EsignCaptureTask t = opt.get(); - try { - Map payload = objectMapper.readValue( - t.getPayloadJson(), - new TypeReference>() { - }); - EsignTaskDetailResponse.EsignTaskDetailData d = new EsignTaskDetailResponse.EsignTaskDetailData(); - d.setTaskId(t.getTaskId()); - d.setStatus(t.getStatus()); - d.setCreatedAt(t.getCreatedAt() != null ? t.getCreatedAt().toString() : null); - d.setSignedAt(t.getSignedAt() != null ? t.getSignedAt().toString() : null); - d.setSignSignature(resolveSignSignatureForDetail(t)); - d.setSignState(computeSignState(t)); - mergeStoredImagesIntoPayload(t.getTaskId(), payload); - d.setPayload(payload); - r.setSuccess(true); - r.setCode("200"); - r.setMessage("ok"); - r.setData(d); - } catch (Exception e) { - r.setSuccess(false); - r.setCode("500"); - r.setMessage("Failed to parse payload"); - } - return r; - } - - private String computeSignState(EsignCaptureTask t) { - if (STATUS_SIGNED.equals(t.getStatus())) { - return "signed"; - } - if (STATUS_PENDING_SIGNATURE.equals(t.getStatus())) { - Instant cutoff = signWindowCutoff(); - if (t.getCreatedAt() != null && t.getCreatedAt().isBefore(cutoff)) { - return "voided"; - } - return "pending"; - } - return "unknown"; - } - - private String readDocTitle(String payloadJson) { - if (StringUtils.isBlank(payloadJson)) { - return null; - } - try { - JsonNode root = objectMapper.readTree(payloadJson); - JsonNode n = root.get("docTitle"); - return n != null && !n.isNull() ? n.asText(null) : null; - } catch (Exception e) { - return null; - } - } - - private static final Pattern NAME_KEY_PATTERN = Pattern.compile( - "姓名|英文姓名|中文姓名|^名称$|住客|賓客|客人|Guest|^Name$", - Pattern.CASE_INSENSITIVE); - - private static final Pattern GUEST_NAME_ZH_KEY = Pattern.compile( - "中文姓名|中文名称|Chinese\\s*name|國語姓名|国语姓名", - Pattern.CASE_INSENSITIVE); - - private static final Pattern GUEST_NAME_EN_KEY = Pattern.compile( - "英文姓名|英文名称|English\\s*name|拼音|Pinyin|Surname|Given\\s*name", - Pattern.CASE_INSENSITIVE); - - private String readGuestName(String payloadJson) { - if (StringUtils.isBlank(payloadJson)) { - return null; - } - try { - JsonNode root = objectMapper.readTree(payloadJson); - String[] rowArrays = { - "formFrontRows", "formBackRows", "textRows", "formStampRows", "stampRows" - }; - for (String arrKey : rowArrays) { - JsonNode arr = root.get(arrKey); - if (arr == null || !arr.isArray()) { - continue; - } - for (JsonNode row : arr) { - if (row == null || !row.isObject()) { - continue; - } - JsonNode keyNode = row.get("key"); - JsonNode valueNode = row.get("value"); - if (keyNode == null || !keyNode.isTextual()) { - continue; - } - String k = keyNode.asText(""); - if (!NAME_KEY_PATTERN.matcher(k).find()) { - continue; - } - if (valueNode == null || valueNode.isNull()) { - continue; - } - String v = valueNode.isTextual() - ? valueNode.asText("").trim() - : valueNode.asText().trim(); - if (StringUtils.isNotBlank(v)) { - return v; - } - } - } - } catch (Exception e) { - return null; - } - return null; - } - - private String readSelectedHotelNameZh(String payloadJson) { - if (StringUtils.isBlank(payloadJson)) { - return null; - } - try { - JsonNode root = objectMapper.readTree(payloadJson); - JsonNode n = root.get("selectedHotelNameZh"); - if (n != null && !n.isNull() && n.isTextual()) { - String t = n.asText("").trim(); - return StringUtils.isNotBlank(t) ? t : null; - } - } catch (Exception e) { - return null; - } - return null; - } - - private String readGuestNameEn(String payloadJson) { - return readGuestValueByKeyPattern(payloadJson, GUEST_NAME_EN_KEY); - } - - /** - * 中文姓名:优先「中文姓名」等;否则取首个「姓名类」且非明显英文栏之值;仍无则回退 {@link #readGuestName}。 - */ - private String readGuestNameZh(String payloadJson) { - String zh = readGuestValueByKeyPattern(payloadJson, GUEST_NAME_ZH_KEY); - if (StringUtils.isNotBlank(zh)) { - return zh; - } - String generic = readGuestNameExcludingEnKey(payloadJson); - if (StringUtils.isNotBlank(generic)) { - return generic; - } - if (StringUtils.isNotBlank(readGuestNameEn(payloadJson))) { - return null; - } - return readGuestName(payloadJson); - } - - private String readGuestValueByKeyPattern(String payloadJson, Pattern keyPattern) { - if (StringUtils.isBlank(payloadJson)) { - return null; - } - try { - JsonNode root = objectMapper.readTree(payloadJson); - String[] rowArrays = { - "formFrontRows", "formBackRows", "textRows", "formStampRows", "stampRows" - }; - for (String arrKey : rowArrays) { - JsonNode arr = root.get(arrKey); - if (arr == null || !arr.isArray()) { - continue; - } - for (JsonNode row : arr) { - if (row == null || !row.isObject()) { - continue; - } - JsonNode keyNode = row.get("key"); - JsonNode valueNode = row.get("value"); - if (keyNode == null || !keyNode.isTextual()) { - continue; - } - String k = keyNode.asText(""); - if (!keyPattern.matcher(k).find()) { - continue; - } - if (valueNode == null || valueNode.isNull()) { - continue; - } - String v = valueNode.isTextual() - ? valueNode.asText("").trim() - : valueNode.asText().trim(); - if (StringUtils.isNotBlank(v)) { - return v; - } - } - } - } catch (Exception e) { - return null; - } - return null; - } - - /** 首个匹配「姓名类」且键名不像「英文姓名」专栏的行。 */ - private String readGuestNameExcludingEnKey(String payloadJson) { - if (StringUtils.isBlank(payloadJson)) { - return null; - } - try { - JsonNode root = objectMapper.readTree(payloadJson); - String[] rowArrays = { - "formFrontRows", "formBackRows", "textRows", "formStampRows", "stampRows" - }; - for (String arrKey : rowArrays) { - JsonNode arr = root.get(arrKey); - if (arr == null || !arr.isArray()) { - continue; - } - for (JsonNode row : arr) { - if (row == null || !row.isObject()) { - continue; - } - JsonNode keyNode = row.get("key"); - JsonNode valueNode = row.get("value"); - if (keyNode == null || !keyNode.isTextual()) { - continue; - } - String k = keyNode.asText(""); - if (!NAME_KEY_PATTERN.matcher(k).find()) { - continue; - } - if (GUEST_NAME_EN_KEY.matcher(k).find()) { - continue; - } - if (valueNode == null || valueNode.isNull()) { - continue; - } - String v = valueNode.isTextual() - ? valueNode.asText("").trim() - : valueNode.asText().trim(); - if (StringUtils.isNotBlank(v)) { - return v; - } - } - } - } catch (Exception e) { - return null; - } - return null; - } - - /** - * 將 images / bigImageSrc / faceThumbs 中的大字符串抽出到副表,並在 payload 中置為 {@link #PAYLOAD_IMAGE_DB_MARKER}。 - * 先改內存 payload,由調用方在保存主表後再 saveAll 返回的列表。 - */ - private List extractImageRowsAndStripPayload(String taskId, Map payload) { - List rows = new ArrayList<>(); - Object imgsObj = payload.get("images"); - if (imgsObj instanceof Map) { - @SuppressWarnings("unchecked") - Map imgs = (Map) imgsObj; - for (Map.Entry e : imgs.entrySet()) { - Object v = e.getValue(); - if (!(v instanceof String)) { - continue; - } - String s = ((String) v).trim(); - if (!shouldPersistImageString(s)) { - continue; - } - String slot = "img." + sanitizeSlotSegment(e.getKey()); - rows.add(newImageRow(taskId, slot, s)); - imgs.put(e.getKey(), PAYLOAD_IMAGE_DB_MARKER); - } - } - Object big = payload.get("bigImageSrc"); - if (big instanceof String) { - String s = ((String) big).trim(); - if (shouldPersistImageString(s)) { - rows.add(newImageRow(taskId, "big", s)); - payload.put("bigImageSrc", PAYLOAD_IMAGE_DB_MARKER); - } - } - Object facesObj = payload.get("faceThumbs"); - if (facesObj instanceof List) { - List faces = (List) facesObj; - for (int i = 0; i < faces.size(); i++) { - Object fo = faces.get(i); - if (!(fo instanceof Map)) { - continue; - } - @SuppressWarnings("unchecked") - Map fm = (Map) fo; - Object srcObj = fm.get("src"); - if (!(srcObj instanceof String)) { - continue; - } - String s = ((String) srcObj).trim(); - if (!shouldPersistImageString(s)) { - continue; - } - rows.add(newImageRow(taskId, "face." + i, s)); - fm.put("src", PAYLOAD_IMAGE_DB_MARKER); - } - } - return rows; - } - - private void persistCustomerSignature(String taskId, String imageData) { - if (StringUtils.isBlank(taskId) || StringUtils.isBlank(imageData)) { - return; - } - Optional existing = - esignCaptureTaskImageRepository.findFirstByTaskIdAndSlotKey(taskId, SIGNATURE_IMAGE_SLOT); - if (existing.isPresent()) { - EsignCaptureTaskImage row = existing.get(); - row.setImageData(imageData); - esignCaptureTaskImageRepository.save(row); - } else { - esignCaptureTaskImageRepository.save(newImageRow(taskId, SIGNATURE_IMAGE_SLOT, imageData)); - } - } - - private String resolveSignSignatureForDetail(EsignCaptureTask t) { - if (t == null) { - return null; - } - if (StringUtils.isNotBlank(t.getSignSignature())) { - return t.getSignSignature().trim(); - } - return esignCaptureTaskImageRepository - .findFirstByTaskIdAndSlotKey(t.getTaskId(), SIGNATURE_IMAGE_SLOT) - .map(EsignCaptureTaskImage::getImageData) - .filter(StringUtils::isNotBlank) - .map(String::trim) - .orElse(null); - } - - private void mergeStoredImagesIntoPayload(String taskId, Map payload) { - List list = esignCaptureTaskImageRepository.findByTaskIdOrderBySlotKeyAsc(taskId); - if (list.isEmpty()) { - return; - } - Map bySlot = new HashMap<>(); - for (EsignCaptureTaskImage r : list) { - bySlot.put(r.getSlotKey(), r.getImageData()); - } - Object imgsObj = payload.get("images"); - if (imgsObj instanceof Map) { - @SuppressWarnings("unchecked") - Map imgs = (Map) imgsObj; - for (Map.Entry e : imgs.entrySet()) { - if (!PAYLOAD_IMAGE_DB_MARKER.equals(e.getValue())) { - continue; - } - String slot = "img." + sanitizeSlotSegment(e.getKey()); - String data = bySlot.get(slot); - if (data != null) { - imgs.put(e.getKey(), data); - } - } - } - if (PAYLOAD_IMAGE_DB_MARKER.equals(payload.get("bigImageSrc"))) { - String data = bySlot.get("big"); - if (data != null) { - payload.put("bigImageSrc", data); - } - } - Object facesObj = payload.get("faceThumbs"); - if (facesObj instanceof List) { - List faces = (List) facesObj; - for (int i = 0; i < faces.size(); i++) { - Object fo = faces.get(i); - if (!(fo instanceof Map)) { - continue; - } - @SuppressWarnings("unchecked") - Map fm = (Map) fo; - if (!PAYLOAD_IMAGE_DB_MARKER.equals(fm.get("src"))) { - continue; - } - String data = bySlot.get("face." + i); - if (data != null) { - fm.put("src", data); - } - } - } - } - - private static EsignCaptureTaskImage newImageRow(String taskId, String slotKey, String imageData) { - EsignCaptureTaskImage im = new EsignCaptureTaskImage(); - im.setTaskId(taskId); - im.setSlotKey(slotKey); - im.setImageData(imageData); - return im; - } - - private static boolean shouldPersistImageString(String s) { - if (StringUtils.isBlank(s)) { - return false; - } - String t = s.trim(); - if ("__present__".equalsIgnoreCase(t) || "__PRESENT__".equals(t)) { - return false; - } - if (PAYLOAD_IMAGE_DB_MARKER.equals(t)) { - return false; - } - if (t.startsWith("data:image")) { - return true; - } - return t.length() >= INLINE_IMAGE_OFFLOAD_MIN_LEN; - } - - private static String sanitizeSlotSegment(String key) { - if (key == null || key.isEmpty()) { - return "x"; - } - String u = key.replaceAll("[^a-zA-Z0-9._-]", "_"); - return u.length() > 48 ? u.substring(0, 48) : u; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/capture/services/EsignLogService.java b/src/main/java/com/sjmresorts/opsapp/esign/capture/services/EsignLogService.java deleted file mode 100644 index adc666d..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/capture/services/EsignLogService.java +++ /dev/null @@ -1,114 +0,0 @@ -package com.sjmresorts.opsapp.esign.capture.services; - -import com.sjmresorts.opsapp.esign.capture.dtos.CreateEsignLogRequest; -import com.sjmresorts.opsapp.esign.capture.dtos.EsignLogDetailDto; -import com.sjmresorts.opsapp.esign.capture.dtos.EsignLogListDto; -import com.sjmresorts.opsapp.esign.capture.dtos.EsignLogQueryRequest; -import com.sjmresorts.opsapp.esign.capture.mappers.EsignLogMapper; -import com.sjmresorts.opsapp.esign.capture.models.EsignLog; -import com.sjmresorts.opsapp.esign.capture.repositories.EsignLogRepository; -import lombok.RequiredArgsConstructor; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.stereotype.Service; -import org.springframework.util.StringUtils; - -import java.time.LocalDateTime; -import java.util.List; -import java.util.stream.Collectors; - -@Service -@RequiredArgsConstructor -public class EsignLogService { - - private final Logger logger = LoggerFactory.getLogger(EsignLogService.class); - private final EsignLogRepository esignLogRepository; - private final EsignLogMapper esignLogMapper; - - - public EsignLog createLog(CreateEsignLogRequest request, String operatorId) { - EsignLog entity = new EsignLog(); - entity.setBatchId(request.getBatchId()); - entity.setActionType(request.getActionType()); - entity.setOrcPayload(request.getOrcPayload()); - entity.setRfidPayload(request.getRfidPayload()); - entity.setHotelId(request.getHotelId()); - entity.setTaskId(request.getTaskId()); - entity.setStatus(request.getStatus()); - entity.setErrorMsg(request.getErrorMsg()); - entity.setOperatorId(operatorId); - entity.setCreatedAt(LocalDateTime.now()); - return esignLogRepository.save(entity); - } - - - public Page querySubmitLogs(EsignLogQueryRequest req) { - return querySubmitLogs(req, null); - } - - public Page querySubmitLogs(EsignLogQueryRequest req, String hotelId) { - List targetBatchIds; - boolean filterByHotel = StringUtils.hasText(hotelId); - - if (req.getTaskId() != null && !req.getTaskId().trim().isEmpty()) { - EsignLog logByTask = esignLogRepository.findByTaskId(req.getTaskId()); - if (logByTask == null) { - return Page.empty(); - } - // 如果指定了酒店ID,检查任务所属酒店是否匹配 - if (filterByHotel && !hotelId.equals(logByTask.getHotelId())) { - return Page.empty(); - } - targetBatchIds = List.of(logByTask.getBatchId()); - } - else { - // 1. 所有批次 - List allBatchIds = filterByHotel - ? esignLogRepository.findAllBatchIdsByHotelId(hotelId) - : esignLogRepository.findAllBatchIds(); - - // 2. 有效批次(submit success) - List submitLogs = filterByHotel - ? esignLogRepository.findByHotelIdAndActionType(hotelId, "submit") - : esignLogRepository.findByActionType("submit"); - - List validBatchIds = submitLogs.stream() - .filter(log -> "success".equals(log.getStatus())) - .map(EsignLog::getBatchId) - .toList(); - - // 3. 有效 / 无效判断 - if ("valid".equals(req.getType())) { - targetBatchIds = validBatchIds; - } - // ✅ 无效批次 = 全部 - 有效 - else if ("invalid".equals(req.getType())) { - targetBatchIds = allBatchIds.stream() - .filter(batchId -> !validBatchIds.contains(batchId)) - .toList(); - } else { - throw new IllegalArgumentException("类型仅支持 valid / invalid"); - } - } - - Sort sort = Sort.by(Sort.Direction.ASC, "batchId") - .and(Sort.by(Sort.Direction.DESC, "createdAt")); - Pageable pageable = PageRequest.of(req.getPage() - 1, req.getSize(), sort); - - Page resultPage = filterByHotel - ? esignLogRepository.findByBatchIdsAndHotelId(targetBatchIds, hotelId, pageable) - : esignLogRepository.findByBatchIds(targetBatchIds, pageable); - return resultPage.map(esignLogMapper::toEsignLogListDto); - } - - // 详情不变 - public EsignLogDetailDto getLogDetail(Long id) { - EsignLog log = esignLogRepository.findById(id) - .orElseThrow(() -> new RuntimeException("日志不存在 ID:" + id)); - return esignLogMapper.toEsignLogDetailDto(log); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/common/config/EsignCmsSchemaMigrator.java b/src/main/java/com/sjmresorts/opsapp/esign/common/config/EsignCmsSchemaMigrator.java deleted file mode 100644 index 90fbb05..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/common/config/EsignCmsSchemaMigrator.java +++ /dev/null @@ -1,743 +0,0 @@ -package com.sjmresorts.opsapp.esign.common.config; - -import com.sjmresorts.opsapp.common.db.DbDialect; -import com.sjmresorts.opsapp.esign.capture.config.EsignCaptureSchemaMigrator; - -import java.util.List; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.stereotype.Component; - -import javax.sql.DataSource; - -/** - * 启动时自动补齐 Esign CMS 表字段(display_type / image_url),避免手工执行迁移脚本。 - */ -@Component -public class EsignCmsSchemaMigrator implements InitializingBean { - - private static final Logger log = LoggerFactory.getLogger(EsignCmsSchemaMigrator.class); - - private final JdbcTemplate jdbc; - private final EsignCaptureSchemaMigrator captureSchemaMigrator; - private final DbDialect dialect; - - public EsignCmsSchemaMigrator( - @Qualifier("opsAppDataSource") DataSource dataSource, - EsignCaptureSchemaMigrator captureSchemaMigrator, - DbDialect dialect) { - this.jdbc = new JdbcTemplate(dataSource); - this.captureSchemaMigrator = captureSchemaMigrator; - this.dialect = dialect; - } - - @Override - public void afterPropertiesSet() { - captureSchemaMigrator.ensureTables(); - migrateTable("esign_hub_promotions", "DF_esign_hub_promo_display"); - ensureSurveyCoreTables(); - migrateTable("esign_surveys", "DF_esign_surveys_display"); - ensureSurveyExtendedColumns(); - ensureSurveySubmissionExtendedColumns(); - ensureSurveyPushTable(); - ensureApiLogsTable(); - ensureFormsMenu(); - ensureAppUsersRouter(); - ensureHubPageDesignRouter(); - ensureAppPageLayoutTable(); - ensureSignFieldOhipConfigTable(); - ensureSignFieldOhipMappingRouter(); - ensureSysMenusRouter(); - ensureHubSettingsExtraColumns(); - } - - /** hub 外观扩展字段:设定菜单 JSON、登记卡顶栏显隐 */ - private void ensureHubSettingsExtraColumns() { - for (String table : new String[] {"esign_hub_settings", "sinosecu_hub_settings"}) { - if (!tableExists(table)) { - continue; - } - addNullableColumn(table, "settings_menu_json", dialect.text() + " NULL"); - addNullableColumn(table, "printer_pdf_nav_bar_visible", dialect.boolType() + " NULL"); - } - } - - /** CMS 系统管理:侧栏菜单显示/隐藏 */ - private void ensureSysMenusRouter() { - if (!tableExists("sys_routers")) { - return; - } - Integer routerCount = jdbc.queryForObject( - "SELECT COUNT(*) FROM sys_routers WHERE id = ?", Integer.class, "ESIGN_SYS_MENUS"); - if (routerCount == null || routerCount == 0) { - log.info("Esign cms migrate: inserting ESIGN_SYS_MENUS router"); - jdbc.update( - "INSERT INTO sys_routers (id, parent_id, heading, section_title, route, sort_no, status) " - + "VALUES (?, ?, N'菜單顯示', NULL, ?, ?, ?)", - "ESIGN_SYS_MENUS", - "ESIGN_SYSTEM", - "/checkin/system/menus", - 65, - 1); - } - if (tableExists("permissions")) { - insertPermissionIfMissing(1037L, "ESIGN_SYS_MENUS", "VIEW"); - insertPermissionIfMissing(1038L, "ESIGN_SYS_MENUS", "EDIT"); - } - grantSysMenusToEsignAdminRole(); - } - - private void grantSysMenusToEsignAdminRole() { - if (!tableExists("roles") || !tableExists("role_permissions")) { - return; - } - List roleIds = jdbc.query( - dialect.limitOne("SELECT role_id FROM roles WHERE role_name = ?"), - (rs, rowNum) -> rs.getLong(1), - "e-sign 管理員"); - if (roleIds.isEmpty()) { - return; - } - long roleId = roleIds.get(0); - for (long permissionId : new long[] {1037L, 1038L}) { - Integer linked = jdbc.queryForObject( - "SELECT COUNT(*) FROM role_permissions WHERE role_id = ? AND permission_id = ?", - Integer.class, - roleId, - permissionId); - if (linked == null || linked == 0) { - log.info("Esign cms migrate: grant permission {} to e-sign admin role {}", permissionId, roleId); - jdbc.update( - "INSERT INTO role_permissions (role_id, permission_id) VALUES (?, ?)", - roleId, - permissionId); - } - } - } - - private void ensureSignFieldOhipConfigTable() { - if (tableExists("esign_sign_field_ohip_config")) { - return; - } - log.info("Esign cms migrate: creating esign_sign_field_ohip_config"); - jdbc.execute( - "CREATE TABLE esign_sign_field_ohip_config (" - + "config_id " - + dialect.varchar(36) - + " NOT NULL PRIMARY KEY, " - + "hotel_id " - + dialect.varchar(64) - + " NULL, " - + "mappings_json " - + dialect.text() - + " NOT NULL, " - + "updated_at " - + dialect.datetime() - + " NOT NULL)"); - jdbc.update( - "INSERT INTO esign_sign_field_ohip_config (config_id, hotel_id, mappings_json, updated_at) " - + "VALUES (?, ?, ?, " - + dialect.utcTimestampFunction() - + ")", - "DEFAULT", - null, - "[]"); - } - - private void ensureSignFieldOhipMappingRouter() { - if (!tableExists("sys_routers")) { - return; - } - Integer count = jdbc.queryForObject( - "SELECT COUNT(*) FROM sys_routers WHERE id = ?", Integer.class, "ESIGN_SIGN_OHIP_MAP"); - if (count == null || count == 0) { - log.info("Esign cms migrate: inserting ESIGN_SIGN_OHIP_MAP router under ESIGN_SYSTEM"); - jdbc.update( - "INSERT INTO sys_routers (id, parent_id, heading, section_title, route, sort_no, status) " - + "VALUES (?, ?, N'OHIP 字段映射', NULL, ?, ?, ?)", - "ESIGN_SIGN_OHIP_MAP", - "ESIGN_SYSTEM", - "/checkin/sign-field-ohip-mapping", - 64, - 1); - } else { - Integer underForms = jdbc.queryForObject( - "SELECT COUNT(*) FROM sys_routers WHERE id = ? AND parent_id = ?", - Integer.class, - "ESIGN_SIGN_OHIP_MAP", - "ESIGN_FORMS"); - if (underForms != null && underForms > 0) { - log.info("Esign cms migrate: move ESIGN_SIGN_OHIP_MAP to ESIGN_SYSTEM"); - jdbc.update( - "UPDATE sys_routers SET parent_id = ?, sort_no = ? WHERE id = ?", - "ESIGN_SYSTEM", - 64, - "ESIGN_SIGN_OHIP_MAP"); - } - } - if (tableExists("permissions")) { - insertPermissionIfMissing(1035L, "ESIGN_SIGN_OHIP_MAP", "VIEW"); - insertPermissionIfMissing(1036L, "ESIGN_SIGN_OHIP_MAP", "EDIT"); - } - grantSignFieldOhipMapToEsignAdminRole(); - } - - private void grantSignFieldOhipMapToEsignAdminRole() { - if (!tableExists("roles") || !tableExists("role_permissions")) { - return; - } - List roleIds = jdbc.query( - dialect.limitOne("SELECT role_id FROM roles WHERE role_name = ?"), - (rs, rowNum) -> rs.getLong(1), - "e-sign 管理員"); - if (roleIds.isEmpty()) { - return; - } - long roleId = roleIds.get(0); - for (long permissionId : new long[] {1035L, 1036L}) { - Integer linked = jdbc.queryForObject( - "SELECT COUNT(*) FROM role_permissions WHERE role_id = ? AND permission_id = ?", - Integer.class, - roleId, - permissionId); - if (linked == null || linked == 0) { - log.info("Esign cms migrate: grant OHIP mapping permission {} to e-sign admin", permissionId); - jdbc.update( - "INSERT INTO role_permissions (role_id, permission_id) VALUES (?, ?)", - roleId, - permissionId); - } - } - } - - private void ensureAppPageLayoutTable() { - if (tableExists("esign_app_page_layout")) { - return; - } - log.info("Esign cms migrate: creating esign_app_page_layout"); - jdbc.execute( - "CREATE TABLE esign_app_page_layout (" - + "layout_id " - + dialect.varchar(36) - + " NOT NULL PRIMARY KEY, " - + "page_key " - + dialect.varchar(64) - + dialect.notNullWithDefault("DF_esign_app_page_key", "'home'") - + ", " - + "hotel_id " - + dialect.varchar(64) - + " NULL, " - + "layout_json " - + dialect.text() - + " NOT NULL, " - + "published_layout_json " - + dialect.text() - + " NULL, " - + "status " - + dialect.varchar(16) - + dialect.notNullWithDefault("DF_esign_app_page_status", "'DRAFT'") - + ", " - + "layout_version " - + dialect.intType() - + dialect.notNullWithDefault("DF_esign_app_page_ver", "1") - + ", " - + "updated_at " - + dialect.datetime() - + " NOT NULL, " - + "published_at " - + dialect.datetime() - + " NULL)"); - String defaultJson = - "{\"version\":1,\"blocks\":[{\"id\":\"promo-1\",\"type\":\"promotions\",\"props\":{\"height\":180,\"limit\":5}}," - + "{\"id\":\"menu-1\",\"type\":\"menuGrid\",\"props\":{\"columns\":4,\"items\":[" - + "{\"icon\":\"bi-envelope\",\"label\":\"待簽消息\",\"path\":\"/pages/inbox/index\"}," - + "{\"icon\":\"bi-journal-check\",\"label\":\"簽收記錄\",\"path\":\"/pages/records/index\"}," - + "{\"icon\":\"bi-gear\",\"label\":\"設定\",\"path\":\"/pages/settings/index\"}]}}]}"; - jdbc.update( - "INSERT INTO esign_app_page_layout " - + "(layout_id, page_key, layout_json, published_layout_json, status, layout_version, updated_at, published_at) " - + "VALUES (?, ?, ?, ?, ?, ?, " - + dialect.utcTimestampFunction() - + ", " - + dialect.utcTimestampFunction() - + ")", - "DEFAULT_HOME", - "home", - defaultJson, - defaultJson, - "PUBLISHED", - 1); - } - - /** CMS 表單管理:PDF 歸檔從日誌管理移至表單管理 */ - private void ensureFormsMenu() { - if (!tableExists("sys_routers")) { - return; - } - Integer formsCount = jdbc.queryForObject( - "SELECT COUNT(*) FROM sys_routers WHERE id = ?", Integer.class, "ESIGN_FORMS"); - if (formsCount == null || formsCount == 0) { - log.info("Esign cms migrate: inserting ESIGN_FORMS router"); - jdbc.update( - "INSERT INTO sys_routers (id, parent_id, heading, section_title, route, font_icon, sort_no, status) " - + "VALUES (?, ?, NULL, N'表單管理', ?, ?, ?, ?)", - "ESIGN_FORMS", - "0", - "/checkin/forms", - "bi-file-earmark-text", - 35, - 1); - } - Integer archiveCount = jdbc.queryForObject( - "SELECT COUNT(*) FROM sys_routers WHERE id = ?", Integer.class, "ESIGN_PDF_ARCHIVE"); - if (archiveCount == null || archiveCount == 0) { - log.info("Esign cms migrate: inserting ESIGN_PDF_ARCHIVE under ESIGN_FORMS"); - jdbc.update( - "INSERT INTO sys_routers (id, parent_id, heading, section_title, route, sort_no, status) " - + "VALUES (?, ?, N'PDF 歸檔', NULL, ?, ?, ?)", - "ESIGN_PDF_ARCHIVE", - "ESIGN_FORMS", - "/checkin/pdf-archive", - 36, - 1); - insertPermissionIfMissing(1019L, "ESIGN_PDF_ARCHIVE", "VIEW"); - insertPermissionIfMissing(1020L, "ESIGN_PDF_ARCHIVE", "EDIT"); - grantPdfArchiveToEsignAdminRole(); - } else { - Integer underLogs = jdbc.queryForObject( - "SELECT COUNT(*) FROM sys_routers WHERE id = ? AND parent_id = ?", - Integer.class, - "ESIGN_PDF_ARCHIVE", - "ESIGN_LOGS"); - if (underLogs != null && underLogs > 0) { - log.info("Esign cms migrate: move ESIGN_PDF_ARCHIVE to ESIGN_FORMS"); - jdbc.update( - "UPDATE sys_routers SET parent_id = ?, sort_no = ? WHERE id = ?", - "ESIGN_FORMS", - 36, - "ESIGN_PDF_ARCHIVE"); - } - } - } - - private void grantPdfArchiveToEsignAdminRole() { - if (!tableExists("roles") || !tableExists("role_permissions")) { - return; - } - List roleIds = jdbc.query( - dialect.limitOne("SELECT role_id FROM roles WHERE role_name = ?"), - (rs, rowNum) -> rs.getLong(1), - "e-sign 管理員"); - if (roleIds.isEmpty()) { - return; - } - long roleId = roleIds.get(0); - for (long permissionId : new long[] {1019L, 1020L}) { - Integer linked = jdbc.queryForObject( - "SELECT COUNT(*) FROM role_permissions WHERE role_id = ? AND permission_id = ?", - Integer.class, - roleId, - permissionId); - if (linked == null || linked == 0) { - log.info("Esign cms migrate: grant PDF archive permission {} to e-sign admin", permissionId); - jdbc.update( - "INSERT INTO role_permissions (role_id, permission_id) VALUES (?, ?)", - roleId, - permissionId); - } - } - } - - /** CMS App 內容:首頁 Block 拖拽設計 */ - private void ensureHubPageDesignRouter() { - if (!tableExists("sys_routers")) { - return; - } - Integer routerCount = jdbc.queryForObject( - "SELECT COUNT(*) FROM sys_routers WHERE id = ?", Integer.class, "ESIGN_HUB_PAGE_DESIGN"); - if (routerCount == null || routerCount == 0) { - log.info("Esign cms migrate: inserting ESIGN_HUB_PAGE_DESIGN router"); - jdbc.update( - "INSERT INTO sys_routers (id, parent_id, heading, section_title, route, sort_no, status) " - + "VALUES (?, ?, N'首頁設計', NULL, ?, ?, ?)", - "ESIGN_HUB_PAGE_DESIGN", - "ESIGN_CONTENT", - "/checkin/hub-page-design", - 33, - 1); - } - if (tableExists("permissions")) { - insertPermissionIfMissing(1033L, "ESIGN_HUB_PAGE_DESIGN", "VIEW"); - insertPermissionIfMissing(1034L, "ESIGN_HUB_PAGE_DESIGN", "EDIT"); - } - grantHubPageDesignToEsignAdminRole(); - } - - private void grantHubPageDesignToEsignAdminRole() { - if (!tableExists("roles") || !tableExists("role_permissions")) { - return; - } - List roleIds = jdbc.query( - dialect.limitOne("SELECT role_id FROM roles WHERE role_name = ?"), - (rs, rowNum) -> rs.getLong(1), - "e-sign 管理員"); - if (roleIds.isEmpty()) { - return; - } - long roleId = roleIds.get(0); - for (long permissionId : new long[] {1033L, 1034L}) { - Integer linked = jdbc.queryForObject( - "SELECT COUNT(*) FROM role_permissions WHERE role_id = ? AND permission_id = ?", - Integer.class, - roleId, - permissionId); - if (linked == null || linked == 0) { - log.info("Esign cms migrate: grant hub page design permission {} to e-sign admin", permissionId); - jdbc.update( - "INSERT INTO role_permissions (role_id, permission_id) VALUES (?, ?)", - roleId, - permissionId); - } - } - } - - /** CMS 系统管理:平板 App 账号菜单(登录侧栏来自 sys_routers) */ - private void ensureAppUsersRouter() { - if (!tableExists("sys_routers")) { - return; - } - Integer routerCount = jdbc.queryForObject( - "SELECT COUNT(*) FROM sys_routers WHERE id = ?", Integer.class, "ESIGN_APP_USERS"); - if (routerCount == null || routerCount == 0) { - log.info("Esign cms migrate: inserting ESIGN_APP_USERS router"); - jdbc.update( - "INSERT INTO sys_routers (id, parent_id, heading, section_title, route, sort_no, status) " - + "VALUES (?, ?, N'平板賬號', NULL, ?, ?, ?)", - "ESIGN_APP_USERS", - "ESIGN_SYSTEM", - "/checkin/system/app-users", - 63, - 1); - } - if (tableExists("permissions")) { - insertPermissionIfMissing(1031L, "ESIGN_APP_USERS", "VIEW"); - insertPermissionIfMissing(1032L, "ESIGN_APP_USERS", "EDIT"); - } - grantAppUsersToEsignAdminRole(); - } - - private void insertPermissionIfMissing(long permissionId, String routerId, String permissionName) { - Integer count = jdbc.queryForObject( - "SELECT COUNT(*) FROM permissions WHERE permission_id = ?", Integer.class, permissionId); - if (count != null && count > 0) { - return; - } - log.info("Esign cms migrate: inserting permission {} for {}", permissionId, routerId); - jdbc.update( - "INSERT INTO permissions (permission_id, router_id, permission_name) VALUES (?, ?, ?)", - permissionId, - routerId, - permissionName); - } - - private void grantAppUsersToEsignAdminRole() { - if (!tableExists("roles") || !tableExists("role_permissions")) { - return; - } - List roleIds = jdbc.query( - dialect.limitOne("SELECT role_id FROM roles WHERE role_name = ?"), - (rs, rowNum) -> rs.getLong(1), - "e-sign 管理員"); - if (roleIds.isEmpty()) { - return; - } - long roleId = roleIds.get(0); - for (long permissionId : new long[] {1031L, 1032L}) { - Integer linked = jdbc.queryForObject( - "SELECT COUNT(*) FROM role_permissions WHERE role_id = ? AND permission_id = ?", - Integer.class, - roleId, - permissionId); - if (linked == null || linked == 0) { - log.info("Esign cms migrate: grant permission {} to e-sign admin role {}", permissionId, roleId); - jdbc.update( - "INSERT INTO role_permissions (role_id, permission_id) VALUES (?, ?)", - roleId, - permissionId); - } - } - } - - private void ensureSurveyCoreTables() { - if (!tableExists("esign_surveys")) { - log.info("Esign cms migrate: creating esign_surveys"); - jdbc.execute( - "CREATE TABLE esign_surveys (" - + "survey_id " - + dialect.varchar(36) - + " NOT NULL PRIMARY KEY, " - + "title " - + dialect.varchar(256) - + " NOT NULL, " - + "description " - + dialect.varchar(1000) - + " NULL, " - + "reward_text " - + dialect.varchar(256) - + " NULL, " - + "display_type " - + dialect.varchar(16) - + dialect.notNullWithDefault("DF_esign_surveys_display", "'CARD'") - + ", " - + "image_url " - + dialect.text() - + " NULL, " - + "header_hotel_name " - + dialect.varchar(256) - + " NULL, " - + "hotel_id " - + dialect.varchar(64) - + " NULL, " - + "status " - + dialect.varchar(32) - + " NOT NULL, " - + "questions_json " - + dialect.text() - + dialect.notNullWithDefault("DF_esign_surveys_questions", "'[]'") - + ", " - + "sort_order " - + dialect.intType() - + dialect.notNullWithDefault("DF_esign_surveys_sort", "0") - + ", " - + "created_at " - + dialect.datetime() - + " NOT NULL, " - + "updated_at " - + dialect.datetime() - + " NOT NULL, " - + "published_at " - + dialect.datetime() - + " NULL, " - + "source_type " - + dialect.varchar(16) - + dialect.notNullWithDefault("DF_esign_surveys_source_type", "'NATIVE'") - + ", " - + "external_url " - + dialect.varchar(2000) - + " NULL, " - + "external_provider " - + dialect.varchar(32) - + " NULL, " - + "external_form_id " - + dialect.varchar(512) - + " NULL, " - + "external_embed_url " - + dialect.varchar(2000) - + " NULL, " - + "webhook_secret " - + dialect.varchar(64) - + " NULL, " - + "delivery_mode " - + dialect.varchar(32) - + dialect.notNullWithDefault( - "DF_esign_surveys_delivery_mode", "'MANUAL_PUSH'") - + ")"); - jdbc.execute( - "CREATE INDEX ix_esign_surveys_status_sort ON esign_surveys (status, sort_order, updated_at)"); - } - if (!tableExists("esign_survey_submissions")) { - log.info("Esign cms migrate: creating esign_survey_submissions"); - jdbc.execute( - "CREATE TABLE esign_survey_submissions (" - + "submission_id " - + dialect.varchar(36) - + " NOT NULL PRIMARY KEY, " - + "survey_id " - + dialect.varchar(36) - + " NOT NULL, " - + "answers_json " - + dialect.text() - + " NOT NULL, " - + "device_id " - + dialect.varchar(128) - + " NULL, " - + "hotel_id " - + dialect.varchar(64) - + " NULL, " - + "submitted_at " - + dialect.datetime() - + " NOT NULL, " - + "source_type " - + dialect.varchar(16) - + dialect.notNullWithDefault( - "DF_esign_survey_submissions_source_type", "'NATIVE'") - + ", " - + "external_response_id " - + dialect.varchar(256) - + " NULL, " - + "respondent_meta_json " - + dialect.text() - + " NULL, " - + "CONSTRAINT fk_esign_survey_submission FOREIGN KEY (survey_id) " - + "REFERENCES esign_surveys (survey_id) ON DELETE CASCADE)"); - jdbc.execute( - "CREATE INDEX ix_esign_survey_submissions_survey " - + "ON esign_survey_submissions (survey_id, submitted_at)"); - } - } - - private void ensureSurveyExtendedColumns() { - if (!tableExists("esign_surveys")) { - return; - } - addNullableColumn("esign_surveys", "header_hotel_name", dialect.varchar(256) + " NULL"); - addNotNullColumnWithDefault( - "esign_surveys", "source_type", 16, "DF_esign_surveys_source_type", "NATIVE"); - addNullableColumn("esign_surveys", "external_url", dialect.varchar(2000) + " NULL"); - addNullableColumn("esign_surveys", "external_provider", dialect.varchar(32) + " NULL"); - addNullableColumn("esign_surveys", "external_form_id", dialect.varchar(512) + " NULL"); - addNullableColumn("esign_surveys", "external_embed_url", dialect.varchar(2000) + " NULL"); - addNullableColumn("esign_surveys", "webhook_secret", dialect.varchar(64) + " NULL"); - addNotNullColumnWithDefault( - "esign_surveys", "delivery_mode", 32, "DF_esign_surveys_delivery_mode", "MANUAL_PUSH"); - } - - private void ensureSurveySubmissionExtendedColumns() { - if (!tableExists("esign_survey_submissions")) { - return; - } - addNotNullColumnWithDefault( - "esign_survey_submissions", - "source_type", - 16, - "DF_esign_survey_submissions_source_type", - "NATIVE"); - addNullableColumn("esign_survey_submissions", "external_response_id", dialect.varchar(256) + " NULL"); - addNullableColumn("esign_survey_submissions", "respondent_meta_json", dialect.text() + " NULL"); - } - - private void ensureSurveyPushTable() { - if (tableExists("esign_survey_push")) { - return; - } - if (!tableExists("esign_surveys")) { - return; - } - log.info("Esign cms migrate: creating esign_survey_push"); - jdbc.execute( - "CREATE TABLE esign_survey_push (" - + "survey_id " - + dialect.varchar(36) - + " NOT NULL, " - + "user_id " - + dialect.bigintType() - + " NOT NULL, " - + "client_ip " - + dialect.varchar(64) - + dialect.notNullWithDefault("DF_esign_survey_push_client_ip", "''") - + ", " - + "assigned_at " - + dialect.datetime() - + " NOT NULL, " - + "assigned_by " - + dialect.varchar(64) - + " NULL, " - + "group_id " - + dialect.varchar(36) - + " NULL, " - + "CONSTRAINT pk_esign_survey_push PRIMARY KEY (survey_id, user_id, client_ip), " - + "CONSTRAINT fk_esign_survey_push_survey FOREIGN KEY (survey_id) " - + "REFERENCES esign_surveys (survey_id) ON DELETE CASCADE)"); - jdbc.execute("CREATE INDEX ix_esign_survey_push_user ON esign_survey_push (user_id, assigned_at)"); - } - - /** CMS / 全局 API 请求日志(esign_db_init 精简库默认不含此表) */ - private void ensureApiLogsTable() { - if (tableExists("api_logs")) { - return; - } - log.info("Esign cms migrate: creating api_logs"); - jdbc.execute( - "CREATE TABLE api_logs (" - + dialect.bigintIdentityPrimaryKey("api_log_id") - + ", " - + "url " - + dialect.text() - + " NULL, " - + "type " - + dialect.varchar(255) - + " NULL, " - + "request_dtm " - + dialect.datetime() - + " NULL, " - + "response_dtm " - + dialect.datetime() - + " NULL, " - + "header " - + dialect.text() - + " NULL, " - + "request_body " - + dialect.text() - + " NULL, " - + "request_by " - + dialect.varchar(255) - + " NULL, " - + "response_body " - + dialect.text() - + " NULL, " - + "debug_level " - + dialect.varchar(64) - + " NULL, " - + "status_code " - + dialect.varchar(32) - + " NULL)"); - } - - private void addNullableColumn(String table, String column, String typeAndNullability) { - if (columnExists(table, column)) { - return; - } - log.info("Esign cms migrate: adding {}.{}", table, column); - jdbc.execute(dialect.alterAddColumn(table, column, typeAndNullability)); - } - - private void addNotNullColumnWithDefault( - String table, String column, int varcharLength, String constraintName, String defaultLiteral) { - if (columnExists(table, column)) { - return; - } - log.info("Esign cms migrate: adding {}.{} with default {}", table, column, defaultLiteral); - jdbc.execute( - dialect.alterAddNotNullVarcharWithDefault( - table, column, varcharLength, constraintName, defaultLiteral)); - } - - private void migrateTable(String table, String displayDefaultConstraint) { - if (!tableExists(table)) { - log.debug("Esign cms migrate: table {} not found, skip", table); - return; - } - if (!columnExists(table, "display_type")) { - log.info("Esign cms migrate: adding {}.display_type", table); - jdbc.execute( - dialect.alterAddNotNullVarcharWithDefault( - table, "display_type", 16, displayDefaultConstraint, "CARD")); - } - if (!columnExists(table, "image_url")) { - log.info("Esign cms migrate: adding {}.image_url", table); - jdbc.execute(dialect.alterAddColumn(table, "image_url", dialect.text() + " NULL")); - } - } - - private boolean tableExists(String table) { - return dialect.tableExists(jdbc, table); - } - - private boolean columnExists(String table, String column) { - return dialect.columnExists(jdbc, table, column); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/common/filters/EsignCmsOperationAuditFilter.java b/src/main/java/com/sjmresorts/opsapp/esign/common/filters/EsignCmsOperationAuditFilter.java deleted file mode 100644 index f510454..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/common/filters/EsignCmsOperationAuditFilter.java +++ /dev/null @@ -1,104 +0,0 @@ -package com.sjmresorts.opsapp.esign.common.filters; - -import com.sjmresorts.opsapp.esign.audit.services.EsignAuditLogService; -import com.sjmresorts.opsapp.esign.auth.services.EsignUserPresenceService; -import com.sjmresorts.opsapp.common.utils.NetworkUtil; -import jakarta.annotation.Resource; -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.apache.commons.lang3.StringUtils; -import org.springframework.core.Ordered; -import org.springframework.core.annotation.Order; -import org.springframework.stereotype.Component; -import org.springframework.web.filter.OncePerRequestFilter; - -import java.io.IOException; -import java.net.URLDecoder; -import java.nio.charset.StandardCharsets; -import java.util.Set; - -/** CMS 變更類請求寫入操作日誌(排除 App X-Client-Type=APP)。 */ -@Component -@Order(Ordered.LOWEST_PRECEDENCE - 20) -public class EsignCmsOperationAuditFilter extends OncePerRequestFilter { - - private static final Set MUTATING = Set.of("POST", "PUT", "PATCH", "DELETE"); - - private static final Set SKIP_PATH_FRAGMENTS = Set.of( - "/client-transport-log", - "/audit-logs", - "/printer-event-logs", - "/ios-pending-tasks", - "/ios-task-list", - "/ios-printer-pdf-task-signed", - "/ios-printer-pdf-usb-signed", - "/ios-printer-pdf-task-complete", - "/auth/", - "/surveys/submit", - "/app/deeplink"); - - @Resource(name = "esignAuditLogService") - private EsignAuditLogService auditLogService; - - @Override - protected boolean shouldNotFilter(HttpServletRequest request) { - String uri = request.getRequestURI(); - if (uri == null || (!uri.contains("/esign/") && !uri.contains("/sinosecu/"))) { - return true; - } - String method = request.getMethod(); - if (method == null || !MUTATING.contains(method.toUpperCase())) { - return true; - } - if ("APP".equalsIgnoreCase(StringUtils.trimToEmpty(request.getHeader("X-Client-Type")))) { - return true; - } - for (String skip : SKIP_PATH_FRAGMENTS) { - if (uri.contains(skip)) { - return true; - } - } - return false; - } - - @Override - protected void doFilterInternal( - HttpServletRequest request, - HttpServletResponse response, - FilterChain filterChain) throws ServletException, IOException { - long start = System.currentTimeMillis(); - try { - filterChain.doFilter(request, response); - } finally { - try { - String operator = decodeCmsOperator(request.getHeader("X-CMS-Operator")); - String clientIp = EsignUserPresenceService.normalizeClientIp( - NetworkUtil.getClientIpByRequest(request)); - auditLogService.appendCmsOperation( - request.getMethod(), - request.getRequestURI(), - response.getStatus(), - System.currentTimeMillis() - start, - operator, - clientIp); - } catch (Exception ignored) { - // 審計失敗不影響主流程 - } - } - } - - /** 前端可能对非 ASCII 操作者做 URI 编码 */ - private static String decodeCmsOperator(String raw) { - String operator = StringUtils.trimToNull(raw); - if (operator == null || !operator.contains("%")) { - return operator; - } - try { - return URLDecoder.decode(operator, StandardCharsets.UTF_8); - } catch (IllegalArgumentException ex) { - return operator; - } - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/hub/controllers/EsignAppPageLayoutController.java b/src/main/java/com/sjmresorts/opsapp/esign/hub/controllers/EsignAppPageLayoutController.java deleted file mode 100644 index 0eed40d..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/hub/controllers/EsignAppPageLayoutController.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.sjmresorts.opsapp.esign.hub.controllers; - -import com.sjmresorts.opsapp.esign.hub.dtos.EsignAppPageLayoutResponse; -import com.sjmresorts.opsapp.esign.hub.dtos.EsignAppPageLayoutSaveRequest; -import com.sjmresorts.opsapp.esign.hub.services.EsignAppPageLayoutService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.annotation.Resource; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.CrossOrigin; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -@Tag(name = "Esign App Page Layout", description = "UniApp 页面拖拽布局 JSON") -@RestController -@RequestMapping({"/api/v1/esign/app-page-layout", "/api/v1/sinosecu/app-page-layout"}) -@CrossOrigin(origins = "*") -public class EsignAppPageLayoutController { - - @Resource - private EsignAppPageLayoutService layoutService; - - @Operation(summary = "管理端:读取页面布局(含草稿)") - @GetMapping(produces = "application/json;charset=UTF-8") - public ResponseEntity getForAdmin( - @RequestParam(value = "pageKey", required = false, defaultValue = "home") String pageKey) { - return ResponseEntity.ok(layoutService.getForAdmin(pageKey)); - } - - @Operation(summary = "App:读取已发布页面布局") - @GetMapping(value = "/app", produces = "application/json;charset=UTF-8") - public ResponseEntity getForApp( - @RequestParam(value = "pageKey", required = false, defaultValue = "home") String pageKey, - @RequestParam(value = "hotelId", required = false) String hotelId) { - return ResponseEntity.ok(layoutService.getForApp(pageKey, hotelId)); - } - - @Operation(summary = "管理端:保存页面布局") - @PutMapping(produces = "application/json;charset=UTF-8") - public ResponseEntity save(@RequestBody EsignAppPageLayoutSaveRequest request) { - EsignAppPageLayoutResponse body = layoutService.save(request); - if (!body.isSuccess()) { - return ResponseEntity.badRequest().body(body); - } - return ResponseEntity.ok(body); - } - - @Operation(summary = "管理端:发布当前草稿") - @PostMapping(value = "/publish", produces = "application/json;charset=UTF-8") - public ResponseEntity publish( - @RequestParam(value = "pageKey", required = false, defaultValue = "home") String pageKey) { - EsignAppPageLayoutResponse body = layoutService.publish(pageKey); - if (!body.isSuccess()) { - return ResponseEntity.badRequest().body(body); - } - return ResponseEntity.ok(body); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/hub/controllers/EsignHubPromotionController.java b/src/main/java/com/sjmresorts/opsapp/esign/hub/controllers/EsignHubPromotionController.java deleted file mode 100644 index a718aaa..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/hub/controllers/EsignHubPromotionController.java +++ /dev/null @@ -1,175 +0,0 @@ -package com.sjmresorts.opsapp.esign.hub.controllers; - -import com.sjmresorts.opsapp.esign.hub.dtos.EsignHubPromotionResponse; -import com.sjmresorts.opsapp.esign.hub.dtos.EsignHubPromotionSaveRequest; -import com.sjmresorts.opsapp.esign.hub.services.EsignHubPromotionMediaService; -import com.sjmresorts.opsapp.esign.hub.services.EsignHubPromotionService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.annotation.Resource; -import org.apache.commons.lang3.StringUtils; -import org.springframework.core.io.FileSystemResource; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.http.CacheControl; -import org.springframework.http.MediaType; -import org.springframework.web.bind.annotation.CrossOrigin; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.multipart.MultipartFile; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Optional; - -@Tag(name = "Esign Hub Promotion", description = "首页精选推广(Electron 配置 + App 轮播)") -@RestController -@RequestMapping({"/api/v1/esign/hub-promotions", "/api/v1/sinosecu/hub-promotions"}) -@CrossOrigin(origins = "*") -public class EsignHubPromotionController { - - @Resource - private EsignHubPromotionService promotionService; - - @Resource - private EsignHubPromotionMediaService mediaService; - - @Operation(summary = "管理端:推广列表", description = "免 JWT。Electron 推广配置管理。") - @GetMapping(produces = "application/json;charset=UTF-8") - public ResponseEntity listAll() { - return ResponseEntity.ok(promotionService.listAllForAdmin()); - } - - @Operation(summary = "App:已发布推广列表") - @GetMapping(value = "/published", produces = "application/json;charset=UTF-8") - public ResponseEntity listPublished( - @RequestParam(value = "hotelId", required = false) String hotelId) { - return ResponseEntity.ok(promotionService.listPublished(hotelId)); - } - - @Operation(summary = "推广详情") - @GetMapping(value = "/{promotionId}", produces = "application/json;charset=UTF-8") - public ResponseEntity getDetail(@PathVariable("promotionId") String promotionId) { - EsignHubPromotionResponse body = promotionService.getDetail(promotionId); - if (!body.isSuccess()) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(body); - } - return ResponseEntity.ok(body); - } - - @Operation(summary = "推广媒体文件(图片/视频),供 App 与 CMS 引用") - @GetMapping(value = "/{promotionId}/media") - public ResponseEntity getMedia(@PathVariable("promotionId") String promotionId) { - Optional path = mediaService.resolveMediaFile(promotionId); - if (path.isEmpty()) { - return ResponseEntity.notFound().build(); - } - try { - Path file = path.get(); - String contentType = mediaService.probeMimeForPath(file); - long size = Files.size(file); - long lastModifiedMs = Files.getLastModifiedTime(file).toMillis(); - String etag = "W/\"" + lastModifiedMs + "-" + size + "\""; - FileSystemResource body = new FileSystemResource(file); - // 媒体可被同 id 覆盖;禁止长缓存,靠 ETag/Last-Modified 协商,配合 URL ?v= 失效 - return ResponseEntity.ok() - .contentType(MediaType.parseMediaType(contentType)) - .eTag(etag) - .lastModified(lastModifiedMs) - .cacheControl(CacheControl.noCache().cachePublic().mustRevalidate()) - .contentLength(size) - .body(body); - } catch (IOException e) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); - } - } - - @Operation(summary = "上传推广图片到服务器本地目录(推荐,勿把 Base64 写入 JSON)") - @PostMapping(value = "/{promotionId}/media", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public ResponseEntity uploadMedia( - @PathVariable("promotionId") String promotionId, - @RequestParam("file") MultipartFile file) { - if (file == null || file.isEmpty()) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST) - .body(EsignHubPromotionResponse.fail("400", "file required")); - } - try { - EsignHubPromotionResponse body = promotionService.uploadMedia( - promotionId, - file.getBytes(), - StringUtils.defaultIfBlank(file.getContentType(), MediaType.APPLICATION_OCTET_STREAM_VALUE)); - if (!body.isSuccess()) { - HttpStatus status = "404".equals(body.getCode()) ? HttpStatus.NOT_FOUND : HttpStatus.BAD_REQUEST; - return ResponseEntity.status(status).body(body); - } - return ResponseEntity.ok(body); - } catch (Exception e) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(EsignHubPromotionResponse.fail("500", "upload failed")); - } - } - - @Operation(summary = "创建或更新推广") - @PostMapping(consumes = "application/json;charset=UTF-8", produces = "application/json;charset=UTF-8") - public ResponseEntity save(@RequestBody EsignHubPromotionSaveRequest request) { - EsignHubPromotionResponse body = promotionService.save(request); - if (!body.isSuccess()) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(body); - } - return ResponseEntity.ok(body); - } - - @Operation(summary = "更新推广") - @PutMapping(value = "/{promotionId}", consumes = "application/json;charset=UTF-8", produces = "application/json;charset=UTF-8") - public ResponseEntity update( - @PathVariable("promotionId") String promotionId, - @RequestBody EsignHubPromotionSaveRequest request) { - if (request != null) { - request.setPromotionId(promotionId); - } - EsignHubPromotionResponse body = promotionService.save(request); - if (!body.isSuccess()) { - HttpStatus status = "404".equals(body.getCode()) ? HttpStatus.NOT_FOUND : HttpStatus.BAD_REQUEST; - return ResponseEntity.status(status).body(body); - } - return ResponseEntity.ok(body); - } - - @Operation(summary = "发布推广") - @PostMapping(value = "/{promotionId}/publish", produces = "application/json;charset=UTF-8") - public ResponseEntity publish(@PathVariable("promotionId") String promotionId) { - EsignHubPromotionResponse body = promotionService.publish(promotionId); - if (!body.isSuccess()) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(body); - } - return ResponseEntity.ok(body); - } - - @Operation(summary = "归档推广") - @PostMapping(value = "/{promotionId}/archive", produces = "application/json;charset=UTF-8") - public ResponseEntity archive(@PathVariable("promotionId") String promotionId) { - EsignHubPromotionResponse body = promotionService.archive(promotionId); - if (!body.isSuccess()) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(body); - } - return ResponseEntity.ok(body); - } - - @Operation(summary = "删除推广") - @DeleteMapping(value = "/{promotionId}", produces = "application/json;charset=UTF-8") - public ResponseEntity delete(@PathVariable("promotionId") String promotionId) { - EsignHubPromotionResponse body = promotionService.delete(promotionId); - if (!body.isSuccess()) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(body); - } - return ResponseEntity.ok(body); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/hub/controllers/EsignHubSettingsController.java b/src/main/java/com/sjmresorts/opsapp/esign/hub/controllers/EsignHubSettingsController.java deleted file mode 100644 index e0f294c..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/hub/controllers/EsignHubSettingsController.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.sjmresorts.opsapp.esign.hub.controllers; - -import com.sjmresorts.opsapp.esign.hub.dtos.EsignHubSettingsResponse; -import com.sjmresorts.opsapp.esign.hub.dtos.EsignHubSettingsSaveRequest; -import com.sjmresorts.opsapp.esign.hub.services.EsignHubSettingsService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.annotation.Resource; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.CrossOrigin; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -@Tag(name = "Esign Hub Settings", description = "App 首页外观(导航栏主题色等)") -@RestController -@RequestMapping({"/api/v1/esign/hub-settings", "/api/v1/sinosecu/hub-settings"}) -@CrossOrigin(origins = "*") -public class EsignHubSettingsController { - - @Resource - private EsignHubSettingsService settingsService; - - @Operation(summary = "管理端:读取首页外观配置") - @GetMapping(produces = "application/json;charset=UTF-8") - public ResponseEntity getForAdmin() { - return ResponseEntity.ok(settingsService.getForAdmin()); - } - - @Operation(summary = "App:读取首页外观配置") - @GetMapping(value = "/app", produces = "application/json;charset=UTF-8") - public ResponseEntity getForApp( - @RequestParam(value = "hotelId", required = false) String hotelId) { - return ResponseEntity.ok(settingsService.getForApp(hotelId)); - } - - @Operation(summary = "管理端:保存首页外观配置") - @PutMapping(produces = "application/json;charset=UTF-8") - public ResponseEntity save(@RequestBody EsignHubSettingsSaveRequest request) { - EsignHubSettingsResponse body = settingsService.save(request); - if (!body.isSuccess()) { - return ResponseEntity.badRequest().body(body); - } - return ResponseEntity.ok(body); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/hub/dtos/EsignAppPageLayoutResponse.java b/src/main/java/com/sjmresorts/opsapp/esign/hub/dtos/EsignAppPageLayoutResponse.java deleted file mode 100644 index 9d6c752..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/hub/dtos/EsignAppPageLayoutResponse.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.sjmresorts.opsapp.esign.hub.dtos; - -import com.fasterxml.jackson.annotation.JsonInclude; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@JsonInclude(JsonInclude.Include.NON_NULL) -public class EsignAppPageLayoutResponse { - - private boolean success = true; - private String code = "200"; - private String message; - private AppPageLayoutData data; - - @Data - @NoArgsConstructor - public static class AppPageLayoutData { - private AppPageLayoutItem layout; - } - - @Data - @NoArgsConstructor - public static class AppPageLayoutItem { - private String layoutId; - private String pageKey; - private String hotelId; - private String layoutJson; - private String publishedLayoutJson; - private String status; - private Integer layoutVersion; - private String updatedAt; - private String publishedAt; - } - - public static EsignAppPageLayoutResponse ok(String message) { - EsignAppPageLayoutResponse r = new EsignAppPageLayoutResponse(); - r.setMessage(message); - return r; - } - - public static EsignAppPageLayoutResponse fail(String code, String message) { - EsignAppPageLayoutResponse r = new EsignAppPageLayoutResponse(); - r.setSuccess(false); - r.setCode(code); - r.setMessage(message); - return r; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/hub/dtos/EsignAppPageLayoutSaveRequest.java b/src/main/java/com/sjmresorts/opsapp/esign/hub/dtos/EsignAppPageLayoutSaveRequest.java deleted file mode 100644 index bbcee91..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/hub/dtos/EsignAppPageLayoutSaveRequest.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.sjmresorts.opsapp.esign.hub.dtos; - -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -public class EsignAppPageLayoutSaveRequest { - - /** home 等页面标识 */ - private String pageKey; - - /** 布局 JSON 字符串,含 version + blocks[] */ - private String layoutJson; - - /** true 时同时发布给 App */ - private Boolean publish; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/hub/dtos/EsignHubPromotionResponse.java b/src/main/java/com/sjmresorts/opsapp/esign/hub/dtos/EsignHubPromotionResponse.java deleted file mode 100644 index 4084748..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/hub/dtos/EsignHubPromotionResponse.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.sjmresorts.opsapp.esign.hub.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 = "首页精选推广 API 响应") -public class EsignHubPromotionResponse { - - private boolean success = true; - private String code = "200"; - private String message; - private EsignHubPromotionData data; - - @Data - @NoArgsConstructor - public static class EsignHubPromotionData { - private String promotionId; - private List promotions; - private EsignHubPromotionItem promotion; - } - - @Data - @NoArgsConstructor - public static class EsignHubPromotionItem { - private String promotionId; - private String title; - private String subtitle; - private String tag; - private String theme; - private String displayType; - private String imageUrl; - private String textPosition; - private String hotelId; - private String status; - private Integer sortOrder; - private String createdAt; - private String updatedAt; - private String publishedAt; - } - - public static EsignHubPromotionResponse ok(String message) { - EsignHubPromotionResponse r = new EsignHubPromotionResponse(); - r.setMessage(message); - return r; - } - - public static EsignHubPromotionResponse fail(String code, String message) { - EsignHubPromotionResponse r = new EsignHubPromotionResponse(); - r.setSuccess(false); - r.setCode(code); - r.setMessage(message); - return r; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/hub/dtos/EsignHubPromotionSaveRequest.java b/src/main/java/com/sjmresorts/opsapp/esign/hub/dtos/EsignHubPromotionSaveRequest.java deleted file mode 100644 index 60342ea..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/hub/dtos/EsignHubPromotionSaveRequest.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.sjmresorts.opsapp.esign.hub.dtos; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@Schema(description = "保存首页推广") -public class EsignHubPromotionSaveRequest { - - private String promotionId; - private String title; - private String subtitle; - private String tag; - private String theme; - /** CARD | IMAGE */ - private String displayType; - /** 图片模式:URL 或 data URL */ - private String imageUrl; - /** TOP | CENTER | BOTTOM | NONE */ - private String textPosition; - private String hotelId; - private Integer sortOrder; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/hub/dtos/EsignHubSettingsResponse.java b/src/main/java/com/sjmresorts/opsapp/esign/hub/dtos/EsignHubSettingsResponse.java deleted file mode 100644 index 80904c4..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/hub/dtos/EsignHubSettingsResponse.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.sjmresorts.opsapp.esign.hub.dtos; - -import com.fasterxml.jackson.annotation.JsonInclude; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@JsonInclude(JsonInclude.Include.NON_NULL) -public class EsignHubSettingsResponse { - - private boolean success = true; - private String code = "200"; - private String message; - private HubSettingsData data; - - @Data - @NoArgsConstructor - public static class HubSettingsData { - private HubSettingsItem settings; - } - - @Data - @NoArgsConstructor - public static class HubSettingsItem { - private String settingId; - private String hotelId; - private String navBarColor; - private String navBarColorDark; - /** 首页顶栏是否显示,默认 true */ - private Boolean navBarVisible; - /** 虚拟打印机登记卡页顶栏是否显示,默认 true */ - private Boolean printerPdfNavBarVisible; - private HubUserMenuStyle userMenuStyle; - /** 设定侧栏菜单显隐,默认全部 true */ - private HubSettingsMenu settingsMenu; - private String updatedAt; - } - - public static EsignHubSettingsResponse ok(String message) { - EsignHubSettingsResponse r = new EsignHubSettingsResponse(); - r.setMessage(message); - return r; - } - - public static EsignHubSettingsResponse fail(String code, String message) { - EsignHubSettingsResponse r = new EsignHubSettingsResponse(); - r.setSuccess(false); - r.setCode(code); - r.setMessage(message); - return r; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/hub/dtos/EsignHubSettingsSaveRequest.java b/src/main/java/com/sjmresorts/opsapp/esign/hub/dtos/EsignHubSettingsSaveRequest.java deleted file mode 100644 index dbe3d21..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/hub/dtos/EsignHubSettingsSaveRequest.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.sjmresorts.opsapp.esign.hub.dtos; - -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -public class EsignHubSettingsSaveRequest { - - /** 留空表示全酒店默认 */ - private String hotelId; - private String navBarColor; - private String navBarColorDark; - /** 首页顶栏是否显示;不传则保持原值,新建默认 true */ - private Boolean navBarVisible; - /** 虚拟打印机登记卡页顶栏是否显示;不传则保持原值,新建默认 true */ - private Boolean printerPdfNavBarVisible; - private HubUserMenuStyle userMenuStyle; - /** 设定侧栏菜单显隐;不传则保持原值 */ - private HubSettingsMenu settingsMenu; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/hub/dtos/HubSettingsMenu.java b/src/main/java/com/sjmresorts/opsapp/esign/hub/dtos/HubSettingsMenu.java deleted file mode 100644 index a64fef2..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/hub/dtos/HubSettingsMenu.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.sjmresorts.opsapp.esign.hub.dtos; - -import com.fasterxml.jackson.annotation.JsonInclude; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * App 设定侧栏菜单项显隐(CMS「首页外观」配置,默认全部显示)。 - */ -@Data -@NoArgsConstructor -@JsonInclude(JsonInclude.Include.NON_NULL) -public class HubSettingsMenu { - - /** 后端服务设定 */ - private Boolean apiSettings; - /** 连接打印 PC */ - private Boolean printerAgent; - /** 虚拟打印机登记卡 */ - private Boolean printerPdf; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/hub/dtos/HubUserMenuStyle.java b/src/main/java/com/sjmresorts/opsapp/esign/hub/dtos/HubUserMenuStyle.java deleted file mode 100644 index a38fb8a..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/hub/dtos/HubUserMenuStyle.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.sjmresorts.opsapp.esign.hub.dtos; - -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -public class HubUserMenuStyle { - - private String background; - private String textColor; - private String fontSize; - private String itemPadding; - private String borderRadius; - private String activeBackground; - private String maskBackground; - private String minWidth; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/hub/models/EsignAppPageLayout.java b/src/main/java/com/sjmresorts/opsapp/esign/hub/models/EsignAppPageLayout.java deleted file mode 100644 index 6cc5563..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/hub/models/EsignAppPageLayout.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.sjmresorts.opsapp.esign.hub.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_app_page_layout") -public class EsignAppPageLayout { - - @Id - @Column(name = "layout_id", nullable = false, length = 36) - private String layoutId; - - @Column(name = "page_key", nullable = false, length = 64) - private String pageKey; - - @Column(name = "hotel_id", length = 64) - private String hotelId; - - @Lob - @Column(name = "layout_json", nullable = false) - private String layoutJson; - - @Lob - @Column(name = "published_layout_json") - private String publishedLayoutJson; - - @Column(name = "status", nullable = false, length = 16) - private String status; - - @Column(name = "layout_version", nullable = false) - private int layoutVersion; - - @Column(name = "updated_at", nullable = false) - private Instant updatedAt; - - @Column(name = "published_at") - private Instant publishedAt; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/hub/models/EsignHubPromotion.java b/src/main/java/com/sjmresorts/opsapp/esign/hub/models/EsignHubPromotion.java deleted file mode 100644 index 41187ca..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/hub/models/EsignHubPromotion.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.sjmresorts.opsapp.esign.hub.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_hub_promotions") -public class EsignHubPromotion { - - @Id - @Column(name = "promotion_id", nullable = false, length = 36) - private String promotionId; - - @Column(name = "title", length = 128) - private String title; - - @Column(name = "subtitle", length = 256) - private String subtitle; - - @Column(name = "tag", length = 32) - private String tag; - - @Column(name = "theme", nullable = false, length = 32) - private String theme; - - @Column(name = "display_type", nullable = false, length = 16) - private String displayType; - - @Lob - @Column(name = "image_url") - private String imageUrl; - - /** 图片/视频上文字叠放位置:TOP | CENTER | BOTTOM | NONE */ - @Column(name = "text_position", length = 16) - private String textPosition; - - @Column(name = "hotel_id", length = 64) - private String hotelId; - - @Column(name = "status", nullable = false, length = 32) - private String status; - - @Column(name = "sort_order", nullable = false) - private Integer sortOrder; - - @Column(name = "created_at", nullable = false) - private Instant createdAt; - - @Column(name = "updated_at", nullable = false) - private Instant updatedAt; - - @Column(name = "published_at") - private Instant publishedAt; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/hub/models/EsignHubSettings.java b/src/main/java/com/sjmresorts/opsapp/esign/hub/models/EsignHubSettings.java deleted file mode 100644 index ad93867..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/hub/models/EsignHubSettings.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.sjmresorts.opsapp.esign.hub.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_hub_settings") -public class EsignHubSettings { - - @Id - @Column(name = "setting_id", nullable = false, length = 36) - private String settingId; - - @Column(name = "hotel_id", length = 64) - private String hotelId; - - @Column(name = "nav_bar_color", nullable = false, length = 16) - private String navBarColor; - - @Column(name = "nav_bar_color_dark", length = 16) - private String navBarColorDark; - - /** 首页顶栏(铃铛/品牌/登出)是否显示;null 视为 true */ - @Column(name = "nav_bar_visible") - private Boolean navBarVisible; - - /** 虚拟打印机登记卡页顶栏(返回/标题/齿轮)是否显示;null 视为 true */ - @Column(name = "printer_pdf_nav_bar_visible") - private Boolean printerPdfNavBarVisible; - - @Lob - @Column(name = "user_menu_style_json") - private String userMenuStyleJson; - - /** App 设定侧栏菜单显隐 JSON;null 视为全部显示 */ - @Lob - @Column(name = "settings_menu_json") - private String settingsMenuJson; - - @Column(name = "updated_at", nullable = false) - private Instant updatedAt; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/hub/repositories/EsignAppPageLayoutRepository.java b/src/main/java/com/sjmresorts/opsapp/esign/hub/repositories/EsignAppPageLayoutRepository.java deleted file mode 100644 index 72cc68c..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/hub/repositories/EsignAppPageLayoutRepository.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.sjmresorts.opsapp.esign.hub.repositories; - -import com.sjmresorts.opsapp.esign.hub.models.EsignAppPageLayout; -import org.springframework.data.jpa.repository.JpaRepository; - -import java.util.Optional; - -public interface EsignAppPageLayoutRepository extends JpaRepository { - - Optional findFirstByPageKeyAndHotelIdOrderByUpdatedAtDesc(String pageKey, String hotelId); -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/hub/repositories/EsignHubPromotionRepository.java b/src/main/java/com/sjmresorts/opsapp/esign/hub/repositories/EsignHubPromotionRepository.java deleted file mode 100644 index e8f579f..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/hub/repositories/EsignHubPromotionRepository.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.sjmresorts.opsapp.esign.hub.repositories; - -import com.sjmresorts.opsapp.esign.hub.models.EsignHubPromotion; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.List; - -@Repository -public interface EsignHubPromotionRepository extends JpaRepository { - - List findAllByOrderBySortOrderAscUpdatedAtDesc(); - - List findByStatusOrderBySortOrderAscUpdatedAtDesc(String status); -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/hub/repositories/EsignHubSettingsRepository.java b/src/main/java/com/sjmresorts/opsapp/esign/hub/repositories/EsignHubSettingsRepository.java deleted file mode 100644 index ae69093..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/hub/repositories/EsignHubSettingsRepository.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.sjmresorts.opsapp.esign.hub.repositories; - -import com.sjmresorts.opsapp.esign.hub.models.EsignHubSettings; -import org.springframework.data.jpa.repository.JpaRepository; - -import java.util.Optional; - -public interface EsignHubSettingsRepository extends JpaRepository { - - Optional findFirstByHotelIdOrderByUpdatedAtDesc(String hotelId); - - Optional findFirstByHotelIdIsNullOrderByUpdatedAtDesc(); -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/hub/services/EsignAppPageLayoutService.java b/src/main/java/com/sjmresorts/opsapp/esign/hub/services/EsignAppPageLayoutService.java deleted file mode 100644 index c3d3683..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/hub/services/EsignAppPageLayoutService.java +++ /dev/null @@ -1,218 +0,0 @@ -package com.sjmresorts.opsapp.esign.hub.services; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.sjmresorts.opsapp.esign.hub.dtos.EsignAppPageLayoutResponse; -import com.sjmresorts.opsapp.esign.hub.dtos.EsignAppPageLayoutSaveRequest; -import com.sjmresorts.opsapp.esign.hub.models.EsignAppPageLayout; -import com.sjmresorts.opsapp.esign.hub.repositories.EsignAppPageLayoutRepository; -import com.sjmresorts.opsapp.esign.common.support.EsignReservationParamUtil; -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.Iterator; -import java.util.Set; - -@Service -public class EsignAppPageLayoutService { - - public static final String DEFAULT_LAYOUT_ID = "DEFAULT_HOME"; - public static final String DEFAULT_PAGE_KEY = "home"; - - private static final Set ALLOWED_BLOCK_TYPES = Set.of( - "promotions", - "menuGrid", - "banner", - "spacer", - "text"); - - private static final String DEFAULT_LAYOUT_JSON = """ - {"version":1,"blocks":[ - {"id":"promo-1","type":"promotions","props":{"height":180,"limit":5}}, - {"id":"menu-1","type":"menuGrid","props":{"columns":4,"items":[ - {"icon":"bi-envelope","label":"待簽消息","path":"/pages/inbox/index"}, - {"icon":"bi-journal-check","label":"簽收記錄","path":"/pages/records/index"}, - {"icon":"bi-gear","label":"設定","path":"/pages/settings/index"}]}} - ]}"""; - - @Resource - private EsignAppPageLayoutRepository layoutRepository; - - private final ObjectMapper objectMapper = new ObjectMapper(); - - public EsignAppPageLayoutResponse getForAdmin(String pageKey) { - EsignAppPageLayout layout = ensureDefaultRow(normalizePageKey(pageKey)); - return buildOk(layout); - } - - public EsignAppPageLayoutResponse getForApp(String pageKey, String hotelId) { - String key = normalizePageKey(pageKey); - String normalizedHotelId = StringUtils.isNotBlank(hotelId) - ? EsignReservationParamUtil.normalizeParam(hotelId) - : null; - EsignAppPageLayout layout = null; - if (normalizedHotelId != null) { - layout = layoutRepository.findFirstByPageKeyAndHotelIdOrderByUpdatedAtDesc(key, normalizedHotelId) - .orElse(null); - } - if (layout == null) { - layout = ensureDefaultRow(key); - } - EsignAppPageLayoutResponse r = buildOk(layout); - if (r.getData() != null && r.getData().getLayout() != null) { - String published = layout.getPublishedLayoutJson(); - if (StringUtils.isNotBlank(published)) { - r.getData().getLayout().setLayoutJson(published); - } - } - return r; - } - - @Transactional - public EsignAppPageLayoutResponse save(EsignAppPageLayoutSaveRequest request) { - if (request == null || StringUtils.isBlank(request.getLayoutJson())) { - return EsignAppPageLayoutResponse.fail("400", "layoutJson required"); - } - String normalizedJson; - try { - normalizedJson = validateAndNormalizeLayoutJson(request.getLayoutJson()); - } catch (IllegalArgumentException e) { - return EsignAppPageLayoutResponse.fail("400", e.getMessage()); - } - - String pageKey = normalizePageKey(request.getPageKey()); - EsignAppPageLayout entity = ensureDefaultRow(pageKey); - entity.setLayoutJson(normalizedJson); - entity.setLayoutVersion(entity.getLayoutVersion() + 1); - entity.setUpdatedAt(Instant.now()); - - boolean publish = request.getPublish() == null || Boolean.TRUE.equals(request.getPublish()); - if (publish) { - entity.setPublishedLayoutJson(normalizedJson); - entity.setStatus("PUBLISHED"); - entity.setPublishedAt(Instant.now()); - } else { - entity.setStatus("DRAFT"); - } - - layoutRepository.save(entity); - EsignAppPageLayoutResponse r = buildOk(entity); - r.setMessage(publish ? "published" : "saved"); - return r; - } - - @Transactional - public EsignAppPageLayoutResponse publish(String pageKey) { - EsignAppPageLayout entity = ensureDefaultRow(normalizePageKey(pageKey)); - if (StringUtils.isBlank(entity.getLayoutJson())) { - return EsignAppPageLayoutResponse.fail("400", "no draft layout"); - } - entity.setPublishedLayoutJson(entity.getLayoutJson()); - entity.setStatus("PUBLISHED"); - entity.setPublishedAt(Instant.now()); - entity.setUpdatedAt(Instant.now()); - layoutRepository.save(entity); - EsignAppPageLayoutResponse r = buildOk(entity); - r.setMessage("published"); - return r; - } - - private EsignAppPageLayout ensureDefaultRow(String pageKey) { - if (DEFAULT_PAGE_KEY.equals(pageKey)) { - return layoutRepository.findById(DEFAULT_LAYOUT_ID).orElseGet(() -> { - EsignAppPageLayout row = new EsignAppPageLayout(); - row.setLayoutId(DEFAULT_LAYOUT_ID); - row.setPageKey(DEFAULT_PAGE_KEY); - row.setHotelId(null); - row.setLayoutJson(DEFAULT_LAYOUT_JSON); - row.setPublishedLayoutJson(DEFAULT_LAYOUT_JSON); - row.setStatus("PUBLISHED"); - row.setLayoutVersion(1); - row.setUpdatedAt(Instant.now()); - row.setPublishedAt(Instant.now()); - return layoutRepository.save(row); - }); - } - return layoutRepository.findFirstByPageKeyAndHotelIdOrderByUpdatedAtDesc(pageKey, null) - .orElseGet(() -> { - EsignAppPageLayout row = new EsignAppPageLayout(); - row.setLayoutId(DEFAULT_LAYOUT_ID + "_" + pageKey); - row.setPageKey(pageKey); - row.setHotelId(null); - row.setLayoutJson(DEFAULT_LAYOUT_JSON); - row.setPublishedLayoutJson(DEFAULT_LAYOUT_JSON); - row.setStatus("PUBLISHED"); - row.setLayoutVersion(1); - row.setUpdatedAt(Instant.now()); - row.setPublishedAt(Instant.now()); - return layoutRepository.save(row); - }); - } - - private String validateAndNormalizeLayoutJson(String raw) { - JsonNode root; - try { - root = objectMapper.readTree(raw); - } catch (Exception e) { - throw new IllegalArgumentException("layoutJson invalid JSON"); - } - if (!root.isObject()) { - throw new IllegalArgumentException("layoutJson must be object"); - } - JsonNode blocks = root.get("blocks"); - if (blocks == null || !blocks.isArray()) { - throw new IllegalArgumentException("layoutJson.blocks must be array"); - } - Iterator it = blocks.elements(); - while (it.hasNext()) { - JsonNode block = it.next(); - JsonNode typeNode = block.get("type"); - if (typeNode == null || !typeNode.isTextual()) { - throw new IllegalArgumentException("each block requires type"); - } - if (!ALLOWED_BLOCK_TYPES.contains(typeNode.asText())) { - throw new IllegalArgumentException("unsupported block type: " + typeNode.asText()); - } - if (block.get("id") == null || !block.get("id").isTextual()) { - throw new IllegalArgumentException("each block requires string id"); - } - } - try { - return objectMapper.writeValueAsString(root); - } catch (Exception e) { - throw new IllegalArgumentException("layoutJson serialize failed"); - } - } - - private String normalizePageKey(String pageKey) { - if (StringUtils.isBlank(pageKey)) { - return DEFAULT_PAGE_KEY; - } - return pageKey.trim().toLowerCase(); - } - - private EsignAppPageLayoutResponse buildOk(EsignAppPageLayout layout) { - EsignAppPageLayoutResponse r = EsignAppPageLayoutResponse.ok("ok"); - EsignAppPageLayoutResponse.AppPageLayoutData data = new EsignAppPageLayoutResponse.AppPageLayoutData(); - data.setLayout(toItem(layout)); - r.setData(data); - return r; - } - - private EsignAppPageLayoutResponse.AppPageLayoutItem toItem(EsignAppPageLayout layout) { - EsignAppPageLayoutResponse.AppPageLayoutItem item = new EsignAppPageLayoutResponse.AppPageLayoutItem(); - item.setLayoutId(layout.getLayoutId()); - item.setPageKey(layout.getPageKey()); - item.setHotelId(layout.getHotelId()); - item.setLayoutJson(layout.getLayoutJson()); - item.setPublishedLayoutJson(layout.getPublishedLayoutJson()); - item.setStatus(layout.getStatus()); - item.setLayoutVersion(layout.getLayoutVersion()); - item.setUpdatedAt(layout.getUpdatedAt() != null ? layout.getUpdatedAt().toString() : null); - item.setPublishedAt(layout.getPublishedAt() != null ? layout.getPublishedAt().toString() : null); - return item; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/hub/services/EsignHubPromotionMediaService.java b/src/main/java/com/sjmresorts/opsapp/esign/hub/services/EsignHubPromotionMediaService.java deleted file mode 100644 index 0fdc822..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/hub/services/EsignHubPromotionMediaService.java +++ /dev/null @@ -1,298 +0,0 @@ -package com.sjmresorts.opsapp.esign.hub.services; - -import com.sjmresorts.opsapp.esign.hub.models.EsignHubPromotion; -import com.sjmresorts.opsapp.esign.hub.repositories.EsignHubPromotionRepository; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.Resource; -import org.apache.commons.lang3.StringUtils; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.time.Instant; -import java.util.Base64; -import java.util.Locale; -import java.util.Optional; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -@Service -public class EsignHubPromotionMediaService { - - public static final String STORED_PREFIX = "stored://"; - private static final Pattern DATA_URL = Pattern.compile("^data:([^;]+);base64,(.+)$", Pattern.CASE_INSENSITIVE | Pattern.DOTALL); - - @Value("${ops-app.hub-promotion.media-dir:C:/auto-check-in/hub-promotions}") - private String mediaDir; - - @Resource - private EsignHubPromotionRepository promotionRepository; - - private Path mediaRoot; - - @PostConstruct - void init() throws IOException { - mediaRoot = Paths.get(mediaDir).toAbsolutePath().normalize(); - Files.createDirectories(mediaRoot); - } - - /** - * 将 data URL 落盘,DB 仅存 stored:// 引用,避免列表接口返回数 MB JSON。 - */ - public String persistIfNeeded(String promotionId, String displayType, String imageUrl) throws IOException { - if (StringUtils.isBlank(imageUrl)) { - deleteStoredFile(promotionId); - return null; - } - String trimmed = imageUrl.trim(); - if (trimmed.startsWith(STORED_PREFIX)) { - return trimmed; - } - if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) { - deleteStoredFile(promotionId); - return trimmed; - } - if (!trimmed.startsWith("data:")) { - return trimmed; - } - ParsedDataUrl parsed = parseDataUrl(trimmed); - if (parsed == null) { - throw new IOException("invalid data URL"); - } - String ext = extensionForMime(parsed.mimeType(), displayType); - Path target = mediaRoot.resolve(promotionId + ext); - Files.write(target, parsed.bytes()); - deleteSiblingFiles(promotionId, target); - return STORED_PREFIX + promotionId; - } - - public String saveBytes(String promotionId, String displayType, byte[] bytes, String contentType) throws IOException { - if (StringUtils.isBlank(promotionId) || bytes == null || bytes.length == 0) { - throw new IOException("empty media"); - } - String ext = extensionForMime(StringUtils.defaultString(contentType), displayType); - Path target = mediaRoot.resolve(promotionId.trim() + ext); - Files.write(target, bytes); - deleteSiblingFiles(promotionId.trim(), target); - return STORED_PREFIX + promotionId.trim(); - } - - public Path getMediaRoot() { - return mediaRoot; - } - - public Optional load(String promotionId) { - if (StringUtils.isBlank(promotionId)) { - return Optional.empty(); - } - Optional opt = promotionRepository.findById(promotionId.trim()); - if (opt.isEmpty()) { - return Optional.empty(); - } - return load(opt.get()); - } - - public Optional load(EsignHubPromotion row) { - Optional path = resolveMediaFile(row); - if (path.isEmpty()) { - return Optional.empty(); - } - try { - Path file = path.get(); - return Optional.of(new MediaPayload(Files.readAllBytes(file), probeMime(file))); - } catch (IOException ignored) { - return Optional.empty(); - } - } - - /** 访问 /media 时优先读磁盘文件;遗留 data: 会先落盘再返回路径。 */ - @Transactional - public Optional resolveMediaFile(String promotionId) { - if (StringUtils.isBlank(promotionId)) { - return Optional.empty(); - } - Optional opt = promotionRepository.findById(promotionId.trim()); - return opt.flatMap(this::resolveMediaFile); - } - - @Transactional - public Optional resolveMediaFile(EsignHubPromotion row) { - if (row == null) { - return Optional.empty(); - } - String imageUrl = StringUtils.trimToEmpty(row.getImageUrl()); - if (imageUrl.startsWith("data:")) { - try { - String stored = persistIfNeeded(row.getPromotionId(), row.getDisplayType(), imageUrl); - row.setImageUrl(stored); - promotionRepository.save(row); - imageUrl = StringUtils.trimToEmpty(stored); - } catch (IOException ignored) { - return Optional.empty(); - } - } - String fileId = resolveStoredFileId(row.getPromotionId(), imageUrl); - return findStoredFilePath(fileId); - } - - public String probeMimeForPath(Path path) throws IOException { - return probeMime(path); - } - - private static String resolveStoredFileId(String promotionId, String imageUrl) { - if (StringUtils.isBlank(imageUrl)) { - return promotionId; - } - if (imageUrl.startsWith(STORED_PREFIX)) { - return imageUrl.substring(STORED_PREFIX.length()); - } - if (isHubMediaPath(imageUrl)) { - return promotionId; - } - return promotionId; - } - - private static boolean isHubMediaPath(String url) { - if (StringUtils.isBlank(url) || !url.contains("/esign/hub-promotions/")) { - return false; - } - String path = url.trim(); - int q = path.indexOf('?'); - if (q >= 0) { - path = path.substring(0, q); - } - int hash = path.indexOf('#'); - if (hash >= 0) { - path = path.substring(0, hash); - } - return path.endsWith("/media"); - } - - public boolean shouldUseMediaEndpoint(EsignHubPromotion row) { - if (row == null || StringUtils.isBlank(row.getImageUrl())) { - return false; - } - String imageUrl = row.getImageUrl().trim(); - return imageUrl.startsWith(STORED_PREFIX) - || imageUrl.startsWith("data:") - || isHubMediaPath(imageUrl); - } - - public String toRelativeMediaPath(String promotionId) { - return toRelativeMediaPath(promotionId, null); - } - - public String toRelativeMediaPath(String promotionId, Instant updatedAt) { - String path = "/esign/hub-promotions/" + promotionId + "/media"; - if (updatedAt == null) { - return path; - } - return path + "?v=" + updatedAt.toEpochMilli(); - } - - private Optional readStoredFile(String promotionId) { - return findStoredFilePath(promotionId).map(path -> { - try { - return new MediaPayload(Files.readAllBytes(path), probeMime(path)); - } catch (IOException e) { - return null; - } - }).filter(payload -> payload != null); - } - - private Optional findStoredFilePath(String promotionId) { - if (StringUtils.isBlank(promotionId)) { - return Optional.empty(); - } - for (Path path : candidatePaths(promotionId.trim())) { - if (Files.isRegularFile(path)) { - return Optional.of(path); - } - } - return Optional.empty(); - } - - private Path[] candidatePaths(String promotionId) { - return new Path[] { - mediaRoot.resolve(promotionId + ".jpg"), - mediaRoot.resolve(promotionId + ".jpeg"), - mediaRoot.resolve(promotionId + ".png"), - mediaRoot.resolve(promotionId + ".webp"), - mediaRoot.resolve(promotionId + ".gif"), - mediaRoot.resolve(promotionId + ".mp4"), - mediaRoot.resolve(promotionId + ".webm") - }; - } - - private void deleteStoredFile(String promotionId) throws IOException { - if (StringUtils.isBlank(promotionId)) { - return; - } - deleteSiblingFiles(promotionId, null); - } - - private void deleteSiblingFiles(String promotionId, Path keep) throws IOException { - String prefix = promotionId + "."; - if (!Files.isDirectory(mediaRoot)) { - return; - } - try (var stream = Files.list(mediaRoot)) { - stream.filter(p -> p.getFileName().toString().startsWith(prefix)) - .filter(p -> keep == null || !p.equals(keep)) - .forEach(p -> { - try { - Files.deleteIfExists(p); - } catch (IOException ignored) { - } - }); - } - } - - private static String extensionForMime(String mimeType, String displayType) { - String mime = StringUtils.defaultString(mimeType).toLowerCase(Locale.ROOT); - if (mime.contains("png")) return ".png"; - if (mime.contains("webp")) return ".webp"; - if (mime.contains("gif")) return ".gif"; - if (mime.contains("mp4")) return ".mp4"; - if (mime.contains("webm")) return ".webm"; - if ("VIDEO".equalsIgnoreCase(displayType)) return ".mp4"; - return ".jpg"; - } - - private static String probeMime(Path path) throws IOException { - String probed = Files.probeContentType(path); - if (StringUtils.isNotBlank(probed)) { - return probed; - } - String name = path.getFileName().toString().toLowerCase(Locale.ROOT); - if (name.endsWith(".png")) return "image/png"; - if (name.endsWith(".webp")) return "image/webp"; - if (name.endsWith(".gif")) return "image/gif"; - if (name.endsWith(".mp4")) return "video/mp4"; - if (name.endsWith(".webm")) return "video/webm"; - return "image/jpeg"; - } - - private static ParsedDataUrl parseDataUrl(String dataUrl) { - Matcher m = DATA_URL.matcher(dataUrl); - if (!m.matches()) { - return null; - } - try { - byte[] bytes = Base64.getDecoder().decode(m.group(2).replaceAll("\\s", "")); - return new ParsedDataUrl(StringUtils.defaultString(m.group(1)), bytes); - } catch (IllegalArgumentException e) { - return null; - } - } - - private record ParsedDataUrl(String mimeType, byte[] bytes) { - } - - public record MediaPayload(byte[] bytes, String contentType) { - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/hub/services/EsignHubPromotionService.java b/src/main/java/com/sjmresorts/opsapp/esign/hub/services/EsignHubPromotionService.java deleted file mode 100644 index a47ba25..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/hub/services/EsignHubPromotionService.java +++ /dev/null @@ -1,398 +0,0 @@ -package com.sjmresorts.opsapp.esign.hub.services; - -import com.sjmresorts.opsapp.esign.hub.dtos.EsignHubPromotionResponse; -import com.sjmresorts.opsapp.esign.hub.dtos.EsignHubPromotionSaveRequest; -import com.sjmresorts.opsapp.esign.hub.models.EsignHubPromotion; -import com.sjmresorts.opsapp.esign.hub.repositories.EsignHubPromotionRepository; -import com.sjmresorts.opsapp.esign.common.support.EsignReservationParamUtil; -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.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import java.util.UUID; -import java.util.stream.Collectors; - -@Service -public class EsignHubPromotionService { - - public static final String STATUS_DRAFT = "DRAFT"; - public static final String STATUS_PUBLISHED = "PUBLISHED"; - public static final String STATUS_ARCHIVED = "ARCHIVED"; - - private static final Set ALLOWED_THEMES = Set.of("brand", "gold", "teal"); - private static final Set ALLOWED_DISPLAY_TYPES = Set.of("CARD", "IMAGE", "VIDEO"); - private static final Set ALLOWED_TEXT_POSITIONS = Set.of("TOP", "CENTER", "BOTTOM", "NONE"); - /** Base64 data URL 上限;约可容纳 10MB 原图 */ - private static final int MAX_IMAGE_URL_LENGTH = 15_000_000; - - @Resource - private EsignHubPromotionRepository promotionRepository; - - @Resource - private EsignHubPromotionMediaService mediaService; - - public EsignHubPromotionResponse listAllForAdmin() { - List rows = promotionRepository.findAllByOrderBySortOrderAscUpdatedAtDesc(); - for (EsignHubPromotion row : rows) { - persistMigrationIfNeeded(row); - } - EsignHubPromotionResponse r = EsignHubPromotionResponse.ok("ok"); - EsignHubPromotionResponse.EsignHubPromotionData data = new EsignHubPromotionResponse.EsignHubPromotionData(); - data.setPromotions(rows.stream().map(this::toClientItem).collect(Collectors.toList())); - r.setData(data); - return r; - } - - public EsignHubPromotionResponse listPublished(String hotelId) { - List rows = promotionRepository.findByStatusOrderBySortOrderAscUpdatedAtDesc(STATUS_PUBLISHED); - String normalizedHotelId = StringUtils.isNotBlank(hotelId) - ? EsignReservationParamUtil.normalizeParam(hotelId) - : null; - List filtered = new ArrayList<>(); - for (EsignHubPromotion row : rows) { - if (normalizedHotelId != null && StringUtils.isNotBlank(row.getHotelId()) - && !normalizedHotelId.equals(row.getHotelId())) { - continue; - } - persistMigrationIfNeeded(row); - filtered.add(toClientItem(row)); - } - EsignHubPromotionResponse r = EsignHubPromotionResponse.ok("ok"); - EsignHubPromotionResponse.EsignHubPromotionData data = new EsignHubPromotionResponse.EsignHubPromotionData(); - data.setPromotions(filtered); - r.setData(data); - return r; - } - - public EsignHubPromotionResponse getDetail(String promotionId) { - Optional opt = findPromotion(promotionId); - if (opt.isEmpty()) { - return EsignHubPromotionResponse.fail("404", "Promotion not found"); - } - EsignHubPromotionResponse r = EsignHubPromotionResponse.ok("ok"); - EsignHubPromotionResponse.EsignHubPromotionData data = new EsignHubPromotionResponse.EsignHubPromotionData(); - data.setPromotion(toClientItem(opt.get())); - r.setData(data); - return r; - } - - @Transactional - public EsignHubPromotionResponse save(EsignHubPromotionSaveRequest request) { - if (request == null) { - return EsignHubPromotionResponse.fail("400", "request required"); - } - - Instant now = Instant.now(); - EsignHubPromotion entity; - if (StringUtils.isNotBlank(request.getPromotionId())) { - Optional opt = findPromotion(request.getPromotionId()); - if (opt.isEmpty()) { - return EsignHubPromotionResponse.fail("404", "Promotion not found"); - } - entity = opt.get(); - } else { - entity = new EsignHubPromotion(); - entity.setPromotionId(UUID.randomUUID().toString()); - entity.setStatus(STATUS_DRAFT); - entity.setCreatedAt(now); - } - - entity.setTitle(trim(request.getTitle(), 128)); - entity.setSubtitle(trim(request.getSubtitle(), 256)); - entity.setTag(trim(request.getTag(), 32)); - entity.setTheme(normalizeTheme(request.getTheme())); - entity.setDisplayType(normalizeDisplayType(request.getDisplayType())); - entity.setTextPosition(normalizeTextPosition(request.getTextPosition())); - String imageUrl; - try { - imageUrl = resolveImageUrlForSave(entity, request.getImageUrl()); - } catch (IllegalStateException e) { - return EsignHubPromotionResponse.fail("500", "failed to store promotion media"); - } - if ("VIDEO".equals(entity.getDisplayType()) && StringUtils.isBlank(imageUrl)) { - return EsignHubPromotionResponse.fail("400", "imageUrl required for VIDEO display (video URL)"); - } - if ("CARD".equals(entity.getDisplayType()) - && StringUtils.isBlank(imageUrl) - && StringUtils.isBlank(entity.getTitle()) - && StringUtils.isBlank(entity.getSubtitle()) - && StringUtils.isBlank(entity.getTag())) { - return EsignHubPromotionResponse.fail("400", "card promotion needs title, subtitle, tag, or image"); - } - entity.setImageUrl(imageUrl); - String hotelId = trim(request.getHotelId(), 64); - entity.setHotelId(StringUtils.isNotBlank(hotelId) ? EsignReservationParamUtil.normalizeParam(hotelId) : null); - entity.setSortOrder(request.getSortOrder() != null ? request.getSortOrder() : 0); - entity.setUpdatedAt(now); - promotionRepository.save(entity); - - EsignHubPromotionResponse r = EsignHubPromotionResponse.ok("saved"); - EsignHubPromotionResponse.EsignHubPromotionData data = new EsignHubPromotionResponse.EsignHubPromotionData(); - data.setPromotionId(entity.getPromotionId()); - data.setPromotion(toClientItem(entity)); - r.setData(data); - return r; - } - - @Transactional - public EsignHubPromotionResponse publish(String promotionId) { - Optional opt = findPromotion(promotionId); - if (opt.isEmpty()) { - return EsignHubPromotionResponse.fail("404", "Promotion not found"); - } - EsignHubPromotion entity = opt.get(); - persistMigrationIfNeeded(entity); - if ("IMAGE".equalsIgnoreCase(entity.getDisplayType()) - && mediaService.resolveMediaFile(entity).isEmpty()) { - return EsignHubPromotionResponse.fail("400", "upload image before publish"); - } - entity.setStatus(STATUS_PUBLISHED); - entity.setPublishedAt(Instant.now()); - entity.setUpdatedAt(Instant.now()); - promotionRepository.save(entity); - return EsignHubPromotionResponse.ok("published"); - } - - @Transactional - public EsignHubPromotionResponse archive(String promotionId) { - Optional opt = findPromotion(promotionId); - if (opt.isEmpty()) { - return EsignHubPromotionResponse.fail("404", "Promotion not found"); - } - EsignHubPromotion entity = opt.get(); - entity.setStatus(STATUS_ARCHIVED); - entity.setUpdatedAt(Instant.now()); - promotionRepository.save(entity); - return EsignHubPromotionResponse.ok("archived"); - } - - @Transactional - public EsignHubPromotionResponse delete(String promotionId) { - Optional opt = findPromotion(promotionId); - if (opt.isEmpty()) { - return EsignHubPromotionResponse.fail("404", "Promotion not found"); - } - promotionRepository.delete(opt.get()); - try { - mediaService.persistIfNeeded(promotionId, "CARD", null); - } catch (Exception ignored) { - } - return EsignHubPromotionResponse.ok("deleted"); - } - - @Transactional - public EsignHubPromotionResponse uploadMedia(String promotionId, byte[] bytes, String contentType) { - if (StringUtils.isBlank(promotionId) || bytes == null || bytes.length == 0) { - return EsignHubPromotionResponse.fail("400", "file required"); - } - Optional opt = findPromotion(promotionId); - if (opt.isEmpty()) { - return EsignHubPromotionResponse.fail("404", "Promotion not found"); - } - EsignHubPromotion entity = opt.get(); - try { - String stored = mediaService.saveBytes( - entity.getPromotionId(), entity.getDisplayType(), bytes, contentType); - entity.setImageUrl(stored); - entity.setUpdatedAt(Instant.now()); - promotionRepository.save(entity); - } catch (Exception e) { - return EsignHubPromotionResponse.fail("500", "failed to store promotion media"); - } - EsignHubPromotionResponse r = EsignHubPromotionResponse.ok("media uploaded"); - EsignHubPromotionResponse.EsignHubPromotionData data = new EsignHubPromotionResponse.EsignHubPromotionData(); - data.setPromotionId(entity.getPromotionId()); - data.setPromotion(toClientItem(entity)); - r.setData(data); - return r; - } - - private Optional findPromotion(String promotionId) { - if (StringUtils.isBlank(promotionId)) { - return Optional.empty(); - } - return promotionRepository.findById(promotionId.trim()); - } - - private EsignHubPromotionResponse.EsignHubPromotionItem toClientItem(EsignHubPromotion row) { - EsignHubPromotionResponse.EsignHubPromotionItem item = toItem(row); - item.setImageUrl(resolveClientImageUrl(row)); - return item; - } - - private String resolveClientImageUrl(EsignHubPromotion row) { - if (row == null || StringUtils.isBlank(row.getImageUrl())) { - return null; - } - String imageUrl = row.getImageUrl().trim(); - if (imageUrl.startsWith("http://") || imageUrl.startsWith("https://")) { - return imageUrl; - } - if (imageUrl.startsWith("data:") || mediaService.shouldUseMediaEndpoint(row)) { - return mediaService.toRelativeMediaPath(row.getPromotionId(), row.getUpdatedAt()); - } - return imageUrl; - } - - private String resolveImageUrlForSave(EsignHubPromotion entity, String requestImageUrl) { - String promotionId = entity.getPromotionId(); - String displayType = entity.getDisplayType(); - if (requestImageUrl == null) { - return normalizeExistingImageRef(entity.getImageUrl(), promotionId); - } - if (requestImageUrl.isEmpty()) { - try { - mediaService.persistIfNeeded(promotionId, displayType, null); - } catch (Exception ignored) { - } - return null; - } - return prepareImageUrl(promotionId, displayType, requestImageUrl); - } - - private String normalizeExistingImageRef(String existing, String promotionId) { - if (StringUtils.isBlank(existing)) { - return null; - } - String trimmed = existing.trim(); - if (trimmed.startsWith(EsignHubPromotionMediaService.STORED_PREFIX)) { - return trimmed; - } - if (trimmed.startsWith("data:")) { - return trimmed; - } - if (isHubMediaPath(trimmed)) { - return EsignHubPromotionMediaService.STORED_PREFIX + promotionId; - } - return trimmed; - } - - private static boolean isHubMediaPath(String url) { - if (StringUtils.isBlank(url) || !url.contains("/esign/hub-promotions/")) { - return false; - } - String path = url.trim(); - int q = path.indexOf('?'); - if (q >= 0) { - path = path.substring(0, q); - } - int hash = path.indexOf('#'); - if (hash >= 0) { - path = path.substring(0, hash); - } - return path.endsWith("/media"); - } - - private String prepareImageUrl(String promotionId, String displayType, String rawImageUrl) { - if (StringUtils.isBlank(rawImageUrl)) { - return null; - } - String trimmed = rawImageUrl.trim(); - if (trimmed.startsWith("data:") || trimmed.startsWith(EsignHubPromotionMediaService.STORED_PREFIX)) { - try { - return mediaService.persistIfNeeded(promotionId, displayType, trimmed); - } catch (Exception e) { - throw new IllegalStateException("failed to store promotion media", e); - } - } - if (isHubMediaPath(trimmed)) { - return EsignHubPromotionMediaService.STORED_PREFIX + promotionId; - } - return normalizeImageUrl(trimmed); - } - - private void persistMigrationIfNeeded(EsignHubPromotion entity) { - if (migrateLegacyMediaIfNeeded(entity)) { - entity.setUpdatedAt(Instant.now()); - promotionRepository.save(entity); - } - } - - private boolean migrateLegacyMediaIfNeeded(EsignHubPromotion entity) { - if (entity == null || StringUtils.isBlank(entity.getImageUrl())) { - return false; - } - if (!entity.getImageUrl().trim().startsWith("data:")) { - return false; - } - try { - String stored = mediaService.persistIfNeeded( - entity.getPromotionId(), entity.getDisplayType(), entity.getImageUrl()); - entity.setImageUrl(stored); - return true; - } catch (Exception ignored) { - return false; - } - } - - private EsignHubPromotionResponse.EsignHubPromotionItem toItem(EsignHubPromotion row) { - EsignHubPromotionResponse.EsignHubPromotionItem item = new EsignHubPromotionResponse.EsignHubPromotionItem(); - item.setPromotionId(row.getPromotionId()); - item.setTitle(row.getTitle()); - item.setSubtitle(row.getSubtitle()); - item.setTag(row.getTag()); - item.setTheme(row.getTheme()); - item.setDisplayType(StringUtils.defaultIfBlank(row.getDisplayType(), "CARD")); - item.setImageUrl(row.getImageUrl()); - item.setTextPosition(StringUtils.defaultIfBlank(row.getTextPosition(), "BOTTOM")); - item.setHotelId(row.getHotelId()); - item.setStatus(row.getStatus()); - item.setSortOrder(row.getSortOrder()); - item.setCreatedAt(row.getCreatedAt() != null ? row.getCreatedAt().toString() : null); - item.setUpdatedAt(row.getUpdatedAt() != null ? row.getUpdatedAt().toString() : null); - item.setPublishedAt(row.getPublishedAt() != null ? row.getPublishedAt().toString() : null); - return item; - } - - private static String normalizeTheme(String theme) { - if (StringUtils.isBlank(theme)) { - return "brand"; - } - String t = theme.trim().toLowerCase(); - return ALLOWED_THEMES.contains(t) ? t : "brand"; - } - - private static String normalizeDisplayType(String displayType) { - if (StringUtils.isBlank(displayType)) { - return "CARD"; - } - String t = displayType.trim().toUpperCase(); - return ALLOWED_DISPLAY_TYPES.contains(t) ? t : "CARD"; - } - - private static String normalizeTextPosition(String textPosition) { - if (StringUtils.isBlank(textPosition)) { - return "BOTTOM"; - } - String t = textPosition.trim().toUpperCase(); - return ALLOWED_TEXT_POSITIONS.contains(t) ? t : "BOTTOM"; - } - - private static String normalizeImageUrl(String imageUrl) { - if (StringUtils.isBlank(imageUrl)) { - return null; - } - String t = imageUrl.trim(); - if (t.length() > MAX_IMAGE_URL_LENGTH) { - return t.substring(0, MAX_IMAGE_URL_LENGTH); - } - return t; - } - - 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/hub/services/EsignHubSettingsService.java b/src/main/java/com/sjmresorts/opsapp/esign/hub/services/EsignHubSettingsService.java deleted file mode 100644 index c57e114..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/hub/services/EsignHubSettingsService.java +++ /dev/null @@ -1,325 +0,0 @@ -package com.sjmresorts.opsapp.esign.hub.services; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.sjmresorts.opsapp.esign.hub.dtos.HubSettingsMenu; -import com.sjmresorts.opsapp.esign.hub.dtos.HubUserMenuStyle; -import com.sjmresorts.opsapp.esign.hub.dtos.EsignHubSettingsResponse; -import com.sjmresorts.opsapp.esign.hub.dtos.EsignHubSettingsSaveRequest; -import com.sjmresorts.opsapp.esign.hub.models.EsignHubSettings; -import com.sjmresorts.opsapp.esign.hub.repositories.EsignHubSettingsRepository; -import com.sjmresorts.opsapp.esign.common.support.EsignReservationParamUtil; -import jakarta.annotation.Resource; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.apache.commons.lang3.StringUtils; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.Instant; -import java.util.Optional; -import java.util.regex.Pattern; - -@Service -public class EsignHubSettingsService { - - private static final Logger log = LoggerFactory.getLogger(EsignHubSettingsService.class); - - public static final String DEFAULT_SETTING_ID = "DEFAULT"; - public static final String DEFAULT_NAV_BAR_COLOR = "#266A62"; - public static final String DEFAULT_NAV_BAR_COLOR_DARK = "#1a3d36"; - - private static final Pattern HEX_COLOR = Pattern.compile("^#([0-9A-Fa-f]{6}|[0-9A-Fa-f]{3})$"); - private static final Pattern CSS_LENGTH = Pattern.compile("^\\d+(\\.\\d+)?(px|rpx)$"); - private static final Pattern CSS_PADDING = Pattern.compile( - "^\\d+(\\.\\d+)?(px|rpx)(\\s+\\d+(\\.\\d+)?(px|rpx)){0,3}$"); - - private final ObjectMapper objectMapper = new ObjectMapper(); - - @Resource - private EsignHubSettingsRepository settingsRepository; - - public EsignHubSettingsResponse getForAdmin() { - EsignHubSettings settings = ensureDefaultRow(); - return buildOk(settings); - } - - public EsignHubSettingsResponse getForApp(String hotelId) { - try { - String normalizedHotelId = StringUtils.isNotBlank(hotelId) - ? EsignReservationParamUtil.normalizeParam(hotelId) - : null; - EsignHubSettings settings = null; - if (normalizedHotelId != null) { - settings = settingsRepository.findFirstByHotelIdOrderByUpdatedAtDesc(normalizedHotelId).orElse(null); - } - if (settings == null) { - settings = ensureDefaultRow(); - } - return buildOk(settings); - } catch (Exception e) { - log.warn("hub-settings/app unavailable, returning defaults: {}", e.toString()); - return buildDefaultAppResponse(); - } - } - - @Transactional - public EsignHubSettingsResponse save(EsignHubSettingsSaveRequest request) { - if (request == null) { - return EsignHubSettingsResponse.fail("400", "request required"); - } - String navBarColor = normalizeColor(request.getNavBarColor(), DEFAULT_NAV_BAR_COLOR); - if (navBarColor == null) { - return EsignHubSettingsResponse.fail("400", "navBarColor invalid, use #RRGGBB"); - } - String navBarColorDark = StringUtils.isNotBlank(request.getNavBarColorDark()) - ? normalizeColor(request.getNavBarColorDark(), DEFAULT_NAV_BAR_COLOR_DARK) - : DEFAULT_NAV_BAR_COLOR_DARK; - if (navBarColorDark == null) { - return EsignHubSettingsResponse.fail("400", "navBarColorDark invalid, use #RRGGBB"); - } - - HubUserMenuStyle menuStyle = mergeUserMenuStyle(request.getUserMenuStyle()); - String menuStyleJson; - try { - menuStyleJson = objectMapper.writeValueAsString(menuStyle); - } catch (JsonProcessingException e) { - return EsignHubSettingsResponse.fail("400", "userMenuStyle invalid"); - } - - EsignHubSettings entity = ensureDefaultRow(); - entity.setNavBarColor(navBarColor); - entity.setNavBarColorDark(navBarColorDark); - if (request.getNavBarVisible() != null) { - entity.setNavBarVisible(request.getNavBarVisible()); - } else if (entity.getNavBarVisible() == null) { - entity.setNavBarVisible(Boolean.TRUE); - } - if (request.getPrinterPdfNavBarVisible() != null) { - entity.setPrinterPdfNavBarVisible(request.getPrinterPdfNavBarVisible()); - } else if (entity.getPrinterPdfNavBarVisible() == null) { - entity.setPrinterPdfNavBarVisible(Boolean.TRUE); - } - entity.setUserMenuStyleJson(menuStyleJson); - if (request.getSettingsMenu() != null) { - try { - entity.setSettingsMenuJson( - objectMapper.writeValueAsString(mergeSettingsMenu(request.getSettingsMenu()))); - } catch (JsonProcessingException e) { - return EsignHubSettingsResponse.fail("400", "settingsMenu invalid"); - } - } else if (StringUtils.isBlank(entity.getSettingsMenuJson())) { - entity.setSettingsMenuJson(serializeSettingsMenu(defaultSettingsMenu())); - } - entity.setUpdatedAt(Instant.now()); - settingsRepository.save(entity); - - EsignHubSettingsResponse r = buildOk(entity); - r.setMessage("saved"); - return r; - } - - private EsignHubSettings ensureDefaultRow() { - Optional opt = settingsRepository.findById(DEFAULT_SETTING_ID); - if (opt.isPresent()) { - return opt.get(); - } - EsignHubSettings row = new EsignHubSettings(); - row.setSettingId(DEFAULT_SETTING_ID); - row.setHotelId(null); - row.setNavBarColor(DEFAULT_NAV_BAR_COLOR); - row.setNavBarColorDark(DEFAULT_NAV_BAR_COLOR_DARK); - row.setNavBarVisible(Boolean.TRUE); - row.setPrinterPdfNavBarVisible(Boolean.TRUE); - row.setUserMenuStyleJson(serializeUserMenuStyle(defaultUserMenuStyle())); - row.setSettingsMenuJson(serializeSettingsMenu(defaultSettingsMenu())); - row.setUpdatedAt(Instant.now()); - return settingsRepository.save(row); - } - - private EsignHubSettingsResponse buildDefaultAppResponse() { - EsignHubSettingsResponse r = EsignHubSettingsResponse.ok("ok"); - EsignHubSettingsResponse.HubSettingsData data = new EsignHubSettingsResponse.HubSettingsData(); - EsignHubSettingsResponse.HubSettingsItem item = new EsignHubSettingsResponse.HubSettingsItem(); - item.setSettingId(DEFAULT_SETTING_ID); - item.setNavBarColor(DEFAULT_NAV_BAR_COLOR); - item.setNavBarColorDark(DEFAULT_NAV_BAR_COLOR_DARK); - item.setNavBarVisible(Boolean.TRUE); - item.setPrinterPdfNavBarVisible(Boolean.TRUE); - item.setUserMenuStyle(defaultUserMenuStyle()); - item.setSettingsMenu(defaultSettingsMenu()); - data.setSettings(item); - r.setData(data); - return r; - } - - private EsignHubSettingsResponse buildOk(EsignHubSettings settings) { - EsignHubSettingsResponse r = EsignHubSettingsResponse.ok("ok"); - EsignHubSettingsResponse.HubSettingsData data = new EsignHubSettingsResponse.HubSettingsData(); - data.setSettings(toItem(settings)); - r.setData(data); - return r; - } - - private EsignHubSettingsResponse.HubSettingsItem toItem(EsignHubSettings settings) { - EsignHubSettingsResponse.HubSettingsItem item = new EsignHubSettingsResponse.HubSettingsItem(); - item.setSettingId(settings.getSettingId()); - item.setHotelId(settings.getHotelId()); - item.setNavBarColor(settings.getNavBarColor()); - item.setNavBarColorDark(settings.getNavBarColorDark()); - item.setNavBarVisible(settings.getNavBarVisible() == null || Boolean.TRUE.equals(settings.getNavBarVisible())); - item.setPrinterPdfNavBarVisible( - settings.getPrinterPdfNavBarVisible() == null - || Boolean.TRUE.equals(settings.getPrinterPdfNavBarVisible())); - item.setUserMenuStyle(parseUserMenuStyle(settings.getUserMenuStyleJson())); - item.setSettingsMenu(parseSettingsMenu(settings.getSettingsMenuJson())); - item.setUpdatedAt(settings.getUpdatedAt() != null ? settings.getUpdatedAt().toString() : null); - return item; - } - - public static HubSettingsMenu defaultSettingsMenu() { - HubSettingsMenu menu = new HubSettingsMenu(); - menu.setApiSettings(Boolean.TRUE); - menu.setPrinterAgent(Boolean.TRUE); - menu.setPrinterPdf(Boolean.TRUE); - return menu; - } - - private HubSettingsMenu parseSettingsMenu(String json) { - if (StringUtils.isBlank(json)) { - return defaultSettingsMenu(); - } - try { - HubSettingsMenu parsed = objectMapper.readValue(json, HubSettingsMenu.class); - return mergeSettingsMenu(parsed); - } catch (Exception ignored) { - return defaultSettingsMenu(); - } - } - - private String serializeSettingsMenu(HubSettingsMenu menu) { - try { - return objectMapper.writeValueAsString(menu); - } catch (JsonProcessingException e) { - return null; - } - } - - private HubSettingsMenu mergeSettingsMenu(HubSettingsMenu input) { - HubSettingsMenu base = defaultSettingsMenu(); - if (input == null) { - return base; - } - if (input.getApiSettings() != null) { - base.setApiSettings(input.getApiSettings()); - } - if (input.getPrinterAgent() != null) { - base.setPrinterAgent(input.getPrinterAgent()); - } - if (input.getPrinterPdf() != null) { - base.setPrinterPdf(input.getPrinterPdf()); - } - return base; - } - - public static HubUserMenuStyle defaultUserMenuStyle() { - HubUserMenuStyle style = new HubUserMenuStyle(); - style.setBackground("rgba(38, 106, 98, 0.98)"); - style.setTextColor("#FFFFFF"); - style.setFontSize("14px"); - style.setItemPadding("12px 20px"); - style.setBorderRadius("8px"); - style.setActiveBackground("rgba(255, 255, 255, 0.15)"); - style.setMaskBackground("rgba(0, 0, 0, 0.08)"); - style.setMinWidth("148px"); - return style; - } - - private HubUserMenuStyle parseUserMenuStyle(String json) { - if (StringUtils.isBlank(json)) { - return defaultUserMenuStyle(); - } - try { - HubUserMenuStyle parsed = objectMapper.readValue(json, HubUserMenuStyle.class); - return mergeUserMenuStyle(parsed); - } catch (Exception ignored) { - return defaultUserMenuStyle(); - } - } - - private String serializeUserMenuStyle(HubUserMenuStyle style) { - try { - return objectMapper.writeValueAsString(style); - } catch (JsonProcessingException e) { - return null; - } - } - - private HubUserMenuStyle mergeUserMenuStyle(HubUserMenuStyle input) { - HubUserMenuStyle base = defaultUserMenuStyle(); - if (input == null) { - return base; - } - base.setBackground(normalizeCssColor(input.getBackground(), base.getBackground())); - base.setTextColor(normalizeHexColor(input.getTextColor(), base.getTextColor())); - base.setFontSize(normalizeCssLength(input.getFontSize(), base.getFontSize())); - base.setItemPadding(normalizeCssPadding(input.getItemPadding(), base.getItemPadding())); - base.setBorderRadius(normalizeCssLength(input.getBorderRadius(), base.getBorderRadius())); - base.setActiveBackground(normalizeCssColor(input.getActiveBackground(), base.getActiveBackground())); - base.setMaskBackground(normalizeCssColor(input.getMaskBackground(), base.getMaskBackground())); - base.setMinWidth(normalizeCssLength(input.getMinWidth(), base.getMinWidth())); - return base; - } - - private String normalizeHexColor(String raw, String fallback) { - String normalized = normalizeColor(raw, null); - return normalized != null ? normalized : fallback; - } - - private String normalizeCssColor(String raw, String fallback) { - if (StringUtils.isBlank(raw)) { - return fallback; - } - String trimmed = raw.trim(); - if (HEX_COLOR.matcher(trimmed).matches()) { - return normalizeColor(trimmed, fallback); - } - if (trimmed.toLowerCase().startsWith("rgba(") || trimmed.toLowerCase().startsWith("rgb(")) { - return trimmed.replaceAll("\\s+", " "); - } - return fallback; - } - - private String normalizeCssLength(String raw, String fallback) { - if (StringUtils.isBlank(raw)) { - return fallback; - } - String trimmed = raw.trim(); - return CSS_LENGTH.matcher(trimmed).matches() ? trimmed : fallback; - } - - private String normalizeCssPadding(String raw, String fallback) { - if (StringUtils.isBlank(raw)) { - return fallback; - } - String trimmed = raw.trim().replaceAll("\\s+", " "); - return CSS_PADDING.matcher(trimmed).matches() ? trimmed : fallback; - } - - private String normalizeColor(String raw, String fallback) { - if (StringUtils.isBlank(raw)) { - return fallback; - } - String trimmed = raw.trim(); - if (!HEX_COLOR.matcher(trimmed).matches()) { - return null; - } - if (trimmed.length() == 4) { - char r = trimmed.charAt(1); - char g = trimmed.charAt(2); - char b = trimmed.charAt(3); - return "#" + r + r + g + g + b + b; - } - return trimmed.toUpperCase(); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/config/EsignPrinterSchemaMigrator.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/config/EsignPrinterSchemaMigrator.java deleted file mode 100644 index 5475caa..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/config/EsignPrinterSchemaMigrator.java +++ /dev/null @@ -1,79 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.config; - -import com.sjmresorts.opsapp.common.db.DbDialect; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.stereotype.Component; - -import javax.sql.DataSource; - -/** 启动时补齐 esign_printer_pdf_tasks OCR 检索字段(多库方言)。 */ -@Component -public class EsignPrinterSchemaMigrator implements InitializingBean { - - private static final Logger log = LoggerFactory.getLogger(EsignPrinterSchemaMigrator.class); - - private final JdbcTemplate jdbc; - private final DbDialect dialect; - - public EsignPrinterSchemaMigrator( - @Qualifier("opsAppDataSource") DataSource dataSource, DbDialect dialect) { - this.jdbc = new JdbcTemplate(dataSource); - this.dialect = dialect; - } - - @Override - public void afterPropertiesSet() { - ensurePrinterPdfOcrColumns(); - ensurePrinterPdfSignedDeviceColumns(); - ensurePrinterPdfOhipColumns(); - } - - private void ensurePrinterPdfOcrColumns() { - if (!dialect.tableExists(jdbc, "esign_printer_pdf_tasks")) { - return; - } - add("esign_printer_pdf_tasks", "ocr_guest_name", dialect.varchar(256) + " NULL"); - add("esign_printer_pdf_tasks", "ocr_confirmation_no", dialect.varchar(64) + " NULL"); - add("esign_printer_pdf_tasks", "ocr_phone", dialect.varchar(64) + " NULL"); - add("esign_printer_pdf_tasks", "ocr_email", dialect.varchar(256) + " NULL"); - add("esign_printer_pdf_tasks", "ocr_room_no", dialect.varchar(32) + " NULL"); - add("esign_printer_pdf_tasks", "ocr_check_in_date", dialect.varchar(32) + " NULL"); - add("esign_printer_pdf_tasks", "ocr_check_out_date", dialect.varchar(32) + " NULL"); - add("esign_printer_pdf_tasks", "ocr_extracted_at", dialect.datetime() + " NULL"); - add("esign_printer_pdf_tasks", "ocr_status", dialect.varchar(16) + " NULL"); - } - - private void ensurePrinterPdfSignedDeviceColumns() { - if (!dialect.tableExists(jdbc, "esign_printer_pdf_tasks")) { - return; - } - add("esign_printer_pdf_tasks", "signed_by_user_id", dialect.bigintType() + " NULL"); - add("esign_printer_pdf_tasks", "signed_client_ip", dialect.varchar(64) + " NULL"); - add("esign_printer_pdf_tasks", "signed_device_id", dialect.varchar(64) + " NULL"); - add("esign_printer_pdf_tasks", "signed_device_name", dialect.varchar(128) + " NULL"); - } - - private void ensurePrinterPdfOhipColumns() { - if (!dialect.tableExists(jdbc, "esign_printer_pdf_tasks")) { - return; - } - add("esign_printer_pdf_tasks", "ohip_status", dialect.varchar(16) + " NULL"); - add("esign_printer_pdf_tasks", "ohip_last_error", dialect.varchar(512) + " NULL"); - add("esign_printer_pdf_tasks", "ohip_uploaded_at", dialect.datetime() + " NULL"); - add("esign_printer_pdf_tasks", "ohip_confirmation_no", dialect.varchar(64) + " NULL"); - add("esign_printer_pdf_tasks", "ohip_reservation_id", dialect.varchar(64) + " NULL"); - add("esign_printer_pdf_tasks", "ohip_retry_count", dialect.intType() + " NULL"); - add("esign_printer_pdf_tasks", "sign_fields_json", dialect.text() + " NULL"); - } - - private void add(String table, String column, String typeSql) { - if (!dialect.columnExists(jdbc, table, column)) { - log.info("Esign printer migrate: adding {}.{}", table, column); - jdbc.execute(dialect.alterAddColumn(table, column, typeSql)); - } - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/config/EsignPrinterWebSocketConfig.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/config/EsignPrinterWebSocketConfig.java deleted file mode 100644 index ccb0246..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/config/EsignPrinterWebSocketConfig.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.config; - -import com.sjmresorts.opsapp.esign.printer.websocket.EsignPrinterWebSocketAuthInterceptor; -import com.sjmresorts.opsapp.esign.printer.websocket.EsignPrinterWebSocketHandler; -import lombok.RequiredArgsConstructor; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.socket.config.annotation.EnableWebSocket; -import org.springframework.web.socket.config.annotation.WebSocketConfigurer; -import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; - -@Configuration -@EnableWebSocket -@RequiredArgsConstructor -public class EsignPrinterWebSocketConfig implements WebSocketConfigurer { - - private final EsignPrinterWebSocketHandler printerWebSocketHandler; - private final EsignPrinterWebSocketAuthInterceptor authInterceptor; - - @Override - public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { - registry.addHandler(printerWebSocketHandler, "/api/v1/esign/ws/printer-tasks", "/api/v1/sinosecu/ws/printer-tasks") - .addInterceptors(authInterceptor) - .setAllowedOriginPatterns("*"); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/controllers/EsignPrinterDeviceLinkController.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/controllers/EsignPrinterDeviceLinkController.java deleted file mode 100644 index e70a169..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/controllers/EsignPrinterDeviceLinkController.java +++ /dev/null @@ -1,91 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.controllers; - -import com.sjmresorts.opsapp.authentication.dtos.LoginUserInfo; -import com.sjmresorts.opsapp.common.utils.JwtTokenUtil; -import com.sjmresorts.opsapp.common.utils.NetworkUtil; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterDeviceLinkRequest; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterDeviceLinkResponse; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterDeviceLinkReviewRequest; -import com.sjmresorts.opsapp.esign.printer.services.EsignPrinterDeviceLinkService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.servlet.http.HttpServletRequest; -import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.StringUtils; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.CrossOrigin; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@Tag(name = "Esign Printer Device Link", description = "App 发起 PC 连接申请,CMS 审批") -@RestController -@RequestMapping({"/api/v1/esign", "/api/v1/sinosecu"}) -@CrossOrigin(origins = "*") -@RequiredArgsConstructor -public class EsignPrinterDeviceLinkController { - - private final EsignPrinterDeviceLinkService linkService; - - @Operation(summary = "App 发起连接 PC 申请", description = "需 Bearer Token。填写 PC 状态页上的连接标识。") - @PostMapping(value = "/printer-device-link/request", produces = "application/json;charset=UTF-8") - public ResponseEntity requestLink( - HttpServletRequest request, - @RequestBody EsignPrinterDeviceLinkRequest body) { - Long userId = resolveUserId(request); - String clientIp = NetworkUtil.getClientIpByRequest(request); - EsignPrinterDeviceLinkResponse resp = linkService.requestLink(userId, body, clientIp); - return ResponseEntity.status(httpStatus(resp)).body(resp); - } - - @Operation(summary = "App 查看我的连接", description = "需 Bearer Token。") - @GetMapping(value = "/printer-device-link/my", produces = "application/json;charset=UTF-8") - public ResponseEntity myLinks(HttpServletRequest request) { - Long userId = resolveUserId(request); - EsignPrinterDeviceLinkResponse resp = linkService.listMyLinks(userId); - return ResponseEntity.status(httpStatus(resp)).body(resp); - } - - @Operation(summary = "CMS 连接申请列表", description = "免 JWT。含待审批与已批准记录。") - @GetMapping(value = "/printer-device-links", produces = "application/json;charset=UTF-8") - public ResponseEntity listLinks() { - return ResponseEntity.ok(linkService.listForCms()); - } - - @Operation(summary = "CMS 审批连接", description = "免 JWT。action=approve|reject") - @PostMapping(value = "/printer-device-link/review", produces = "application/json;charset=UTF-8") - public ResponseEntity reviewLink( - @RequestBody EsignPrinterDeviceLinkReviewRequest body) { - EsignPrinterDeviceLinkResponse resp = linkService.reviewLink(body); - return ResponseEntity.status(httpStatus(resp)).body(resp); - } - - private static Long resolveUserId(HttpServletRequest request) { - LoginUserInfo loginUser = JwtTokenUtil.parseUserFromRequest(request); - if (loginUser == null || StringUtils.isBlank(loginUser.getEmployeeId())) { - return null; - } - try { - return Long.parseLong(loginUser.getEmployeeId()); - } catch (NumberFormatException e) { - return null; - } - } - - private static HttpStatus httpStatus(EsignPrinterDeviceLinkResponse resp) { - if (resp.isSuccess()) { - return HttpStatus.OK; - } - return switch (StringUtils.defaultString(resp.getCode())) { - case "401" -> HttpStatus.UNAUTHORIZED; - case "403" -> HttpStatus.FORBIDDEN; - case "404" -> HttpStatus.NOT_FOUND; - case "409" -> HttpStatus.CONFLICT; - case "400" -> HttpStatus.BAD_REQUEST; - default -> HttpStatus.INTERNAL_SERVER_ERROR; - }; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/controllers/EsignPrinterPcRegistryController.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/controllers/EsignPrinterPcRegistryController.java deleted file mode 100644 index 3cdd442..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/controllers/EsignPrinterPcRegistryController.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.controllers; - -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterPcRegistryDeleteRequest; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterPcRegistryResponse; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterPcRegistrySaveRequest; -import com.sjmresorts.opsapp.esign.printer.services.EsignPrinterPcRegistryService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.CrossOrigin; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@Tag(name = "Esign Printer PC Registry", description = "CMS 预登记 PC 上报端(名称 + IP)") -@RestController -@RequestMapping({"/api/v1/esign", "/api/v1/sinosecu"}) -@CrossOrigin(origins = "*") -@RequiredArgsConstructor -public class EsignPrinterPcRegistryController { - - private final EsignPrinterPcRegistryService registryService; - - @Operation(summary = "CMS PC 登记列表", description = "免 JWT。") - @GetMapping(value = "/printer-pc-registry", produces = "application/json;charset=UTF-8") - public ResponseEntity listRegistry() { - return ResponseEntity.ok(registryService.listAll()); - } - - @Operation(summary = "CMS 添加/更新 PC 登记", description = "免 JWT。名称 + IP 生成连接标识 name@ip。") - @PostMapping(value = "/printer-pc-registry/save", produces = "application/json;charset=UTF-8") - public ResponseEntity saveRegistry( - @RequestBody EsignPrinterPcRegistrySaveRequest body) { - EsignPrinterPcRegistryResponse resp = registryService.save(body); - return ResponseEntity.ok(resp); - } - - @Operation(summary = "CMS 删除 PC 登记", description = "免 JWT。") - @PostMapping(value = "/printer-pc-registry/delete", produces = "application/json;charset=UTF-8") - public ResponseEntity deleteRegistry( - @RequestBody EsignPrinterPcRegistryDeleteRequest body) { - EsignPrinterPcRegistryResponse resp = registryService.delete(body); - return ResponseEntity.ok(resp); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/controllers/EsignPrinterPdfTaskController.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/controllers/EsignPrinterPdfTaskController.java deleted file mode 100644 index 322c23a..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/controllers/EsignPrinterPdfTaskController.java +++ /dev/null @@ -1,488 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.controllers; - -import com.auth0.jwt.interfaces.JWTVerifier; -import com.sjmresorts.opsapp.authentication.dtos.LoginUserInfo; -import com.sjmresorts.opsapp.common.utils.JwtTokenUtil; -import com.sjmresorts.opsapp.esign.capture.dtos.EsignIosCaptureResponse; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterDashboardResponse; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterDevicesResponse; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterEventLogResponse; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterQueueResponse; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterQueueWatchResponse; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterPdfArchiveOcrBatchResponse; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterPdfArchiveResponse; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterPdfSignedSubmitRequest; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterPdfUsbSignedSubmitRequest; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterPdfTaskCompleteRequest; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterPdfFieldExtractRequest; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterPdfFieldExtractResponse; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterPdfFieldPrefillRequest; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterPdfFieldPrefillResponse; -import com.sjmresorts.opsapp.esign.printer.services.EsignPrinterPdfFieldPrefillService; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterPdfTaskDetailResponse; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterPdfTaskSubmitRequest; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterPdfTaskUpdateRequest; -import com.sjmresorts.opsapp.esign.printer.filters.EsignPrinterPdfArchiveFilter; -import com.sjmresorts.opsapp.esign.printer.services.EsignPrinterPdfArchiveOcrBatchService; -import com.sjmresorts.opsapp.esign.printer.services.EsignPrinterEventLogService; -import com.sjmresorts.opsapp.esign.printer.services.EsignPrinterPdfTaskService; -import com.sjmresorts.opsapp.esign.auth.services.EsignUserPresenceService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import com.sjmresorts.opsapp.common.utils.NetworkUtil; -import jakarta.annotation.Resource; -import jakarta.servlet.http.HttpServletRequest; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.springframework.http.ContentDisposition; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.CrossOrigin; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; -import com.sjmresorts.opsapp.esign.printer.support.EsignPrinterQueueSseHub; - -@Tag(name = "Esign Printer PDF", description = "PC 虚拟打印机 PDF 任务(Electron 上报 + App 补拉)") -@RestController -@RequestMapping({"/api/v1/esign", "/api/v1/sinosecu"}) -@CrossOrigin(origins = "*") -@Slf4j -public class EsignPrinterPdfTaskController { - - @Resource - private EsignPrinterPdfTaskService printerPdfTaskService; - - @Resource - private EsignPrinterPdfFieldPrefillService fieldPrefillService; - - @Resource - private EsignPrinterPdfArchiveOcrBatchService archiveOcrBatchService; - - @Resource - private EsignPrinterEventLogService printerEventLogService; - - @Resource - private EsignUserPresenceService userPresenceService; - - @Resource - private EsignPrinterQueueSseHub printerQueueSseHub; - - @Operation(summary = "e-sign 控制台聚合", description = "免 JWT。CMS 控制台/传输监控:任务统计、上报电脑聚合、接收平板列表与最近传输。") - @GetMapping(value = "/printer-dashboard", produces = "application/json;charset=UTF-8") - public ResponseEntity getDashboard() { - return ResponseEntity.status(HttpStatus.OK).body(printerPdfTaskService.getDashboard()); - } - - @Operation(summary = "24h PDF 队列", description = "免 JWT。CMS 平板队列:24h 内上报 PDF、按上报电脑筛选、推送目标设备。") - @GetMapping(value = "/printer-queue", produces = "application/json;charset=UTF-8") - public ResponseEntity getQueue( - @RequestParam(value = "agentSource", required = false) String agentSource, - @RequestParam(value = "status", required = false) String status, - @RequestParam(value = "page", defaultValue = "0") int page, - @RequestParam(value = "size", defaultValue = "50") int size) { - return ResponseEntity.ok(printerPdfTaskService.getQueue(agentSource, status, page, size)); - } - - @Operation(summary = "24h PDF 队列变更探测", description = "免 JWT。CMS 轮询用:revision 变化表示有新上报/签收/状态变更。") - @GetMapping(value = "/printer-queue/watch", produces = "application/json;charset=UTF-8") - public ResponseEntity getQueueWatch() { - return ResponseEntity.ok(printerPdfTaskService.getQueueWatch()); - } - - @Operation(summary = "24h PDF 队列 SSE", description = "免 JWT。CMS 实时订阅:queue-changed 事件携带 revision,有变更时刷新列表。") - @GetMapping(value = "/printer-queue/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) - public SseEmitter streamQueue() { - return printerQueueSseHub.subscribe(); - } - - @Operation(summary = "设备管理", description = "免 JWT。PC 上报端与平板接收端列表。") - @GetMapping(value = "/printer-devices", produces = "application/json;charset=UTF-8") - public ResponseEntity getDevices() { - return ResponseEntity.ok(printerPdfTaskService.getDevices()); - } - - @Operation(summary = "PDF 归档列表", description = "免 JWT。已签收/已签名归档记录。") - @GetMapping(value = "/printer-pdf-archive", produces = "application/json;charset=UTF-8") - public ResponseEntity listArchive( - @RequestParam(value = "page", defaultValue = "0") int page, - @RequestParam(value = "size", defaultValue = "20") int size, - @RequestParam(value = "guestName", required = false) String guestName, - @RequestParam(value = "confirmationNo", required = false) String confirmationNo, - @RequestParam(value = "reservationId", required = false) String reservationId, - @RequestParam(value = "phone", required = false) String phone, - @RequestParam(value = "roomNo", required = false) String roomNo, - @RequestParam(value = "checkInDate", required = false) String checkInDate, - @RequestParam(value = "checkOutDate", required = false) String checkOutDate, - @RequestParam(value = "fileName", required = false) String fileName, - @RequestParam(value = "status", required = false) String status) { - var filter = EsignPrinterPdfArchiveFilter.of( - guestName, confirmationNo, reservationId, phone, roomNo, checkInDate, checkOutDate, fileName, status); - return ResponseEntity.ok(printerPdfTaskService.listArchive(page, size, filter)); - } - - @Operation(summary = "PDF 归档 OCR 批量重跑", description = "免 JWT。按筛选条件异步重跑 OCR,立即返回 jobId。") - @PostMapping(value = "/printer-pdf-archive/ocr-batch", produces = "application/json;charset=UTF-8") - public ResponseEntity startArchiveOcrBatch( - @RequestParam(value = "guestName", required = false) String guestName, - @RequestParam(value = "confirmationNo", required = false) String confirmationNo, - @RequestParam(value = "reservationId", required = false) String reservationId, - @RequestParam(value = "phone", required = false) String phone, - @RequestParam(value = "roomNo", required = false) String roomNo, - @RequestParam(value = "checkInDate", required = false) String checkInDate, - @RequestParam(value = "checkOutDate", required = false) String checkOutDate, - @RequestParam(value = "fileName", required = false) String fileName, - @RequestParam(value = "status", required = false) String status, - @RequestParam(value = "failedOnly", defaultValue = "true") boolean failedOnly) { - var filter = EsignPrinterPdfArchiveFilter.of( - guestName, confirmationNo, reservationId, phone, roomNo, checkInDate, checkOutDate, fileName, status); - EsignPrinterPdfArchiveOcrBatchResponse body = archiveOcrBatchService.startBatch(filter, failedOnly); - HttpStatus httpStatus = body.isSuccess() ? HttpStatus.OK : HttpStatus.BAD_REQUEST; - if ("404".equals(body.getCode())) { - httpStatus = HttpStatus.NOT_FOUND; - } - return ResponseEntity.status(httpStatus).body(body); - } - - @Operation(summary = "PDF 归档 OCR 批量任务状态", description = "免 JWT。轮询批量 OCR 进度。") - @GetMapping(value = "/printer-pdf-archive/ocr-batch/{jobId}", produces = "application/json;charset=UTF-8") - public ResponseEntity getArchiveOcrBatchStatus( - @PathVariable("jobId") String jobId) { - EsignPrinterPdfArchiveOcrBatchResponse body = archiveOcrBatchService.getBatchStatus(jobId); - HttpStatus httpStatus = body.isSuccess() ? HttpStatus.OK : HttpStatus.NOT_FOUND; - if (!body.isSuccess() && "400".equals(body.getCode())) { - httpStatus = HttpStatus.BAD_REQUEST; - } - return ResponseEntity.status(httpStatus).body(body); - } - - @Operation(summary = "PDF 归档 OCR 批量失败日志", description = "免 JWT。下载批量 OCR 失败明细。") - @GetMapping(value = "/printer-pdf-archive/ocr-batch/{jobId}/error-log", produces = "text/plain;charset=UTF-8") - public ResponseEntity downloadArchiveOcrBatchErrorLog(@PathVariable("jobId") String jobId) { - byte[] bytes = archiveOcrBatchService.readErrorLog(jobId); - if (bytes == null || bytes.length == 0) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); - } - ContentDisposition disposition = ContentDisposition.attachment() - .filename(archiveOcrBatchService.errorLogFileName(jobId)) - .build(); - return ResponseEntity.ok() - .header(HttpHeaders.CONTENT_DISPOSITION, disposition.toString()) - .contentType(MediaType.TEXT_PLAIN) - .body(bytes); - } - - @Operation(summary = "打印机事件日志", description = "免 JWT。上报/签收/签名归档等操作日志。") - @GetMapping(value = "/printer-event-logs", produces = "application/json;charset=UTF-8") - public ResponseEntity listEventLogs( - @RequestParam(value = "page", defaultValue = "0") int page, - @RequestParam(value = "size", defaultValue = "50") int size, - @RequestParam(value = "eventType", required = false) String eventType, - @RequestParam(value = "taskId", required = false) String taskId) { - return ResponseEntity.ok(printerEventLogService.listLogs(page, size, eventType, taskId)); - } - - @Operation(summary = "Electron 上报 PDF 任务", description = "免 JWT。PC 收到 PDF 后写入任务池,供平板离线补拉。") - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "保存成功"), - @ApiResponse(responseCode = "400", description = "参数无效"), - @ApiResponse(responseCode = "413", description = "PDF 过大") - }) - @PostMapping(value = "/printer-pdf-task-submit", consumes = "application/json;charset=UTF-8", produces = "application/json;charset=UTF-8") - public ResponseEntity submitTask(@RequestBody EsignPrinterPdfTaskSubmitRequest request) { - EsignIosCaptureResponse body = printerPdfTaskService.submitTask(request); - HttpStatus status = HttpStatus.OK; - if (!body.isSuccess()) { - if ("413".equals(body.getCode())) { - status = HttpStatus.PAYLOAD_TOO_LARGE; - } else { - status = HttpStatus.BAD_REQUEST; - } - } - return ResponseEntity.status(status).body(body); - } - - @Operation(summary = "CMS 更新 PDF 任务", description = "免 JWT。待签收任务可修改文件名或替换 PDF 内容;已签名归档可修改签名 PDF。") - @PostMapping(value = "/ios-printer-pdf-task-update", consumes = "application/json;charset=UTF-8", produces = "application/json;charset=UTF-8") - public ResponseEntity updateTask( - @RequestBody EsignPrinterPdfTaskUpdateRequest request) { - EsignIosCaptureResponse body = printerPdfTaskService.updateTask(request); - HttpStatus status = HttpStatus.OK; - if (!body.isSuccess()) { - if ("413".equals(body.getCode())) { - status = HttpStatus.PAYLOAD_TOO_LARGE; - } else if ("404".equals(body.getCode())) { - status = HttpStatus.NOT_FOUND; - } else { - status = HttpStatus.BAD_REQUEST; - } - } - return ResponseEntity.status(status).body(body); - } - - @Operation(summary = "已签名 PDF 重新推送 OPERA", description = "免 JWT。与平板签名归档后 OHIP 上传相同,将当前签名 PDF 再次提交至 OPERA。") - @PostMapping(value = "/ios-printer-pdf-task/{taskId}/ohip-reupload", produces = "application/json;charset=UTF-8") - public ResponseEntity reuploadSignedToOhip( - @PathVariable("taskId") String taskId) { - EsignIosCaptureResponse body = printerPdfTaskService.reuploadSignedToOhip(taskId); - HttpStatus status = HttpStatus.OK; - if (!body.isSuccess()) { - if ("404".equals(body.getCode())) { - status = HttpStatus.NOT_FOUND; - } else if ("502".equals(body.getCode())) { - status = HttpStatus.BAD_GATEWAY; - } else { - status = HttpStatus.BAD_REQUEST; - } - } - return ResponseEntity.status(status).body(body); - } - - @Operation(summary = "PDF 任务详情(元数据)", description = "免 JWT。不含 Base64,请用 …/pdf 下载二进制。") - @GetMapping(value = "/ios-printer-pdf-task/{taskId}", produces = "application/json;charset=UTF-8") - public ResponseEntity getTaskDetail( - @PathVariable("taskId") String taskId, - HttpServletRequest request) { - Long appUserId = resolveUserIdIfVerifiedJwt(request); - String clientIp = NetworkUtil.getClientIpByRequest(request); - if (appUserId != null) { - userPresenceService.touchClientIp(appUserId, clientIp); - } - EsignPrinterPdfTaskDetailResponse body = printerPdfTaskService.getTaskDetail(taskId, appUserId, clientIp); - if (!body.isSuccess() && "404".equals(body.getCode())) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(body); - } - if (!body.isSuccess()) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(body); - } - return ResponseEntity.status(HttpStatus.OK).body(body); - } - - @Operation( - summary = "签名字段预填", - description = "免 JWT。打开 PDF 时拉取:任务 OCR 元数据 + OHIP Profile,点击热区无需 OCR。") - @PostMapping( - value = "/ios-printer-pdf-task/{taskId}/field-prefill", - consumes = "application/json;charset=UTF-8", - produces = "application/json;charset=UTF-8") - public ResponseEntity getFieldPrefill( - @PathVariable("taskId") String taskId, - @RequestBody(required = false) EsignPrinterPdfFieldPrefillRequest request, - HttpServletRequest httpRequest) { - Long appUserId = resolveUserIdIfVerifiedJwt(httpRequest); - String clientIp = NetworkUtil.getClientIpByRequest(httpRequest); - if (appUserId != null) { - userPresenceService.touchClientIp(appUserId, clientIp); - } - String signFieldsJson = request != null ? request.getSignFieldsJson() : null; - EsignPrinterPdfFieldPrefillResponse body = - fieldPrefillService.buildPrefill(taskId, signFieldsJson, appUserId, clientIp); - if (!body.isSuccess() && "404".equals(body.getCode())) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(body); - } - if (!body.isSuccess()) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(body); - } - return ResponseEntity.ok(body); - } - - @Operation(summary = "PDF 热区文字提取", description = "免 JWT。扫描件登记卡按热区比例 OCR 预填。") - @PostMapping( - value = "/ios-printer-pdf-task/{taskId}/extract-field-text", - consumes = "application/json;charset=UTF-8", - produces = "application/json;charset=UTF-8") - public ResponseEntity extractFieldText( - @PathVariable("taskId") String taskId, - @RequestBody EsignPrinterPdfFieldExtractRequest request, - HttpServletRequest httpRequest) { - Long appUserId = resolveUserIdIfVerifiedJwt(httpRequest); - String clientIp = NetworkUtil.getClientIpByRequest(httpRequest); - if (appUserId != null) { - userPresenceService.touchClientIp(appUserId, clientIp); - } - EsignPrinterPdfFieldExtractResponse body = - printerPdfTaskService.extractFieldText(taskId, request, appUserId, clientIp); - if (!body.isSuccess() && "404".equals(body.getCode())) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(body); - } - if (!body.isSuccess() && "503".equals(body.getCode())) { - return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(body); - } - if (!body.isSuccess()) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(body); - } - return ResponseEntity.ok(body); - } - - @Operation(summary = "热区截图 OCR", description = "免 JWT。优先 OCR 请求体 cropImageBase64(平板框内截图)。") - @PostMapping( - value = "/ios-printer-pdf-field-extract", - consumes = "application/json;charset=UTF-8", - produces = "application/json;charset=UTF-8") - public ResponseEntity extractFieldFromCrop( - @RequestBody EsignPrinterPdfFieldExtractRequest request, - HttpServletRequest httpRequest) { - Long appUserId = resolveUserIdIfVerifiedJwt(httpRequest); - String clientIp = NetworkUtil.getClientIpByRequest(httpRequest); - if (appUserId != null) { - userPresenceService.touchClientIp(appUserId, clientIp); - } - EsignPrinterPdfFieldExtractResponse body = - printerPdfTaskService.extractFieldText(null, request, appUserId, clientIp); - if (!body.isSuccess() && "404".equals(body.getCode())) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(body); - } - if (!body.isSuccess() && "503".equals(body.getCode())) { - return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(body); - } - if (!body.isSuccess()) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(body); - } - return ResponseEntity.ok(body); - } - - @Operation(summary = "PDF 任务二进制下载", description = "免 JWT。返回 application/pdf。") - @GetMapping(value = "/ios-printer-pdf-task/{taskId}/pdf", produces = MediaType.APPLICATION_PDF_VALUE) - public ResponseEntity downloadTaskPdf( - @PathVariable("taskId") String taskId, - HttpServletRequest request) { - Long appUserId = resolveUserIdIfVerifiedJwt(request); - String clientIp = NetworkUtil.getClientIpByRequest(request); - if (appUserId != null) { - userPresenceService.touchClientIp(appUserId, clientIp); - } - return printerPdfTaskService - .getTaskPdfContent(taskId, appUserId, clientIp) - .map((content) -> { - ContentDisposition disposition = ContentDisposition.inline() - .filename(content.fileName()) - .build(); - return ResponseEntity.ok() - .header(HttpHeaders.CONTENT_DISPOSITION, disposition.toString()) - .contentType(MediaType.APPLICATION_PDF) - .body(content.bytes()); - }) - .orElseGet(() -> ResponseEntity.status(HttpStatus.NOT_FOUND).build()); - } - - @Operation(summary = "签名 PDF 下载", description = "免 JWT。返回签名后 application/pdf(若已回传)。") - @GetMapping(value = "/ios-printer-pdf-task/{taskId}/signed-pdf", produces = MediaType.APPLICATION_PDF_VALUE) - public ResponseEntity downloadTaskSignedPdf(@PathVariable("taskId") String taskId) { - return printerPdfTaskService - .getTaskSignedPdfContent(taskId) - .map((content) -> { - ContentDisposition disposition = ContentDisposition.inline() - .filename(content.fileName()) - .build(); - return ResponseEntity.ok() - .header(HttpHeaders.CONTENT_DISPOSITION, disposition.toString()) - .contentType(MediaType.APPLICATION_PDF) - .body(content.bytes()); - }) - .orElseGet(() -> ResponseEntity.status(HttpStatus.NOT_FOUND).build()); - } - - @Operation(summary = "USB 直传签名归档", description = "平板 USB 收到 PDF 后签名:一次请求归档,OPERA 异步上传。免推送。") - @PostMapping(value = "/ios-printer-pdf-usb-signed", consumes = "application/json;charset=UTF-8", produces = "application/json;charset=UTF-8") - public ResponseEntity submitUsbDirectSigned( - @RequestBody EsignPrinterPdfUsbSignedSubmitRequest request, - HttpServletRequest httpRequest) { - log.info( - "POST /ios-printer-pdf-usb-signed reservationId={}, externalId={}, signatureCount={}", - request != null ? request.getReservationId() : null, - request != null ? request.getExternalId() : null, - request != null ? request.getSignatureCount() : null); - Long appUserId = resolveUserIdIfVerifiedJwt(httpRequest); - String clientIp = NetworkUtil.getClientIpByRequest(httpRequest); - EsignIosCaptureResponse body = printerPdfTaskService.submitUsbDirectSigned(request, appUserId, clientIp); - log.info( - "POST /ios-printer-pdf-usb-signed done success={} code={} message={}", - body.isSuccess(), - body.getCode(), - body.getMessage()); - HttpStatus status = HttpStatus.OK; - if (!body.isSuccess()) { - if ("413".equals(body.getCode())) { - status = HttpStatus.PAYLOAD_TOO_LARGE; - } else { - status = HttpStatus.BAD_REQUEST; - } - } - return ResponseEntity.status(status).body(body); - } - - @Operation(summary = "签名 PDF / 签名图层回传", description = "免 JWT。平板签名后归档至 Java。") - @PostMapping(value = "/ios-printer-pdf-task-signed", consumes = "application/json;charset=UTF-8", produces = "application/json;charset=UTF-8") - public ResponseEntity submitSignedPdf( - @RequestBody EsignPrinterPdfSignedSubmitRequest request, - HttpServletRequest httpRequest) { - log.info( - "POST /ios-printer-pdf-task-signed taskId={}, externalId={}, signatureCount={}", - request != null ? request.getTaskId() : null, - request != null ? request.getExternalId() : null, - request != null ? request.getSignatureCount() : null); - Long appUserId = resolveUserIdIfVerifiedJwt(httpRequest); - String clientIp = NetworkUtil.getClientIpByRequest(httpRequest); - EsignIosCaptureResponse body = printerPdfTaskService.submitSignedPdf(request, appUserId, clientIp); - log.info( - "POST /ios-printer-pdf-task-signed done success={} code={} message={}", - body.isSuccess(), - body.getCode(), - body.getMessage()); - HttpStatus status = HttpStatus.OK; - if (!body.isSuccess()) { - if ("413".equals(body.getCode())) { - status = HttpStatus.PAYLOAD_TOO_LARGE; - } else if ("404".equals(body.getCode())) { - status = HttpStatus.NOT_FOUND; - } else { - status = HttpStatus.BAD_REQUEST; - } - } - return ResponseEntity.status(status).body(body); - } - - @Operation(summary = "PDF 任务完成", description = "免 JWT。平板打开/签收后从待签列表移除。") - @PostMapping(value = "/ios-printer-pdf-task-complete", consumes = "application/json;charset=UTF-8", produces = "application/json;charset=UTF-8") - public ResponseEntity completeTask(@RequestBody EsignPrinterPdfTaskCompleteRequest request) { - String taskId = request != null ? request.getTaskId() : null; - String externalId = request != null ? request.getExternalId() : null; - EsignIosCaptureResponse body = printerPdfTaskService.completeTask(taskId, externalId); - return ResponseEntity.status(HttpStatus.OK).body(body); - } - - /** 白名单接口:仅在校验通过后才从 JWT 取 employeeId,避免伪造。 */ - private static Long resolveUserIdIfVerifiedJwt(HttpServletRequest request) { - String auth = request.getHeader("Authorization"); - if (StringUtils.isBlank(auth) || !auth.startsWith("Bearer ")) { - return null; - } - String token = auth.substring(7).trim(); - if (StringUtils.isBlank(token)) { - return null; - } - try { - JWTVerifier jwtVerifier = JwtTokenUtil.getJwtVerifier(); - var jwt = jwtVerifier.verify(token); - if (!"esign_login".equals(jwt.getIssuer())) { - return null; - } - LoginUserInfo user = JwtTokenUtil.parseUserFromToken(token); - if (user == null || StringUtils.isBlank(user.getEmployeeId())) { - return null; - } - return Long.parseLong(user.getEmployeeId()); - } catch (Exception e) { - return null; - } - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/controllers/EsignPrinterPushController.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/controllers/EsignPrinterPushController.java deleted file mode 100644 index 5b8045c..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/controllers/EsignPrinterPushController.java +++ /dev/null @@ -1,92 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.controllers; - -import com.sjmresorts.opsapp.esign.capture.dtos.EsignIosCaptureResponse; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterPushAssignRequest; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterPushCandidatesResponse; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterPushGroupListResponse; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterPushGroupSaveRequest; -import com.sjmresorts.opsapp.esign.printer.services.EsignPrinterPushService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.StringUtils; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.CrossOrigin; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -@Tag(name = "Esign Printer Push", description = "PDF 队列推送目标:设备/组选择与 FCM") -@RestController -@RequestMapping({"/api/v1/esign", "/api/v1/sinosecu"}) -@CrossOrigin(origins = "*") -@RequiredArgsConstructor -public class EsignPrinterPushController { - - private final EsignPrinterPushService printerPushService; - - @Operation(summary = "推送候选(设备+组)", description = "免 JWT。按 taskId 返回可推送的已连接平板与推送组。") - @GetMapping(value = "/printer-push-candidates", produces = "application/json;charset=UTF-8") - public ResponseEntity getCandidates( - @RequestParam("taskId") String taskId) { - EsignPrinterPushCandidatesResponse body = printerPushService.getPushCandidates(taskId); - return ResponseEntity.status(httpStatus(body.isSuccess(), body.getCode())).body(body); - } - - @Operation(summary = "设置 PDF 推送目标", description = "免 JWT。选择设备 userIds 和/或 groupIds,可选 FCM 提醒。") - @PostMapping(value = "/printer-pdf-task-push", produces = "application/json;charset=UTF-8") - public ResponseEntity assignPush(@RequestBody EsignPrinterPushAssignRequest body) { - EsignIosCaptureResponse resp = printerPushService.assignPushTargets(body); - return ResponseEntity.status(httpStatus(resp.isSuccess(), resp.getCode())).body(resp); - } - - @Operation(summary = "快捷推送", description = "免 JWT。自动模式补推;手动模式按推送规则默认映射一键推送。") - @PostMapping(value = "/printer-pdf-task-quick-push", produces = "application/json;charset=UTF-8") - public ResponseEntity quickPush( - @RequestParam("taskId") String taskId, - @RequestParam(value = "sendFcm", defaultValue = "false") boolean sendFcm) { - EsignIosCaptureResponse resp = printerPushService.quickPushWithRules(taskId, sendFcm); - return ResponseEntity.status(httpStatus(resp.isSuccess(), resp.getCode())).body(resp); - } - - @Operation(summary = "推送组列表", description = "免 JWT。按 PC agentSource 查询。") - @GetMapping(value = "/printer-push-groups", produces = "application/json;charset=UTF-8") - public ResponseEntity listGroups( - @RequestParam("agentSource") String agentSource) { - return ResponseEntity.ok(printerPushService.listGroups(agentSource)); - } - - @Operation(summary = "保存推送组", description = "免 JWT。新建或更新组及成员。") - @PostMapping(value = "/printer-push-groups", produces = "application/json;charset=UTF-8") - public ResponseEntity saveGroup( - @RequestBody EsignPrinterPushGroupSaveRequest body) { - EsignPrinterPushGroupListResponse resp = printerPushService.saveGroup(body); - return ResponseEntity.status(httpStatus(resp.isSuccess(), resp.getCode())).body(resp); - } - - @Operation(summary = "删除推送组") - @DeleteMapping(value = "/printer-push-groups", produces = "application/json;charset=UTF-8") - public ResponseEntity deleteGroup( - @RequestParam("groupId") String groupId, - @RequestParam("agentSource") String agentSource) { - EsignPrinterPushGroupListResponse resp = printerPushService.deleteGroup(groupId, agentSource); - return ResponseEntity.status(httpStatus(resp.isSuccess(), resp.getCode())).body(resp); - } - - private static HttpStatus httpStatus(boolean success, String code) { - if (success) { - return HttpStatus.OK; - } - return switch (StringUtils.defaultString(code)) { - case "400" -> HttpStatus.BAD_REQUEST; - case "404" -> HttpStatus.NOT_FOUND; - case "409" -> HttpStatus.CONFLICT; - default -> HttpStatus.INTERNAL_SERVER_ERROR; - }; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/controllers/EsignPrinterPushRuleController.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/controllers/EsignPrinterPushRuleController.java deleted file mode 100644 index add45ed..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/controllers/EsignPrinterPushRuleController.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.controllers; - -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterPushRuleResponse; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterPushRuleSaveRequest; -import com.sjmresorts.opsapp.esign.printer.services.EsignPrinterPdfTaskService; -import com.sjmresorts.opsapp.esign.printer.services.EsignPrinterPushRuleService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.StringUtils; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.CrossOrigin; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import java.util.List; - -@Tag(name = "Esign Printer Push Rules", description = "PDF 推送规则:自动/手动与上报设备映射") -@RestController -@RequestMapping({"/api/v1/esign", "/api/v1/sinosecu"}) -@CrossOrigin(origins = "*") -@RequiredArgsConstructor -public class EsignPrinterPushRuleController { - - private final EsignPrinterPushRuleService pushRuleService; - private final EsignPrinterPdfTaskService printerPdfTaskService; - - @Operation(summary = "获取推送规则配置") - @GetMapping(value = "/printer-push-rules", produces = "application/json;charset=UTF-8") - public ResponseEntity getRules() { - List agents = printerPdfTaskService.listDistinctAgentSources24h(); - EsignPrinterPushRuleResponse body = pushRuleService.getRules(agents); - return ResponseEntity.ok(body); - } - - @Operation(summary = "保存推送规则配置") - @PostMapping(value = "/printer-push-rules", produces = "application/json;charset=UTF-8") - public ResponseEntity saveRules( - @RequestBody EsignPrinterPushRuleSaveRequest request) { - EsignPrinterPushRuleResponse body = pushRuleService.saveRules(request); - if (!body.isSuccess()) { - return ResponseEntity.status(httpStatus(body.getCode())).body(body); - } - List agents = printerPdfTaskService.listDistinctAgentSources24h(); - body = pushRuleService.getRules(agents); - return ResponseEntity.ok(body); - } - - private static HttpStatus httpStatus(String code) { - return switch (StringUtils.defaultString(code)) { - case "400" -> HttpStatus.BAD_REQUEST; - case "404" -> HttpStatus.NOT_FOUND; - default -> HttpStatus.INTERNAL_SERVER_ERROR; - }; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/controllers/EsignPrinterTabletRegistryController.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/controllers/EsignPrinterTabletRegistryController.java deleted file mode 100644 index b26c061..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/controllers/EsignPrinterTabletRegistryController.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.controllers; - -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterTabletRegistryDeleteRequest; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterTabletRegistryResponse; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterTabletRegistrySaveRequest; -import com.sjmresorts.opsapp.esign.printer.services.EsignPrinterTabletRegistryService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.CrossOrigin; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@Tag(name = "Esign Printer Tablet Registry", description = "CMS 预登记 App 平板(按登录账号 + 预绑 PC)") -@RestController -@RequestMapping({"/api/v1/esign", "/api/v1/sinosecu"}) -@CrossOrigin(origins = "*") -@RequiredArgsConstructor -public class EsignPrinterTabletRegistryController { - - private final EsignPrinterTabletRegistryService registryService; - - @Operation(summary = "CMS 平板登记列表", description = "免 JWT。") - @GetMapping(value = "/printer-tablet-registry", produces = "application/json;charset=UTF-8") - public ResponseEntity listRegistry() { - return ResponseEntity.ok(registryService.listAll()); - } - - @Operation(summary = "CMS 添加/更新平板登记", description = "免 JWT。按 App 账号登记,可选预绑 PC,无需 IP。") - @PostMapping(value = "/printer-tablet-registry/save", produces = "application/json;charset=UTF-8") - public ResponseEntity saveRegistry( - @RequestBody EsignPrinterTabletRegistrySaveRequest body) { - EsignPrinterTabletRegistryResponse resp = registryService.save(body); - return ResponseEntity.ok(resp); - } - - @Operation(summary = "CMS 删除平板登记", description = "免 JWT。") - @PostMapping(value = "/printer-tablet-registry/delete", produces = "application/json;charset=UTF-8") - public ResponseEntity deleteRegistry( - @RequestBody EsignPrinterTabletRegistryDeleteRequest body) { - EsignPrinterTabletRegistryResponse resp = registryService.delete(body); - return ResponseEntity.ok(resp); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/controllers/EsignPrinterWebSocketStatusController.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/controllers/EsignPrinterWebSocketStatusController.java deleted file mode 100644 index f2342b9..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/controllers/EsignPrinterWebSocketStatusController.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.controllers; - -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import java.util.LinkedHashMap; -import java.util.Map; - -@RestController -@RequestMapping({"/api/v1/esign/ws", "/api/v1/sinosecu/ws"}) -public class EsignPrinterWebSocketStatusController { - - @GetMapping("/printer-tasks/ready") - public Map ready() { - Map body = new LinkedHashMap<>(); - body.put("websocket", true); - body.put("path", "/api/v1/esign/ws/printer-tasks"); - return body; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignClientTransportLogRequest.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignClientTransportLogRequest.java deleted file mode 100644 index 68f6e16..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignClientTransportLogRequest.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.dtos; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@Schema(description = "App 上報輪詢 / WebSocket 連接事件") -public class EsignClientTransportLogRequest { - - @Schema(description = "POLL_FAILED | POLL_RECOVERED | WS_DISCONNECTED | WS_CONNECTED | WS_ERROR") - private String eventType; - - @Schema(description = "websocket | java-poll") - private String transport; - - private String message; - - private Long userId; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPendingListResponse.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPendingListResponse.java deleted file mode 100644 index 22b7c23..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPendingListResponse.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.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 = "证件待签任务列表") -public class EsignPendingListResponse { - - private boolean success = true; - private String code = "200"; - private String message; - private EsignPendingData data; - - @Data - @NoArgsConstructor - public static class EsignPendingData { - private List tasks; - } - - @Data - @NoArgsConstructor - public static class EsignPendingItem { - /** CAPTURE(默认)| RESERVATION | PRINTER_PDF */ - private String taskType; - private String taskId; - /** 预订任务:预订号 */ - private String reservationId; - /** 预订 / 打印机任务:酒店 ID */ - private String hotelId; - /** PC 打印机任务:Electron 推送 ID */ - private String externalId; - /** PC 打印机任务:文件名 */ - private String fileName; - /** PC 打印机任务:文件大小(字节) */ - private Long fileSize; - private String createdAt; - private String docTitle; - /** 预订推送标题/正文(列表展示用) */ - private String notificationTitle; - private String notificationBody; - /** 从采集 payload 中解析的住客姓名 */ - private String guestName; - /** payload.selectedHotelNameZh,供列表标题展示 */ - private String selectedHotelNameZh; - /** 从采集行解析的中文姓名(优先匹配「中文姓名」等) */ - private String guestNameZh; - /** 从采集行解析的英文姓名(优先匹配「英文姓名」等;无则空) */ - private String guestNameEn; - /** 签收时间(仅已签收列表有值) */ - private String signedAt; - /** PC 打印机任务:上报电脑标识(Electron agentSource) */ - private String agentSource; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterDashboardResponse.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterDashboardResponse.java deleted file mode 100644 index cf2c0eb..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterDashboardResponse.java +++ /dev/null @@ -1,77 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.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 EsignPrinterDashboardResponse { - - private boolean success = true; - private String code = "200"; - private String message; - private EsignPrinterDashboardData data; - - @Data - @NoArgsConstructor - public static class EsignPrinterDashboardData { - private String generatedAt; - private int taskWindowHours; - private long pendingCount; - private long signedCount; - private long voidedCount; - private long totalSubmitted24h; - private Double completionRatePercent; - private long ohipPendingCount; - private long ohipFailedCount; - private List recentTasks; - /** 24h 内按上报电脑聚合 */ - private List pcSources; - /** 可接收任务的旅宿平板账号(含推送 Token 状态) */ - private List tabletDevices; - } - - @Data - @NoArgsConstructor - public static class EsignPrinterPcSourceStat { - private String agentSource; - private long submitted24h; - private long pendingCount; - private long signed24h; - private String lastSubmitAt; - } - - @Data - @NoArgsConstructor - public static class EsignPrinterTabletDeviceStat { - private Long userId; - private String userName; - private String hotelId; - private boolean pushEnabled; - private String lastLoginAt; - private String status; - private Boolean wsConnected; - private Boolean online; - } - - @Data - @NoArgsConstructor - public static class EsignPrinterDashboardRecentTask { - private String taskId; - private String externalId; - private String fileName; - private Long fileSize; - private String status; - private String displayStatus; - private String createdAt; - private String completedAt; - private String hotelId; - private String agentSource; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterDeviceLinkRequest.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterDeviceLinkRequest.java deleted file mode 100644 index ee38d2b..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterDeviceLinkRequest.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.dtos; - -import com.fasterxml.jackson.annotation.JsonInclude; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@JsonInclude(JsonInclude.Include.NON_NULL) -public class EsignPrinterDeviceLinkRequest { - - /** PC 上报端标识(与 Electron 状态页「本机名称@IP」一致) */ - private String agentSource; - - /** App 端自定义设备备注(可选) */ - private String appLabel; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterDeviceLinkResponse.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterDeviceLinkResponse.java deleted file mode 100644 index 07831cc..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterDeviceLinkResponse.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.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 EsignPrinterDeviceLinkResponse { - - private boolean success = true; - private String code = "200"; - private String message; - private EsignPrinterDeviceLinkItem data; - private List items; - - @Data - @NoArgsConstructor - public static class EsignPrinterDeviceLinkItem { - private String linkId; - private Long userId; - private String userName; - private String agentSource; - private String appLabel; - private String pcDisplayName; - private String status; - private String requestedAt; - private String reviewedAt; - private String reviewedBy; - private Boolean pushEnabled; - private String deliveryMode; - private String lastLoginAt; - private String lastLoginIp; - } - - public static EsignPrinterDeviceLinkResponse ok(String message) { - EsignPrinterDeviceLinkResponse r = new EsignPrinterDeviceLinkResponse(); - r.setMessage(message); - return r; - } - - public static EsignPrinterDeviceLinkResponse fail(String code, String message) { - EsignPrinterDeviceLinkResponse r = new EsignPrinterDeviceLinkResponse(); - r.setSuccess(false); - r.setCode(code); - r.setMessage(message); - return r; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterDeviceLinkReviewRequest.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterDeviceLinkReviewRequest.java deleted file mode 100644 index 937c2be..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterDeviceLinkReviewRequest.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.dtos; - -import com.fasterxml.jackson.annotation.JsonInclude; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@JsonInclude(JsonInclude.Include.NON_NULL) -public class EsignPrinterDeviceLinkReviewRequest { - - private String linkId; - - /** approve | reject */ - private String action; - - private String reviewedBy; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterDevicesResponse.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterDevicesResponse.java deleted file mode 100644 index 0c56562..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterDevicesResponse.java +++ /dev/null @@ -1,103 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.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 设备管理:PC 上报端 + 平板接收端") -public class EsignPrinterDevicesResponse { - - private boolean success = true; - private String code = "200"; - private String message; - private EsignPrinterDevicesData data; - - @Data - @NoArgsConstructor - public static class EsignPrinterDevicesData { - private String generatedAt; - private int taskWindowHours; - private List pcDevices; - private List tabletDevices; - private List linkRequests; - private List approvedLinks; - } - - @Data - @NoArgsConstructor - public static class EsignPrinterDeviceLinkItem { - private String linkId; - private Long userId; - private String userName; - private String agentSource; - private String appLabel; - private String status; - private String requestedAt; - private String reviewedAt; - private Boolean pushEnabled; - private String deliveryMode; - private String lastLoginAt; - private String lastLoginIp; - } - - @Data - @NoArgsConstructor - public static class EsignPrinterPcDevice { - private String agentSource; - private long submitted24h; - private long pendingCount; - private long signed24h; - private String lastSubmitAt; - private String lastFileName; - /** 已批准连接的平板数量(按客户端 IP 去重) */ - private Integer linkedTabletCount; - /** CMS 预登记 */ - private Boolean registered; - private String registryId; - private String displayName; - private String hostIp; - private Boolean enabled; - private String notes; - } - - @Data - @NoArgsConstructor - public static class EsignPrinterTabletDevice { - private Long userId; - private String userName; - private String status; - private boolean pushEnabled; - private String lastLoginAt; - private String lastLoginIp; - private String deliveryMode; - /** WebSocket 实时连接 */ - private Boolean wsConnected; - /** 在线:WebSocket 已连接,或 30 分钟内有过 App 活跃 */ - private Boolean online; - /** 已批准连接的 PC 数量 */ - private Integer linkedPcCount; - private List linkedAgentSources; - /** CMS 预登记 */ - private Boolean registered; - private String registryId; - private String displayName; - private Boolean enabled; - private String notes; - /** 绑定 IP,空表示整账号 */ - private String boundClientIp; - /** CMS 预绑 PC(尚未登录时也可展示) */ - private List prelinkedAgentSources; - } - - public static EsignPrinterDevicesResponse ok(String message) { - EsignPrinterDevicesResponse r = new EsignPrinterDevicesResponse(); - r.setMessage(message); - return r; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterEventLogResponse.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterEventLogResponse.java deleted file mode 100644 index c17f4e6..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterEventLogResponse.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.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 EsignPrinterEventLogResponse { - - private boolean success = true; - private String code = "200"; - private String message; - private EsignPrinterEventLogData data; - - @Data - @NoArgsConstructor - public static class EsignPrinterEventLogData { - private long total; - private int page; - private int size; - private List logs; - } - - @Data - @NoArgsConstructor - public static class EsignPrinterEventLogItem { - private String logId; - private String taskId; - private String eventType; - private String message; - private String fileName; - private String agentSource; - private String hotelId; - private String createdAt; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterOhipStatusRow.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterOhipStatusRow.java deleted file mode 100644 index 317bcc3..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterOhipStatusRow.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.dtos; - -import java.time.Instant; - -/** OHIP 状态轻量投影(队列列表批量填充)。 */ -public interface EsignPrinterOhipStatusRow { - - String getTaskId(); - - String getOhipStatus(); - - String getOhipLastError(); - - Instant getOhipUploadedAt(); -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPcRegistryDeleteRequest.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPcRegistryDeleteRequest.java deleted file mode 100644 index b5eb7b6..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPcRegistryDeleteRequest.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.dtos; - -import com.fasterxml.jackson.annotation.JsonInclude; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@JsonInclude(JsonInclude.Include.NON_NULL) -public class EsignPrinterPcRegistryDeleteRequest { - - private String pcId; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPcRegistryResponse.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPcRegistryResponse.java deleted file mode 100644 index 3da382a..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPcRegistryResponse.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.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 EsignPrinterPcRegistryResponse { - - private boolean success = true; - private String code = "200"; - private String message; - private EsignPrinterPcRegistryItem data; - private List items; - - @Data - @NoArgsConstructor - public static class EsignPrinterPcRegistryItem { - private String pcId; - private String displayName; - private String hostIp; - private String agentSource; - private boolean enabled; - private String notes; - private String createdBy; - private String createdAt; - private String updatedAt; - } - - public static EsignPrinterPcRegistryResponse ok(String message) { - EsignPrinterPcRegistryResponse r = new EsignPrinterPcRegistryResponse(); - r.setMessage(message); - return r; - } - - public static EsignPrinterPcRegistryResponse fail(String code, String message) { - EsignPrinterPcRegistryResponse r = new EsignPrinterPcRegistryResponse(); - r.setSuccess(false); - r.setCode(code); - r.setMessage(message); - return r; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPcRegistrySaveRequest.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPcRegistrySaveRequest.java deleted file mode 100644 index b573cde..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPcRegistrySaveRequest.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.dtos; - -import com.fasterxml.jackson.annotation.JsonInclude; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@JsonInclude(JsonInclude.Include.NON_NULL) -public class EsignPrinterPcRegistrySaveRequest { - - private String pcId; - private String displayName; - private String hostIp; - private Boolean enabled; - private String notes; - private String createdBy; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPcSourceRow.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPcSourceRow.java deleted file mode 100644 index c00e80b..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPcSourceRow.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.dtos; - -import java.time.Instant; - -/** 按上报电脑聚合的 24h 传输统计 */ -public interface EsignPrinterPcSourceRow { - - String getAgentSource(); - - long getSubmitted24h(); - - long getPendingCount(); - - long getSigned24h(); - - Instant getLastSubmitAt(); -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPdfArchiveListRow.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPdfArchiveListRow.java deleted file mode 100644 index 5cd7033..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPdfArchiveListRow.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.dtos; - -import java.time.Instant; - -/** 归档列表轻量行(不含 PDF / JSON LOB)。 */ -public record EsignPrinterPdfArchiveListRow( - String taskId, - String externalId, - String fileName, - Long fileSize, - String hotelId, - String status, - Instant createdAt, - Instant completedAt, - Instant signedAt, - Instant archivedAt, - String agentSource, - Long signedByUserId, - String signedClientIp, - String signedDeviceId, - String signedDeviceName, - String ocrGuestName, - String ocrConfirmationNo, - String ocrPhone, - String ocrEmail, - String ocrRoomNo, - String ocrCheckInDate, - String ocrCheckOutDate, - String ocrStatus, - Instant ocrExtractedAt, - String ohipStatus, - String ohipLastError, - Instant ohipUploadedAt, - String ohipConfirmationNo, - String ohipReservationId, - Integer ohipRetryCount) {} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPdfArchiveOcrBatchResponse.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPdfArchiveOcrBatchResponse.java deleted file mode 100644 index 387bd4b..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPdfArchiveOcrBatchResponse.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.dtos; - -import com.fasterxml.jackson.annotation.JsonInclude; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@JsonInclude(JsonInclude.Include.NON_NULL) -@Schema(description = "PDF 归档 OCR 批量重跑") -public class EsignPrinterPdfArchiveOcrBatchResponse { - - private boolean success = true; - private String code = "200"; - private String message; - private EsignPrinterPdfArchiveOcrBatchData data; - - @Data - @NoArgsConstructor - public static class EsignPrinterPdfArchiveOcrBatchData { - private String jobId; - private String status; - private Integer total; - private Integer processed; - private Integer successCount; - private Integer failedCount; - private Integer skippedCount; - private Boolean hasErrorLog; - private String startedAt; - private String finishedAt; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPdfArchiveOcrEvent.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPdfArchiveOcrEvent.java deleted file mode 100644 index c5d5e1a..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPdfArchiveOcrEvent.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.dtos; - -/** 归档 PDF 异步 OCR 请求(事务提交后发布)。 */ -public record EsignPrinterPdfArchiveOcrEvent(String taskId) {} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPdfArchiveResponse.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPdfArchiveResponse.java deleted file mode 100644 index 702b672..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPdfArchiveResponse.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.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 PDF 归档列表") -public class EsignPrinterPdfArchiveResponse { - - private boolean success = true; - private String code = "200"; - private String message; - private EsignPrinterPdfArchiveData data; - - @Data - @NoArgsConstructor - public static class EsignPrinterPdfArchiveData { - private long total; - private int page; - private int size; - private List items; - } - - @Data - @NoArgsConstructor - public static class EsignPrinterPdfArchiveItem { - private String taskId; - private String externalId; - private String fileName; - private Long fileSize; - private String hotelId; - private String status; - private String createdAt; - private String completedAt; - private String signedAt; - private String archivedAt; - private boolean hasOriginalPdf = true; - private boolean hasSignedPdf; - private boolean hasSignedMeta; - private String agentSource; - private Long signedByUserId; - private String signedByUserName; - private String signedClientIp; - private String signedDeviceId; - private String signedDeviceName; - private String submittedDeviceLabel; - private String ocrGuestName; - private String ocrConfirmationNo; - private String ocrPhone; - private String ocrEmail; - private String ocrRoomNo; - private String ocrCheckInDate; - private String ocrCheckOutDate; - private String ocrStatus; - private String ocrExtractedAt; - private String ohipStatus; - private String ohipLastError; - private String ohipUploadedAt; - private String ohipConfirmationNo; - private String ohipReservationId; - private Integer ohipRetryCount; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPdfArchiveRow.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPdfArchiveRow.java deleted file mode 100644 index e20adbb..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPdfArchiveRow.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.dtos; - -import java.time.Instant; - -public interface EsignPrinterPdfArchiveRow { - - String getTaskId(); - - String getExternalId(); - - String getFileName(); - - Long getFileSize(); - - String getHotelId(); - - String getStatus(); - - Instant getCreatedAt(); - - Instant getCompletedAt(); - - Instant getSignedAt(); - - Instant getArchivedAt(); - - Boolean getHasSignedPdf(); - - Boolean getHasSignedMeta(); - - String getAgentSource(); -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPdfArchiveSpecs.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPdfArchiveSpecs.java deleted file mode 100644 index 067a178..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPdfArchiveSpecs.java +++ /dev/null @@ -1,116 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.dtos; - -import com.sjmresorts.opsapp.esign.printer.filters.EsignPrinterPdfArchiveFilter; -import com.sjmresorts.opsapp.esign.printer.models.EsignPrinterPdfTask; -import com.sjmresorts.opsapp.esign.printer.services.EsignPrinterPdfArchiveOcrService; -import jakarta.persistence.criteria.CriteriaBuilder; -import jakarta.persistence.criteria.Expression; -import jakarta.persistence.criteria.Predicate; -import jakarta.persistence.criteria.Root; -import org.apache.commons.lang3.StringUtils; -import org.springframework.data.jpa.domain.Specification; - -import java.time.Instant; -import java.util.ArrayList; -import java.util.List; - -public final class EsignPrinterPdfArchiveSpecs { - - private EsignPrinterPdfArchiveSpecs() { - } - - /** archivedAt → completedAt → signedAt → createdAt */ - public static Expression archiveSortTime( - Root root, CriteriaBuilder cb) { - return cb.coalesce( - root.get("archivedAt"), - cb.coalesce( - root.get("completedAt"), - cb.coalesce(root.get("signedAt"), root.get("createdAt")))); - } - - public static Specification archived(EsignPrinterPdfArchiveFilter filter) { - return (root, query, cb) -> { - List predicates = new ArrayList<>(); - predicates.add(cb.or( - cb.equal(root.get("status"), "COMPLETED"), - cb.isNotNull(root.get("signedAt")))); - - if (filter != null) { - like(predicates, cb, root.get("ocrGuestName"), filter.getGuestName()); - applyConfirmationFilter(predicates, cb, root, filter.getConfirmationNo()); - applyReservationFilter(predicates, cb, root, filter.getReservationId()); - like(predicates, cb, root.get("ocrPhone"), filter.getPhone()); - like(predicates, cb, root.get("ocrRoomNo"), filter.getRoomNo()); - like(predicates, cb, root.get("ocrCheckInDate"), filter.getCheckInDate()); - like(predicates, cb, root.get("ocrCheckOutDate"), filter.getCheckOutDate()); - like(predicates, cb, root.get("fileName"), filter.getFileName()); - applyStatusFilter(predicates, cb, root, filter.getStatus()); - } - - // COUNT 等聚合查询不能带 ORDER BY(SQL Server 会报错) - if (!Long.class.equals(query.getResultType()) && !long.class.equals(query.getResultType())) { - query.orderBy(cb.desc(archiveSortTime(root, cb))); - } - return cb.and(predicates.toArray(new Predicate[0])); - }; - } - - private static void like( - List predicates, - jakarta.persistence.criteria.CriteriaBuilder cb, - jakarta.persistence.criteria.Path path, - String value) { - if (StringUtils.isBlank(value)) { - return; - } - predicates.add(cb.like(cb.lower(path), "%" + value.trim().toLowerCase() + "%")); - } - - private static void applyConfirmationFilter( - List predicates, - jakarta.persistence.criteria.CriteriaBuilder cb, - jakarta.persistence.criteria.Root root, - String confirmationNo) { - if (StringUtils.isBlank(confirmationNo)) { - return; - } - String pattern = "%" + confirmationNo.trim().toLowerCase() + "%"; - predicates.add(cb.or( - cb.like(cb.lower(root.get("ocrConfirmationNo")), pattern), - cb.like(cb.lower(root.get("ohipConfirmationNo")), pattern))); - } - - private static void applyReservationFilter( - List predicates, - jakarta.persistence.criteria.CriteriaBuilder cb, - jakarta.persistence.criteria.Root root, - String reservationId) { - if (StringUtils.isBlank(reservationId)) { - return; - } - like(predicates, cb, root.get("ohipReservationId"), reservationId); - } - - private static void applyStatusFilter( - List predicates, - jakarta.persistence.criteria.CriteriaBuilder cb, - jakarta.persistence.criteria.Root root, - String status) { - if (StringUtils.isBlank(status) || "ALL".equalsIgnoreCase(status)) { - return; - } - if ("SIGNED".equalsIgnoreCase(status)) { - predicates.add(cb.isNotNull(root.get("signedAt"))); - return; - } - if ("COMPLETED".equalsIgnoreCase(status)) { - predicates.add(cb.equal(root.get("status"), "COMPLETED")); - predicates.add(cb.isNull(root.get("signedAt"))); - return; - } - if ("OCR_FAILED".equalsIgnoreCase(status)) { - predicates.add(cb.equal(root.get("ocrStatus"), EsignPrinterPdfArchiveOcrService.OCR_FAILED)); - } - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPdfFieldExtractRequest.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPdfFieldExtractRequest.java deleted file mode 100644 index 72d6ca3..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPdfFieldExtractRequest.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.dtos; - -import com.fasterxml.jackson.annotation.JsonInclude; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@JsonInclude(JsonInclude.Include.NON_NULL) -@Schema(description = "PDF 热区文字提取请求") -public class EsignPrinterPdfFieldExtractRequest { - - @Schema(description = "页码,0 起") - private Integer page; - - @Schema(description = "热区 left,0~1") - private Double left; - - @Schema(description = "热区 top,0~1") - private Double top; - - @Schema(description = "热区 width,0~1") - private Double width; - - @Schema(description = "热区 height,0~1") - private Double height; - - @Schema(description = "输入格式:email / phone / text 等") - private String inputFormat; - - @Schema(description = "可选:任务 ID,用于服务端 PDF 回退 OCR") - private String taskId; - - @Schema(description = "平板裁剪的热区 PNG Base64(优先 OCR 此图)") - private String cropImageBase64; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPdfFieldExtractResponse.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPdfFieldExtractResponse.java deleted file mode 100644 index 158e6df..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPdfFieldExtractResponse.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.dtos; - -import com.fasterxml.jackson.annotation.JsonInclude; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@JsonInclude(JsonInclude.Include.NON_NULL) -@Schema(description = "PDF 热区文字提取响应") -public class EsignPrinterPdfFieldExtractResponse { - - private boolean success = true; - private String code = "200"; - private String message; - private EsignPrinterPdfFieldExtractData data; - - @Data - @NoArgsConstructor - public static class EsignPrinterPdfFieldExtractData { - private String text; - private String inputFormat; - private String ocrHint; - } - - public static EsignPrinterPdfFieldExtractResponse ok(String text, String inputFormat, String ocrHint) { - EsignPrinterPdfFieldExtractResponse resp = new EsignPrinterPdfFieldExtractResponse(); - EsignPrinterPdfFieldExtractData data = new EsignPrinterPdfFieldExtractData(); - data.setText(text); - data.setInputFormat(inputFormat); - data.setOcrHint(ocrHint); - resp.setData(data); - return resp; - } - - public static EsignPrinterPdfFieldExtractResponse fail(String code, String message) { - EsignPrinterPdfFieldExtractResponse resp = new EsignPrinterPdfFieldExtractResponse(); - resp.setSuccess(false); - resp.setCode(code); - resp.setMessage(message); - return resp; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPdfFieldPrefillRequest.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPdfFieldPrefillRequest.java deleted file mode 100644 index 2fae3de..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPdfFieldPrefillRequest.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.dtos; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@Schema(description = "登记卡签名字段预填请求") -public class EsignPrinterPdfFieldPrefillRequest { - - @Schema(description = "App 签名字段 JSON 数组(含 id、fieldName、inputFormat)") - private String signFieldsJson; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPdfFieldPrefillResponse.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPdfFieldPrefillResponse.java deleted file mode 100644 index 3f0cd73..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPdfFieldPrefillResponse.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.dtos; - -import com.fasterxml.jackson.annotation.JsonInclude; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -@Data -@NoArgsConstructor -@JsonInclude(JsonInclude.Include.NON_NULL) -@Schema(description = "登记卡签名字段预填响应") -public class EsignPrinterPdfFieldPrefillResponse { - - private boolean success = true; - private String code = "200"; - private String message; - private FieldPrefillData data; - - @Data - @NoArgsConstructor - public static class FieldPrefillData { - /** fieldId → 预填文字 */ - private Map fieldPrefillMap = new LinkedHashMap<>(); - /** 任务 OCR 状态:PENDING | OK | FAILED | SKIPPED */ - private String ocrStatus; - /** 数据来源:task_ocr / ohip_profile */ - private List sources; - } - - public static EsignPrinterPdfFieldPrefillResponse ok(FieldPrefillData data) { - EsignPrinterPdfFieldPrefillResponse r = new EsignPrinterPdfFieldPrefillResponse(); - r.setData(data); - return r; - } - - public static EsignPrinterPdfFieldPrefillResponse fail(String code, String message) { - EsignPrinterPdfFieldPrefillResponse r = new EsignPrinterPdfFieldPrefillResponse(); - r.setSuccess(false); - r.setCode(code); - r.setMessage(message); - return r; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPdfSignedOhipEvent.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPdfSignedOhipEvent.java deleted file mode 100644 index 31d3763..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPdfSignedOhipEvent.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.dtos; - -/** 平板签名归档后异步上传 OPERA(事务提交后发布)。 */ -public record EsignPrinterPdfSignedOhipEvent( - String taskId, - String confirmationNumberFromRequest, - String signFieldsJson, - String profileId, - String profileType, - String reservationId, - String hotelId) {} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPdfSignedSubmitRequest.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPdfSignedSubmitRequest.java deleted file mode 100644 index ccd2ba9..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPdfSignedSubmitRequest.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.dtos; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@Schema(description = "平板/客户端回传签名 PDF 或签名图层") -public class EsignPrinterPdfSignedSubmitRequest { - - @Schema(description = "后端任务 ID") - private String taskId; - - @Schema(description = "Electron externalId(与 taskId 二选一)") - private String externalId; - - @Schema(description = "签名后完整 PDF Base64(可选)") - private String base64; - - @Schema(description = "签名图层 JSON(无合并 PDF 时存档)") - private String signaturesJson; - - @Schema(description = "签名数量") - private Integer signatureCount; - - @Schema(description = "文件名") - private String fileName; - - @Schema(description = "从 PDF 解析的确认号(可选,未传则服务端自动解析)") - private String confirmationNumber; - - @Schema(description = "酒店 ID(可选,默认取任务或 OAuth 配置)") - private String hotelId; - - @Schema(description = "签名热区 JSON(与 App loadSignFields 一致,用于服务端合成 PDF)") - private String signFieldsJson; - - @Schema(description = "OHIP reservation id(可选,用于解析 Profile ID)") - private String reservationId; - - @Schema(description = "OHIP Guest Profile ID(可选,未传且提供 reservationId 时服务端解析)") - private String profileId; - - @Schema(description = "OHIP profile.profileType(可选,例如 Guest)") - private String profileType; - - @Schema(description = "提交设备 ID(平板 UUID)") - private String deviceId; - - @Schema(description = "提交设备名称(平板展示名)") - private String deviceName; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPdfTaskCompleteRequest.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPdfTaskCompleteRequest.java deleted file mode 100644 index d27ac65..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPdfTaskCompleteRequest.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.dtos; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@Schema(description = "PC 虚拟打印机 PDF 任务完成") -public class EsignPrinterPdfTaskCompleteRequest { - - @Schema(description = "后端任务 ID") - private String taskId; - - @Schema(description = "Electron 推送 ID(与 taskId 二选一)") - private String externalId; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPdfTaskDetailResponse.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPdfTaskDetailResponse.java deleted file mode 100644 index 2fc4825..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPdfTaskDetailResponse.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.dtos; - -import com.fasterxml.jackson.annotation.JsonInclude; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@JsonInclude(JsonInclude.Include.NON_NULL) -@Schema(description = "PC 虚拟打印机 PDF 任务详情") -public class EsignPrinterPdfTaskDetailResponse { - - private boolean success = true; - private String code = "200"; - private String message; - private EsignPrinterPdfTaskDetailData data; - - @Data - @NoArgsConstructor - public static class EsignPrinterPdfTaskDetailData { - private String taskId; - private String externalId; - private String fileName; - private String filePath; - private Long fileSize; - private String hotelId; - /** 详情接口不再返回 Base64,请用 GET …/pdf 下载二进制 */ - private Boolean hasPdf; - private Boolean hasSignedPdf; - private Boolean hasSignedMeta; - private String createdAt; - private String status; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPdfTaskListRow.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPdfTaskListRow.java deleted file mode 100644 index 118370d..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPdfTaskListRow.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.dtos; - -import java.time.Instant; - -/** - * 列表查询投影:不含 pdf_base64,避免 ios-task-list / ios-pending-tasks 拉取大字段占满连接池。 - */ -public interface EsignPrinterPdfTaskListRow { - - String getTaskId(); - - String getExternalId(); - - String getFileName(); - - Long getFileSize(); - - String getHotelId(); - - String getStatus(); - - Instant getCreatedAt(); - - Instant getCompletedAt(); - - Instant getSignedAt(); - - String getAgentSource(); -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPdfTaskSubmitRequest.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPdfTaskSubmitRequest.java deleted file mode 100644 index 61d904a..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPdfTaskSubmitRequest.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.dtos; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@Schema(description = "PC 虚拟打印机 PDF 任务提交(Electron 代理)") -public class EsignPrinterPdfTaskSubmitRequest { - - @Schema(description = "Electron 侧推送 ID,用于去重", requiredMode = Schema.RequiredMode.REQUIRED) - private String externalId; - - @Schema(description = "PDF 文件名", requiredMode = Schema.RequiredMode.REQUIRED) - private String fileName; - - @Schema(description = "PC 本地路径(可选)") - private String filePath; - - @Schema(description = "PDF Base64(可带 data:application/pdf;base64, 前缀)", requiredMode = Schema.RequiredMode.REQUIRED) - private String base64; - - @Schema(description = "文件大小(字节)") - private Long fileSize; - - @Schema(description = "酒店 ID(可选,用于列表筛选)") - private String hotelId; - - @Schema(description = "PC 代理来源标识(如 IP:port)") - private String agentSource; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPdfTaskUpdateRequest.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPdfTaskUpdateRequest.java deleted file mode 100644 index 24706e0..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPdfTaskUpdateRequest.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.dtos; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@Schema(description = "CMS 更新待签收 PDF 任务(文件名 / 替换 PDF);已签名归档替换签名 PDF") -public class EsignPrinterPdfTaskUpdateRequest { - - @Schema(description = "任务 ID", requiredMode = Schema.RequiredMode.REQUIRED) - private String taskId; - - @Schema(description = "新文件名(可选)") - private String fileName; - - @Schema(description = "替换 PDF Base64(可选,可带 data:application/pdf;base64, 前缀)") - private String base64; - - @Schema(description = "替换后文件大小(字节,可选)") - private Long fileSize; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPdfUsbSignedSubmitRequest.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPdfUsbSignedSubmitRequest.java deleted file mode 100644 index d209a3f..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPdfUsbSignedSubmitRequest.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.dtos; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@Schema(description = "USB 直传签名:一次请求完成归档,OPERA 异步上传") -public class EsignPrinterPdfUsbSignedSubmitRequest { - - @Schema(description = "USB localTaskId / 去重 ID", requiredMode = Schema.RequiredMode.REQUIRED) - private String externalId; - - @Schema(description = "OHIP reservation id", requiredMode = Schema.RequiredMode.REQUIRED) - private String reservationId; - - @Schema(description = "酒店 ID") - private String hotelId; - - @Schema(description = "文件名") - private String fileName; - - @Schema(description = "原始 PDF Base64(用于服务端合成签名)", requiredMode = Schema.RequiredMode.REQUIRED) - private String pdfBase64; - - @Schema(description = "签名图层 JSON", requiredMode = Schema.RequiredMode.REQUIRED) - private String signaturesJson; - - @Schema(description = "签名热区 JSON") - private String signFieldsJson; - - @Schema(description = "签名数量") - private Integer signatureCount; - - @Schema(description = "OHIP Guest Profile ID(可选)") - private String profileId; - - @Schema(description = "OHIP profileType(可选)") - private String profileType; - - @Schema(description = "提交设备 ID") - private String deviceId; - - @Schema(description = "提交设备名称") - private String deviceName; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPushAssignRequest.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPushAssignRequest.java deleted file mode 100644 index f530f2a..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPushAssignRequest.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.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 EsignPrinterPushAssignRequest { - - private String taskId; - private List userIds; - private List groupIds; - private boolean sendFcm = false; - private String assignedBy; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPushCandidatesResponse.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPushCandidatesResponse.java deleted file mode 100644 index b07bff0..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPushCandidatesResponse.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.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 EsignPrinterPushCandidatesResponse { - - private boolean success = true; - private String code = "200"; - private String message; - private EsignPrinterPushCandidatesData data; - - @Data - @NoArgsConstructor - public static class EsignPrinterPushCandidatesData { - private String taskId; - private String agentSource; - private List devices; - private List groups; - private List selectedUserIds; - private List selectedGroupIds; - /** 已选中的平板登记 ID(与 devices.registryId 对应) */ - private List selectedRegistryIds; - private String pushMode; - } - - @Data - @NoArgsConstructor - public static class EsignPrinterPushDeviceOption { - private Long userId; - private String userName; - private String appLabel; - private boolean pushEnabled; - private String deliveryMode; - /** CMS 平板登记显示名(如 hotel-1) */ - private String displayName; - /** 绑定的客户端 IP;空表示不限 IP */ - private String clientIp; - /** 平板登记 ID,推送时优先使用 */ - private String registryId; - } - - @Data - @NoArgsConstructor - public static class EsignPrinterPushGroupOption { - private String groupId; - private String groupName; - private int memberCount; - private List memberUserIds; - } - - public static EsignPrinterPushCandidatesResponse ok(String message) { - EsignPrinterPushCandidatesResponse r = new EsignPrinterPushCandidatesResponse(); - r.setMessage(message); - return r; - } - - public static EsignPrinterPushCandidatesResponse fail(String code, String message) { - EsignPrinterPushCandidatesResponse r = new EsignPrinterPushCandidatesResponse(); - r.setSuccess(false); - r.setCode(code); - r.setMessage(message); - return r; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPushGroupListResponse.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPushGroupListResponse.java deleted file mode 100644 index 4aa483c..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPushGroupListResponse.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.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 EsignPrinterPushGroupListResponse { - - private boolean success = true; - private String code = "200"; - private String message; - private List data; - - @Data - @NoArgsConstructor - public static class EsignPrinterPushGroupItem { - private String groupId; - private String groupName; - private String agentSource; - private int memberCount; - private List memberUserIds; - private String createdAt; - private String updatedAt; - } - - public static EsignPrinterPushGroupListResponse ok(String message) { - EsignPrinterPushGroupListResponse r = new EsignPrinterPushGroupListResponse(); - r.setMessage(message); - return r; - } - - public static EsignPrinterPushGroupListResponse fail(String code, String message) { - EsignPrinterPushGroupListResponse r = new EsignPrinterPushGroupListResponse(); - r.setSuccess(false); - r.setCode(code); - r.setMessage(message); - return r; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPushGroupSaveRequest.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPushGroupSaveRequest.java deleted file mode 100644 index 56b33d7..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPushGroupSaveRequest.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.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 EsignPrinterPushGroupSaveRequest { - - private String groupId; - private String groupName; - private String agentSource; - private List memberUserIds; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPushRuleResponse.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPushRuleResponse.java deleted file mode 100644 index ea4b2d1..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPushRuleResponse.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.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 EsignPrinterPushRuleResponse { - - private boolean success = true; - private String code = "200"; - private String message; - private EsignPrinterPushRuleData data; - - @Data - @NoArgsConstructor - public static class EsignPrinterPushRuleData { - private String pushMode; - private boolean autoSendFcm; - /** 指定 PC 對應(PER_AGENT)是否參與解析 */ - private boolean perAgentEnabled = true; - /** 未標識 PC 規則(UNKNOWN_AGENT)是否參與解析 */ - private boolean unknownAgentEnabled = true; - /** 兜底規則(AUTO=ALL_AGENTS / MANUAL=全局預設)是否參與解析 */ - private boolean fallbackEnabled = true; - private EsignPrinterPushRuleMappingItem allAgents; - private EsignPrinterPushRuleMappingItem unknownAgent; - private EsignPrinterPushRuleMappingItem manualDefault; - private List perAgentMappings; - private List knownAgentSources; - } - - @Data - @NoArgsConstructor - public static class EsignPrinterPushRuleMappingItem { - private List userIds; - private List groupIds; - } - - @Data - @NoArgsConstructor - public static class EsignPrinterPushRulePerAgentItem { - private String agentSource; - private List userIds; - private List groupIds; - } - - public static EsignPrinterPushRuleResponse ok(String message) { - EsignPrinterPushRuleResponse r = new EsignPrinterPushRuleResponse(); - r.setMessage(message); - return r; - } - - public static EsignPrinterPushRuleResponse fail(String code, String message) { - EsignPrinterPushRuleResponse r = new EsignPrinterPushRuleResponse(); - r.setSuccess(false); - r.setCode(code); - r.setMessage(message); - return r; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPushRuleSaveRequest.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPushRuleSaveRequest.java deleted file mode 100644 index 9a4788d..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterPushRuleSaveRequest.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.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 EsignPrinterPushRuleSaveRequest { - - private String pushMode; - private Boolean autoSendFcm; - private Boolean perAgentEnabled; - private Boolean unknownAgentEnabled; - private Boolean fallbackEnabled; - private EsignPrinterPushRuleResponse.EsignPrinterPushRuleMappingItem allAgents; - private EsignPrinterPushRuleResponse.EsignPrinterPushRuleMappingItem unknownAgent; - private EsignPrinterPushRuleResponse.EsignPrinterPushRuleMappingItem manualDefault; - private List perAgentMappings; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterQueueResponse.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterQueueResponse.java deleted file mode 100644 index 0d6a2b4..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterQueueResponse.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.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 = "24h PDF 队列(CMS 平板队列页)") -public class EsignPrinterQueueResponse { - - private boolean success = true; - private String code = "200"; - private String message; - private EsignPrinterQueueData data; - - @Data - @NoArgsConstructor - public static class EsignPrinterQueueData { - private String generatedAt; - private int taskWindowHours; - private long total; - private int page; - private int size; - private List agentSourceOptions; - private String pushMode; - private List items; - } - - @Data - @NoArgsConstructor - public static class EsignPrinterQueueItem { - private String taskId; - private String externalId; - private String fileName; - private Long fileSize; - private String hotelId; - private String status; - private String displayStatus; - private String agentSource; - private String createdAt; - private String completedAt; - private boolean pushConfigured; - private List pushTargets; - private String ohipStatus; - private String ohipLastError; - private String ohipUploadedAt; - } - - @Data - @NoArgsConstructor - public static class EsignPrinterPushTarget { - private Long userId; - private String userName; - private String agentSource; - private String appLabel; - private String linkStatus; - private boolean pushEnabled; - private String deliveryMode; - } - - public static EsignPrinterQueueResponse ok(String message) { - EsignPrinterQueueResponse r = new EsignPrinterQueueResponse(); - r.setMessage(message); - return r; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterQueueWatchResponse.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterQueueWatchResponse.java deleted file mode 100644 index 290d269..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterQueueWatchResponse.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.dtos; - -import com.fasterxml.jackson.annotation.JsonInclude; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@JsonInclude(JsonInclude.Include.NON_NULL) -@Schema(description = "24h PDF 队列变更探测(CMS 轮询用,轻量)") -public class EsignPrinterQueueWatchResponse { - - private boolean success = true; - private String code = "200"; - private String message; - private EsignPrinterQueueWatchData data; - - @Data - @NoArgsConstructor - public static class EsignPrinterQueueWatchData { - /** 合成版本号:队列有新增/签收/推送变化时改变 */ - private String revision; - private long total24h; - private long pendingCount; - private long signedCount; - private String latestCreatedAt; - private String latestCompletedAt; - private int taskWindowHours; - } - - public static EsignPrinterQueueWatchResponse ok(String message) { - EsignPrinterQueueWatchResponse r = new EsignPrinterQueueWatchResponse(); - r.setMessage(message); - return r; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterTabletRegistryDeleteRequest.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterTabletRegistryDeleteRequest.java deleted file mode 100644 index 39e7c6e..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterTabletRegistryDeleteRequest.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.dtos; - -import com.fasterxml.jackson.annotation.JsonInclude; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@JsonInclude(JsonInclude.Include.NON_NULL) -public class EsignPrinterTabletRegistryDeleteRequest { - - private String registryId; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterTabletRegistryResponse.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterTabletRegistryResponse.java deleted file mode 100644 index ce95f1d..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterTabletRegistryResponse.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.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 EsignPrinterTabletRegistryResponse { - - private boolean success = true; - private String code = "200"; - private String message; - private EsignPrinterTabletRegistryItem data; - private List items; - - @Data - @NoArgsConstructor - public static class EsignPrinterTabletRegistryItem { - private String registryId; - private String userName; - private Long userId; - private String clientIp; - private String displayName; - private boolean enabled; - private String notes; - private String createdBy; - private String createdAt; - private String updatedAt; - private List agentSources; - } - - public static EsignPrinterTabletRegistryResponse ok(String message) { - EsignPrinterTabletRegistryResponse r = new EsignPrinterTabletRegistryResponse(); - r.setMessage(message); - return r; - } - - public static EsignPrinterTabletRegistryResponse fail(String code, String message) { - EsignPrinterTabletRegistryResponse r = new EsignPrinterTabletRegistryResponse(); - r.setSuccess(false); - r.setCode(code); - r.setMessage(message); - return r; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterTabletRegistrySaveRequest.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterTabletRegistrySaveRequest.java deleted file mode 100644 index fd81732..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignPrinterTabletRegistrySaveRequest.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.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 EsignPrinterTabletRegistrySaveRequest { - - private String registryId; - /** App 登录账号 */ - private String userName; - /** 绑定的客户端 IP,留空表示整账号(多设备共用账号时请填写) */ - private String clientIp; - /** 设备备注,如「前台签收平板」 */ - private String displayName; - private Boolean enabled; - private String notes; - private String createdBy; - /** 预绑 PC 连接标识列表(来自 CMS 登记的 PC) */ - private List agentSources; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignSignTaskRequest.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignSignTaskRequest.java deleted file mode 100644 index b2de704..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignSignTaskRequest.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.dtos; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@Schema(description = "iOS 签收提交") -public class EsignSignTaskRequest { - - @Schema(description = "任务 ID", requiredMode = Schema.RequiredMode.REQUIRED) - private String taskId; - - @Schema(description = "签收备注(可选文字)") - private String signRemark; - - @Schema(description = "客户手写签名 PNG Base64(data:image/png;base64,...)", requiredMode = Schema.RequiredMode.REQUIRED) - private String signSignature; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignTaskDetailResponse.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignTaskDetailResponse.java deleted file mode 100644 index 10a5d70..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/EsignTaskDetailResponse.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.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 = "证件任务详情(含采集 JSON)") -public class EsignTaskDetailResponse { - - private boolean success; - private String code; - private String message; - private EsignTaskDetailData data; - - @Data - @NoArgsConstructor - public static class EsignTaskDetailData { - private String taskId; - private String status; - /** pending | signed | voided,由 status + 创建时间推导 */ - private String signState; - private String createdAt; - private String signedAt; - /** 已签收时返回手写签名图(data URL) */ - private String signSignature; - private Map payload; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/LocalExeLaunchResult.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/LocalExeLaunchResult.java deleted file mode 100644 index 99339e3..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/LocalExeLaunchResult.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.dtos; - -import lombok.Getter; - -@Getter -public final class LocalExeLaunchResult { - - private final boolean launched; - private final boolean skipped; - private final String message; - - private LocalExeLaunchResult(boolean launched, boolean skipped, String message) { - this.launched = launched; - this.skipped = skipped; - this.message = message; - } - - public static LocalExeLaunchResult success(String exePath) { - return new LocalExeLaunchResult(true, false, "已启动: " + exePath); - } - - public static LocalExeLaunchResult skipped(String reason) { - return new LocalExeLaunchResult(false, true, reason); - } - - public static LocalExeLaunchResult failed(String reason) { - return new LocalExeLaunchResult(false, false, reason); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/RegistrationCardMeta.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/RegistrationCardMeta.java deleted file mode 100644 index efe0447..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/RegistrationCardMeta.java +++ /dev/null @@ -1,107 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.dtos; - -import lombok.Builder; -import lombok.Value; - -/** 登记卡 PDF OCR / 文本层解析出的检索字段。 */ -@Value -@Builder -public class RegistrationCardMeta { - String guestName; - String confirmationNo; - String phone; - String email; - String roomNo; - String checkInDate; - String checkOutDate; - - public boolean hasAnyField() { - return isPresent(guestName) - || isPresent(confirmationNo) - || isPresent(phone) - || isPresent(email) - || isPresent(roomNo) - || isPresent(checkInDate) - || isPresent(checkOutDate); - } - - /** 文本层与 OCR 结果合并:优先 primary,缺失字段用 secondary 补齐。 */ - public RegistrationCardMeta mergedWith(RegistrationCardMeta other) { - if (other == null) { - return this; - } - return RegistrationCardMeta.builder() - .guestName(firstNonBlank(guestName, other.guestName)) - .confirmationNo(firstNonBlank(confirmationNo, other.confirmationNo)) - .phone(firstNonBlank(phone, other.phone)) - .email(firstNonBlank(email, other.email)) - .roomNo(pickBetterRoomNo(roomNo, other.roomNo, phone, other.phone)) - .checkInDate(firstNonBlank(checkInDate, other.checkInDate)) - .checkOutDate(firstNonBlank(checkOutDate, other.checkOutDate)) - .build(); - } - - private static String firstNonBlank(String primary, String secondary) { - return isPresent(primary) ? primary : secondary; - } - - private static boolean isPresent(String v) { - return v != null && !v.isBlank(); - } - - /** 文本层与 OCR 房号冲突时,优先更长、更像真实房号的值(如 0202 优于 3 / 0101)。 */ - private static String pickBetterRoomNo(String primary, String secondary, String primaryPhone, String secondaryPhone) { - if (!isPresent(primary)) { - return secondary; - } - if (!isPresent(secondary)) { - return primary; - } - if (primary.matches("\\d{1,2}")) { - return secondary; - } - if (secondary.matches("\\d{1,2}")) { - return primary; - } - int primaryScore = roomNoScore(primary, primaryPhone, secondaryPhone); - int secondaryScore = roomNoScore(secondary, secondaryPhone, primaryPhone); - if (secondaryScore > primaryScore) { - return secondary; - } - if (primaryScore > secondaryScore) { - return primary; - } - if (secondary.length() > primary.length()) { - return secondary; - } - if (primary.length() > secondary.length()) { - return primary; - } - return secondary; - } - - private static int roomNoScore(String room, String ownPhone, String otherPhone) { - if (room == null || room.isBlank()) { - return 0; - } - String v = room.trim(); - if (RegistrationCardMetaExtractor.isPhoneDigitFragment(v, ownPhone) - || RegistrationCardMetaExtractor.isPhoneDigitFragment(v, otherPhone)) { - return 0; - } - if (!v.matches("\\d{1,5}")) { - return 50; - } - int len = v.length(); - if (len == 4 && v.startsWith("0")) { - return 124; - } - if (len >= 3) { - return 100 + len; - } - if (len == 2) { - return 0; - } - return 10; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/RegistrationCardMetaExtractor.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/RegistrationCardMetaExtractor.java deleted file mode 100644 index d0b4973..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/RegistrationCardMetaExtractor.java +++ /dev/null @@ -1,732 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.dtos; - -import com.sjmresorts.opsapp.esign.reservation.support.RegistrationConfirmationExtractor; -import org.apache.commons.lang3.StringUtils; - -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; -import java.util.Optional; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** 从登记卡 PDF 文本(文本层或 OCR)提取姓名、确认号、电话、房号、入退日期。 */ -public final class RegistrationCardMetaExtractor { - - private static final Pattern DATE_TOKEN = - Pattern.compile("(\\d{1,2}[-/.\\s]\\d{1,2}[-/.\\s]\\d{2,4})"); - - private static final Pattern PHONE_LIKE = - Pattern.compile("(\\+?\\d[\\d\\s\\-().]{6,18}\\d)"); - - private static final Pattern TITLE_NAME = - Pattern.compile("(?i)(?:mr\\.?|mrs\\.?|ms\\.?|miss\\.?|dr\\.?)\\s+([A-Za-z][A-Za-z\\s'.-]{2,48})"); - - private static final Pattern SLASH_NAME = - Pattern.compile("(?i)([A-Z][A-Z' -]{1,30}/[A-Z][A-Z' -]{1,30})"); - - private static final Pattern CJK_NAME = - Pattern.compile("(?:(?:姓名|住客姓名|賓客姓名|宾客姓名)\\s*[::]?\\s*)([\\u4e00-\\u9fff]{2,8})"); - - private static final Pattern EMAIL_VALUE = - Pattern.compile("([a-zA-Z0-9._%+\\-]+@[a-zA-Z0-9.\\-]+\\.[a-zA-Z]{2,})"); - - private static final Pattern STANDALONE_ROOM_LINE = Pattern.compile("^(\\d{3,5})$"); - - /** 仅同行匹配,禁止 (?s) 跨行,避免误抓日期/电话中的数字 */ - private static final Pattern[] ROOM_NUMBER_PATTERNS = { - Pattern.compile("(?i)room\\s*number\\s*房[号號]?\\s*[::]\\s*(\\d{3,5})\\b"), - Pattern.compile("(?i)room\\s*number(?![^\\n]{0,12}\\bof\\b)\\s*[::]\\s*(\\d{3,5})\\b"), - Pattern.compile("(?i)room\\s*no\\.?\\s*房[号號]?\\s*[::]\\s*(\\d{3,5})\\b"), - Pattern.compile("(?i)room\\s*(?:no\\.?\\b|#\\b|number\\b)[^\\n\\r]{0,40}?\\b(\\d{3,5})\\b"), - Pattern.compile("(?i)assigned\\s+room\\s*[::]?\\s*(\\d{3,5})\\b"), - Pattern.compile("(?i)(? confirmation = RegistrationConfirmationExtractor.extractFromText(normalized); - String confirmationNo = confirmation.orElse(null); - String checkOut = extractDateByLabels(normalized, CHECK_OUT_LABELS).orElse(null); - String checkIn = extractDateByLabels(normalized, CHECK_IN_LABELS) - .or(() -> extractCheckInFallback(normalized, checkOut)) - .orElse(null); - String phone = normalizePhone(extractByLabels(normalized, PHONE_LABELS, 48).orElse(null)); - return RegistrationCardMeta.builder() - .guestName(extractGuestName(normalized).orElse(null)) - .confirmationNo(confirmationNo) - .phone(phone) - .email(extractEmail(normalized).orElse(null)) - .roomNo(extractRoomNo(normalized, confirmationNo, phone).orElse(null)) - .checkInDate(checkIn) - .checkOutDate(checkOut) - .build(); - } - - private static Optional extractGuestName(String text) { - Optional nextLine = extractNextLineAfterLabel(text, GUEST_LABELS, 80); - if (nextLine.isPresent()) { - return nextLine; - } - Optional inline = extractByLabels(text, GUEST_LABELS, 80); - if (inline.isPresent()) { - return inline; - } - Optional title = extractFirst(TITLE_NAME, text, 1); - if (title.isPresent() && isPlausibleGuestName(title.get())) { - return title; - } - Optional slash = extractFirst(SLASH_NAME, text, 1); - if (slash.isPresent() && isPlausibleGuestName(slash.get())) { - return slash; - } - Optional cjk = extractFirst(CJK_NAME, text, 1); - if (cjk.isPresent()) { - return cjk; - } - return extractGuestNameNearConfirmation(text); - } - - private static Optional extractGuestNameNearConfirmation(String text) { - Optional confNo = RegistrationConfirmationExtractor.extractFromText(text); - if (confNo.isEmpty()) { - return Optional.empty(); - } - int idx = text.indexOf(confNo.get()); - if (idx <= 0) { - return Optional.empty(); - } - String before = text.substring(Math.max(0, idx - 240), idx); - String[] lines = before.split("\\n"); - for (int i = lines.length - 1; i >= 0; i--) { - String line = cleanValue(lines[i]); - if (line == null || !isPlausibleGuestName(line)) { - continue; - } - if (line.matches("(?i)[A-Za-z][A-Za-z\\s.,'/\\-]{2,60}")) { - return Optional.of(line); - } - } - return Optional.empty(); - } - - private static Optional extractFirst(Pattern pattern, String text, int group) { - Matcher m = pattern.matcher(text); - if (m.find()) { - return Optional.ofNullable(cleanValue(m.group(group))); - } - return Optional.empty(); - } - - private static Optional extractRoomNo(String text, String confirmationNo, String phone) { - List candidates = new ArrayList<>(); - collectRoomCandidatesFromLabelLines(text, confirmationNo, phone, candidates); - collectRoomCandidatesFromLabeledText(text, confirmationNo, phone, candidates); - collectRoomCandidatesFromPatterns(text, confirmationNo, phone, candidates); - return candidates.stream() - .max(Comparator.comparingInt(RoomCandidate::score)) - .map(RoomCandidate::value); - } - - private static void collectRoomCandidatesFromLabelLines( - String text, String confirmationNo, String phone, List candidates) { - String[] lines = text.split("\\n"); - for (int i = 0; i < lines.length; i++) { - if (!isRoomNumberLabelLine(lines[i])) { - continue; - } - addRoomCandidate(candidates, parseRoomToken(lines[i], confirmationNo, phone, true), 80); - collectRoomCandidatesAfterLabel(lines, i, confirmationNo, phone, candidates); - } - } - - private static void collectRoomCandidatesAfterLabel( - String[] lines, int labelIndex, String confirmationNo, String phone, List candidates) { - for (int j = labelIndex + 1; j < Math.min(lines.length, labelIndex + 10); j++) { - String candidate = lines[j].trim(); - if (StringUtils.isBlank(candidate) || isNonRoomMetaLine(candidate)) { - continue; - } - Matcher standalone = STANDALONE_ROOM_LINE.matcher(candidate); - if (standalone.matches()) { - addRoomCandidate( - candidates, - plausibleRoomOrNull(standalone.group(1), confirmationNo, phone), - 320); - continue; - } - addRoomCandidate( - candidates, parseRoomToken(candidate, confirmationNo, phone, true), 120); - } - } - - private static void collectRoomCandidatesFromLabeledText( - String text, String confirmationNo, String phone, List candidates) { - Optional nextLine = extractLabeledValueOnNextLine(text, ROOM_LABELS, 64); - nextLine.ifPresent(v -> addRoomCandidate( - candidates, parseRoomToken(v, confirmationNo, phone, true), 200)); - Optional block = extractRoomBlockAfterLabel(text, ROOM_LABELS, confirmationNo, phone); - block.ifPresent(v -> addRoomCandidate(candidates, v, 180)); - Optional inline = extractByRoomLabels(text, ROOM_LABELS, confirmationNo, phone); - inline.ifPresent(v -> addRoomCandidate(candidates, v, 160)); - } - - private static void collectRoomCandidatesFromPatterns( - String text, String confirmationNo, String phone, List candidates) { - for (Pattern pattern : ROOM_NUMBER_PATTERNS) { - Matcher m = pattern.matcher(text); - while (m.find()) { - addRoomCandidate( - candidates, - plausibleRoomOrNull(m.group(1), confirmationNo, phone), - 100); - } - } - for (Pattern pattern : ROOM_TYPE_PATTERNS) { - Matcher m = pattern.matcher(text); - if (m.find()) { - addRoomCandidate( - candidates, - parseRoomToken(m.group(1), confirmationNo, phone, false), - 30); - } - } - extractByLabels(text, ROOM_FALLBACK_LABELS, 48) - .map(v -> parseRoomToken(v, confirmationNo, phone, false)) - .ifPresent(v -> addRoomCandidate(candidates, v, 20)); - } - - private static String plausibleRoomOrNull(String digits, String confirmationNo, String phone) { - if (!isPlausibleRoomDigits(digits, confirmationNo, phone, true)) { - return null; - } - return digits; - } - - private static void addRoomCandidate(List candidates, String room, int bonus) { - if (StringUtils.isBlank(room)) { - return; - } - candidates.add(new RoomCandidate(room, roomDigitScore(room) + bonus)); - } - - private static final class RoomCandidate { - private final String value; - private final int score; - - private RoomCandidate(String value, int score) { - this.value = value; - this.score = score; - } - - private String value() { - return value; - } - - private int score() { - return score; - } - } - - private static Optional extractRoomBlockAfterLabel( - String text, String[] labels, String confirmationNo, String phone) { - for (String label : labels) { - Pattern p = Pattern.compile("(?i)" + label + "[^\\n]{0,48}\\n((?:\\s*[^\\n]+\\n?){1,3})"); - Matcher m = p.matcher(text); - if (m.find()) { - String block = m.group(1); - String best = null; - int bestScore = Integer.MIN_VALUE; - for (String line : block.split("\\n")) { - if (isNonRoomMetaLine(line)) { - continue; - } - String trimmed = line.trim(); - Matcher standalone = STANDALONE_ROOM_LINE.matcher(trimmed); - if (standalone.matches()) { - String room = plausibleRoomOrNull(standalone.group(1), confirmationNo, phone); - if (room != null) { - int score = roomDigitScore(room) + 300; - if (score > bestScore) { - bestScore = score; - best = room; - } - } - continue; - } - String room = parseRoomToken(line, confirmationNo, phone, true); - if (StringUtils.isNotBlank(room)) { - int score = roomDigitScore(room); - if (score > bestScore) { - bestScore = score; - best = room; - } - } - } - if (best != null) { - return Optional.of(best); - } - } - } - return Optional.empty(); - } - - private static boolean isNonRoomMetaLine(String line) { - String lower = line.toLowerCase(); - return lower.contains("room rate") - || lower.contains("room charge") - || lower.contains("room type") - || lower.contains("arrival") - || lower.contains("departure") - || lower.contains("guest") - || lower.contains("payment") - || lower.contains("confirmation") - || lower.contains("conf.") - || lower.contains("email") - || lower.contains("phone") - || lower.contains("telephone") - || lower.contains("mobile") - || lower.contains("tel") - || isPhoneLikeLine(line) - || line.contains("房价") - || line.contains("房型") - || line.contains("入住") - || line.contains("退房") - || line.contains("付款") - || line.contains("確認") - || line.contains("确认") - || line.contains("人數") - || line.contains("人数") - || isAmountOrRateLine(line); - } - - private static boolean isRoomNumberLabelLine(String line) { - String lower = line.toLowerCase(); - if (lower.contains("room type") || line.contains("房型")) { - return false; - } - return lower.contains("room number") || lower.contains("room no") || line.contains("房號") - || line.contains("房号"); - } - - /** 标签与冒号之间可有中文(如 Room Number房號: 0202) */ - private static Optional extractByRoomLabels( - String text, String[] labels, String confirmationNo, String phone) { - for (String label : labels) { - Pattern inlineDigits = Pattern.compile( - "(?i)" + label + "(?:[^\\n::\\d]{0,24})[::]?\\s*(\\d{3,5})\\b"); - Matcher digitMatch = inlineDigits.matcher(text); - if (digitMatch.find()) { - String room = plausibleRoomOrNull(digitMatch.group(1), confirmationNo, phone); - if (StringUtils.isNotBlank(room)) { - return Optional.of(room); - } - } - Pattern inlineValue = Pattern.compile( - "(?i)" + label + "(?:[^\\n::]{0,24})[::]?\\s*([^\\n\\r]{1,64})"); - Matcher valueMatch = inlineValue.matcher(text); - if (valueMatch.find()) { - String room = parseRoomToken(valueMatch.group(1), confirmationNo, phone, true); - if (StringUtils.isNotBlank(room)) { - return Optional.of(room); - } - } - } - return Optional.empty(); - } - - private static String parseRoomToken(String raw, String confirmationNo, String phone, boolean labeledContext) { - if (StringUtils.isBlank(raw)) { - return null; - } - String v = cleanValue(raw); - if (v == null || isDateLikeLine(v) || isPhoneLikeLine(v) || isAmountOrRateLine(v)) { - return null; - } - Matcher afterSlash = Pattern.compile("/\\s*(\\d{3,5})\\b").matcher(v); - if (afterSlash.find()) { - String room = afterSlash.group(1); - if (isPlausibleRoomDigits(room, confirmationNo, phone, labeledContext)) { - return room; - } - } - if (v.matches("\\d{3,5}")) { - return isPlausibleRoomDigits(v, confirmationNo, phone, labeledContext) ? v : null; - } - Matcher m = Pattern.compile("\\b(\\d{3,5})\\b").matcher(v); - String best = null; - int bestScore = Integer.MIN_VALUE; - while (m.find()) { - String cand = m.group(1); - if (!isPlausibleRoomDigits(cand, confirmationNo, phone, labeledContext)) { - continue; - } - int score = roomDigitScore(cand); - if (score > bestScore) { - bestScore = score; - best = cand; - } - } - return best; - } - - private static boolean isAmountOrRateLine(String line) { - if (StringUtils.isBlank(line)) { - return false; - } - String lower = line.toLowerCase(); - if (lower.contains("rate") - || lower.contains("charge") - || lower.contains("amount") - || lower.contains("total") - || lower.contains("balance") - || lower.contains("folio") - || line.contains("房价") - || line.contains("房價") - || line.contains("费用") - || line.contains("費用") - || line.contains("金额") - || line.contains("金額")) { - return true; - } - return AMOUNT_LINE.matcher(line).find(); - } - - private static boolean isPhoneLikeLine(String v) { - if (StringUtils.isBlank(v)) { - return false; - } - String lower = v.toLowerCase(); - if (lower.contains("phone") - || lower.contains("telephone") - || lower.contains("mobile") - || lower.contains("tel") - || v.contains("電話") - || v.contains("电话")) { - return true; - } - return PHONE_LIKE.matcher(v).find(); - } - - static boolean isPhoneDigitFragment(String digits, String phone) { - if (StringUtils.isBlank(digits) || StringUtils.isBlank(phone) || !digits.matches("\\d{3,5}")) { - return false; - } - String compactPhone = phone.replaceAll("\\D", ""); - return compactPhone.length() >= 7 && compactPhone.contains(digits); - } - - private static boolean isDateLikeLine(String v) { - return DATE_TOKEN.matcher(v).matches() - || v.matches("(?i).*(?:arrival|departure|check[-\\s]*in|check[-\\s]*out|入住|退房).*"); - } - - private static int roomDigitScore(String digits) { - int len = digits.length(); - if (len == 4 && digits.startsWith("0")) { - return 120 + len; - } - if (len >= 3 && len <= 5) { - return 100 + len; - } - if (len == 2) { - return 40; - } - return 10; - } - - private static boolean isPlausibleRoomDigits( - String digits, String confirmationNo, String phone, boolean labeledContext) { - if (StringUtils.isBlank(digits) || !digits.matches("\\d{3,5}")) { - return false; - } - if (StringUtils.isNotBlank(confirmationNo) && confirmationNo.equals(digits)) { - return false; - } - if (StringUtils.isNotBlank(confirmationNo) - && confirmationNo.contains(digits) - && digits.length() >= 3) { - return false; - } - if (isPhoneDigitFragment(digits, phone)) { - return false; - } - return true; - } - - private static Optional extractEmail(String text) { - for (String label : EMAIL_LABELS) { - Pattern inline = Pattern.compile("(?is)" + label + "\\s*[::]?\\s*" + EMAIL_VALUE.pattern()); - Matcher inlineMatch = inline.matcher(text); - if (inlineMatch.find()) { - return Optional.of(normalizeEmail(inlineMatch.group(1))); - } - Pattern nextLine = Pattern.compile( - "(?is)" + label + "[^\\n@]{0,48}\\n\\s*(" + EMAIL_VALUE.pattern() + ")"); - Matcher nextLineMatch = nextLine.matcher(text); - if (nextLineMatch.find()) { - return Optional.of(normalizeEmail(nextLineMatch.group(1))); - } - } - Matcher any = EMAIL_VALUE.matcher(text); - if (any.find()) { - return Optional.of(normalizeEmail(any.group(1))); - } - return Optional.empty(); - } - - private static String normalizeEmail(String raw) { - if (StringUtils.isBlank(raw)) { - return null; - } - String v = raw.trim().toLowerCase(); - v = v.replaceAll("[,;]+$", ""); - return EMAIL_VALUE.matcher(v).matches() ? v : null; - } - - private static Optional extractLabeledValueOnNextLine(String text, String[] labels, int maxLen) { - for (String label : labels) { - Pattern p = Pattern.compile( - "(?is)" + label + "(?:[^\\n::]{0,24})[::]?[^\\n]{0,40}\\n\\s*([^\\n]{1," + maxLen + "})"); - Matcher m = p.matcher(text); - if (m.find()) { - String v = cleanValue(m.group(1)); - if (StringUtils.isNotBlank(v)) { - return Optional.of(v); - } - } - Pattern simple = Pattern.compile("(?is)" + label + "[^\\n]{0,40}\\n\\s*([^\\n]{1," + maxLen + "})"); - Matcher simpleMatch = simple.matcher(text); - if (simpleMatch.find()) { - String v = cleanValue(simpleMatch.group(1)); - if (StringUtils.isNotBlank(v)) { - return Optional.of(v); - } - } - } - return Optional.empty(); - } - - private static String normalizeRoomNo(String raw) { - return parseRoomToken(raw, null, null, false); - } - - private static String normalizePhone(String raw) { - if (StringUtils.isBlank(raw)) { - return null; - } - String v = raw.trim().replaceAll("(?i)^[@\\s]*(?:lx|tel|phone)[::.\\s]*", "").trim(); - Matcher m = Pattern.compile("(\\+?\\d[\\d\\s\\-().]{6,18}\\d)").matcher(v); - if (m.find()) { - return cleanValue(m.group(1)); - } - return cleanValue(v); - } - - private static Optional extractNextLineAfterLabel(String text, String[] labels, int maxLen) { - for (String label : labels) { - Pattern p = Pattern.compile("(?is)" + label + "[^\\n]{0,40}\\n\\s*([^\\n]{2," + maxLen + "})"); - Matcher m = p.matcher(text); - if (m.find()) { - String v = cleanValue(m.group(1)); - if (isPlausibleGuestName(v)) { - return Optional.of(v); - } - } - } - return Optional.empty(); - } - - private static Optional extractByLabels(String text, String[] labels, int maxLen) { - for (String label : labels) { - Pattern p = Pattern.compile("(?is)" + label + "\\s*[::]?\\s*([^\\n\\r]{1," + maxLen + "})"); - Matcher m = p.matcher(text); - if (m.find()) { - String v = cleanValue(m.group(1)); - if (StringUtils.isNotBlank(v) && !looksLikeNextLabel(v)) { - return Optional.of(v); - } - } - } - return Optional.empty(); - } - - private static Optional extractDateByLabels(String text, String[] labels) { - for (String label : labels) { - Pattern p = Pattern.compile("(?is)" + label + "[^0-9]{0,40}" + DATE_TOKEN.pattern()); - Matcher m = p.matcher(text); - if (m.find()) { - return Optional.of(normalizeDateToken(m.group(1))); - } - } - return Optional.empty(); - } - - private static Optional extractCheckInFallback(String text, String checkOut) { - Matcher fuzzy = Pattern.compile("(?is)a\\w{0,8}val[^0-9]{0,48}" + DATE_TOKEN.pattern()).matcher(text); - if (fuzzy.find()) { - String date = normalizeDateToken(fuzzy.group(1)); - if (!date.equals(checkOut)) { - return Optional.of(date); - } - } - if (StringUtils.isBlank(checkOut)) { - return Optional.empty(); - } - List dates = collectDates(text); - if (dates.size() == 2) { - for (String date : dates) { - if (!date.equals(checkOut)) { - return Optional.of(date); - } - } - } - return Optional.empty(); - } - - private static List collectDates(String text) { - List dates = new ArrayList<>(); - Matcher m = DATE_TOKEN.matcher(text); - while (m.find()) { - String date = normalizeDateToken(m.group(1)); - if (!dates.contains(date)) { - dates.add(date); - } - } - return dates; - } - - private static String normalizeDateToken(String raw) { - if (raw == null) { - return null; - } - return raw.trim().replaceAll("\\s+", "-").replace('/', '-').replace('.', '-'); - } - - private static String cleanValue(String raw) { - if (raw == null) { - return null; - } - String v = raw.trim(); - v = v.replaceAll("\\s{2,}", " "); - v = v.replaceAll("[|_]+$", "").trim(); - return v.isEmpty() ? null : v; - } - - private static boolean isPlausibleGuestName(String v) { - if (StringUtils.isBlank(v) || v.length() < 2) { - return false; - } - if (DATE_TOKEN.matcher(v).matches()) { - return false; - } - if (v.matches("\\d{4,12}")) { - return false; - } - return !looksLikeNextLabel(v); - } - - private static boolean looksLikeNextLabel(String v) { - String lower = v.toLowerCase(); - return lower.contains("conf") - || lower.contains("phone") - || lower.contains("email") - || lower.contains("e-mail") - || lower.contains("room") - || lower.contains("number") - || lower.contains("arrival") - || lower.contains("departure") - || v.contains("確認") - || v.contains("确认") - || v.contains("电话") - || v.contains("電話") - || v.contains("房號") - || v.contains("房号") - || v.contains("入住") - || v.contains("退房"); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/RegistrationCardResponseParser.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/RegistrationCardResponseParser.java deleted file mode 100644 index 3273da3..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/RegistrationCardResponseParser.java +++ /dev/null @@ -1,117 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.dtos; - -import com.alibaba.fastjson2.JSON; -import com.alibaba.fastjson2.JSONArray; -import com.alibaba.fastjson2.JSONObject; -import org.apache.commons.lang3.StringUtils; - -import java.util.Base64; -import java.util.Optional; - -public final class RegistrationCardResponseParser { - - private RegistrationCardResponseParser() { - } - - public record PdfPayload(String pdfBase64, String fileName, String fileType) { - } - - public static Optional extract(String body) { - if (StringUtils.isBlank(body)) { - return Optional.empty(); - } - String trimmed = body.trim(); - if (trimmed.startsWith("%PDF")) { - return Optional.of(new PdfPayload( - Base64.getEncoder().encodeToString(trimmed.getBytes()), - "registration-card.pdf", - "PDF")); - } - if (trimmed.startsWith("{") || trimmed.startsWith("[")) { - try { - Object parsed = JSON.parse(trimmed); - Optional fromJson = findInNode(parsed); - if (fromJson.isPresent()) { - return fromJson; - } - } catch (Exception ignored) { - // fall through - } - } - if (looksLikeBase64(trimmed)) { - return Optional.of(new PdfPayload(trimmed, "registration-card.pdf", "PDF")); - } - return Optional.empty(); - } - - private static Optional findInNode(Object node) { - if (node instanceof JSONObject obj) { - String fileData = firstNonBlank( - obj.getString("fileData"), - obj.getString("pdf"), - obj.getString("content"), - obj.getString("registrationCard")); - if (StringUtils.isNotBlank(fileData) && looksLikeBase64OrPdfMarker(fileData)) { - return Optional.of(buildPayload(obj, normalizeBase64(fileData))); - } - for (String key : obj.keySet()) { - Optional nested = findInNode(obj.get(key)); - if (nested.isPresent()) { - return nested; - } - } - } else if (node instanceof JSONArray arr) { - for (int i = 0; i < arr.size(); i++) { - Optional nested = findInNode(arr.get(i)); - if (nested.isPresent()) { - return nested; - } - } - } - return Optional.empty(); - } - - private static PdfPayload buildPayload(JSONObject obj, String pdfBase64) { - String fileName = firstNonBlank( - obj.getString("fileName"), - obj.getString("filename"), - "registration-card.pdf"); - String fileType = StringUtils.defaultIfBlank( - firstNonBlank(obj.getString("fileType"), obj.getString("mimeType")), - "PDF"); - return new PdfPayload(pdfBase64, fileName, fileType); - } - - private static String normalizeBase64(String value) { - String trimmed = value.trim(); - int comma = trimmed.indexOf(','); - if (trimmed.startsWith("data:") && comma > 0) { - return trimmed.substring(comma + 1).trim(); - } - return trimmed; - } - - private static boolean looksLikeBase64OrPdfMarker(String value) { - if (value.startsWith("%PDF") || value.startsWith("data:")) { - return true; - } - return looksLikeBase64(value); - } - - private static boolean looksLikeBase64(String value) { - if (StringUtils.isBlank(value) || value.length() < 32) { - return false; - } - String sample = value.length() > 256 ? value.substring(0, 256) : value; - return sample.matches("(?s)[A-Za-z0-9+/=\\r\\n-_.]+"); - } - - private static String firstNonBlank(String... values) { - for (String value : values) { - if (StringUtils.isNotBlank(value)) { - return value.trim(); - } - } - return null; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/RegistrationCardResult.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/RegistrationCardResult.java deleted file mode 100644 index 94309d4..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/RegistrationCardResult.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.dtos; - -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Getter -@Setter -@NoArgsConstructor -public class RegistrationCardResult { - - private boolean invoked; - private boolean success; - private int statusCode; - private String errorMessage; - private String fileName; - private String fileType; - private String pdfBase64; - private String responseSummary; - - public static RegistrationCardResult skipped(String reason) { - RegistrationCardResult r = new RegistrationCardResult(); - r.setInvoked(false); - r.setSuccess(false); - r.setErrorMessage(reason); - return r; - } - - public static RegistrationCardResult failure(int statusCode, String summary, String errorMessage) { - RegistrationCardResult r = new RegistrationCardResult(); - r.setInvoked(true); - r.setSuccess(false); - r.setStatusCode(statusCode); - r.setResponseSummary(summary); - r.setErrorMessage(errorMessage); - return r; - } - - public static RegistrationCardResult success( - int statusCode, - String fileName, - String fileType, - String pdfBase64, - String responseSummary) { - RegistrationCardResult r = new RegistrationCardResult(); - r.setInvoked(true); - r.setSuccess(true); - r.setStatusCode(statusCode); - r.setFileName(fileName); - r.setFileType(fileType); - r.setPdfBase64(pdfBase64); - r.setResponseSummary(responseSummary); - return r; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/RegistrationCardSignFieldParser.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/RegistrationCardSignFieldParser.java deleted file mode 100644 index d8a74da..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/RegistrationCardSignFieldParser.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.dtos; - -import com.alibaba.fastjson2.JSON; -import com.alibaba.fastjson2.JSONArray; -import com.alibaba.fastjson2.JSONObject; -import com.sjmresorts.opsapp.esign.printer.models.RegistrationCardSignFieldDefaults; -import org.apache.commons.lang3.StringUtils; - -import java.util.ArrayList; -import java.util.List; - -/** 解析 App 提交的签名热区 JSON(与 registrationCardSignFields.js 结构一致)。 */ -public final class RegistrationCardSignFieldParser { - - private RegistrationCardSignFieldParser() { - } - - public static List parse(String signFieldsJson) { - if (StringUtils.isBlank(signFieldsJson)) { - return RegistrationCardSignFieldDefaults.defaultFields(); - } - try { - JSONArray arr = JSON.parseArray(signFieldsJson.trim()); - if (arr == null || arr.isEmpty()) { - return RegistrationCardSignFieldDefaults.defaultFields(); - } - List fields = new ArrayList<>(); - for (int i = 0; i < arr.size(); i++) { - JSONObject obj = arr.getJSONObject(i); - if (obj == null) { - continue; - } - String id = StringUtils.defaultIfBlank(obj.getString("id"), "guest"); - int page = obj.getIntValue("page", 0); - double left = obj.getDoubleValue("left"); - double top = obj.getDoubleValue("top"); - double width = obj.getDoubleValue("width"); - double height = obj.getDoubleValue("height"); - if (width <= 0 || height <= 0) { - continue; - } - String type = StringUtils.defaultIfBlank(obj.getString("type"), "signature"); - double fontSize = obj.getDoubleValue("fontSize"); - String orientation = StringUtils.trimToNull(obj.getString("orientation")); - if (orientation == null) { - orientation = StringUtils.trimToNull(obj.getString("rotate")); - } - if (orientation == null) { - orientation = StringUtils.trimToNull(obj.getString("rotation")); - } - if (orientation != null && !"landscape".equalsIgnoreCase(orientation) - && !"portrait".equalsIgnoreCase(orientation)) { - // 兼容数值角度:90/-90 视为横签热区 - if ("90".equals(orientation) || "-90".equals(orientation)) { - orientation = "landscape"; - } else { - orientation = null; - } - } - fields.add(new SignedRegistrationPdfComposer.SignFieldRect( - id, type, page, left, top, width, height, fontSize, orientation)); - } - if (fields.isEmpty()) { - return RegistrationCardSignFieldDefaults.defaultFields(); - } - return fields; - } catch (Exception e) { - return RegistrationCardSignFieldDefaults.defaultFields(); - } - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/SignedRegistrationPdfComposer.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/SignedRegistrationPdfComposer.java deleted file mode 100644 index 7a635ec..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/dtos/SignedRegistrationPdfComposer.java +++ /dev/null @@ -1,336 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.dtos; - -import com.alibaba.fastjson2.JSON; -import com.alibaba.fastjson2.JSONObject; -import org.apache.commons.lang3.StringUtils; -import org.apache.pdfbox.Loader; -import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.pdmodel.PDPage; -import org.apache.pdfbox.pdmodel.PDPageContentStream; -import org.apache.pdfbox.pdmodel.common.PDRectangle; -import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; -import org.apache.pdfbox.util.Matrix; - -import org.apache.pdfbox.pdmodel.font.PDFont; -import org.apache.pdfbox.pdmodel.font.PDType0Font; -import org.apache.pdfbox.pdmodel.font.PDType1Font; -import org.apache.pdfbox.pdmodel.font.Standard14Fonts; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.util.Base64; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -/** - * 将签名 PNG 按热区比例叠加到原 PDF,生成可上传的签名 PDF。 - * 热区坐标与 App/pdf.js 一致:相对「旋转后可视页面」宽高比例,原点在左上角。 - */ -public final class SignedRegistrationPdfComposer { - - public record SignFieldRect( - String id, - String type, - int page, - double left, - double top, - double width, - double height, - double fontSize, - String orientation) { - - public SignFieldRect( - String id, - String type, - int page, - double left, - double top, - double width, - double height, - double fontSize) { - this(id, type, page, left, top, width, height, fontSize, null); - } - - public boolean isTextField() { - return "text".equalsIgnoreCase(type); - } - - public boolean isLandscape() { - return "landscape".equalsIgnoreCase(StringUtils.trimToEmpty(orientation)); - } - } - - private SignedRegistrationPdfComposer() { - } - - public static String composeBase64(String pdfBase64, String signaturesJson, List fields) { - if (StringUtils.isBlank(pdfBase64)) { - throw new IllegalArgumentException("PDF base64 required"); - } - Map signatures = parseSignatures(signaturesJson); - if (signatures.isEmpty()) { - return normalizeBase64(pdfBase64); - } - byte[] pdfBytes = Base64.getDecoder().decode(normalizeBase64(pdfBase64)); - try (PDDocument doc = Loader.loadPDF(pdfBytes); - ByteArrayOutputStream out = new ByteArrayOutputStream()) { - PDFont textFont = null; - for (SignFieldRect field : fields) { - String value = signatures.get(field.id()); - if (StringUtils.isBlank(value)) { - continue; - } - int pageIndex = Math.max(0, field.page()); - if (pageIndex >= doc.getNumberOfPages()) { - continue; - } - PDPage page = doc.getPage(pageIndex); - if (field.isTextField() || !looksLikeImageBase64(value)) { - if (textFont == null) { - textFont = resolveTextFont(doc); - } - try (PDPageContentStream cs = new PDPageContentStream( - doc, page, PDPageContentStream.AppendMode.APPEND, true, true)) { - drawFieldText(cs, page, field, value, textFont); - } - continue; - } - byte[] imageBytes = decodeImageBase64(value); - if (imageBytes.length == 0) { - continue; - } - PDImageXObject image = PDImageXObject.createFromByteArray(doc, imageBytes, field.id()); - try (PDPageContentStream cs = new PDPageContentStream( - doc, page, PDPageContentStream.AppendMode.APPEND, true, true)) { - drawFieldImage(cs, page, field, image); - } - } - doc.save(out); - return Base64.getEncoder().encodeToString(out.toByteArray()); - } catch (Exception e) { - throw new IllegalStateException("Failed to compose signed PDF: " + e.getMessage(), e); - } - } - - /** - * 与 App renderjs / pdf.js viewport 一致:热区为「可视页面」宽高比例,原点在左上角。 - * 虚拟打印机 PDF 常带 /Rotate,必须把可视坐标映射到 PDF 用户空间,否则会出现错位和横变竖。 - */ - private static void drawFieldImage( - PDPageContentStream cs, - PDPage page, - SignFieldRect field, - PDImageXObject image) throws IOException { - VisualRect rect = resolveVisualRect(page, field); - cs.saveGraphicsState(); - cs.transform(visualToUserSpaceMatrix(page)); - if (field.isLandscape()) { - // 与 App applyFieldContentOrientation 一致:内容相对热区旋转 -90° - float cx = rect.x + rect.w / 2f; - float cy = rect.yFromBottom + rect.h / 2f; - cs.transform(Matrix.getTranslateInstance(cx, cy)); - cs.transform(Matrix.getRotateInstance(Math.toRadians(-90), 0, 0)); - cs.drawImage(image, -rect.h / 2f, -rect.w / 2f, rect.h, rect.w); - } else { - cs.drawImage(image, rect.x, rect.yFromBottom, rect.w, rect.h); - } - cs.restoreGraphicsState(); - } - - private static void drawFieldText( - PDPageContentStream cs, - PDPage page, - SignFieldRect field, - String text, - PDFont font) throws IOException { - VisualRect rect = resolveVisualRect(page, field); - float fontSize; - if (field.fontSize() > 0) { - fontSize = (float) Math.max(6, Math.min(field.fontSize(), 28)); - } else { - fontSize = Math.max(6f, Math.min(rect.h * 0.72f, 14f)); - } - String safe = StringUtils.defaultString(text).trim(); - if (safe.isEmpty()) { - return; - } - cs.saveGraphicsState(); - cs.transform(visualToUserSpaceMatrix(page)); - float padX = Math.max(2f, Math.min(6f, rect.w * 0.04f)); - float padY = Math.max(2f, Math.min(6f, rect.h * 0.12f)); - cs.setNonStrokingColor(1f, 1f, 1f); - cs.addRect(rect.x - padX, rect.yFromBottom - padY, rect.w + padX * 2f, rect.h + padY * 2f); - cs.fill(); - cs.setNonStrokingColor(0f, 0f, 0f); - if (field.isLandscape()) { - float cx = rect.x + rect.w / 2f; - float cy = rect.yFromBottom + rect.h / 2f; - cs.transform(Matrix.getTranslateInstance(cx, cy)); - cs.transform(Matrix.getRotateInstance(Math.toRadians(-90), 0, 0)); - float textW = rect.h; - float textY = -fontSize * 0.35f; - cs.setFont(font, fontSize); - cs.beginText(); - cs.newLineAtOffset(-textW / 2f + 2f, textY); - cs.showText(truncateToWidth(font, safe, fontSize, textW - 4f)); - cs.endText(); - } else { - float textY = rect.yFromBottom + Math.max(1f, (rect.h - fontSize) * 0.35f); - cs.setFont(font, fontSize); - cs.beginText(); - cs.newLineAtOffset(rect.x + 2f, textY); - cs.showText(truncateToWidth(font, safe, fontSize, rect.w - 4f)); - cs.endText(); - } - cs.restoreGraphicsState(); - } - - private static VisualRect resolveVisualRect(PDPage page, SignFieldRect field) { - PDRectangle crop = page.getCropBox(); - int rot = normalizeRotation(page.getRotation()); - float cropW = crop.getWidth(); - float cropH = crop.getHeight(); - float pageW = (rot == 90 || rot == 270) ? cropH : cropW; - float pageH = (rot == 90 || rot == 270) ? cropW : cropH; - float x = (float) (field.left() * pageW); - float w = (float) (field.width() * pageW); - float h = (float) (field.height() * pageH); - float yFromTop = (float) (field.top() * pageH); - float yFromBottom = pageH - yFromTop - h; - return new VisualRect(x, yFromBottom, w, h); - } - - /** - * 将「可视直立页面」坐标(原点左下、x 右、y 上)映射到 PDF 用户空间, - * 与页面 /Rotate 配套,使绘制内容随页面一起被阅读器旋转后位置正确。 - */ - private static Matrix visualToUserSpaceMatrix(PDPage page) { - PDRectangle crop = page.getCropBox(); - float llx = crop.getLowerLeftX(); - float lly = crop.getLowerLeftY(); - float cropW = crop.getWidth(); - float cropH = crop.getHeight(); - int rot = normalizeRotation(page.getRotation()); - return switch (rot) { - case 90 -> new Matrix(0, 1, -1, 0, llx + cropW, lly); - case 180 -> new Matrix(-1, 0, 0, -1, llx + cropW, lly + cropH); - case 270 -> new Matrix(0, -1, 1, 0, llx, lly + cropH); - default -> new Matrix(1, 0, 0, 1, llx, lly); - }; - } - - private static int normalizeRotation(int rotation) { - int rot = rotation % 360; - if (rot < 0) { - rot += 360; - } - if (rot == 90 || rot == 180 || rot == 270) { - return rot; - } - return 0; - } - - private record VisualRect(float x, float yFromBottom, float w, float h) { - } - - private static String truncateToWidth(PDFont font, String text, float fontSize, float maxWidth) - throws IOException { - if (maxWidth <= 0) { - return ""; - } - int end = text.length(); - while (end > 0) { - String part = text.substring(0, end); - float width = font.getStringWidth(part) / 1000f * fontSize; - if (width <= maxWidth) { - return part; - } - end--; - } - return ""; - } - - private static PDFont resolveTextFont(PDDocument doc) { - String[] candidates = { - "C:/Windows/Fonts/msyh.ttc", - "C:/Windows/Fonts/msyhbd.ttc", - "C:/Windows/Fonts/simhei.ttf", - "C:/Windows/Fonts/arial.ttf", - "/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc", - "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc", - "/System/Library/Fonts/PingFang.ttc" - }; - for (String path : candidates) { - File file = new File(path); - if (!file.isFile()) { - continue; - } - try { - return PDType0Font.load(doc, file); - } catch (IOException ignored) { - // try next font - } - } - return new PDType1Font(Standard14Fonts.FontName.HELVETICA); - } - - public static boolean isImageSignatureValue(String value) { - return looksLikeImageBase64(value); - } - - private static boolean looksLikeImageBase64(String raw) { - if (StringUtils.isBlank(raw)) { - return false; - } - String value = raw.trim(); - if (value.startsWith("data:image")) { - return true; - } - String normalized = value.replaceAll("\\s", ""); - return normalized.length() > 200 && normalized.matches("^[A-Za-z0-9+/=]+$"); - } - - private static Map parseSignatures(String signaturesJson) { - Map map = new LinkedHashMap<>(); - if (StringUtils.isBlank(signaturesJson)) { - return map; - } - try { - JSONObject obj = JSON.parseObject(signaturesJson); - if (obj == null) { - return map; - } - for (String key : obj.keySet()) { - String value = obj.getString(key); - if (StringUtils.isNotBlank(value)) { - map.put(key, value); - } - } - } catch (Exception ignored) { - // ignore malformed json - } - return map; - } - - private static byte[] decodeImageBase64(String raw) { - if (StringUtils.isBlank(raw)) { - return new byte[0]; - } - try { - return Base64.getDecoder().decode(normalizeBase64(raw)); - } catch (IllegalArgumentException e) { - return new byte[0]; - } - } - - 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/printer/filters/EsignPrinterPdfArchiveFilter.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/filters/EsignPrinterPdfArchiveFilter.java deleted file mode 100644 index e94657c..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/filters/EsignPrinterPdfArchiveFilter.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.filters; - -import lombok.Builder; -import lombok.Value; -import org.apache.commons.lang3.StringUtils; - -@Value -@Builder -public class EsignPrinterPdfArchiveFilter { - String guestName; - String confirmationNo; - String reservationId; - String phone; - String roomNo; - String checkInDate; - String checkOutDate; - String fileName; - /** ALL | SIGNED | COMPLETED | OCR_FAILED */ - String status; - - public static EsignPrinterPdfArchiveFilter of( - String guestName, - String confirmationNo, - String reservationId, - String phone, - String roomNo, - String checkInDate, - String checkOutDate, - String fileName, - String status) { - return EsignPrinterPdfArchiveFilter.builder() - .guestName(trim(guestName)) - .confirmationNo(trim(confirmationNo)) - .reservationId(trim(reservationId)) - .phone(trim(phone)) - .roomNo(trim(roomNo)) - .checkInDate(trim(checkInDate)) - .checkOutDate(trim(checkOutDate)) - .fileName(trim(fileName)) - .status(trim(status)) - .build(); - } - - private static String trim(String v) { - return StringUtils.isBlank(v) ? null : v.trim(); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/models/EsignPrinterDeviceLink.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/models/EsignPrinterDeviceLink.java deleted file mode 100644 index 526d22e..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/models/EsignPrinterDeviceLink.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.models; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; -import jakarta.persistence.PrePersist; -import jakarta.persistence.PreUpdate; -import jakarta.persistence.Table; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -import java.time.Instant; - -@Getter -@Setter -@NoArgsConstructor -@Entity -@Table(name = "esign_printer_device_link") -public class EsignPrinterDeviceLink { - - @Id - @Column(name = "link_id", nullable = false, length = 36) - private String linkId; - - @Column(name = "user_id", nullable = false) - private Long userId; - - @Column(name = "agent_source", nullable = false, length = 256) - private String agentSource; - - @Column(name = "app_label", length = 128) - private String appLabel; - - @Column(name = "pc_display_name", length = 128) - private String pcDisplayName; - - @Column(name = "status", nullable = false, length = 20) - private String status; - - @Column(name = "requested_at", nullable = false) - private Instant requestedAt; - - @Column(name = "reviewed_at") - private Instant reviewedAt; - - @Column(name = "reviewed_by", length = 64) - private String reviewedBy; - - @Column(name = "created_at", nullable = false) - private Instant createdAt; - - @Column(name = "updated_at", nullable = false) - private Instant updatedAt; - - @PrePersist - protected void onCreate() { - Instant now = Instant.now(); - if (createdAt == null) { - createdAt = now; - } - if (updatedAt == null) { - updatedAt = now; - } - if (requestedAt == null) { - requestedAt = now; - } - } - - @PreUpdate - protected void onUpdate() { - updatedAt = Instant.now(); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/models/EsignPrinterEventLog.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/models/EsignPrinterEventLog.java deleted file mode 100644 index c890912..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/models/EsignPrinterEventLog.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.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_printer_event_logs") -public class EsignPrinterEventLog { - - @Id - @Column(name = "log_id", nullable = false, length = 36) - private String logId; - - @Column(name = "task_id", length = 36) - private String taskId; - - @Column(name = "event_type", nullable = false, length = 64) - private String eventType; - - @Column(name = "message", length = 512) - private String message; - - @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; - - @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/printer/models/EsignPrinterPcRegistry.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/models/EsignPrinterPcRegistry.java deleted file mode 100644 index b427d41..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/models/EsignPrinterPcRegistry.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.models; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; -import jakarta.persistence.PrePersist; -import jakarta.persistence.PreUpdate; -import jakarta.persistence.Table; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -import java.time.Instant; - -@Getter -@Setter -@NoArgsConstructor -@Entity -@Table(name = "esign_printer_pc_registry") -public class EsignPrinterPcRegistry { - - @Id - @Column(name = "pc_id", nullable = false, length = 36) - private String pcId; - - @Column(name = "display_name", nullable = false, length = 128) - private String displayName; - - @Column(name = "host_ip", nullable = false, length = 64) - private String hostIp; - - @Column(name = "agent_source", nullable = false, length = 256) - private String agentSource; - - @Column(name = "enabled", nullable = false) - private boolean enabled = true; - - @Column(name = "notes", length = 256) - private String notes; - - @Column(name = "created_by", length = 64) - private String createdBy; - - @Column(name = "created_at", nullable = false) - private Instant createdAt; - - @Column(name = "updated_at", nullable = false) - private Instant updatedAt; - - @PrePersist - protected void onCreate() { - Instant now = Instant.now(); - if (createdAt == null) { - createdAt = now; - } - if (updatedAt == null) { - updatedAt = now; - } - } - - @PreUpdate - protected void onUpdate() { - updatedAt = Instant.now(); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/models/EsignPrinterPdfTask.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/models/EsignPrinterPdfTask.java deleted file mode 100644 index 3ad5e85..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/models/EsignPrinterPdfTask.java +++ /dev/null @@ -1,132 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.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_printer_pdf_tasks") -public class EsignPrinterPdfTask { - - @Id - @Column(name = "task_id", nullable = false, length = 36) - private String taskId; - - @Column(name = "external_id", nullable = false, length = 64, unique = true) - private String externalId; - - @Column(name = "file_name", nullable = false, length = 512) - private String fileName; - - @Column(name = "file_path", length = 1024) - private String filePath; - - @Column(name = "file_size") - private Long fileSize; - - @Lob - @Column(name = "pdf_base64", nullable = false) - private String pdfBase64; - - @Column(name = "hotel_id", length = 64) - private String hotelId; - - @Column(name = "agent_source", length = 256) - private String agentSource; - - @Column(name = "status", nullable = false, length = 32) - private String status; - - @Column(name = "created_at", nullable = false) - private Instant createdAt; - - @Column(name = "completed_at") - private Instant completedAt; - - @Lob - @Column(name = "signed_pdf_base64") - private String signedPdfBase64; - - @Lob - @Column(name = "signed_meta_json") - private String signedMetaJson; - - @Column(name = "signed_at") - private Instant signedAt; - - @Column(name = "signed_by_user_id") - private Long signedByUserId; - - @Column(name = "signed_client_ip", length = 64) - private String signedClientIp; - - @Column(name = "signed_device_id", length = 64) - private String signedDeviceId; - - @Column(name = "signed_device_name", length = 128) - private String signedDeviceName; - - @Column(name = "archived_at") - private Instant archivedAt; - - @Column(name = "ocr_guest_name", length = 256) - private String ocrGuestName; - - @Column(name = "ocr_confirmation_no", length = 64) - private String ocrConfirmationNo; - - @Column(name = "ocr_phone", length = 64) - private String ocrPhone; - - @Column(name = "ocr_email", length = 256) - private String ocrEmail; - - @Column(name = "ocr_room_no", length = 32) - private String ocrRoomNo; - - @Column(name = "ocr_check_in_date", length = 32) - private String ocrCheckInDate; - - @Column(name = "ocr_check_out_date", length = 32) - private String ocrCheckOutDate; - - @Column(name = "ocr_extracted_at") - private Instant ocrExtractedAt; - - /** PENDING | OK | FAILED | SKIPPED */ - @Column(name = "ocr_status", length = 16) - private String ocrStatus; - - /** PENDING | SUCCESS | FAILED | SKIPPED */ - @Column(name = "ohip_status", length = 16) - private String ohipStatus; - - @Column(name = "ohip_last_error", length = 512) - private String ohipLastError; - - @Column(name = "ohip_uploaded_at") - private Instant ohipUploadedAt; - - @Column(name = "ohip_confirmation_no", length = 64) - private String ohipConfirmationNo; - - @Column(name = "ohip_reservation_id", length = 64) - private String ohipReservationId; - - @Column(name = "ohip_retry_count") - private Integer ohipRetryCount; - - @Lob - @Column(name = "sign_fields_json") - private String signFieldsJson; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/models/EsignPrinterPdfTaskPush.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/models/EsignPrinterPdfTaskPush.java deleted file mode 100644 index a03b92e..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/models/EsignPrinterPdfTaskPush.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.models; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; -import jakarta.persistence.IdClass; -import jakarta.persistence.Table; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -import java.io.Serializable; -import java.time.Instant; - -@Getter -@Setter -@NoArgsConstructor -@Entity -@Table(name = "esign_printer_pdf_task_push") -@IdClass(EsignPrinterPdfTaskPush.TaskPushId.class) -public class EsignPrinterPdfTaskPush { - - @Id - @Column(name = "task_id", nullable = false, length = 36) - private String taskId; - - @Id - @Column(name = "user_id", nullable = false) - private Long userId; - - /** 推送目标设备 IP;空字符串表示不限 IP(整账号可见,兼容旧数据) */ - @Id - @Column(name = "client_ip", nullable = false, length = 64) - private String clientIp = ""; - - @Column(name = "assigned_at", nullable = false) - private Instant assignedAt; - - @Column(name = "assigned_by", length = 64) - private String assignedBy; - - @Column(name = "group_id", length = 36) - private String groupId; - - @Getter - @Setter - @NoArgsConstructor - public static class TaskPushId implements Serializable { - private String taskId; - private Long userId; - private String clientIp; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/models/EsignPrinterPushGroup.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/models/EsignPrinterPushGroup.java deleted file mode 100644 index 806bd94..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/models/EsignPrinterPushGroup.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.models; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; -import jakarta.persistence.PrePersist; -import jakarta.persistence.PreUpdate; -import jakarta.persistence.Table; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -import java.time.Instant; - -@Getter -@Setter -@NoArgsConstructor -@Entity -@Table(name = "esign_printer_push_group") -public class EsignPrinterPushGroup { - - @Id - @Column(name = "group_id", nullable = false, length = 36) - private String groupId; - - @Column(name = "group_name", nullable = false, length = 128) - private String groupName; - - @Column(name = "agent_source", nullable = false, length = 256) - private String agentSource; - - @Column(name = "created_at", nullable = false) - private Instant createdAt; - - @Column(name = "updated_at", nullable = false) - private Instant updatedAt; - - @PrePersist - protected void onCreate() { - Instant now = Instant.now(); - if (createdAt == null) { - createdAt = now; - } - if (updatedAt == null) { - updatedAt = now; - } - } - - @PreUpdate - protected void onUpdate() { - updatedAt = Instant.now(); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/models/EsignPrinterPushGroupMember.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/models/EsignPrinterPushGroupMember.java deleted file mode 100644 index d1e7a9f..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/models/EsignPrinterPushGroupMember.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.models; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; -import jakarta.persistence.IdClass; -import jakarta.persistence.Table; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -import java.io.Serializable; - -@Getter -@Setter -@NoArgsConstructor -@Entity -@Table(name = "esign_printer_push_group_member") -@IdClass(EsignPrinterPushGroupMember.MemberId.class) -public class EsignPrinterPushGroupMember { - - @Id - @Column(name = "group_id", nullable = false, length = 36) - private String groupId; - - @Id - @Column(name = "user_id", nullable = false) - private Long userId; - - @Getter - @Setter - @NoArgsConstructor - public static class MemberId implements Serializable { - private String groupId; - private Long userId; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/models/EsignPrinterPushRuleConfig.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/models/EsignPrinterPushRuleConfig.java deleted file mode 100644 index 3568c2c..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/models/EsignPrinterPushRuleConfig.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.models; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; -import jakarta.persistence.PrePersist; -import jakarta.persistence.PreUpdate; -import jakarta.persistence.Table; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -import java.time.Instant; - -@Getter -@Setter -@NoArgsConstructor -@Entity -@Table(name = "esign_printer_push_rule_config") -public class EsignPrinterPushRuleConfig { - - public static final String DEFAULT_ID = "default"; - public static final String MODE_AUTO = "AUTO"; - public static final String MODE_MANUAL = "MANUAL"; - - @Id - @Column(name = "config_id", nullable = false, length = 32) - private String configId; - - @Column(name = "push_mode", nullable = false, length = 16) - private String pushMode; - - @Column(name = "auto_send_fcm", nullable = false) - private boolean autoSendFcm = false; - - @Column(name = "per_agent_enabled", nullable = false) - private boolean perAgentEnabled = true; - - @Column(name = "unknown_agent_enabled", nullable = false) - private boolean unknownAgentEnabled = true; - - @Column(name = "fallback_enabled", nullable = false) - private boolean fallbackEnabled = true; - - @Column(name = "updated_at", nullable = false) - private Instant updatedAt; - - @PrePersist - @PreUpdate - protected void touch() { - updatedAt = Instant.now(); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/models/EsignPrinterPushRuleMapping.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/models/EsignPrinterPushRuleMapping.java deleted file mode 100644 index 0202b39..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/models/EsignPrinterPushRuleMapping.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.models; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; -import jakarta.persistence.Lob; -import jakarta.persistence.PrePersist; -import jakarta.persistence.PreUpdate; -import jakarta.persistence.Table; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -import java.time.Instant; - -@Getter -@Setter -@NoArgsConstructor -@Entity -@Table(name = "esign_printer_push_rule_mapping") -public class EsignPrinterPushRuleMapping { - - public static final String SCOPE_PER_AGENT = "PER_AGENT"; - public static final String SCOPE_ALL_AGENTS = "ALL_AGENTS"; - public static final String SCOPE_UNKNOWN_AGENT = "UNKNOWN_AGENT"; - public static final String SCOPE_MANUAL_DEFAULT = "MANUAL_DEFAULT"; - - @Id - @Column(name = "mapping_id", nullable = false, length = 36) - private String mappingId; - - @Column(name = "scope_type", nullable = false, length = 32) - private String scopeType; - - @Column(name = "agent_source", length = 256) - private String agentSource; - - @Lob - @Column(name = "user_ids_json") - private String userIdsJson; - - @Lob - @Column(name = "group_ids_json") - private String groupIdsJson; - - @Column(name = "updated_at", nullable = false) - private Instant updatedAt; - - @PrePersist - @PreUpdate - protected void touch() { - updatedAt = Instant.now(); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/models/EsignPrinterTabletRegistry.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/models/EsignPrinterTabletRegistry.java deleted file mode 100644 index 6094247..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/models/EsignPrinterTabletRegistry.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.models; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; -import jakarta.persistence.PrePersist; -import jakarta.persistence.PreUpdate; -import jakarta.persistence.Table; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -import java.time.Instant; - -@Getter -@Setter -@NoArgsConstructor -@Entity -@Table(name = "esign_printer_tablet_registry") -public class EsignPrinterTabletRegistry { - - @Id - @Column(name = "registry_id", nullable = false, length = 36) - private String registryId; - - @Column(name = "user_name", nullable = false, length = 50) - private String userName; - - @Column(name = "user_id") - private Long userId; - - /** 绑定的 App 客户端 IP;空字符串表示不限制(整账号,不推荐多设备共用账号时使用) */ - @Column(name = "client_ip", nullable = false, length = 64) - private String clientIp = ""; - - @Column(name = "display_name", length = 128) - private String displayName; - - @Column(name = "enabled", nullable = false) - private boolean enabled = true; - - @Column(name = "notes", length = 256) - private String notes; - - @Column(name = "created_by", length = 64) - private String createdBy; - - @Column(name = "created_at", nullable = false) - private Instant createdAt; - - @Column(name = "updated_at", nullable = false) - private Instant updatedAt; - - @PrePersist - protected void onCreate() { - Instant now = Instant.now(); - if (createdAt == null) { - createdAt = now; - } - if (updatedAt == null) { - updatedAt = now; - } - } - - @PreUpdate - protected void onUpdate() { - updatedAt = Instant.now(); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/models/EsignPrinterTabletRegistryPc.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/models/EsignPrinterTabletRegistryPc.java deleted file mode 100644 index f42ca26..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/models/EsignPrinterTabletRegistryPc.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.models; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; -import jakarta.persistence.PrePersist; -import jakarta.persistence.Table; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -import java.time.Instant; - -@Getter -@Setter -@NoArgsConstructor -@Entity -@Table(name = "esign_printer_tablet_registry_pc") -public class EsignPrinterTabletRegistryPc { - - @Id - @Column(name = "binding_id", nullable = false, length = 36) - private String bindingId; - - @Column(name = "registry_id", nullable = false, length = 36) - private String registryId; - - @Column(name = "agent_source", nullable = false, length = 256) - private String agentSource; - - @Column(name = "created_at", nullable = false) - private Instant createdAt; - - @PrePersist - protected void onCreate() { - if (createdAt == null) { - createdAt = Instant.now(); - } - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/models/RegistrationCardSignFieldDefaults.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/models/RegistrationCardSignFieldDefaults.java deleted file mode 100644 index 18b876d..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/models/RegistrationCardSignFieldDefaults.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.models; - -import com.sjmresorts.opsapp.esign.printer.dtos.SignedRegistrationPdfComposer; - -import java.util.List; - -/** 与 App registrationCardSignFields.js 默认热区一致。 */ -public final class RegistrationCardSignFieldDefaults { - - private RegistrationCardSignFieldDefaults() { - } - - public static List defaultFields() { - return List.of(new SignedRegistrationPdfComposer.SignFieldRect( - "guest", "signature", 0, 0.06, 0.88, 0.44, 0.08, 0)); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/repositories/EsignPrinterDeviceLinkRepository.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/repositories/EsignPrinterDeviceLinkRepository.java deleted file mode 100644 index 5807e99..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/repositories/EsignPrinterDeviceLinkRepository.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.repositories; - -import com.sjmresorts.opsapp.esign.printer.models.EsignPrinterDeviceLink; -import org.springframework.data.jpa.repository.JpaRepository; - -import java.util.Collection; -import java.util.List; -import java.util.Optional; - -public interface EsignPrinterDeviceLinkRepository extends JpaRepository { - - List findByStatusOrderByRequestedAtDesc(String status); - - List findByStatusInOrderByRequestedAtDesc(Collection statuses); - - List findByUserIdOrderByRequestedAtDesc(Long userId); - - List findByAgentSourceAndStatus(String agentSource, String status); - - List findByUserIdAndStatus(Long userId, String status); - - Optional findFirstByUserIdAndAgentSourceAndStatusIn( - Long userId, String agentSource, Collection statuses); -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/repositories/EsignPrinterEventLogRepository.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/repositories/EsignPrinterEventLogRepository.java deleted file mode 100644 index bf08ea6..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/repositories/EsignPrinterEventLogRepository.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.repositories; - -import com.sjmresorts.opsapp.esign.printer.models.EsignPrinterEventLog; -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 EsignPrinterEventLogRepository extends JpaRepository { - - Page findByOrderByCreatedAtDesc(Pageable pageable); - - Page findByEventTypeOrderByCreatedAtDesc(String eventType, Pageable pageable); - - Page findByTaskIdOrderByCreatedAtDesc(String taskId, Pageable pageable); -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/repositories/EsignPrinterPcRegistryRepository.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/repositories/EsignPrinterPcRegistryRepository.java deleted file mode 100644 index 377f405..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/repositories/EsignPrinterPcRegistryRepository.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.repositories; - -import com.sjmresorts.opsapp.esign.printer.models.EsignPrinterPcRegistry; -import org.springframework.data.jpa.repository.JpaRepository; - -import java.util.List; -import java.util.Optional; - -public interface EsignPrinterPcRegistryRepository extends JpaRepository { - - List findAllByOrderByDisplayNameAsc(); - - List findByEnabledTrueOrderByDisplayNameAsc(); - - Optional findByAgentSource(String agentSource); - - boolean existsByAgentSource(String agentSource); - - boolean existsByAgentSourceAndPcIdNot(String agentSource, String pcId); -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/repositories/EsignPrinterPdfTaskPushRepository.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/repositories/EsignPrinterPdfTaskPushRepository.java deleted file mode 100644 index a17ccc7..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/repositories/EsignPrinterPdfTaskPushRepository.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.repositories; - -import com.sjmresorts.opsapp.esign.printer.models.EsignPrinterPdfTaskPush; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; - -import java.util.Collection; -import java.util.List; -import java.util.Set; - -public interface EsignPrinterPdfTaskPushRepository - extends JpaRepository { - - List findByTaskId(String taskId); - - List findByTaskIdIn(Collection taskIds); - - void deleteByTaskId(String taskId); - - @Query(""" - SELECT p.taskId FROM EsignPrinterPdfTaskPush p - WHERE p.userId = :userId - AND (p.clientIp = '' OR p.clientIp = :clientIp) - """) - Set findTaskIdsByUserIdAndClientIp( - @Param("userId") Long userId, @Param("clientIp") String clientIp); - - @Query(""" - SELECT p.taskId FROM EsignPrinterPdfTaskPush p - WHERE p.userId = :userId AND p.clientIp = '' - """) - Set findTaskIdsByUserIdWildcardOnly(@Param("userId") Long userId); - - /** @deprecated 请使用 {@link #findTaskIdsByUserIdAndClientIp} */ - @Deprecated - @Query("SELECT p.taskId FROM EsignPrinterPdfTaskPush p WHERE p.userId = :userId") - Set findTaskIdsByUserId(@Param("userId") Long userId); - - @Query("SELECT COUNT(p) > 0 FROM EsignPrinterPdfTaskPush p WHERE p.taskId = :taskId") - boolean existsByTaskId(@Param("taskId") String taskId); - - @Query(""" - SELECT MAX(p.assignedAt) FROM EsignPrinterPdfTaskPush p, EsignPrinterPdfTask t - WHERE p.taskId = t.taskId AND t.createdAt >= :cutoff - """) - java.time.Instant findMaxAssignedAtForTasksSince(@Param("cutoff") java.time.Instant cutoff); - - @Query(""" - SELECT COUNT(DISTINCT p.taskId) FROM EsignPrinterPdfTaskPush p, EsignPrinterPdfTask t - WHERE p.taskId = t.taskId AND t.createdAt >= :cutoff - """) - long countDistinctPushedTasksSince(@Param("cutoff") java.time.Instant cutoff); -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/repositories/EsignPrinterPdfTaskRepository.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/repositories/EsignPrinterPdfTaskRepository.java deleted file mode 100644 index 73ad9f2..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/repositories/EsignPrinterPdfTaskRepository.java +++ /dev/null @@ -1,300 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.repositories; - -import com.sjmresorts.opsapp.esign.printer.models.EsignPrinterPdfTask; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterOhipStatusRow; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterPdfArchiveRow; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterPcSourceRow; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterPdfTaskListRow; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.JpaSpecificationExecutor; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; -import org.springframework.stereotype.Repository; - -import java.time.Instant; -import java.util.List; -import java.util.Optional; - -@Repository -public interface EsignPrinterPdfTaskRepository - extends JpaRepository, JpaSpecificationExecutor { - - Optional findFirstByExternalId(String externalId); - - Optional findFirstByExternalIdAndStatus(String externalId, String status); - - @Query(""" - SELECT t.taskId AS taskId, t.externalId AS externalId, t.fileName AS fileName, - t.fileSize AS fileSize, t.hotelId AS hotelId, t.status AS status, - t.createdAt AS createdAt, t.completedAt AS completedAt, t.signedAt AS signedAt, - t.agentSource AS agentSource - FROM EsignPrinterPdfTask t - WHERE t.status = :status AND t.createdAt >= :createdAtMin - ORDER BY t.createdAt DESC - """) - List findListRowsByStatusAndCreatedAtGreaterThanEqual( - @Param("status") String status, - @Param("createdAtMin") Instant createdAtMin, - Pageable pageable); - - @Query(""" - SELECT t.taskId AS taskId, t.externalId AS externalId, t.fileName AS fileName, - t.fileSize AS fileSize, t.hotelId AS hotelId, t.status AS status, - t.createdAt AS createdAt, t.completedAt AS completedAt, t.signedAt AS signedAt, - t.agentSource AS agentSource - FROM EsignPrinterPdfTask t - WHERE t.status = :status AND t.createdAt < :createdAtMax - ORDER BY t.createdAt DESC - """) - List findListRowsByStatusAndCreatedAtLessThan( - @Param("status") String status, - @Param("createdAtMax") Instant createdAtMax, - Pageable pageable); - - @Query(""" - SELECT t.taskId AS taskId, t.externalId AS externalId, t.fileName AS fileName, - t.fileSize AS fileSize, t.hotelId AS hotelId, t.status AS status, - t.createdAt AS createdAt, t.completedAt AS completedAt, t.signedAt AS signedAt, - t.agentSource AS agentSource - FROM EsignPrinterPdfTask t - WHERE t.status = :status - ORDER BY t.completedAt DESC - """) - List findListRowsByStatusOrderByCompletedAtDesc( - @Param("status") String status, - Pageable pageable); - - @Query(""" - SELECT t.taskId AS taskId, t.externalId AS externalId, t.fileName AS fileName, - t.fileSize AS fileSize, t.hotelId AS hotelId, t.status AS status, - t.createdAt AS createdAt, t.completedAt AS completedAt, t.signedAt AS signedAt, - t.agentSource AS agentSource - FROM EsignPrinterPdfTask t - WHERE t.signedAt IS NOT NULL AND t.createdAt >= :createdAtMin - ORDER BY t.signedAt DESC - """) - List findListRowsSignedSince( - @Param("createdAtMin") Instant createdAtMin, - Pageable pageable); - - @Query(""" - SELECT t.taskId AS taskId, t.externalId AS externalId, t.fileName AS fileName, - t.fileSize AS fileSize, t.hotelId AS hotelId, t.status AS status, - t.createdAt AS createdAt, t.completedAt AS completedAt, t.signedAt AS signedAt, - t.agentSource AS agentSource - FROM EsignPrinterPdfTask t - WHERE t.signedAt IS NULL AND t.createdAt >= :createdAtMin - ORDER BY t.createdAt DESC - """) - List findListRowsUnsignedSince( - @Param("createdAtMin") Instant createdAtMin, - Pageable pageable); - - @Query(""" - SELECT t.taskId AS taskId, t.externalId AS externalId, t.fileName AS fileName, - t.fileSize AS fileSize, t.hotelId AS hotelId, t.status AS status, - t.createdAt AS createdAt, t.completedAt AS completedAt, t.signedAt AS signedAt, - t.agentSource AS agentSource - FROM EsignPrinterPdfTask t - WHERE t.signedAt IS NULL AND t.createdAt < :createdAtMax - ORDER BY t.createdAt DESC - """) - List findListRowsUnsignedBefore( - @Param("createdAtMax") Instant createdAtMax, - Pageable pageable); - - long countByStatusAndCreatedAtGreaterThanEqual(String status, Instant createdAtMin); - - long countByStatusAndCreatedAtLessThan(String status, Instant createdAtMax); - - long countByStatus(String status); - - long countByCreatedAtGreaterThanEqual(Instant createdAtMin); - - @Query("SELECT MAX(t.createdAt) FROM EsignPrinterPdfTask t WHERE t.createdAt >= :cutoff") - Instant findMaxCreatedAtSince(@Param("cutoff") Instant cutoff); - - @Query(""" - SELECT MAX(t.completedAt) FROM EsignPrinterPdfTask t - WHERE t.createdAt >= :cutoff AND t.completedAt IS NOT NULL - """) - Instant findMaxCompletedAtSince(@Param("cutoff") Instant cutoff); - - @Query(""" - SELECT CASE - WHEN t.agentSource IS NULL OR TRIM(t.agentSource) = '' THEN 'printer-agent' - ELSE t.agentSource - END AS agentSource, - COUNT(t) AS submitted24h, - SUM(CASE WHEN t.status = 'PENDING' THEN 1 ELSE 0 END) AS pendingCount, - SUM(CASE WHEN t.status = 'COMPLETED' THEN 1 ELSE 0 END) AS signed24h, - MAX(t.createdAt) AS lastSubmitAt - FROM EsignPrinterPdfTask t - WHERE t.createdAt >= :cutoff - GROUP BY CASE - WHEN t.agentSource IS NULL OR TRIM(t.agentSource) = '' THEN 'printer-agent' - ELSE t.agentSource - END - ORDER BY MAX(t.createdAt) DESC - """) - List aggregatePcSourcesSince(@Param("cutoff") Instant cutoff); - - @Query(""" - SELECT t.taskId AS taskId, t.externalId AS externalId, t.fileName AS fileName, - t.fileSize AS fileSize, t.hotelId AS hotelId, t.status AS status, - t.createdAt AS createdAt, t.completedAt AS completedAt, t.signedAt AS signedAt, - t.agentSource AS agentSource - FROM EsignPrinterPdfTask t - WHERE t.createdAt >= :cutoff - AND (:agentSource IS NULL OR :agentSource = '' - OR (CASE WHEN t.agentSource IS NULL OR TRIM(t.agentSource) = '' - THEN 'printer-agent' ELSE t.agentSource END) = :agentSource) - ORDER BY t.createdAt DESC - """) - List findQueueListRows( - @Param("cutoff") Instant cutoff, - @Param("agentSource") String agentSource, - Pageable pageable); - - @Query(""" - SELECT t.taskId AS taskId, t.externalId AS externalId, t.fileName AS fileName, - t.fileSize AS fileSize, t.hotelId AS hotelId, t.status AS status, - t.createdAt AS createdAt, t.completedAt AS completedAt, t.signedAt AS signedAt, - t.agentSource AS agentSource - FROM EsignPrinterPdfTask t - WHERE t.createdAt >= :cutoff - AND ( - :agentSource IS NULL OR :agentSource = '' - OR (CASE WHEN t.agentSource IS NULL OR TRIM(t.agentSource) = '' - THEN 'printer-agent' ELSE t.agentSource END) = :agentSource - OR ( - :agentHostIp IS NOT NULL AND :agentHostIp <> '' - AND ( - (CASE WHEN t.agentSource IS NULL OR TRIM(t.agentSource) = '' - THEN 'printer-agent' ELSE t.agentSource END) LIKE CONCAT('%@', :agentHostIp) - OR (CASE WHEN t.agentSource IS NULL OR TRIM(t.agentSource) = '' - THEN 'printer-agent' ELSE t.agentSource END) = :agentHostIp - ) - ) - ) - AND (:taskStatus IS NULL OR t.status = :taskStatus) - AND ( - :signFilter IS NULL - OR (:signFilter = 'SIGNED' AND t.signedAt IS NOT NULL) - OR (:signFilter = 'UNSIGNED' AND t.signedAt IS NULL) - ) - AND ( - :pushFilter IS NULL - OR (:pushFilter = 'PUSHED' AND EXISTS ( - SELECT 1 FROM EsignPrinterPdfTaskPush p WHERE p.taskId = t.taskId)) - OR (:pushFilter = 'NOT_PUSHED' AND NOT EXISTS ( - SELECT 1 FROM EsignPrinterPdfTaskPush p WHERE p.taskId = t.taskId)) - ) - ORDER BY t.createdAt DESC - """) - List findQueueListRowsFiltered( - @Param("cutoff") Instant cutoff, - @Param("agentSource") String agentSource, - @Param("agentHostIp") String agentHostIp, - @Param("taskStatus") String taskStatus, - @Param("signFilter") String signFilter, - @Param("pushFilter") String pushFilter, - Pageable pageable); - - @Query(""" - SELECT COUNT(t) FROM EsignPrinterPdfTask t - WHERE t.createdAt >= :cutoff - AND ( - :agentSource IS NULL OR :agentSource = '' - OR (CASE WHEN t.agentSource IS NULL OR TRIM(t.agentSource) = '' - THEN 'printer-agent' ELSE t.agentSource END) = :agentSource - OR ( - :agentHostIp IS NOT NULL AND :agentHostIp <> '' - AND ( - (CASE WHEN t.agentSource IS NULL OR TRIM(t.agentSource) = '' - THEN 'printer-agent' ELSE t.agentSource END) LIKE CONCAT('%@', :agentHostIp) - OR (CASE WHEN t.agentSource IS NULL OR TRIM(t.agentSource) = '' - THEN 'printer-agent' ELSE t.agentSource END) = :agentHostIp - ) - ) - ) - AND (:taskStatus IS NULL OR t.status = :taskStatus) - AND ( - :signFilter IS NULL - OR (:signFilter = 'SIGNED' AND t.signedAt IS NOT NULL) - OR (:signFilter = 'UNSIGNED' AND t.signedAt IS NULL) - ) - AND ( - :pushFilter IS NULL - OR (:pushFilter = 'PUSHED' AND EXISTS ( - SELECT 1 FROM EsignPrinterPdfTaskPush p WHERE p.taskId = t.taskId)) - OR (:pushFilter = 'NOT_PUSHED' AND NOT EXISTS ( - SELECT 1 FROM EsignPrinterPdfTaskPush p WHERE p.taskId = t.taskId)) - ) - """) - long countQueueRowsFiltered( - @Param("cutoff") Instant cutoff, - @Param("agentSource") String agentSource, - @Param("agentHostIp") String agentHostIp, - @Param("taskStatus") String taskStatus, - @Param("signFilter") String signFilter, - @Param("pushFilter") String pushFilter); - - @Query(""" - SELECT COUNT(t) FROM EsignPrinterPdfTask t - WHERE t.createdAt >= :cutoff - AND (:agentSource IS NULL OR :agentSource = '' - OR (CASE WHEN t.agentSource IS NULL OR TRIM(t.agentSource) = '' - THEN 'printer-agent' ELSE t.agentSource END) = :agentSource) - """) - long countQueueRows(@Param("cutoff") Instant cutoff, @Param("agentSource") String agentSource); - - @Query(""" - SELECT t.taskId AS taskId, t.externalId AS externalId, t.fileName AS fileName, - t.fileSize AS fileSize, t.hotelId AS hotelId, t.status AS status, - t.createdAt AS createdAt, t.completedAt AS completedAt, t.signedAt AS signedAt, - t.agentSource AS agentSource - FROM EsignPrinterPdfTask t - ORDER BY t.createdAt DESC - """) - List findRecentListRows(Pageable pageable); - - @Query(""" - SELECT t.taskId AS taskId, t.externalId AS externalId, t.fileName AS fileName, - t.fileSize AS fileSize, t.hotelId AS hotelId, t.status AS status, - t.createdAt AS createdAt, t.completedAt AS completedAt, - t.signedAt AS signedAt, t.archivedAt AS archivedAt, - CASE WHEN t.signedPdfBase64 IS NOT NULL AND LENGTH(t.signedPdfBase64) > 0 THEN true ELSE false END AS hasSignedPdf, - CASE WHEN t.signedMetaJson IS NOT NULL AND LENGTH(t.signedMetaJson) > 0 THEN true ELSE false END AS hasSignedMeta - FROM EsignPrinterPdfTask t - WHERE t.status = 'COMPLETED' OR t.signedAt IS NOT NULL - ORDER BY COALESCE(t.archivedAt, t.completedAt, t.signedAt, t.createdAt) DESC - """) - List findArchiveRows(Pageable pageable); - - @Query(""" - SELECT COUNT(t) FROM EsignPrinterPdfTask t - WHERE t.status = 'COMPLETED' OR t.signedAt IS NOT NULL - """) - long countArchiveRows(); - - @Query(""" - SELECT t FROM EsignPrinterPdfTask t - WHERE t.signedAt IS NOT NULL - AND t.ohipStatus IN ('PENDING', 'FAILED') - AND (t.ohipRetryCount IS NULL OR t.ohipRetryCount < 5) - ORDER BY COALESCE(t.signedAt, t.archivedAt) ASC - """) - List findOhipRetryCandidates(Pageable pageable); - - long countBySignedAtIsNotNullAndOhipStatus(String ohipStatus); - - @Query(""" - SELECT t.taskId AS taskId, t.ohipStatus AS ohipStatus, t.ohipLastError AS ohipLastError, - t.ohipUploadedAt AS ohipUploadedAt - FROM EsignPrinterPdfTask t - WHERE t.taskId IN :taskIds - """) - List findOhipStatusByTaskIdIn(@Param("taskIds") List taskIds); -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/repositories/EsignPrinterPushGroupMemberRepository.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/repositories/EsignPrinterPushGroupMemberRepository.java deleted file mode 100644 index 45b2e53..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/repositories/EsignPrinterPushGroupMemberRepository.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.repositories; - -import com.sjmresorts.opsapp.esign.printer.models.EsignPrinterPushGroupMember; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; - -import java.util.Collection; -import java.util.List; - -public interface EsignPrinterPushGroupMemberRepository - extends JpaRepository { - - List findByGroupId(String groupId); - - List findByGroupIdIn(Collection groupIds); - - void deleteByGroupId(String groupId); - - @Query("SELECT m.userId FROM EsignPrinterPushGroupMember m WHERE m.groupId IN :groupIds") - List findUserIdsByGroupIdIn(@Param("groupIds") Collection groupIds); -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/repositories/EsignPrinterPushGroupRepository.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/repositories/EsignPrinterPushGroupRepository.java deleted file mode 100644 index c9ebee5..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/repositories/EsignPrinterPushGroupRepository.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.repositories; - -import com.sjmresorts.opsapp.esign.printer.models.EsignPrinterPushGroup; -import org.springframework.data.jpa.repository.JpaRepository; - -import java.util.List; - -public interface EsignPrinterPushGroupRepository extends JpaRepository { - - List findByAgentSourceOrderByGroupNameAsc(String agentSource); -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/repositories/EsignPrinterPushRuleConfigRepository.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/repositories/EsignPrinterPushRuleConfigRepository.java deleted file mode 100644 index 154ed19..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/repositories/EsignPrinterPushRuleConfigRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.repositories; - -import com.sjmresorts.opsapp.esign.printer.models.EsignPrinterPushRuleConfig; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface EsignPrinterPushRuleConfigRepository extends JpaRepository { -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/repositories/EsignPrinterPushRuleMappingRepository.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/repositories/EsignPrinterPushRuleMappingRepository.java deleted file mode 100644 index 0e2f2a1..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/repositories/EsignPrinterPushRuleMappingRepository.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.repositories; - -import com.sjmresorts.opsapp.esign.printer.models.EsignPrinterPushRuleMapping; -import org.springframework.data.jpa.repository.JpaRepository; - -import java.util.List; -import java.util.Optional; - -public interface EsignPrinterPushRuleMappingRepository extends JpaRepository { - - List findByScopeTypeOrderByUpdatedAtDesc(String scopeType); - - Optional findFirstByScopeTypeAndAgentSource(String scopeType, String agentSource); - - List findByScopeTypeAndAgentSourceIsNotNullOrderByAgentSourceAsc(String scopeType); - - boolean existsByScopeTypeAndAgentSource(String scopeType, String agentSource); -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/repositories/EsignPrinterTabletRegistryPcRepository.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/repositories/EsignPrinterTabletRegistryPcRepository.java deleted file mode 100644 index 8fc5abd..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/repositories/EsignPrinterTabletRegistryPcRepository.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.repositories; - -import com.sjmresorts.opsapp.esign.printer.models.EsignPrinterTabletRegistryPc; -import org.springframework.data.jpa.repository.JpaRepository; - -import java.util.List; - -public interface EsignPrinterTabletRegistryPcRepository extends JpaRepository { - - List findByRegistryIdOrderByAgentSourceAsc(String registryId); - - void deleteByRegistryId(String registryId); -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/repositories/EsignPrinterTabletRegistryRepository.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/repositories/EsignPrinterTabletRegistryRepository.java deleted file mode 100644 index aa7f3ba..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/repositories/EsignPrinterTabletRegistryRepository.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.repositories; - -import com.sjmresorts.opsapp.esign.printer.models.EsignPrinterTabletRegistry; -import org.springframework.data.jpa.repository.JpaRepository; - -import java.util.List; -import java.util.Optional; - -public interface EsignPrinterTabletRegistryRepository extends JpaRepository { - - List findAllByOrderByUserNameAsc(); - - List findByEnabledTrueOrderByUserNameAsc(); - - List findAllByUserIdOrderByClientIpAsc(Long userId); - - List findAllByUserIdAndEnabledTrueOrderByClientIpAsc(Long userId); - - Optional findByUserIdAndClientIp(Long userId, String clientIp); - - boolean existsByUserIdAndClientIpAndRegistryIdNot(Long userId, String clientIp, String registryId); - - boolean existsByUserIdAndClientIp(Long userId, String clientIp); -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/services/EsignPrinterDeviceLinkService.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/services/EsignPrinterDeviceLinkService.java deleted file mode 100644 index 28a0475..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/services/EsignPrinterDeviceLinkService.java +++ /dev/null @@ -1,395 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.services; - -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterDeviceLinkRequest; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterDeviceLinkResponse; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterDeviceLinkReviewRequest; -import com.sjmresorts.opsapp.esign.printer.models.EsignPrinterDeviceLink; -import com.sjmresorts.opsapp.esign.auth.models.EsignUser; -import com.sjmresorts.opsapp.esign.printer.repositories.EsignPrinterDeviceLinkRepository; -import com.sjmresorts.opsapp.esign.auth.repositories.EsignUserRepository; -import com.sjmresorts.opsapp.esign.auth.services.EsignUserPresenceService; -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.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.UUID; -import java.util.stream.Collectors; - -@Slf4j -@Service -public class EsignPrinterDeviceLinkService { - - public static final String STATUS_PENDING = "PENDING"; - public static final String STATUS_APPROVED = "APPROVED"; - public static final String STATUS_REJECTED = "REJECTED"; - public static final String STATUS_REVOKED = "REVOKED"; - - @Resource - private EsignPrinterDeviceLinkRepository linkRepository; - - @Resource - private EsignUserRepository esignUserRepository; - - @Resource - private EsignUserPresenceService userPresenceService; - - @Resource - private EsignPrinterPcRegistryService pcRegistryService; - - @Resource - private EsignPrinterTabletRegistryService tabletRegistryService; - - @Transactional - public EsignPrinterDeviceLinkResponse requestLink( - Long userId, EsignPrinterDeviceLinkRequest request, String clientIp) { - if (userId == null) { - return EsignPrinterDeviceLinkResponse.fail("401", "请先登录 App"); - } - if (request == null || StringUtils.isBlank(request.getAgentSource())) { - return EsignPrinterDeviceLinkResponse.fail("400", "请填写 PC 连接标识(agentSource)"); - } - Optional userOpt = esignUserRepository.findById(userId); - if (userOpt.isEmpty() || !"ACTIVE".equalsIgnoreCase(userOpt.get().getStatus())) { - return EsignPrinterDeviceLinkResponse.fail("403", "账号不可用"); - } - userPresenceService.touchClientIp(userId, clientIp); - - String agentSource = normalizeAgentSource(request.getAgentSource()); - Optional existing = - linkRepository.findFirstByUserIdAndAgentSourceAndStatusIn( - userId, agentSource, Arrays.asList(STATUS_PENDING, STATUS_APPROVED)); - if (existing.isPresent()) { - EsignPrinterDeviceLink link = existing.get(); - String beforeStatus = link.getStatus(); - maybeAutoApproveRegisteredDevices(link, userId, clientIp); - if (!beforeStatus.equals(link.getStatus())) { - linkRepository.save(link); - } - EsignPrinterDeviceLinkResponse resp = EsignPrinterDeviceLinkResponse.ok( - STATUS_APPROVED.equals(link.getStatus()) ? "连接已批准" : "连接申请已存在"); - resp.setData(toItem(link, userOpt.get())); - return resp; - } - - EsignPrinterDeviceLink link = new EsignPrinterDeviceLink(); - link.setLinkId(UUID.randomUUID().toString()); - link.setUserId(userId); - link.setAgentSource(agentSource); - link.setAppLabel(trim(request.getAppLabel(), 128)); - pcRegistryService.findByAgentSource(agentSource).ifPresent(reg -> link.setPcDisplayName(reg.getDisplayName())); - link.setStatus(STATUS_PENDING); - link.setRequestedAt(Instant.now()); - maybeAutoApproveRegisteredDevices(link, userId, clientIp); - linkRepository.save(link); - log.info( - "Printer device link requested linkId={}, userId={}, agentSource={}, status={}", - link.getLinkId(), - userId, - agentSource, - link.getStatus()); - - String message = STATUS_APPROVED.equals(link.getStatus()) - ? "已连接(PC 与平板均已 CMS 登记,自动批准)" - : "连接申请已提交,等待 CMS 审批"; - EsignPrinterDeviceLinkResponse resp = EsignPrinterDeviceLinkResponse.ok(message); - resp.setData(toItem(link, userOpt.get())); - return resp; - } - - @Transactional(readOnly = true) - public EsignPrinterDeviceLinkResponse listMyLinks(Long userId) { - if (userId == null) { - return EsignPrinterDeviceLinkResponse.fail("401", "请先登录 App"); - } - List links = linkRepository.findByUserIdOrderByRequestedAtDesc(userId); - EsignPrinterDeviceLinkResponse resp = EsignPrinterDeviceLinkResponse.ok("ok"); - resp.setItems(toItems(links)); - return resp; - } - - @Transactional(readOnly = true) - public EsignPrinterDeviceLinkResponse listForCms() { - List links = - linkRepository.findByStatusInOrderByRequestedAtDesc( - Arrays.asList(STATUS_PENDING, STATUS_APPROVED, STATUS_REJECTED)); - EsignPrinterDeviceLinkResponse resp = EsignPrinterDeviceLinkResponse.ok("ok"); - resp.setItems(toItems(links)); - return resp; - } - - @Transactional - public EsignPrinterDeviceLinkResponse reviewLink(EsignPrinterDeviceLinkReviewRequest request) { - if (request == null || StringUtils.isBlank(request.getLinkId())) { - return EsignPrinterDeviceLinkResponse.fail("400", "linkId required"); - } - String action = StringUtils.defaultString(request.getAction()).trim().toLowerCase(); - if (!"approve".equals(action) && !"reject".equals(action)) { - return EsignPrinterDeviceLinkResponse.fail("400", "action must be approve or reject"); - } - Optional opt = linkRepository.findById(request.getLinkId().trim()); - if (opt.isEmpty()) { - return EsignPrinterDeviceLinkResponse.fail("404", "连接申请不存在"); - } - EsignPrinterDeviceLink link = opt.get(); - if (!STATUS_PENDING.equals(link.getStatus())) { - return EsignPrinterDeviceLinkResponse.fail("409", "该申请已处理"); - } - link.setStatus("approve".equals(action) ? STATUS_APPROVED : STATUS_REJECTED); - link.setReviewedAt(Instant.now()); - link.setReviewedBy(trim(request.getReviewedBy(), 64)); - linkRepository.save(link); - log.info( - "Printer device link reviewed linkId={}, action={}, by={}", - link.getLinkId(), - action, - link.getReviewedBy()); - - Optional userOpt = esignUserRepository.findById(link.getUserId()); - EsignPrinterDeviceLinkResponse resp = - EsignPrinterDeviceLinkResponse.ok("approve".equals(action) ? "已批准连接" : "已拒绝连接"); - resp.setData(toItem(link, userOpt.orElse(null))); - return resp; - } - - @Transactional(readOnly = true) - public Set approvedAgentSourcesForUser(Long userId) { - if (userId == null) { - return Set.of(); - } - return linkRepository.findByUserIdAndStatus(userId, STATUS_APPROVED).stream() - .map(EsignPrinterDeviceLink::getAgentSource) - .collect(Collectors.toSet()); - } - - public static final String UNIDENTIFIED_AGENT = "printer-agent"; - - public static boolean isUnidentifiedAgent(String agentSource) { - return UNIDENTIFIED_AGENT.equals(normalizeAgentSource(agentSource)); - } - - @Transactional(readOnly = true) - public List approvedLinksForAgent(String agentSource) { - return linkRepository.findByAgentSourceAndStatus(normalizeAgentSource(agentSource), STATUS_APPROVED); - } - - /** 全部已批准设备连接(不限 PC 标识),用于推送候选与映射。 */ - @Transactional(readOnly = true) - public List allApprovedLinks() { - return linkRepository.findByStatusOrderByRequestedAtDesc(STATUS_APPROVED); - } - - /** - * 推送目标解析:未标识 PC(printer-agent)可使用任意已批准平板,与 App 申请时填写的连接标识无关。 - */ - @Transactional(readOnly = true) - public List approvedLinksForPushTarget(String agentSource) { - if (isUnidentifiedAgent(agentSource)) { - return linkRepository.findByStatusOrderByRequestedAtDesc(STATUS_APPROVED); - } - return approvedLinksForAgent(agentSource); - } - - @Transactional(readOnly = true) - public Set approvedUserIdsForPushTarget(String agentSource) { - Set raw = approvedLinksForPushTarget(agentSource).stream() - .map(EsignPrinterDeviceLink::getUserId) - .filter(Objects::nonNull) - .collect(Collectors.toSet()); - return userPresenceService.dedupeUserIdsByClientIp(raw); - } - - /** 任意已批准连接的平板(按 IP 去重),用于 PC→平板推送映射。 */ - @Transactional(readOnly = true) - public Set approvedUserIdsWithAnyApprovedLink() { - Set raw = linkRepository.findByStatusOrderByRequestedAtDesc(STATUS_APPROVED).stream() - .map(EsignPrinterDeviceLink::getUserId) - .filter(Objects::nonNull) - .collect(Collectors.toSet()); - return userPresenceService.dedupeUserIdsByClientIp(raw); - } - - @Transactional(readOnly = true) - public Map loadUsersById(List links) { - Set ids = links.stream().map(EsignPrinterDeviceLink::getUserId).collect(Collectors.toSet()); - Map map = new HashMap<>(); - for (Long id : ids) { - esignUserRepository.findById(id).ifPresent(u -> map.put(id, u)); - } - return map; - } - - @Transactional(readOnly = true) - public Map> approvedLinksByAgent() { - List links = linkRepository.findByStatusOrderByRequestedAtDesc(STATUS_APPROVED); - Map users = loadUsersById(links); - Map> map = new HashMap<>(); - for (EsignPrinterDeviceLink link : links) { - String key = link.getAgentSource(); - map.computeIfAbsent(key, k -> new ArrayList<>()) - .add(toItem(link, users.get(link.getUserId()))); - } - return map; - } - - @Transactional(readOnly = true) - public Map> approvedLinksByUser() { - List links = linkRepository.findByStatusOrderByRequestedAtDesc(STATUS_APPROVED); - Map users = loadUsersById(links); - Map> map = new HashMap<>(); - for (EsignPrinterDeviceLink link : links) { - map.computeIfAbsent(link.getUserId(), k -> new ArrayList<>()) - .add(toItem(link, users.get(link.getUserId()))); - } - return map; - } - - public static String normalizeAgentSource(String agentSource) { - if (StringUtils.isBlank(agentSource)) { - return "printer-agent"; - } - String t = agentSource.trim(); - return t.isEmpty() ? "printer-agent" : t; - } - - /** 从 Electron/CMS 上报标识解析主机 IP(如 frontdesk@192.168.3.9 或 192.168.3.9:39201)。 */ - public static String extractHostIp(String agentSource) { - if (StringUtils.isBlank(agentSource)) { - return null; - } - String t = stripPushPortSuffix(agentSource); - int at = t.lastIndexOf('@'); - if (at >= 0 && at < t.length() - 1) { - String ip = t.substring(at + 1).trim(); - return ip.isEmpty() ? null : ip; - } - return looksLikeHostIp(t) ? t : null; - } - - /** 连接标识相同,或解析出的 IP 相同(用于 PC 登记名与实际上报名不一致时的合并/匹配)。 */ - public static boolean sameHostIp(String left, String right) { - return agentSourcesEquivalent(left, right); - } - - /** 两个上报/登记 PC 标识是否指向同一台机器。 */ - public static boolean agentSourcesEquivalent(String left, String right) { - String a = normalizeAgentSource(left); - String b = normalizeAgentSource(right); - if (a.equals(b)) { - return true; - } - String ipA = extractHostIp(a); - String ipB = extractHostIp(b); - return StringUtils.isNotBlank(ipA) && ipA.equals(ipB); - } - - private static String stripPushPortSuffix(String agentSource) { - String t = agentSource.trim(); - t = t.replaceFirst("(?i)^https?://", ""); - int portIdx = t.indexOf(":39201"); - if (portIdx > 0) { - t = t.substring(0, portIdx); - } - return t.trim(); - } - - private static boolean looksLikeHostIp(String value) { - if (StringUtils.isBlank(value)) { - return false; - } - String t = value.trim(); - if (t.contains(":")) { - return false; - } - String[] parts = t.split("\\."); - if (parts.length != 4) { - return false; - } - for (String part : parts) { - if (part.isEmpty() || part.length() > 3) { - return false; - } - for (int i = 0; i < part.length(); i++) { - if (!Character.isDigit(part.charAt(i))) { - return false; - } - } - int n = Integer.parseInt(part); - if (n < 0 || n > 255) { - return false; - } - } - return true; - } - - private List toItems( - List links) { - Map users = loadUsersById(links); - List items = new ArrayList<>(); - for (EsignPrinterDeviceLink link : links) { - items.add(toItem(link, users.get(link.getUserId()))); - } - return items; - } - - private EsignPrinterDeviceLinkResponse.EsignPrinterDeviceLinkItem toItem( - EsignPrinterDeviceLink link, EsignUser user) { - EsignPrinterDeviceLinkResponse.EsignPrinterDeviceLinkItem item = - new EsignPrinterDeviceLinkResponse.EsignPrinterDeviceLinkItem(); - item.setLinkId(link.getLinkId()); - item.setUserId(link.getUserId()); - item.setUserName(user != null ? user.getUserName() : null); - item.setAgentSource(link.getAgentSource()); - item.setAppLabel(link.getAppLabel()); - item.setPcDisplayName(link.getPcDisplayName()); - item.setStatus(link.getStatus()); - item.setRequestedAt(link.getRequestedAt() != null ? link.getRequestedAt().toString() : null); - item.setReviewedAt(link.getReviewedAt() != null ? link.getReviewedAt().toString() : null); - item.setReviewedBy(link.getReviewedBy()); - if (user != null) { - boolean pushEnabled = StringUtils.isNotBlank(user.getDeviceToken()); - item.setPushEnabled(pushEnabled); - item.setDeliveryMode(pushEnabled ? "FCM 推送" : "仅轮询"); - item.setLastLoginAt(user.getLastLoginAt() != null ? user.getLastLoginAt().toString() : null); - item.setLastLoginIp(user.getLastLoginIp()); - } - return item; - } - - private void maybeAutoApproveRegisteredDevices( - EsignPrinterDeviceLink link, Long userId, String clientIp) { - if (link == null || !STATUS_PENDING.equals(link.getStatus())) { - return; - } - if (!pcRegistryService.isRegisteredAndEnabled(link.getAgentSource())) { - return; - } - if (!tabletRegistryService.isRegisteredAndEnabled(userId, clientIp)) { - return; - } - link.setStatus(STATUS_APPROVED); - link.setReviewedAt(Instant.now()); - link.setReviewedBy("pc-tablet-registry"); - } - - 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/printer/services/EsignPrinterEventLogService.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/services/EsignPrinterEventLogService.java deleted file mode 100644 index f85fc0e..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/services/EsignPrinterEventLogService.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.services; - -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterEventLogResponse; -import com.sjmresorts.opsapp.esign.printer.models.EsignPrinterEventLog; -import com.sjmresorts.opsapp.esign.printer.models.EsignPrinterPdfTask; -import com.sjmresorts.opsapp.esign.printer.repositories.EsignPrinterEventLogRepository; -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 -public class EsignPrinterEventLogService { - - public static final String EVENT_PDF_SUBMIT = "PDF_SUBMIT"; - public static final String EVENT_PDF_UPDATE = "PDF_UPDATE"; - public static final String EVENT_PDF_PUSH_ASSIGN = "PDF_PUSH_ASSIGN"; - public static final String EVENT_TASK_COMPLETE = "TASK_COMPLETE"; - public static final String EVENT_SIGNED_UPLOAD = "SIGNED_UPLOAD"; - public static final String EVENT_OHIP_REUPLOAD = "OHIP_REUPLOAD"; - - @Resource - private EsignPrinterEventLogRepository eventLogRepository; - - @Transactional - public void append(String eventType, EsignPrinterPdfTask task, String message, String detailJson) { - if (StringUtils.isBlank(eventType)) { - return; - } - EsignPrinterEventLog log = new EsignPrinterEventLog(); - log.setLogId(UUID.randomUUID().toString()); - log.setEventType(eventType.trim()); - log.setMessage(trim(message, 512)); - log.setDetailJson(detailJson); - log.setCreatedAt(Instant.now()); - if (task != null) { - log.setTaskId(task.getTaskId()); - log.setFileName(task.getFileName()); - log.setAgentSource(task.getAgentSource()); - log.setHotelId(task.getHotelId()); - } - eventLogRepository.save(log); - } - - @Transactional(readOnly = true) - public EsignPrinterEventLogResponse listLogs(int page, int size, String eventType, String taskId) { - int safePage = Math.max(page, 0); - int safeSize = size > 0 ? Math.min(size, 200) : 50; - PageRequest pageable = PageRequest.of(safePage, safeSize); - Page result; - if (StringUtils.isNotBlank(taskId)) { - result = eventLogRepository.findByTaskIdOrderByCreatedAtDesc(taskId.trim(), pageable); - } else if (StringUtils.isNotBlank(eventType)) { - result = eventLogRepository.findByEventTypeOrderByCreatedAtDesc(eventType.trim(), pageable); - } else { - result = eventLogRepository.findByOrderByCreatedAtDesc(pageable); - } - - List logs = new ArrayList<>(); - for (EsignPrinterEventLog row : result.getContent()) { - EsignPrinterEventLogResponse.EsignPrinterEventLogItem item = - new EsignPrinterEventLogResponse.EsignPrinterEventLogItem(); - item.setLogId(row.getLogId()); - item.setTaskId(row.getTaskId()); - item.setEventType(row.getEventType()); - item.setMessage(row.getMessage()); - item.setFileName(row.getFileName()); - item.setAgentSource(row.getAgentSource()); - item.setHotelId(row.getHotelId()); - item.setCreatedAt(row.getCreatedAt() != null ? row.getCreatedAt().toString() : null); - logs.add(item); - } - - EsignPrinterEventLogResponse resp = new EsignPrinterEventLogResponse(); - EsignPrinterEventLogResponse.EsignPrinterEventLogData data = - new EsignPrinterEventLogResponse.EsignPrinterEventLogData(); - 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 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/printer/services/EsignPrinterPcRegistryService.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/services/EsignPrinterPcRegistryService.java deleted file mode 100644 index 856dd13..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/services/EsignPrinterPcRegistryService.java +++ /dev/null @@ -1,224 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.services; - -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterPcRegistryDeleteRequest; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterPcRegistryResponse; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterPcRegistrySaveRequest; -import com.sjmresorts.opsapp.esign.printer.models.EsignPrinterPcRegistry; -import com.sjmresorts.opsapp.esign.printer.repositories.EsignPrinterPcRegistryRepository; -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.util.ArrayList; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import java.util.UUID; -import java.util.regex.Pattern; - -@Slf4j -@Service -public class EsignPrinterPcRegistryService { - - private static final Pattern IPV4 = - Pattern.compile("^(?:25[0-5]|2[0-4]\\d|1?\\d{1,2})(?:\\.(?:25[0-5]|2[0-4]\\d|1?\\d{1,2})){3}$"); - - @Resource - private EsignPrinterPcRegistryRepository registryRepository; - - @Transactional(readOnly = true) - public EsignPrinterPcRegistryResponse listAll() { - EsignPrinterPcRegistryResponse resp = EsignPrinterPcRegistryResponse.ok("ok"); - resp.setItems(toItems(registryRepository.findAllByOrderByDisplayNameAsc())); - return resp; - } - - @Transactional(readOnly = true) - public List listAllOrdered() { - return registryRepository.findAllByOrderByDisplayNameAsc(); - } - - @Transactional(readOnly = true) - public List listEnabled() { - return registryRepository.findByEnabledTrueOrderByDisplayNameAsc(); - } - - @Transactional(readOnly = true) - public List listEnabledAgentSources() { - List agents = new ArrayList<>(); - for (EsignPrinterPcRegistry row : listEnabled()) { - if (StringUtils.isNotBlank(row.getAgentSource())) { - agents.add(row.getAgentSource()); - } - } - return agents; - } - - @Transactional(readOnly = true) - public boolean isRegisteredAndEnabled(String agentSource) { - String key = EsignPrinterDeviceLinkService.normalizeAgentSource(agentSource); - if (EsignPrinterDeviceLinkService.isUnidentifiedAgent(key)) { - return false; - } - return registryRepository.findByAgentSource(key).filter(EsignPrinterPcRegistry::isEnabled).isPresent(); - } - - @Transactional(readOnly = true) - public Optional findByAgentSource(String agentSource) { - return registryRepository.findByAgentSource(EsignPrinterDeviceLinkService.normalizeAgentSource(agentSource)); - } - - public static String buildAgentSource(String displayName, String hostIp) { - String name = trim(displayName, 128); - String ip = trim(hostIp, 64); - if (StringUtils.isBlank(name)) { - throw new IllegalArgumentException("displayName required"); - } - if (StringUtils.isBlank(ip)) { - return name; - } - return name + "@" + ip; - } - - @Transactional - public EsignPrinterPcRegistryResponse save(EsignPrinterPcRegistrySaveRequest request) { - if (request == null) { - return EsignPrinterPcRegistryResponse.fail("400", "请求体不能为空"); - } - String displayName = trim(request.getDisplayName(), 128); - String hostIp = trim(request.getHostIp(), 64); - if (StringUtils.isBlank(displayName)) { - return EsignPrinterPcRegistryResponse.fail("400", "请填写 PC 名称"); - } - if (StringUtils.isBlank(hostIp)) { - return EsignPrinterPcRegistryResponse.fail("400", "请填写 PC IP"); - } - if (!isValidHostIp(hostIp)) { - return EsignPrinterPcRegistryResponse.fail("400", "IP 格式无效"); - } - - String agentSource; - try { - agentSource = EsignPrinterDeviceLinkService.normalizeAgentSource(buildAgentSource(displayName, hostIp)); - } catch (IllegalArgumentException e) { - return EsignPrinterPcRegistryResponse.fail("400", e.getMessage()); - } - - EsignPrinterPcRegistry row; - boolean creating = StringUtils.isBlank(request.getPcId()); - if (creating) { - if (registryRepository.existsByAgentSource(agentSource)) { - return EsignPrinterPcRegistryResponse.fail("409", "该连接标识已登记:" + agentSource); - } - row = new EsignPrinterPcRegistry(); - row.setPcId(UUID.randomUUID().toString()); - row.setCreatedBy(trim(request.getCreatedBy(), 64)); - } else { - Optional opt = registryRepository.findById(request.getPcId().trim()); - if (opt.isEmpty()) { - return EsignPrinterPcRegistryResponse.fail("404", "登记记录不存在"); - } - row = opt.get(); - if (registryRepository.existsByAgentSourceAndPcIdNot(agentSource, row.getPcId())) { - return EsignPrinterPcRegistryResponse.fail("409", "该连接标识已被其他 PC 使用"); - } - } - - row.setDisplayName(displayName); - row.setHostIp(hostIp); - row.setAgentSource(agentSource); - if (request.getEnabled() != null) { - row.setEnabled(request.getEnabled()); - } else if (creating) { - row.setEnabled(true); - } - row.setNotes(trim(request.getNotes(), 256)); - registryRepository.save(row); - log.info( - "Printer PC registry {} pcId={}, agentSource={}", - creating ? "created" : "updated", - row.getPcId(), - agentSource); - - EsignPrinterPcRegistryResponse resp = - EsignPrinterPcRegistryResponse.ok(creating ? "已添加 PC" : "已更新 PC"); - resp.setData(toItem(row)); - return resp; - } - - @Transactional - public EsignPrinterPcRegistryResponse delete(EsignPrinterPcRegistryDeleteRequest request) { - if (request == null || StringUtils.isBlank(request.getPcId())) { - return EsignPrinterPcRegistryResponse.fail("400", "pcId required"); - } - Optional opt = registryRepository.findById(request.getPcId().trim()); - if (opt.isEmpty()) { - return EsignPrinterPcRegistryResponse.fail("404", "登记记录不存在"); - } - registryRepository.delete(opt.get()); - log.info("Printer PC registry deleted pcId={}", request.getPcId()); - return EsignPrinterPcRegistryResponse.ok("已删除 PC 登记"); - } - - public static List mergeAgentSources(List primary, List extra) { - Set merged = new LinkedHashSet<>(); - if (primary != null) { - for (String s : primary) { - if (StringUtils.isNotBlank(s)) { - merged.add(s.trim()); - } - } - } - if (extra != null) { - for (String s : extra) { - if (StringUtils.isNotBlank(s)) { - merged.add(s.trim()); - } - } - } - return new ArrayList<>(merged); - } - - private static boolean isValidHostIp(String ip) { - return IPV4.matcher(ip.trim()).matches(); - } - - 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; - } - - private List toItems( - List rows) { - List items = new ArrayList<>(); - for (EsignPrinterPcRegistry row : rows) { - items.add(toItem(row)); - } - return items; - } - - private EsignPrinterPcRegistryResponse.EsignPrinterPcRegistryItem toItem(EsignPrinterPcRegistry row) { - EsignPrinterPcRegistryResponse.EsignPrinterPcRegistryItem item = - new EsignPrinterPcRegistryResponse.EsignPrinterPcRegistryItem(); - item.setPcId(row.getPcId()); - item.setDisplayName(row.getDisplayName()); - item.setHostIp(row.getHostIp()); - item.setAgentSource(row.getAgentSource()); - item.setEnabled(row.isEnabled()); - item.setNotes(row.getNotes()); - item.setCreatedBy(row.getCreatedBy()); - item.setCreatedAt(row.getCreatedAt() != null ? row.getCreatedAt().toString() : null); - item.setUpdatedAt(row.getUpdatedAt() != null ? row.getUpdatedAt().toString() : null); - return item; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/services/EsignPrinterPdfArchiveOcrBatchService.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/services/EsignPrinterPdfArchiveOcrBatchService.java deleted file mode 100644 index 45e7f59..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/services/EsignPrinterPdfArchiveOcrBatchService.java +++ /dev/null @@ -1,302 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.services; - -import com.sjmresorts.opsapp.esign.printer.filters.EsignPrinterPdfArchiveFilter; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterPdfArchiveOcrBatchResponse; -import com.sjmresorts.opsapp.esign.printer.support.EsignPrinterPdfArchiveOcrBatchQuery; -import com.sjmresorts.opsapp.esign.printer.support.EsignPrinterPdfArchiveOcrBatchRunner; -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.context.annotation.Lazy; -import org.springframework.stereotype.Service; - -import java.io.BufferedWriter; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.time.Instant; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; - -@Slf4j -@Service -public class EsignPrinterPdfArchiveOcrBatchService { - - private static final DateTimeFormatter TS_FMT = - DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.systemDefault()); - private static final int PAGE_SIZE = 10; - - public static final String JOB_QUEUED = "QUEUED"; - public static final String JOB_RUNNING = "RUNNING"; - public static final String JOB_COMPLETED = "COMPLETED"; - public static final String JOB_FAILED = "FAILED"; - - @Value("${ops-app.esign.pdf-archive-ocr.log-dir:C:/auto-check-in/pdf-archive-ocr-logs}") - private String logDir; - - @Value("${ops-app.esign.pdf-archive-ocr.batch-max:2000}") - private int batchMax; - - private final Map jobs = new ConcurrentHashMap<>(); - - @Resource - private EsignPrinterPdfArchiveOcrBatchQuery batchQuery; - - @Resource - private EsignPrinterPdfArchiveOcrService ocrService; - - @Resource - @Lazy - private EsignPrinterPdfArchiveOcrBatchRunner batchRunner; - - public EsignPrinterPdfArchiveOcrBatchResponse startBatch( - EsignPrinterPdfArchiveFilter filter, boolean failedOnly) { - long total = batchQuery.countTaskIds(filter, failedOnly); - if (total <= 0) { - return error("404", "没有符合條件的歸檔記錄"); - } - if (total > batchMax) { - return error( - "400", - "符合條件的記錄共 " + total + " 條,超過單次上限 " + batchMax + ",請縮小篩選範圍"); - } - - String jobId = UUID.randomUUID().toString(); - BatchJobState state = new BatchJobState(jobId, (int) total); - jobs.put(jobId, state); - evictOldJobs(); - batchRunner.runAsync(jobId, filter, failedOnly); - - EsignPrinterPdfArchiveOcrBatchResponse resp = new EsignPrinterPdfArchiveOcrBatchResponse(); - resp.setSuccess(true); - resp.setCode("200"); - resp.setMessage("批量 OCR 已在後台啟動"); - resp.setData(state.toDto()); - return resp; - } - - public EsignPrinterPdfArchiveOcrBatchResponse getBatchStatus(String jobId) { - if (StringUtils.isBlank(jobId)) { - return error("400", "jobId 無效"); - } - BatchJobState state = jobs.get(jobId.trim()); - if (state == null) { - return error("404", "任務不存在或已過期"); - } - EsignPrinterPdfArchiveOcrBatchResponse resp = new EsignPrinterPdfArchiveOcrBatchResponse(); - resp.setSuccess(true); - resp.setCode("200"); - resp.setMessage("ok"); - resp.setData(state.toDto()); - return resp; - } - - public byte[] readErrorLog(String jobId) { - BatchJobState state = jobs.get(StringUtils.trimToEmpty(jobId)); - if (state == null || StringUtils.isBlank(state.errorLogPath)) { - return null; - } - Path path = Path.of(state.errorLogPath); - if (!Files.isRegularFile(path)) { - return null; - } - try { - return Files.readAllBytes(path); - } catch (IOException e) { - log.warn("read OCR batch error log failed jobId={}", jobId, e); - return null; - } - } - - public String errorLogFileName(String jobId) { - BatchJobState state = jobs.get(StringUtils.trimToEmpty(jobId)); - if (state == null || StringUtils.isBlank(state.errorLogFileName)) { - return "ocr-batch-errors.log"; - } - return state.errorLogFileName; - } - - public void executeBatch(String jobId, EsignPrinterPdfArchiveFilter filter, boolean failedOnly) { - BatchJobState state = jobs.get(jobId); - if (state == null) { - return; - } - state.status = JOB_RUNNING; - state.startedAt = Instant.now(); - - Path logPath = null; - try { - logPath = prepareErrorLogFile(jobId); - state.errorLogPath = logPath.toString(); - state.errorLogFileName = logPath.getFileName().toString(); - - try (BufferedWriter writer = Files.newBufferedWriter(logPath, StandardCharsets.UTF_8)) { - writer.write("PDF Archive OCR Batch Job"); - writer.newLine(); - writer.write("jobId: " + jobId); - writer.newLine(); - writer.write("startedAt: " + TS_FMT.format(state.startedAt)); - writer.newLine(); - writer.write("failedOnly: " + failedOnly); - writer.newLine(); - writer.write("total: " + state.total); - writer.newLine(); - writer.write("--- failures ---"); - writer.newLine(); - - int page = 0; - while (state.processed < state.total) { - var taskIds = batchQuery.findTaskIds(filter, failedOnly, page, PAGE_SIZE); - if (taskIds.isEmpty()) { - break; - } - for (String taskId : taskIds) { - EsignPrinterPdfArchiveOcrService.OcrExtractOutcome outcome = - ocrService.extractAndPersist(taskId); - state.processed++; - if (outcome.isOk()) { - state.successCount++; - } else if (EsignPrinterPdfArchiveOcrService.OCR_SKIPPED.equals(outcome.getOcrStatus())) { - state.skippedCount++; - writeFailureLine(writer, outcome); - } else { - state.failedCount++; - writeFailureLine(writer, outcome); - } - writer.flush(); - } - page++; - } - } - - state.hasErrorLog = state.failedCount + state.skippedCount > 0; - if (!state.hasErrorLog) { - Files.deleteIfExists(logPath); - state.errorLogPath = null; - state.errorLogFileName = null; - } - state.status = JOB_COMPLETED; - } catch (Exception e) { - log.error("OCR batch job failed jobId={}", jobId, e); - state.status = JOB_FAILED; - state.message = e.getMessage(); - if (logPath != null) { - try (BufferedWriter writer = Files.newBufferedWriter( - logPath, StandardCharsets.UTF_8, java.nio.file.StandardOpenOption.APPEND)) { - writer.newLine(); - writer.write("--- batch error ---"); - writer.newLine(); - writer.write(StringUtils.defaultString(e.getMessage())); - writer.newLine(); - } catch (IOException io) { - log.warn("append batch fatal error to log failed jobId={}", jobId, io); - } - state.hasErrorLog = true; - } - } finally { - state.finishedAt = Instant.now(); - } - } - - private static void writeFailureLine( - BufferedWriter writer, EsignPrinterPdfArchiveOcrService.OcrExtractOutcome outcome) - throws IOException { - writer.write(String.format( - "[%s] taskId=%s fileName=%s status=%s reason=%s", - TS_FMT.format(Instant.now()), - StringUtils.defaultString(outcome.getTaskId()), - StringUtils.defaultString(outcome.getFileName(), "—"), - StringUtils.defaultString(outcome.getOcrStatus()), - StringUtils.defaultString(outcome.getFailureReason(), "—"))); - writer.newLine(); - } - - private Path prepareErrorLogFile(String jobId) throws IOException { - Path dir = Path.of(StringUtils.defaultIfBlank(logDir, "C:/auto-check-in/pdf-archive-ocr-logs")); - Files.createDirectories(dir); - String stamp = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss").withZone(ZoneId.systemDefault()).format(Instant.now()); - return dir.resolve("ocr-batch-" + jobId.substring(0, 8) + "-" + stamp + ".log"); - } - - private void evictOldJobs() { - if (jobs.size() <= 100) { - return; - } - jobs.entrySet().stream() - .sorted((a, b) -> { - Instant left = a.getValue().finishedAt != null - ? a.getValue().finishedAt - : a.getValue().startedAt; - Instant right = b.getValue().finishedAt != null - ? b.getValue().finishedAt - : b.getValue().startedAt; - if (left == null && right == null) { - return 0; - } - if (left == null) { - return -1; - } - if (right == null) { - return 1; - } - return left.compareTo(right); - }) - .limit(Math.max(0, jobs.size() - 80)) - .map(Map.Entry::getKey) - .forEach(jobs::remove); - } - - private static EsignPrinterPdfArchiveOcrBatchResponse error(String code, String message) { - EsignPrinterPdfArchiveOcrBatchResponse resp = new EsignPrinterPdfArchiveOcrBatchResponse(); - resp.setSuccess(false); - resp.setCode(code); - resp.setMessage(message); - return resp; - } - - static final class BatchJobState { - final String jobId; - final int total; - String status = JOB_QUEUED; - int processed; - int successCount; - int failedCount; - int skippedCount; - boolean hasErrorLog; - String message; - String errorLogPath; - String errorLogFileName; - Instant startedAt; - Instant finishedAt; - - BatchJobState(String jobId, int total) { - this.jobId = jobId; - this.total = total; - } - - EsignPrinterPdfArchiveOcrBatchResponse.EsignPrinterPdfArchiveOcrBatchData toDto() { - EsignPrinterPdfArchiveOcrBatchResponse.EsignPrinterPdfArchiveOcrBatchData data = - new EsignPrinterPdfArchiveOcrBatchResponse.EsignPrinterPdfArchiveOcrBatchData(); - data.setJobId(jobId); - data.setStatus(status); - data.setTotal(total); - data.setProcessed(processed); - data.setSuccessCount(successCount); - data.setFailedCount(failedCount); - data.setSkippedCount(skippedCount); - data.setHasErrorLog(hasErrorLog); - if (startedAt != null) { - data.setStartedAt(startedAt.toString()); - } - if (finishedAt != null) { - data.setFinishedAt(finishedAt.toString()); - } - return data; - } - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/services/EsignPrinterPdfArchiveOcrService.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/services/EsignPrinterPdfArchiveOcrService.java deleted file mode 100644 index 668f33d..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/services/EsignPrinterPdfArchiveOcrService.java +++ /dev/null @@ -1,191 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.services; - -import com.sjmresorts.opsapp.esign.printer.models.EsignPrinterPdfTask; -import com.sjmresorts.opsapp.esign.printer.repositories.EsignPrinterPdfTaskRepository; -import com.sjmresorts.opsapp.esign.printer.dtos.RegistrationCardMeta; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterPdfArchiveOcrEvent; -import com.sjmresorts.opsapp.esign.printer.support.EsignPrinterPdfFileStorage; -import jakarta.annotation.Resource; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import java.time.Instant; -import java.util.Optional; - -@Slf4j -@Service -public class EsignPrinterPdfArchiveOcrService { - - public static final String OCR_PENDING = "PENDING"; - public static final String OCR_OK = "OK"; - public static final String OCR_FAILED = "FAILED"; - public static final String OCR_SKIPPED = "SKIPPED"; - - @Resource - private EsignPrinterPdfTaskRepository printerPdfTaskRepository; - - @Resource - private RegistrationCardMetaExtractService registrationCardMetaExtractService; - - @Resource - private EsignPrinterPdfFileStorage pdfFileStorage; - - @Resource - private ApplicationEventPublisher applicationEventPublisher; - - public void scheduleAfterArchive(String taskId) { - if (StringUtils.isBlank(taskId)) { - return; - } - String id = taskId.trim(); - applicationEventPublisher.publishEvent(new EsignPrinterPdfArchiveOcrEvent(id)); - } - - /** - * 同步 OCR:UniApp 提交/签收时立即解析 PDF 并写入 task 的 ocr_* 字段(调用方负责 save)。 - */ - public void extractFromPdfBytesAndApply(EsignPrinterPdfTask task, byte[] pdfBytes) { - extractFromPdfBytesAndApply(task, pdfBytes, null); - } - - public void extractFromPdfBytesAndApply( - EsignPrinterPdfTask task, byte[] pdfBytes, String confirmationFallback) { - if (task == null) { - return; - } - try { - if (pdfBytes == null || pdfBytes.length == 0) { - task.setOcrStatus(OCR_SKIPPED); - task.setOcrExtractedAt(Instant.now()); - return; - } - RegistrationCardMeta meta = registrationCardMetaExtractService.extractFromPdfBytes(pdfBytes); - applyMeta(task, meta); - if (StringUtils.isBlank(task.getOcrConfirmationNo()) - && StringUtils.isNotBlank(confirmationFallback)) { - task.setOcrConfirmationNo(trim(confirmationFallback, 64)); - } - task.setOcrExtractedAt(Instant.now()); - task.setOcrStatus( - StringUtils.isNotBlank(task.getOcrConfirmationNo()) - || meta.hasAnyField() - ? OCR_OK - : OCR_FAILED); - log.info( - "archive OCR inline taskId={}, status={}, guest={}, conf={}", - task.getTaskId(), - task.getOcrStatus(), - task.getOcrGuestName(), - task.getOcrConfirmationNo()); - } catch (Exception e) { - log.warn("archive OCR inline failed taskId={}", task.getTaskId(), e); - task.setOcrStatus(OCR_FAILED); - task.setOcrExtractedAt(Instant.now()); - } - } - - @Transactional - public OcrExtractOutcome extractAndPersist(String taskId) { - if (StringUtils.isBlank(taskId)) { - return OcrExtractOutcome.failed(null, null, "taskId 為空"); - } - String id = taskId.trim(); - try { - Optional opt = printerPdfTaskRepository.findById(id); - if (opt.isEmpty()) { - return OcrExtractOutcome.failed(id, null, "任務不存在"); - } - EsignPrinterPdfTask task = opt.get(); - String fileName = task.getFileName(); - byte[] pdfBytes = pdfFileStorage.readArchivePdfBytes(task).orElse(null); - if (pdfBytes == null || pdfBytes.length == 0) { - task.setOcrStatus(OCR_SKIPPED); - task.setOcrExtractedAt(Instant.now()); - printerPdfTaskRepository.save(task); - return OcrExtractOutcome.skipped(id, fileName, "無 PDF 內容"); - } - RegistrationCardMeta meta = registrationCardMetaExtractService.extractFromPdfBytes(pdfBytes); - applyMeta(task, meta); - task.setOcrExtractedAt(Instant.now()); - task.setOcrStatus( - StringUtils.isNotBlank(task.getOcrConfirmationNo()) || meta.hasAnyField() - ? OCR_OK - : OCR_FAILED); - printerPdfTaskRepository.save(task); - log.info( - "archive OCR saved taskId={}, status={}, guest={}, conf={}", - id, - task.getOcrStatus(), - task.getOcrGuestName(), - task.getOcrConfirmationNo()); - if (meta.hasAnyField()) { - return OcrExtractOutcome.ok(id, fileName, task.getOcrStatus()); - } - return OcrExtractOutcome.failed(id, fileName, "未識別到有效字段"); - } catch (Exception e) { - log.warn("archive OCR failed taskId={}", id, e); - markFailed(id, e.getMessage()); - return OcrExtractOutcome.failed(id, null, StringUtils.defaultIfBlank(e.getMessage(), "OCR 異常")); - } - } - - private void markFailed(String taskId, String reason) { - printerPdfTaskRepository.findById(taskId).ifPresent(task -> { - task.setOcrStatus(OCR_FAILED); - task.setOcrExtractedAt(Instant.now()); - printerPdfTaskRepository.save(task); - }); - } - - private static void applyMeta(EsignPrinterPdfTask task, RegistrationCardMeta meta) { - task.setOcrGuestName(trim(meta.getGuestName(), 256)); - task.setOcrConfirmationNo(trim(meta.getConfirmationNo(), 64)); - task.setOcrPhone(trim(meta.getPhone(), 64)); - task.setOcrEmail(trim(meta.getEmail(), 256)); - task.setOcrRoomNo(trim(meta.getRoomNo(), 32)); - task.setOcrCheckInDate(trim(meta.getCheckInDate(), 32)); - task.setOcrCheckOutDate(trim(meta.getCheckOutDate(), 32)); - } - - private static String trim(String v, int max) { - if (StringUtils.isBlank(v)) { - return null; - } - String t = v.trim(); - return t.length() <= max ? t : t.substring(0, max); - } - - @Getter - public static final class OcrExtractOutcome { - private final String taskId; - private final String fileName; - private final String ocrStatus; - private final String failureReason; - - private OcrExtractOutcome(String taskId, String fileName, String ocrStatus, String failureReason) { - this.taskId = taskId; - this.fileName = fileName; - this.ocrStatus = ocrStatus; - this.failureReason = failureReason; - } - - public static OcrExtractOutcome ok(String taskId, String fileName, String ocrStatus) { - return new OcrExtractOutcome(taskId, fileName, ocrStatus, null); - } - - public static OcrExtractOutcome failed(String taskId, String fileName, String reason) { - return new OcrExtractOutcome(taskId, fileName, OCR_FAILED, reason); - } - - public static OcrExtractOutcome skipped(String taskId, String fileName, String reason) { - return new OcrExtractOutcome(taskId, fileName, OCR_SKIPPED, reason); - } - - public boolean isOk() { - return OCR_OK.equals(ocrStatus); - } - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/services/EsignPrinterPdfFieldPrefillService.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/services/EsignPrinterPdfFieldPrefillService.java deleted file mode 100644 index 5c5bddb..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/services/EsignPrinterPdfFieldPrefillService.java +++ /dev/null @@ -1,286 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.services; - -import com.alibaba.fastjson2.JSON; -import com.alibaba.fastjson2.JSONArray; -import com.alibaba.fastjson2.JSONObject; -import com.sjmresorts.opsapp.esign.common.dtos.SignFieldOhipMappingItem; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterPdfFieldPrefillResponse; -import com.sjmresorts.opsapp.esign.printer.models.EsignPrinterPdfTask; -import com.sjmresorts.opsapp.esign.printer.repositories.EsignPrinterPdfTaskRepository; -import com.sjmresorts.opsapp.esign.reservation.dtos.GuestProfileRef; -import com.sjmresorts.opsapp.esign.common.support.OhipProfileFieldCatalog; -import com.sjmresorts.opsapp.esign.common.support.ProfileDetailsJsonReader; -import com.sjmresorts.opsapp.esign.reservation.support.RegistrationConfirmationExtractor; -import com.sjmresorts.opsapp.esign.reservation.services.EsignSignFieldOhipMappingService; -import com.sjmresorts.opsapp.esign.reservation.services.ReservationThirdPartyClient; -import com.sjmresorts.opsapp.esign.printer.support.EsignPrinterPdfFileStorage; -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.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Optional; - -/** - * 登记卡签名字段预填:优先任务 OCR 元数据,其次 OHIP Profile(免点击 OCR)。 - */ -@Slf4j -@Service -public class EsignPrinterPdfFieldPrefillService { - - private static final Map INPUT_FORMAT_TO_OHIP_CODE = Map.of( - "email", "email.address", - "phone", "phone.number"); - - @Resource - private EsignPrinterPdfTaskRepository printerPdfTaskRepository; - - @Resource - private EsignSignFieldOhipMappingService signFieldOhipMappingService; - - @Resource - private ReservationThirdPartyClient reservationThirdPartyClient; - - @Resource - private EsignPrinterPdfFileStorage pdfFileStorage; - - @Transactional(readOnly = true) - public EsignPrinterPdfFieldPrefillResponse buildPrefill( - String taskId, String signFieldsJson, Long appUserId, String clientIp) { - if (StringUtils.isBlank(taskId)) { - return EsignPrinterPdfFieldPrefillResponse.fail("400", "taskId required"); - } - Optional opt = printerPdfTaskRepository.findById(taskId.trim()); - if (opt.isEmpty()) { - return EsignPrinterPdfFieldPrefillResponse.fail("404", "Task not found"); - } - EsignPrinterPdfTask task = opt.get(); - JSONArray fields = parseSignFields(signFieldsJson); - Map prefill = new LinkedHashMap<>(); - List sources = new ArrayList<>(); - - applyTaskOcr(task, fields, prefill); - if (!prefill.isEmpty()) { - sources.add("task_ocr"); - } - - Map profileByPath = loadOhipProfileValues(task); - if (!profileByPath.isEmpty()) { - applyOhipMappings(task.getHotelId(), fields, profileByPath, prefill); - applyInputFormatProfileDefaults(fields, profileByPath, prefill); - sources.add("ohip_profile"); - } - - EsignPrinterPdfFieldPrefillResponse.FieldPrefillData data = - new EsignPrinterPdfFieldPrefillResponse.FieldPrefillData(); - data.setFieldPrefillMap(prefill); - data.setOcrStatus(task.getOcrStatus()); - data.setSources(sources.isEmpty() ? null : sources); - log.info( - "field prefill taskId={}, fields={}, ocrStatus={}, sources={}", - taskId, - prefill.size(), - task.getOcrStatus(), - sources); - return EsignPrinterPdfFieldPrefillResponse.ok(data); - } - - private void applyTaskOcr(EsignPrinterPdfTask task, JSONArray fields, Map out) { - if (fields == null || fields.isEmpty()) { - putIfBlank(out, "email", task.getOcrEmail()); - putIfBlank(out, "phone", task.getOcrPhone()); - return; - } - for (int i = 0; i < fields.size(); i++) { - JSONObject field = fields.getJSONObject(i); - if (field == null) { - continue; - } - String key = fieldKey(field); - if (key == null) { - continue; - } - String fmt = normalizeInputFormat(field.getString("inputFormat")); - String value = taskOcrValue(task, fmt); - putIfBlank(out, key, value); - } - } - - private static String taskOcrValue(EsignPrinterPdfTask task, String fmt) { - if (task == null || fmt == null) { - return null; - } - return switch (fmt) { - case "email" -> task.getOcrEmail(); - case "phone" -> task.getOcrPhone(); - default -> null; - }; - } - - private void applyOhipMappings( - String hotelId, - JSONArray fields, - Map profileByPath, - Map out) { - if (fields == null || fields.isEmpty() || profileByPath.isEmpty()) { - return; - } - List mappings = signFieldOhipMappingService.getMappingsForHotel(hotelId); - for (SignFieldOhipMappingItem mapping : mappings) { - if (mapping == null || Boolean.FALSE.equals(mapping.getEnabled())) { - continue; - } - if ("signature".equalsIgnoreCase(StringUtils.trimToEmpty(mapping.getValueType()))) { - continue; - } - String path = OhipProfileFieldCatalog.resolvePath(mapping.getOhipFieldCode()).orElse(null); - if (path == null) { - continue; - } - JSONObject field = findField(fields, mapping.getFieldName()); - if (field == null) { - continue; - } - String key = fieldKey(field); - putIfBlank(out, key, profileByPath.get(path)); - } - } - - private static void applyInputFormatProfileDefaults( - JSONArray fields, Map profileByPath, Map out) { - if (fields == null || fields.isEmpty()) { - return; - } - for (int i = 0; i < fields.size(); i++) { - JSONObject field = fields.getJSONObject(i); - if (field == null) { - continue; - } - String fmt = normalizeInputFormat(field.getString("inputFormat")); - String ohipCode = INPUT_FORMAT_TO_OHIP_CODE.get(fmt); - if (ohipCode == null) { - continue; - } - String path = OhipProfileFieldCatalog.resolvePath(ohipCode).orElse(null); - if (path == null) { - continue; - } - putIfBlank(out, fieldKey(field), profileByPath.get(path)); - } - } - - private Map loadOhipProfileValues(EsignPrinterPdfTask task) { - String hotelId = StringUtils.trimToNull(task.getHotelId()); - if (hotelId == null) { - return Map.of(); - } - String confirmation = StringUtils.firstNonBlank( - StringUtils.trimToNull(task.getOcrConfirmationNo()), - resolveConfirmationFromPdf(task).orElse(null)); - if (confirmation == null) { - return Map.of(); - } - Optional reservationId = - reservationThirdPartyClient.lookupReservationIdByConfirmation(hotelId, confirmation); - if (reservationId.isEmpty()) { - log.debug("field prefill no reservation hotelId={}, confirmation={}", hotelId, confirmation); - return Map.of(); - } - Optional guest = - reservationThirdPartyClient.fetchGuestProfile(hotelId, reservationId.get()); - if (guest.isEmpty() || StringUtils.isBlank(guest.get().profileId())) { - return Map.of(); - } - Optional profileDetails = - reservationThirdPartyClient.fetchGuestProfileDetails(hotelId, guest.get().profileId()); - if (profileDetails.isEmpty() || profileDetails.get().isEmpty()) { - return Map.of(); - } - Map out = new LinkedHashMap<>(); - for (var fieldDef : OhipProfileFieldCatalog.all()) { - String value = ProfileDetailsJsonReader.getString(profileDetails.get(), fieldDef.getPath()); - if (StringUtils.isNotBlank(value)) { - out.put(fieldDef.getPath(), value.trim()); - } - } - return out; - } - - private Optional resolveConfirmationFromPdf(EsignPrinterPdfTask task) { - if (task == null || !pdfFileStorage.hasOriginal(task)) { - return Optional.empty(); - } - return pdfFileStorage - .readOriginalBytes(task) - .flatMap(RegistrationConfirmationExtractor::extractFromPdfBytes); - } - - private static JSONArray parseSignFields(String signFieldsJson) { - if (StringUtils.isBlank(signFieldsJson)) { - return new JSONArray(); - } - try { - JSONArray arr = JSON.parseArray(signFieldsJson.trim()); - return arr != null ? arr : new JSONArray(); - } catch (Exception e) { - return new JSONArray(); - } - } - - private static JSONObject findField(JSONArray fields, String fieldName) { - if (fields == null || StringUtils.isBlank(fieldName)) { - return null; - } - String target = fieldName.trim(); - for (int i = 0; i < fields.size(); i++) { - JSONObject obj = fields.getJSONObject(i); - if (obj == null) { - continue; - } - if (target.equalsIgnoreCase(StringUtils.trimToEmpty(obj.getString("fieldName")))) { - return obj; - } - if (target.equalsIgnoreCase(StringUtils.trimToEmpty(obj.getString("id")))) { - return obj; - } - } - return null; - } - - private static String fieldKey(JSONObject field) { - if (field == null) { - return null; - } - return StringUtils.firstNonBlank(field.getString("id"), field.getString("fieldName")); - } - - private static void putIfBlank(Map out, String key, String value) { - if (StringUtils.isAnyBlank(key, value)) { - return; - } - String existing = out.get(key); - if (StringUtils.isBlank(existing)) { - out.put(key, value.trim()); - } - } - - 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; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/services/EsignPrinterPdfOhipUploadService.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/services/EsignPrinterPdfOhipUploadService.java deleted file mode 100644 index fca5fa1..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/services/EsignPrinterPdfOhipUploadService.java +++ /dev/null @@ -1,282 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.services; - -import com.sjmresorts.opsapp.esign.printer.models.EsignPrinterPdfTask; -import com.sjmresorts.opsapp.esign.printer.repositories.EsignPrinterPdfTaskRepository; -import com.sjmresorts.opsapp.esign.printer.support.EsignPrinterOhipStatus; -import com.sjmresorts.opsapp.esign.printer.support.SignedPdfArchiveReader; -import com.sjmresorts.opsapp.esign.reservation.dtos.SignedRegistrationCardUploadResult; -import com.sjmresorts.opsapp.esign.reservation.services.ReservationThirdPartyClient; -import com.sjmresorts.opsapp.esign.reservation.services.SignedRegistrationCardOhipService; -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; - -@Slf4j -@Service -public class EsignPrinterPdfOhipUploadService { - - @Resource - private EsignPrinterPdfTaskRepository printerPdfTaskRepository; - - @Resource - private SignedPdfArchiveReader signedPdfArchiveReader; - - @Resource - private SignedRegistrationCardOhipService signedRegistrationCardOhipService; - - @Resource - private ReservationThirdPartyClient reservationThirdPartyClient; - - @Resource - private EsignPrinterEventLogService printerEventLogService; - - @Transactional - public SignedRegistrationCardUploadResult uploadFromArchive( - String taskId, - String confirmationNumberFromRequest, - String signFieldsJson, - String profileId, - String profileType) { - return uploadFromArchive( - taskId, confirmationNumberFromRequest, signFieldsJson, profileId, profileType, false, false, null); - } - - @Transactional - public SignedRegistrationCardUploadResult reuploadFromArchive(String taskId) { - EsignPrinterPdfTask task = - printerPdfTaskRepository.findById(taskId.trim()).orElse(null); - String confirmation = task != null && StringUtils.isNotBlank(task.getOcrConfirmationNo()) - ? task.getOcrConfirmationNo().trim() - : null; - return uploadFromArchive(taskId, confirmation, null, null, null, false, true, null); - } - - @Transactional - public SignedRegistrationCardUploadResult uploadFromArchive( - String taskId, - String confirmationNumberFromRequest, - String signFieldsJson, - String profileId, - String profileType, - boolean incrementRetry, - boolean manualReupload) { - return uploadFromArchive( - taskId, - confirmationNumberFromRequest, - signFieldsJson, - profileId, - profileType, - incrementRetry, - manualReupload, - null); - } - - @Transactional - public SignedRegistrationCardUploadResult uploadFromArchive( - String taskId, - String confirmationNumberFromRequest, - String signFieldsJson, - String profileId, - String profileType, - boolean incrementRetry, - boolean manualReupload, - String reservationIdOverride) { - if (StringUtils.isBlank(taskId)) { - return SignedRegistrationCardUploadResult.skipped("taskId required"); - } - EsignPrinterPdfTask task = - printerPdfTaskRepository.findById(taskId.trim()).orElse(null); - if (task == null) { - return SignedRegistrationCardUploadResult.skipped("Task not found"); - } - if (StringUtils.isNotBlank(reservationIdOverride) - && !"printer-import".equalsIgnoreCase(reservationIdOverride.trim()) - && StringUtils.isBlank(task.getOhipReservationId())) { - task.setOhipReservationId(reservationIdOverride.trim()); - } - if (incrementRetry) { - int next = task.getOhipRetryCount() != null ? task.getOhipRetryCount() + 1 : 1; - task.setOhipRetryCount(next); - if (next > EsignPrinterOhipStatus.MAX_RETRY) { - task.setOhipStatus(EsignPrinterOhipStatus.FAILED); - task.setOhipLastError("超过最大重试次数"); - printerPdfTaskRepository.save(task); - return SignedRegistrationCardUploadResult.failure(429, null, "超过最大重试次数"); - } - } - - SignedRegistrationCardUploadResult result; - try { - String signedBase64 = signedPdfArchiveReader.requireSignedBase64(task); - // 若从 meta 重合成并回写了 ref,需要落库,避免下次再丢 - if (StringUtils.isNotBlank(task.getSignedPdfBase64())) { - printerPdfTaskRepository.save(task); - } - String reservationId = StringUtils.firstNonBlank( - task.getOhipReservationId(), - StringUtils.trimToNull(reservationIdOverride)); - log.info( - "OHIP upload from archive taskId={}, signedPdfLen={}, reservationId={}", - task.getTaskId(), - StringUtils.length(signedBase64), - reservationId); - - String resolvedProfileId = profileId; - String resolvedProfileType = profileType; - if (StringUtils.isBlank(resolvedProfileId) - && StringUtils.isNotBlank(reservationId) - && !"printer-import".equalsIgnoreCase(reservationId)) { - try { - var guest = reservationThirdPartyClient.fetchGuestProfile(task.getHotelId(), reservationId); - if (guest.isPresent()) { - resolvedProfileId = guest.get().profileId(); - resolvedProfileType = StringUtils.defaultIfBlank( - resolvedProfileType, guest.get().profileType()); - log.info( - "OHIP async resolved profile taskId={}, profileId={}", - task.getTaskId(), - resolvedProfileId); - } - } catch (Exception e) { - log.warn("OHIP async profile resolve failed taskId={}", task.getTaskId(), e); - } - } - - // 已有 reservationId:直传 OPERA,跳过确认号 OCR/查找(USB / 已知预订) - if (StringUtils.isNotBlank(reservationId) - && !"printer-import".equalsIgnoreCase(reservationId)) { - result = signedRegistrationCardOhipService.uploadForReservation( - reservationId, - task.getHotelId(), - null, - signedBase64, - task.getSignedMetaJson(), - task.getFileName(), - StringUtils.trimToNull(confirmationNumberFromRequest), - StringUtils.defaultIfBlank(signFieldsJson, task.getSignFieldsJson()), - resolvedProfileId, - resolvedProfileType); - } else { - log.info( - "OHIP upload from archive via confirmation lookup taskId={}, requestConfirmation={}, ohipConfirmation={}, ocrConfirmation={}", - task.getTaskId(), - StringUtils.trimToNull(confirmationNumberFromRequest), - StringUtils.trimToNull(task.getOhipConfirmationNo()), - StringUtils.trimToNull(task.getOcrConfirmationNo())); - result = signedRegistrationCardOhipService.uploadAfterSign( - task, - signedBase64, - task.getSignedMetaJson(), - null, - StringUtils.defaultIfBlank(signFieldsJson, task.getSignFieldsJson()), - resolvedProfileId, - resolvedProfileType); - } - } catch (IllegalStateException e) { - log.warn("OHIP upload from archive failed taskId={}: {}", task.getTaskId(), e.getMessage()); - result = SignedRegistrationCardUploadResult.failure(500, null, e.getMessage()); - } catch (Exception e) { - log.warn("OHIP upload from archive error taskId={}", task.getTaskId(), e); - result = SignedRegistrationCardUploadResult.failure( - 502, null, StringUtils.defaultIfBlank(e.getMessage(), "OHIP upload error")); - } - - applyResult(task, result); - printerPdfTaskRepository.save(task); - - if (task.getSignedAt() != null || StringUtils.isNotBlank(task.getSignedPdfBase64())) { - String eventType = manualReupload - ? EsignPrinterEventLogService.EVENT_OHIP_REUPLOAD - : EsignPrinterEventLogService.EVENT_SIGNED_UPLOAD; - printerEventLogService.append( - eventType, task, buildEventMessage(result), result.getErrorMessage()); - } - return result; - } - - @Transactional - public void markPending(EsignPrinterPdfTask task) { - if (task == null) { - return; - } - task.setOhipStatus(EsignPrinterOhipStatus.PENDING); - task.setOhipLastError(null); - task.setOhipRetryCount(0); - printerPdfTaskRepository.save(task); - } - - private static void applyResult(EsignPrinterPdfTask task, SignedRegistrationCardUploadResult result) { - if (result == null) { - task.setOhipStatus(EsignPrinterOhipStatus.FAILED); - task.setOhipLastError("无上传结果"); - return; - } - if (!result.isInvoked()) { - task.setOhipStatus(EsignPrinterOhipStatus.SKIPPED); - task.setOhipLastError(trimError(result.getErrorMessage())); - task.setOhipRetryCount(EsignPrinterOhipStatus.MAX_RETRY); - return; - } - if (result.isSuccess()) { - task.setOhipStatus(EsignPrinterOhipStatus.SUCCESS); - task.setOhipLastError(null); - task.setOhipUploadedAt(Instant.now()); - if (StringUtils.isNotBlank(result.getConfirmationNumber())) { - task.setOhipConfirmationNo(result.getConfirmationNumber().trim()); - } - if (StringUtils.isNotBlank(result.getReservationId())) { - task.setOhipReservationId(result.getReservationId().trim()); - } - return; - } - task.setOhipStatus(EsignPrinterOhipStatus.FAILED); - task.setOhipLastError(trimError(result.getErrorMessage())); - if (!isRetryable(result)) { - task.setOhipRetryCount(EsignPrinterOhipStatus.MAX_RETRY); - } - } - - private static String buildEventMessage(SignedRegistrationCardUploadResult result) { - if (result == null) { - return "OPERA 上传无结果"; - } - if (!result.isInvoked()) { - return "未调用 OPERA:" + StringUtils.defaultIfBlank(result.getErrorMessage(), "配置未就绪"); - } - if (result.isSuccess()) { - return "签名 PDF 已提交至 OPERA"; - } - return "提交 OPERA 失败:" + StringUtils.defaultIfBlank(result.getErrorMessage(), "unknown"); - } - - private static String trimError(String message) { - if (StringUtils.isBlank(message)) { - return null; - } - String t = message.trim(); - return t.length() <= 512 ? t : t.substring(0, 512); - } - - /** 是否值得自动重试(网络/网关/OAuth 临时故障)。 */ - public static boolean isRetryable(SignedRegistrationCardUploadResult result) { - if (result == null || result.isSuccess() || !result.isInvoked()) { - return false; - } - int code = result.getStatusCode(); - if (code == 404 || code == 400 || code == 429) { - return false; - } - if (code >= 500 || code <= 0) { - return true; - } - String msg = StringUtils.defaultString(result.getErrorMessage()).toLowerCase(); - return msg.contains("timeout") - || msg.contains("transport") - || msg.contains("oauth") - || msg.contains("token"); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/services/EsignPrinterPdfTaskService.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/services/EsignPrinterPdfTaskService.java deleted file mode 100644 index 5916a39..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/services/EsignPrinterPdfTaskService.java +++ /dev/null @@ -1,2095 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.services; - -import com.sjmresorts.opsapp.esign.capture.dtos.EsignIosCaptureResponse; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPendingListResponse; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterDashboardResponse; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterDevicesResponse; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterQueueResponse; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterQueueWatchResponse; -import com.sjmresorts.opsapp.esign.printer.filters.EsignPrinterPdfArchiveFilter; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterPdfArchiveResponse; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterPdfSignedSubmitRequest; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterPdfUsbSignedSubmitRequest; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterPdfFieldExtractRequest; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterPdfFieldExtractResponse; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterPdfTaskDetailResponse; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterPdfTaskSubmitRequest; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterPdfTaskUpdateRequest; -import com.sjmresorts.opsapp.esign.reservation.dtos.SignedRegistrationCardUploadResult; -import com.sjmresorts.opsapp.esign.printer.dtos.RegistrationCardSignFieldParser; -import com.sjmresorts.opsapp.esign.printer.dtos.SignedRegistrationPdfComposer; -import com.sjmresorts.opsapp.esign.printer.models.EsignPrinterPcRegistry; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterPcSourceRow; -import com.sjmresorts.opsapp.esign.printer.models.EsignPrinterTabletRegistry; -import com.sjmresorts.opsapp.esign.printer.models.EsignPrinterPdfTask; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterPdfTaskListRow; -import com.sjmresorts.opsapp.esign.auth.models.EsignUser; -import com.sjmresorts.opsapp.esign.printer.support.EsignPrinterPdfFileStorage; -import com.sjmresorts.opsapp.esign.printer.support.EsignPrinterQueueNotifier; -import com.sjmresorts.opsapp.esign.printer.support.EsignPrinterPdfArchiveListQuery; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterOhipStatusRow; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterPdfArchiveListRow; -import com.sjmresorts.opsapp.esign.common.support.EsignLogMaskUtil; -import com.sjmresorts.opsapp.esign.common.support.EsignReservationParamUtil; -import com.sjmresorts.opsapp.esign.auth.services.EsignUserPresenceService; -import com.sjmresorts.opsapp.esign.survey.services.EsignSurveyPushService; -import com.sjmresorts.opsapp.esign.printer.support.EsignPrinterOhipStatus; -import com.sjmresorts.opsapp.esign.printer.support.EsignPrinterPdfSignedSubmitAsyncScheduler; -import com.sjmresorts.opsapp.esign.printer.support.SignedPdfComposeValidator; -import com.sjmresorts.opsapp.esign.printer.support.PdfSignFieldTextExtractor; -import com.sjmresorts.opsapp.esign.printer.support.RegistrationConfirmationOcrExtractor; -import com.sjmresorts.opsapp.esign.printer.repositories.EsignPrinterPdfTaskPushRepository; -import com.sjmresorts.opsapp.esign.printer.repositories.EsignPrinterPdfTaskRepository; -import com.sjmresorts.opsapp.esign.auth.repositories.EsignUserRepository; -import jakarta.annotation.Resource; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.springframework.data.domain.PageRequest; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Base64; -import java.util.Comparator; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Objects; -import java.util.Set; -import java.util.UUID; - -@Slf4j -@Service -public class EsignPrinterPdfTaskService { - - public static final String TASK_TYPE = "PRINTER_PDF"; - public static final String STATUS_PENDING = "PENDING"; - public static final String STATUS_COMPLETED = "COMPLETED"; - - private static final int TASK_WINDOW_HOURS = 24; - private static final int TABLET_ONLINE_MINUTES = 30; - private static final int LIST_PAGE_SIZE = 30; - private static final int DASHBOARD_RECENT_SIZE = 12; - private static final int ARCHIVE_PAGE_SIZE = 20; - private static final int MAX_BASE64_LEN = 20_000_000; - private static final int MAX_FILE_NAME_LEN = 512; - private static final int MAX_FILE_PATH_LEN = 1024; - - @Resource - private EsignPrinterPdfTaskRepository printerPdfTaskRepository; - - @Resource - private EsignPrinterPdfTaskPushRepository printerPdfTaskPushRepository; - - @Resource - private EsignPrinterEventLogService printerEventLogService; - - @Resource - private EsignUserRepository esignUserRepository; - - @Resource - private EsignPrinterDeviceLinkService printerDeviceLinkService; - - @Resource - private EsignPrinterPcRegistryService printerPcRegistryService; - - @Resource - private EsignPrinterTabletRegistryService printerTabletRegistryService; - - @Resource - private EsignPrinterPushService printerPushService; - - @Resource - private EsignUserPresenceService userPresenceService; - - @Resource - private com.sjmresorts.opsapp.esign.printer.websocket.EsignPrinterWebSocketHub printerWebSocketHub; - - @Resource - private EsignPrinterPdfArchiveOcrService archiveOcrService; - - @Resource - private EsignPrinterPdfArchiveListQuery archiveListQuery; - - @Resource - private EsignPrinterPdfFileStorage pdfFileStorage; - - @Resource - private EsignPrinterQueueNotifier printerQueueNotifier; - - @Resource - private EsignSurveyPushService surveyPushService; - - @Resource - private PdfSignFieldTextExtractor pdfSignFieldTextExtractor; - - @Resource - private RegistrationConfirmationOcrExtractor registrationConfirmationOcrExtractor; - - @Resource - private EsignPrinterPdfSignedSubmitAsyncScheduler signedSubmitAsyncScheduler; - - @Resource - private EsignPrinterPdfOhipUploadService ohipUploadService; - - private Instant taskWindowCutoff() { - return Instant.now().minus(TASK_WINDOW_HOURS, ChronoUnit.HOURS); - } - - @Transactional - public EsignIosCaptureResponse submitTask(EsignPrinterPdfTaskSubmitRequest request) { - EsignIosCaptureResponse resp = new EsignIosCaptureResponse(); - if (request == null) { - resp.setSuccess(false); - resp.setCode("400"); - resp.setMessage("request body required"); - return resp; - } - String externalId = trim(request.getExternalId(), 64); - String fileName = trim(request.getFileName(), MAX_FILE_NAME_LEN); - String base64 = normalizePdfBase64(request.getBase64()); - if (StringUtils.isAnyBlank(externalId, fileName, base64)) { - resp.setSuccess(false); - resp.setCode("400"); - resp.setMessage("externalId, fileName and base64 required"); - return resp; - } - if (base64.length() > MAX_BASE64_LEN) { - resp.setSuccess(false); - resp.setCode("413"); - resp.setMessage("PDF too large"); - return resp; - } - - Instant now = Instant.now(); - Optional existing = printerPdfTaskRepository.findFirstByExternalId(externalId); - EsignPrinterPdfTask task; - if (existing.isPresent()) { - task = existing.get(); - if (STATUS_COMPLETED.equals(task.getStatus())) { - // PC 重复扫描同一 PDF 时,勿把已签收任务重新变为待签 - resp.setSuccess(true); - resp.setCode("200"); - resp.setMessage("Already completed"); - EsignIosCaptureResponse.EsignIosCaptureData data = - new EsignIosCaptureResponse.EsignIosCaptureData(); - data.setTaskId(task.getTaskId()); - resp.setData(data); - return resp; - } - task.setCreatedAt(now); - } else { - task = new EsignPrinterPdfTask(); - task.setTaskId(UUID.randomUUID().toString()); - task.setExternalId(externalId); - task.setStatus(STATUS_PENDING); - task.setCreatedAt(now); - } - task.setFileName(fileName); - task.setFilePath(trim(request.getFilePath(), MAX_FILE_PATH_LEN)); - try { - task.setPdfBase64(pdfFileStorage.persistOriginalFromBase64(task.getTaskId(), base64)); - } catch (IOException e) { - log.error("Printer PDF persist failed taskId={}", task.getTaskId(), e); - resp.setSuccess(false); - resp.setCode("500"); - resp.setMessage("PDF 保存失败"); - return resp; - } - task.setFileSize(request.getFileSize()); - String hotelId = trim(request.getHotelId(), 64); - if (StringUtils.isNotBlank(hotelId)) { - task.setHotelId(EsignReservationParamUtil.normalizeParam(hotelId)); - } - task.setAgentSource(trim(request.getAgentSource(), 256)); - printerPdfTaskRepository.save(task); - archiveOcrService.scheduleAfterArchive(task.getTaskId()); - printerEventLogService.append( - EsignPrinterEventLogService.EVENT_PDF_SUBMIT, - task, - "PC 上报 PDF", - null); - printerPushService.autoAssignOnSubmit(task); - printerQueueNotifier.notifyChangedAfterCommit(); - log.info( - "Printer PDF task saved taskId={}, externalId={}, fileName={}", - task.getTaskId(), - externalId, - fileName); - - resp.setSuccess(true); - resp.setCode("200"); - resp.setMessage("Task saved"); - EsignIosCaptureResponse.EsignIosCaptureData data = new EsignIosCaptureResponse.EsignIosCaptureData(); - data.setTaskId(task.getTaskId()); - resp.setData(data); - return resp; - } - - @Transactional - public EsignIosCaptureResponse updateTask(EsignPrinterPdfTaskUpdateRequest request) { - EsignIosCaptureResponse resp = new EsignIosCaptureResponse(); - if (request == null || StringUtils.isBlank(request.getTaskId())) { - resp.setSuccess(false); - resp.setCode("400"); - resp.setMessage("taskId required"); - return resp; - } - String taskId = request.getTaskId().trim(); - Optional opt = printerPdfTaskRepository.findById(taskId); - if (opt.isEmpty()) { - resp.setSuccess(false); - resp.setCode("404"); - resp.setMessage("Task not found"); - return resp; - } - EsignPrinterPdfTask task = opt.get(); - boolean signedArchive = isSignedArchiveTask(task); - if (STATUS_COMPLETED.equals(task.getStatus()) && !signedArchive) { - resp.setSuccess(false); - resp.setCode("400"); - resp.setMessage("已签收任务不可编辑"); - return resp; - } - - boolean changed = false; - boolean signedPdfReplaced = false; - if (request.getFileName() != null) { - String fileName = trim(request.getFileName(), MAX_FILE_NAME_LEN); - if (StringUtils.isBlank(fileName)) { - resp.setSuccess(false); - resp.setCode("400"); - resp.setMessage("fileName cannot be empty"); - return resp; - } - if (!fileName.equals(task.getFileName())) { - task.setFileName(fileName); - changed = true; - } - } - - if (request.getBase64() != null) { - String base64 = normalizePdfBase64(request.getBase64()); - if (StringUtils.isBlank(base64)) { - resp.setSuccess(false); - resp.setCode("400"); - resp.setMessage("base64 required when replacing PDF"); - return resp; - } - if (base64.length() > MAX_BASE64_LEN) { - resp.setSuccess(false); - resp.setCode("413"); - resp.setMessage("PDF too large"); - return resp; - } - try { - byte[] pdfBytes = Base64.getDecoder().decode(base64); - if (signedArchive) { - task.setSignedPdfBase64(pdfFileStorage.persistSigned(task.getTaskId(), pdfBytes)); - signedPdfReplaced = true; - archiveOcrService.extractFromPdfBytesAndApply(task, pdfBytes, null); - } else { - task.setPdfBase64(pdfFileStorage.persistOriginal(task.getTaskId(), pdfBytes)); - } - } catch (IOException e) { - log.error("Printer PDF update persist failed taskId={}", task.getTaskId(), e); - resp.setSuccess(false); - resp.setCode("500"); - resp.setMessage("PDF 保存失败"); - return resp; - } catch (IllegalArgumentException e) { - resp.setSuccess(false); - resp.setCode("400"); - resp.setMessage("invalid PDF base64"); - return resp; - } - if (request.getFileSize() != null && request.getFileSize() > 0) { - task.setFileSize(request.getFileSize()); - } - changed = true; - } - - if (!changed) { - resp.setSuccess(true); - resp.setCode("200"); - resp.setMessage("No changes"); - EsignIosCaptureResponse.EsignIosCaptureData data = - new EsignIosCaptureResponse.EsignIosCaptureData(); - data.setTaskId(task.getTaskId()); - resp.setData(data); - return resp; - } - - printerPdfTaskRepository.save(task); - printerEventLogService.append( - EsignPrinterEventLogService.EVENT_PDF_UPDATE, - task, - signedArchive ? "CMS 更新签名 PDF 归档" : "CMS 更新 PDF 任务", - null); - printerQueueNotifier.notifyChangedAfterCommit(); - log.info( - "Printer PDF task updated taskId={}, fileName={}, signedArchive={}", - task.getTaskId(), - task.getFileName(), - signedArchive); - - resp.setSuccess(true); - resp.setCode("200"); - resp.setMessage("Task updated"); - EsignIosCaptureResponse.EsignIosCaptureData data = new EsignIosCaptureResponse.EsignIosCaptureData(); - data.setTaskId(task.getTaskId()); - resp.setData(data); - return resp; - } - - @Transactional - public EsignIosCaptureResponse reuploadSignedToOhip(String taskId) { - EsignIosCaptureResponse resp = new EsignIosCaptureResponse(); - if (StringUtils.isBlank(taskId)) { - resp.setSuccess(false); - resp.setCode("400"); - resp.setMessage("taskId required"); - return resp; - } - Optional opt = printerPdfTaskRepository.findById(taskId.trim()); - if (opt.isEmpty()) { - resp.setSuccess(false); - resp.setCode("404"); - resp.setMessage("Task not found"); - return resp; - } - EsignPrinterPdfTask task = opt.get(); - if (!isSignedArchiveTask(task)) { - resp.setSuccess(false); - resp.setCode("400"); - resp.setMessage("仅已签名归档可推送至 OPERA"); - return resp; - } - - SignedRegistrationCardUploadResult ohipResult = ohipUploadService.reuploadFromArchive(task.getTaskId()); - log.info( - "reuploadSignedToOhip taskId={}, success={}, statusCode={}", - task.getTaskId(), - ohipResult.isSuccess(), - ohipResult.getStatusCode()); - - EsignIosCaptureResponse.EsignIosCaptureData data = new EsignIosCaptureResponse.EsignIosCaptureData(); - data.setTaskId(task.getTaskId()); - data.setArchived(true); - return buildOhipUploadResponse(resp, data, ohipResult); - } - - private static boolean isSignedArchiveTask(EsignPrinterPdfTask task) { - return task != null - && (task.getSignedAt() != null - || StringUtils.isNotBlank(task.getSignedPdfBase64()) - || StringUtils.isNotBlank(task.getSignedMetaJson())); - } - - private static void applySignedSubmitIdentifiers( - EsignPrinterPdfTask task, String confirmationFromRequest, String reservationIdFromRequest) { - if (task == null) { - return; - } - if (StringUtils.isNotBlank(confirmationFromRequest)) { - if (StringUtils.isBlank(task.getOcrConfirmationNo())) { - task.setOcrConfirmationNo(confirmationFromRequest.trim()); - } - if (StringUtils.isBlank(task.getOhipConfirmationNo())) { - task.setOhipConfirmationNo(confirmationFromRequest.trim()); - } - } - if (StringUtils.isNotBlank(reservationIdFromRequest) - && !"printer-import".equalsIgnoreCase(reservationIdFromRequest.trim())) { - task.setOhipReservationId(reservationIdFromRequest.trim()); - } - } - - private static boolean shouldScheduleSignedArchiveOcr(EsignPrinterPdfTask task, boolean hasSignedPdf) { - if (!hasSignedPdf || task == null) { - return false; - } - if (StringUtils.isNotBlank(task.getOcrConfirmationNo()) - || StringUtils.isNotBlank(task.getOhipConfirmationNo())) { - return false; - } - if (StringUtils.isNotBlank(task.getOhipReservationId()) - && !"printer-import".equalsIgnoreCase(task.getOhipReservationId())) { - return false; - } - return !EsignPrinterPdfArchiveOcrService.OCR_OK.equals(task.getOcrStatus()); - } - - private static String resolveArchiveOcrDisplayStatus(EsignPrinterPdfArchiveListRow row) { - if (row == null) { - return null; - } - String status = row.ocrStatus(); - if (EsignPrinterPdfArchiveOcrService.OCR_PENDING.equals(status) && hasArchiveSearchIdentifiers(row)) { - return EsignPrinterPdfArchiveOcrService.OCR_SKIPPED; - } - return status; - } - - private static boolean hasArchiveSearchIdentifiers(EsignPrinterPdfArchiveListRow row) { - return row != null - && (StringUtils.isNotBlank(row.ocrConfirmationNo()) - || StringUtils.isNotBlank(row.ohipConfirmationNo()) - || (StringUtils.isNotBlank(row.ohipReservationId()) - && !"printer-import".equalsIgnoreCase(row.ohipReservationId())) - || EsignPrinterOhipStatus.SUCCESS.equals(row.ohipStatus())); - } - - private EsignIosCaptureResponse buildOhipUploadResponse( - EsignIosCaptureResponse resp, - EsignIosCaptureResponse.EsignIosCaptureData data, - SignedRegistrationCardUploadResult ohipResult) { - if (ohipResult.isInvoked()) { - data.setConfirmationNumber(ohipResult.getConfirmationNumber()); - data.setReservationId(ohipResult.getReservationId()); - data.setOhipUploaded(ohipResult.isSuccess()); - if (!ohipResult.isSuccess()) { - resp.setSuccess(false); - resp.setCode(ohipResult.getStatusCode() > 0 ? String.valueOf(ohipResult.getStatusCode()) : "502"); - resp.setMessage(StringUtils.defaultIfBlank( - ohipResult.getErrorMessage(), - "提交 OPERA 失败")); - resp.setData(data); - return resp; - } - resp.setSuccess(true); - resp.setCode("200"); - resp.setMessage("已提交至 OPERA(确认号 " - + ohipResult.getConfirmationNumber() - + ",reservation " - + ohipResult.getReservationId() + ")"); - resp.setData(data); - return resp; - } - resp.setSuccess(false); - resp.setCode("502"); - resp.setMessage(StringUtils.defaultIfBlank( - ohipResult.getErrorMessage(), - "未上传 OPERA,请检查 signed-upload / OAuth 配置")); - resp.setData(data); - return resp; - } - - public record TaskPdfContent(byte[] bytes, String fileName) {} - - @Transactional(readOnly = true) - public Optional getTaskPdfContent(String taskId) { - return getTaskPdfContent(taskId, null, null); - } - - @Transactional(readOnly = true) - public Optional getTaskPdfContent(String taskId, Long appUserId) { - return getTaskPdfContent(taskId, appUserId, null); - } - - @Transactional(readOnly = true) - public Optional getTaskPdfContent(String taskId, Long appUserId, String clientIp) { - if (StringUtils.isBlank(taskId)) { - return Optional.empty(); - } - Optional opt = printerPdfTaskRepository.findById(taskId.trim()); - if (opt.isEmpty()) { - return Optional.empty(); - } - EsignPrinterPdfTask task = opt.get(); - if (appUserId != null - && !printerPushService.isTaskVisibleToUser(task.getTaskId(), appUserId, clientIp)) { - return Optional.empty(); - } - Optional pdfBytes = pdfFileStorage.readOriginalBytes(task); - if (pdfBytes.isEmpty()) { - return Optional.empty(); - } - String fileName = StringUtils.defaultIfBlank(task.getFileName(), "printer-import.pdf"); - return Optional.of(new TaskPdfContent(pdfBytes.get(), fileName)); - } - - @Transactional(readOnly = true) - public EsignPrinterPdfFieldExtractResponse extractFieldText( - String taskId, - EsignPrinterPdfFieldExtractRequest request, - Long appUserId, - String clientIp) { - if (request == null) { - return EsignPrinterPdfFieldExtractResponse.fail("400", "request required"); - } - String inputFormat = StringUtils.defaultIfBlank(request.getInputFormat(), "text"); - String resolvedTaskId = StringUtils.firstNonBlank( - StringUtils.trimToNull(taskId), - StringUtils.trimToNull(request.getTaskId())); - boolean hasCrop = StringUtils.isNotBlank(request.getCropImageBase64()); - int cropLen = hasCrop ? request.getCropImageBase64().trim().length() : 0; - log.info( - "field OCR request taskId={} format={} hasCrop={} cropLen={} page={} rect=[{},{},{},{}] userId={}", - resolvedTaskId, - inputFormat, - hasCrop, - cropLen, - request.getPage(), - request.getLeft(), - request.getTop(), - request.getWidth(), - request.getHeight(), - appUserId); - - if (hasCrop) { - Optional fromCrop = - pdfSignFieldTextExtractor.extractFromCropImage(request.getCropImageBase64(), inputFormat); - if (fromCrop.isPresent()) { - String text = fromCrop.get().trim(); - log.info( - "field OCR crop ok taskId={} format={} textLen={} sample={}", - resolvedTaskId, - inputFormat, - text.length(), - EsignLogMaskUtil.truncate(text)); - return EsignPrinterPdfFieldExtractResponse.ok( - text, - inputFormat, - registrationConfirmationOcrExtractor.getLastOcrHint()); - } - log.info( - "field OCR crop empty taskId={} format={} hint={}", - resolvedTaskId, - inputFormat, - registrationConfirmationOcrExtractor.getLastOcrHint()); - } - - if (StringUtils.isBlank(resolvedTaskId)) { - String unavailable = registrationConfirmationOcrExtractor.getUnavailableReason(); - String reason = StringUtils.firstNonBlank( - registrationConfirmationOcrExtractor.getLastOcrHint(), - unavailable, - "crop OCR failed and taskId missing"); - String code = isOcrInfrastructureFailure(reason, unavailable) ? "503" : "404"; - log.warn("field OCR fail code={} taskId={} format={} reason={}", code, resolvedTaskId, inputFormat, reason); - return EsignPrinterPdfFieldExtractResponse.fail(code, reason); - } - if (request.getLeft() == null - || request.getTop() == null - || request.getWidth() == null - || request.getHeight() == null) { - log.warn("field OCR fail code=400 taskId={} reason=field rect required", resolvedTaskId); - return EsignPrinterPdfFieldExtractResponse.fail("400", "field rect required"); - } - Optional pdfOpt = getTaskPdfContent(resolvedTaskId, appUserId, clientIp); - if (pdfOpt.isEmpty()) { - log.warn("field OCR fail code=404 taskId={} reason=PDF not found", resolvedTaskId); - return EsignPrinterPdfFieldExtractResponse.fail("404", "PDF not found"); - } - int page = request.getPage() != null ? Math.max(0, request.getPage()) : 0; - Optional text = pdfSignFieldTextExtractor.extract( - pdfOpt.get().bytes(), - page, - request.getLeft(), - request.getTop(), - request.getWidth(), - request.getHeight(), - inputFormat); - String ocrHint = registrationConfirmationOcrExtractor.getLastOcrHint(); - if (text.isEmpty() || StringUtils.isBlank(text.get())) { - String unavailable = registrationConfirmationOcrExtractor.getUnavailableReason(); - String reason = StringUtils.firstNonBlank(ocrHint, unavailable, "no text extracted"); - String code = isOcrInfrastructureFailure(reason, unavailable) ? "503" : "404"; - log.warn( - "field OCR region fail code={} taskId={} format={} page={} reason={}", - code, - resolvedTaskId, - inputFormat, - page, - reason); - return EsignPrinterPdfFieldExtractResponse.fail(code, reason); - } - String okText = text.get().trim(); - log.info( - "field OCR region ok taskId={} format={} page={} textLen={} sample={} hint={}", - resolvedTaskId, - inputFormat, - page, - okText.length(), - EsignLogMaskUtil.truncate(okText), - ocrHint); - return EsignPrinterPdfFieldExtractResponse.ok(okText, inputFormat, ocrHint); - } - - private static boolean isOcrInfrastructureFailure(String reason, String unavailable) { - String combined = StringUtils.defaultString(reason) + " " + StringUtils.defaultString(unavailable); - return combined.contains("Tesseract") - || combined.contains("tessdata") - || combined.contains("libtesseract") - || combined.contains("OCR 已关闭") - || combined.contains("Could not initialize") - || combined.contains("native library"); - } - - @Transactional(readOnly = true) - public Optional getTaskSignedPdfContent(String taskId) { - if (StringUtils.isBlank(taskId)) { - return Optional.empty(); - } - Optional opt = printerPdfTaskRepository.findById(taskId.trim()); - if (opt.isEmpty()) { - return Optional.empty(); - } - EsignPrinterPdfTask task = opt.get(); - String base64 = pdfFileStorage.materializeSignedBase64(task); - if (StringUtils.isBlank(base64) - && StringUtils.isNotBlank(task.getSignedMetaJson()) - && pdfFileStorage.hasOriginal(task) - && StringUtils.isNotBlank(task.getSignFieldsJson())) { - try { - var signFields = RegistrationCardSignFieldParser.parse(task.getSignFieldsJson()); - base64 = SignedRegistrationPdfComposer.composeBase64( - pdfFileStorage.materializeOriginalBase64(task), - task.getSignedMetaJson(), - signFields); - log.info( - "getTaskSignedPdfContent composed from meta taskId={}, signedPdfLen={}", - taskId, - StringUtils.length(base64)); - } catch (Exception e) { - log.warn("getTaskSignedPdfContent compose from meta failed taskId={}", taskId, e); - } - } - if (StringUtils.isBlank(base64)) { - return Optional.empty(); - } - try { - byte[] bytes = Base64.getDecoder().decode(base64); - if (bytes.length == 0) { - return Optional.empty(); - } - String fileName = StringUtils.defaultIfBlank(task.getFileName(), "signed.pdf"); - if (!fileName.toLowerCase().endsWith(".pdf")) { - fileName = fileName + "-signed.pdf"; - } - return Optional.of(new TaskPdfContent(bytes, fileName)); - } catch (IllegalArgumentException e) { - log.warn("Invalid signed PDF base64 taskId={}", taskId); - return Optional.empty(); - } - } - - @Transactional(readOnly = true) - public EsignPrinterPdfTaskDetailResponse getTaskDetail(String taskId) { - return getTaskDetail(taskId, null, null); - } - - @Transactional(readOnly = true) - public EsignPrinterPdfTaskDetailResponse getTaskDetail(String taskId, Long appUserId, String clientIp) { - EsignPrinterPdfTaskDetailResponse resp = new EsignPrinterPdfTaskDetailResponse(); - if (StringUtils.isBlank(taskId)) { - resp.setSuccess(false); - resp.setCode("400"); - resp.setMessage("taskId required"); - return resp; - } - Optional opt = printerPdfTaskRepository.findById(taskId.trim()); - if (opt.isEmpty()) { - resp.setSuccess(false); - resp.setCode("404"); - resp.setMessage("Task not found"); - return resp; - } - EsignPrinterPdfTask task = opt.get(); - if (appUserId != null - && !printerPushService.isTaskVisibleToUser(task.getTaskId(), appUserId, clientIp)) { - resp.setSuccess(false); - resp.setCode("404"); - resp.setMessage("Task not found"); - return resp; - } - return toDetailResponse(task, resp); - } - - @Transactional(readOnly = true) - public EsignPrinterPdfTaskDetailResponse getTaskDetail(String taskId, Long appUserId) { - return getTaskDetail(taskId, appUserId, null); - } - - @Transactional(readOnly = true) - public List listItemsForBucket( - String bucket, String hotelId, Long userId, String clientIp) { - if (userId == null) { - return new ArrayList<>(); - } - String b = StringUtils.isBlank(bucket) ? "pending" : bucket.trim().toLowerCase(); - Instant cutoff = taskWindowCutoff(); - PageRequest page = PageRequest.of(0, LIST_PAGE_SIZE); - List rows; - if ("signed".equals(b)) { - rows = printerPdfTaskRepository.findListRowsSignedSince(cutoff, page); - } else if ("voided".equals(b)) { - rows = printerPdfTaskRepository.findListRowsUnsignedBefore(cutoff, page); - } else { - rows = printerPdfTaskRepository.findListRowsUnsignedSince(cutoff, page); - } - - boolean filterByHotel = StringUtils.isNotBlank(hotelId); - String normalizedHotelId = filterByHotel ? EsignReservationParamUtil.normalizeParam(hotelId) : null; - Set assignedTaskIds = printerPushService.assignedTaskIdsForUser(userId, clientIp); - List items = new ArrayList<>(); - for (EsignPrinterPdfTaskListRow row : rows) { - if (filterByHotel - && StringUtils.isNotBlank(row.getHotelId()) - && !normalizedHotelId.equals(row.getHotelId())) { - continue; - } - if (!assignedTaskIds.contains(row.getTaskId())) { - continue; - } - items.add(toPendingItem(row, "signed".equals(b))); - } - return items; - } - - /** @deprecated 使用 {@link #listItemsForBucket(String, String, Long, String)} */ - @Transactional(readOnly = true) - public List listItemsForBucket( - String bucket, String hotelId, Long userId) { - return listItemsForBucket(bucket, hotelId, userId, null); - } - - /** @deprecated 使用 {@link #listItemsForBucket(String, String, Long, String)} */ - @Transactional(readOnly = true) - public List listItemsForBucket(String bucket, String hotelId) { - return listItemsForBucket(bucket, hotelId, null, null); - } - - @Transactional(readOnly = true) - public EsignPrinterDashboardResponse getDashboard() { - Instant cutoff = taskWindowCutoff(); - Instant now = Instant.now(); - - long pendingCount = printerPdfTaskRepository.countByStatusAndCreatedAtGreaterThanEqual( - STATUS_PENDING, cutoff); - long voidedCount = printerPdfTaskRepository.countByStatusAndCreatedAtLessThan( - STATUS_PENDING, cutoff); - long signedCount = printerPdfTaskRepository.countByStatus(STATUS_COMPLETED); - long totalSubmitted24h = printerPdfTaskRepository.countByCreatedAtGreaterThanEqual(cutoff); - long signedInWindow = printerPdfTaskRepository.countByStatusAndCreatedAtGreaterThanEqual( - STATUS_COMPLETED, cutoff); - - Double completionRate = null; - if (totalSubmitted24h > 0) { - completionRate = Math.round((signedInWindow * 10000.0) / totalSubmitted24h) / 100.0; - } - - List recentRows = printerPdfTaskRepository.findRecentListRows( - PageRequest.of(0, DASHBOARD_RECENT_SIZE)); - Map registryByIp = buildPcRegistryByIpMap(); - List recentTasks = - new ArrayList<>(); - for (EsignPrinterPdfTaskListRow row : recentRows) { - recentTasks.add(toDashboardRecentTask(row, cutoff, registryByIp)); - } - - List pcSources = new ArrayList<>(); - for (EsignPrinterPcSourceRow row : printerPdfTaskRepository.aggregatePcSourcesSince(cutoff)) { - pcSources.add(toPcSourceStat(row, registryByIp)); - } - - List tabletDevices = new ArrayList<>(); - List activeUsers = - userPresenceService.dedupeUsersByClientIp( - esignUserRepository.findByStatusOrderByLastLoginAtDesc("ACTIVE")); - for (EsignUser user : activeUsers) { - tabletDevices.add(toTabletDeviceStat(user)); - } - tabletDevices.sort(Comparator - .comparing( - (EsignPrinterDashboardResponse.EsignPrinterTabletDeviceStat t) -> - Boolean.TRUE.equals(t.getOnline()), - Comparator.reverseOrder()) - .thenComparing(EsignPrinterDashboardResponse.EsignPrinterTabletDeviceStat::isPushEnabled) - .reversed() - .thenComparing( - EsignPrinterDashboardResponse.EsignPrinterTabletDeviceStat::getLastLoginAt, - Comparator.nullsLast(Comparator.reverseOrder()))); - - EsignPrinterDashboardResponse resp = new EsignPrinterDashboardResponse(); - EsignPrinterDashboardResponse.EsignPrinterDashboardData data = - new EsignPrinterDashboardResponse.EsignPrinterDashboardData(); - data.setGeneratedAt(now.toString()); - data.setTaskWindowHours(TASK_WINDOW_HOURS); - data.setPendingCount(pendingCount); - data.setSignedCount(signedCount); - data.setVoidedCount(voidedCount); - data.setTotalSubmitted24h(totalSubmitted24h); - data.setCompletionRatePercent(completionRate); - data.setOhipPendingCount(printerPdfTaskRepository.countBySignedAtIsNotNullAndOhipStatus( - EsignPrinterOhipStatus.PENDING)); - data.setOhipFailedCount(printerPdfTaskRepository.countBySignedAtIsNotNullAndOhipStatus( - EsignPrinterOhipStatus.FAILED)); - data.setRecentTasks(recentTasks); - data.setPcSources(pcSources); - data.setTabletDevices(tabletDevices); - resp.setSuccess(true); - resp.setCode("200"); - resp.setMessage("ok"); - resp.setData(data); - return resp; - } - - @Transactional - public EsignIosCaptureResponse completeTask(String taskId, String externalId) { - EsignIosCaptureResponse resp = new EsignIosCaptureResponse(); - Optional opt = resolveTask(taskId, externalId); - if (opt.isEmpty()) { - resp.setSuccess(false); - resp.setCode("404"); - resp.setMessage("Printer PDF task not found"); - return resp; - } - EsignPrinterPdfTask task = opt.get(); - if (STATUS_COMPLETED.equals(task.getStatus())) { - resp.setSuccess(true); - resp.setCode("200"); - resp.setMessage("Already completed"); - EsignIosCaptureResponse.EsignIosCaptureData data = new EsignIosCaptureResponse.EsignIosCaptureData(); - data.setTaskId(task.getTaskId()); - resp.setData(data); - return resp; - } - task.setStatus(STATUS_COMPLETED); - Instant now = Instant.now(); - task.setCompletedAt(now); - task.setArchivedAt(now); - byte[] pdfBytes = pdfFileStorage.readOriginalBytes(task).orElse(null); - archiveOcrService.extractFromPdfBytesAndApply(task, pdfBytes, null); - printerPdfTaskRepository.save(task); - printerEventLogService.append( - EsignPrinterEventLogService.EVENT_TASK_COMPLETE, - task, - "平板签收完成", - null); - printerQueueNotifier.notifyChangedAfterCommit(); - resp.setSuccess(true); - resp.setCode("200"); - resp.setMessage("Completed"); - EsignIosCaptureResponse.EsignIosCaptureData data = new EsignIosCaptureResponse.EsignIosCaptureData(); - data.setTaskId(task.getTaskId()); - resp.setData(data); - return resp; - } - - @Transactional - public EsignIosCaptureResponse submitUsbDirectSigned( - EsignPrinterPdfUsbSignedSubmitRequest request, Long signedByUserId, String signedClientIp) { - EsignIosCaptureResponse resp = new EsignIosCaptureResponse(); - if (request == null) { - resp.setSuccess(false); - resp.setCode("400"); - resp.setMessage("request body required"); - return resp; - } - String reservationId = trim(request.getReservationId(), 64); - String externalId = trim(request.getExternalId(), 64); - String originalBase64 = normalizePdfBase64(request.getPdfBase64()); - String signaturesJson = trim(request.getSignaturesJson(), 2_000_000); - if (StringUtils.isBlank(reservationId) || "printer-import".equalsIgnoreCase(reservationId)) { - resp.setSuccess(false); - resp.setCode("400"); - resp.setMessage("reservationId required"); - return resp; - } - if (StringUtils.isBlank(originalBase64)) { - resp.setSuccess(false); - resp.setCode("400"); - resp.setMessage("pdfBase64 required"); - return resp; - } - if (StringUtils.isBlank(signaturesJson)) { - resp.setSuccess(false); - resp.setCode("400"); - resp.setMessage("signaturesJson required"); - return resp; - } - if (originalBase64.length() > MAX_BASE64_LEN) { - resp.setSuccess(false); - resp.setCode("413"); - resp.setMessage("PDF too large"); - return resp; - } - if (StringUtils.isBlank(externalId)) { - externalId = "usb-" + reservationId + "-" + UUID.randomUUID().toString().replace("-", "").substring(0, 12); - } - - Instant now = Instant.now(); - Optional existing = printerPdfTaskRepository.findFirstByExternalId(externalId); - EsignPrinterPdfTask task; - if (existing.isPresent()) { - task = existing.get(); - if (STATUS_COMPLETED.equals(task.getStatus()) && StringUtils.isNotBlank(task.getSignedPdfBase64())) { - EsignIosCaptureResponse.EsignIosCaptureData data = - new EsignIosCaptureResponse.EsignIosCaptureData(); - data.setTaskId(task.getTaskId()); - data.setArchived(true); - applyNextSurveyAfterPdfSign(data, task.getHotelId()); - resp.setSuccess(true); - resp.setCode("200"); - resp.setMessage("Already archived"); - resp.setData(data); - return resp; - } - } else { - task = new EsignPrinterPdfTask(); - task.setTaskId(UUID.randomUUID().toString()); - task.setExternalId(externalId); - task.setCreatedAt(now); - } - - String fileName = trim(request.getFileName(), MAX_FILE_NAME_LEN); - if (StringUtils.isBlank(fileName)) { - fileName = "registration-" + reservationId + ".pdf"; - } - task.setFileName(fileName); - task.setStatus(STATUS_PENDING); - String hotelId = trim(request.getHotelId(), 64); - if (StringUtils.isNotBlank(hotelId)) { - task.setHotelId(EsignReservationParamUtil.normalizeParam(hotelId)); - } - task.setAgentSource("USB"); - task.setOhipReservationId(reservationId); - try { - task.setPdfBase64(pdfFileStorage.persistOriginalFromBase64(task.getTaskId(), originalBase64)); - } catch (IOException e) { - log.error("USB signed persist original failed taskId={}", task.getTaskId(), e); - resp.setSuccess(false); - resp.setCode("500"); - resp.setMessage("PDF 保存失败"); - return resp; - } - - String signedBase64; - try { - var signFields = RegistrationCardSignFieldParser.parse(request.getSignFieldsJson()); - signedBase64 = SignedRegistrationPdfComposer.composeBase64( - originalBase64, signaturesJson, signFields); - SignedPdfComposeValidator.assertRequiredSignaturesPresent( - signaturesJson, request.getSignFieldsJson()); - } catch (IllegalStateException e) { - resp.setSuccess(false); - resp.setCode("400"); - resp.setMessage(e.getMessage()); - return resp; - } catch (Exception e) { - log.warn("USB compose signed PDF failed externalId={}", externalId, e); - resp.setSuccess(false); - resp.setCode("500"); - resp.setMessage("签名 PDF 合成失败,无法归档"); - return resp; - } - - try { - byte[] signedPdfBytes = Base64.getDecoder().decode(signedBase64); - task.setSignedPdfBase64(pdfFileStorage.persistSigned(task.getTaskId(), signedPdfBytes)); - } catch (Exception e) { - log.error("USB persist signed PDF failed taskId={}", task.getTaskId(), e); - resp.setSuccess(false); - resp.setCode("500"); - resp.setMessage("签名 PDF 保存失败"); - return resp; - } - - task.setSignedMetaJson(signaturesJson); - if (StringUtils.isNotBlank(request.getSignFieldsJson())) { - task.setSignFieldsJson(trim(request.getSignFieldsJson(), 2_000_000)); - } - task.setSignedAt(now); - task.setStatus(STATUS_COMPLETED); - task.setCompletedAt(now); - task.setArchivedAt(now); - if (signedByUserId != null) { - task.setSignedByUserId(signedByUserId); - } - if (StringUtils.isNotBlank(signedClientIp)) { - task.setSignedClientIp(trim(signedClientIp, 64)); - } - if (StringUtils.isNotBlank(request.getDeviceId())) { - task.setSignedDeviceId(trim(request.getDeviceId(), 64)); - } - if (StringUtils.isNotBlank(request.getDeviceName())) { - task.setSignedDeviceName(trim(request.getDeviceName(), 128)); - } - task.setOcrStatus(EsignPrinterPdfArchiveOcrService.OCR_SKIPPED); - task.setOcrExtractedAt(now); - task.setOhipStatus(EsignPrinterOhipStatus.PENDING); - task.setOhipLastError(null); - task.setOhipRetryCount(0); - printerPdfTaskRepository.save(task); - // 不调用 autoAssignOnSubmit:平板已持有 PDF,避免再推一条 Java PRINTER_PDF - printerEventLogService.append( - EsignPrinterEventLogService.EVENT_SIGNED_UPLOAD, - task, - "USB 直传签名已归档", - request.getSignatureCount() != null - ? "{\"signatureCount\":" + request.getSignatureCount() + ",\"source\":\"USB\"}" - : "{\"source\":\"USB\"}"); - printerQueueNotifier.notifyChangedAfterCommit(); - - // Profile 与 OPERA 上传全部异步,避免阻塞平板 - String profileId = trim(request.getProfileId(), 64); - String profileType = trim(request.getProfileType(), 64); - signedSubmitAsyncScheduler.scheduleAfterSignedSubmit( - task.getTaskId(), - null, - request.getSignFieldsJson(), - profileId, - profileType, - reservationId, - task.getHotelId(), - false); - - EsignIosCaptureResponse.EsignIosCaptureData data = new EsignIosCaptureResponse.EsignIosCaptureData(); - data.setTaskId(task.getTaskId()); - data.setArchived(true); - applyNextSurveyAfterPdfSign(data, task.getHotelId()); - resp.setSuccess(true); - resp.setCode("200"); - resp.setMessage("签名已归档,OPERA 后台上传中"); - resp.setData(data); - log.info( - "USB signed archived taskId={}, reservationId={}, hotelId={}", - task.getTaskId(), - reservationId, - task.getHotelId()); - return resp; - } - - @Transactional - public EsignIosCaptureResponse submitSignedPdf( - EsignPrinterPdfSignedSubmitRequest request, Long signedByUserId, String signedClientIp) { - log.info( - "submitSignedPdf taskId={}, externalId={}, signatureCount={}, hasBase64={}, hasSignaturesJson={}, hasSignFieldsJson={}", - request != null ? request.getTaskId() : null, - request != null ? request.getExternalId() : null, - request != null ? request.getSignatureCount() : null, - request != null && StringUtils.isNotBlank(request.getBase64()), - request != null && StringUtils.isNotBlank(request.getSignaturesJson()), - request != null && StringUtils.isNotBlank(request.getSignFieldsJson())); - EsignIosCaptureResponse resp = new EsignIosCaptureResponse(); - if (request == null) { - resp.setSuccess(false); - resp.setCode("400"); - resp.setMessage("request body required"); - return resp; - } - Optional opt = resolveTask(request.getTaskId(), request.getExternalId()); - if (opt.isEmpty()) { - resp.setSuccess(false); - resp.setCode("404"); - resp.setMessage("Printer PDF task not found"); - return resp; - } - String signedBase64 = normalizePdfBase64(request.getBase64()); - String signaturesJson = trim(request.getSignaturesJson(), 2_000_000); - if (StringUtils.isAllBlank(signedBase64, signaturesJson)) { - resp.setSuccess(false); - resp.setCode("400"); - resp.setMessage("base64 or signaturesJson required"); - return resp; - } - if (signedBase64 != null && signedBase64.length() > MAX_BASE64_LEN) { - resp.setSuccess(false); - resp.setCode("413"); - resp.setMessage("Signed PDF too large"); - return resp; - } - - EsignPrinterPdfTask task = opt.get(); - log.info( - "submitSignedPdf task resolved: taskId={}, hotelId={}, fileName={}, hasOriginalPdf={}", - task.getTaskId(), - task.getHotelId(), - task.getFileName(), - pdfFileStorage.hasOriginal(task)); - Instant now = Instant.now(); - if (StringUtils.isBlank(signedBase64) && StringUtils.isNotBlank(signaturesJson) - && pdfFileStorage.hasOriginal(task)) { - try { - var signFields = RegistrationCardSignFieldParser.parse(request.getSignFieldsJson()); - signedBase64 = com.sjmresorts.opsapp.esign.printer.dtos.SignedRegistrationPdfComposer.composeBase64( - pdfFileStorage.materializeOriginalBase64(task), - signaturesJson, - signFields); - log.info( - "submitSignedPdf composed signed PDF taskId={}, signedPdfLen={}, signFields={}", - task.getTaskId(), - StringUtils.length(signedBase64), - signFields.size()); - } catch (Exception e) { - log.warn("Compose signed PDF before archive failed taskId={}", task.getTaskId(), e); - } - } - if (StringUtils.isBlank(signedBase64) && StringUtils.isNotBlank(signaturesJson)) { - log.warn( - "submitSignedPdf no signed PDF after compose taskId={}, hasOriginalPdf={}", - task.getTaskId(), - pdfFileStorage.hasOriginal(task)); - resp.setSuccess(false); - resp.setCode("500"); - resp.setMessage("签名 PDF 合成失败,无法归档"); - return resp; - } - try { - SignedPdfComposeValidator.assertRequiredSignaturesPresent(signaturesJson, request.getSignFieldsJson()); - } catch (IllegalStateException e) { - resp.setSuccess(false); - resp.setCode("400"); - resp.setMessage(e.getMessage()); - return resp; - } - byte[] signedPdfBytes = null; - if (StringUtils.isNotBlank(signedBase64)) { - try { - signedPdfBytes = Base64.getDecoder().decode(signedBase64); - task.setSignedPdfBase64( - pdfFileStorage.persistSigned(task.getTaskId(), signedPdfBytes)); - } catch (IOException e) { - log.error("submitSignedPdf persist signed PDF failed taskId={}", task.getTaskId(), e); - resp.setSuccess(false); - resp.setCode("500"); - resp.setMessage("签名 PDF 保存失败"); - return resp; - } catch (IllegalArgumentException e) { - resp.setSuccess(false); - resp.setCode("400"); - resp.setMessage("invalid signed PDF base64"); - return resp; - } - } - // OPERA 只读归档 signed.pdf:未落盘则不得标完成 / 触发 OHIP - if (signedPdfBytes == null - || signedPdfBytes.length == 0 - || StringUtils.isBlank(task.getSignedPdfBase64()) - || !pdfFileStorage.hasSigned(task)) { - log.error( - "submitSignedPdf missing signed archive taskId={}, bytes={}, ref={}, hasSignedFile={}", - task.getTaskId(), - signedPdfBytes != null ? signedPdfBytes.length : 0, - task.getSignedPdfBase64(), - pdfFileStorage.hasSignedFile(task.getTaskId())); - resp.setSuccess(false); - resp.setCode("500"); - resp.setMessage("签名 PDF 归档失败,无法上传 OPERA"); - return resp; - } - if (StringUtils.isNotBlank(signaturesJson)) { - task.setSignedMetaJson(signaturesJson); - } - if (StringUtils.isNotBlank(request.getSignFieldsJson())) { - task.setSignFieldsJson(trim(request.getSignFieldsJson(), 2_000_000)); - } - task.setOhipStatus(EsignPrinterOhipStatus.PENDING); - task.setOhipLastError(null); - task.setOhipRetryCount(0); - if (signedByUserId != null) { - task.setSignedByUserId(signedByUserId); - } - String normalizedClientIp = EsignUserPresenceService.normalizeClientIp(signedClientIp); - if (StringUtils.isNotBlank(normalizedClientIp)) { - task.setSignedClientIp(normalizedClientIp); - } - String deviceId = trim(request.getDeviceId(), 64); - String deviceName = trim(request.getDeviceName(), 128); - if (StringUtils.isNotBlank(deviceId)) { - task.setSignedDeviceId(deviceId); - } - if (StringUtils.isNotBlank(deviceName)) { - task.setSignedDeviceName(deviceName); - } - task.setSignedAt(now); - task.setStatus(STATUS_COMPLETED); - if (task.getCompletedAt() == null) { - task.setCompletedAt(now); - } - if (task.getArchivedAt() == null) { - task.setArchivedAt(now); - } - String confirmationFromRequest = null; - String ignoredConfirmationFromRequest = trim(request.getConfirmationNumber(), 64); - String reservationIdFromRequest = trim(request.getReservationId(), 64); - if (StringUtils.isNotBlank(ignoredConfirmationFromRequest)) { - log.info( - "submitSignedPdf ignore request confirmation for printer flow taskId={}, requestConfirmation={}", - task.getTaskId(), - ignoredConfirmationFromRequest); - } - applySignedSubmitIdentifiers(task, confirmationFromRequest, reservationIdFromRequest); - boolean scheduleSignedArchiveOcr = - shouldScheduleSignedArchiveOcr(task, signedPdfBytes != null && signedPdfBytes.length > 0); - if (scheduleSignedArchiveOcr) { - task.setOcrStatus(EsignPrinterPdfArchiveOcrService.OCR_PENDING); - } else if (signedPdfBytes != null && signedPdfBytes.length > 0) { - if (StringUtils.isBlank(task.getOcrStatus()) - || EsignPrinterPdfArchiveOcrService.OCR_PENDING.equals(task.getOcrStatus())) { - task.setOcrStatus(EsignPrinterPdfArchiveOcrService.OCR_SKIPPED); - task.setOcrExtractedAt(now); - } - } - printerPdfTaskRepository.save(task); - printerEventLogService.append( - EsignPrinterEventLogService.EVENT_SIGNED_UPLOAD, - task, - "签名结果已归档", - request.getSignatureCount() != null ? "{\"signatureCount\":" + request.getSignatureCount() + "}" : null); - log.info( - "submitSignedPdf archived taskId={}, ocrStatus={}, guest={}, conf={}, reservation={}, archivedAt={}", - task.getTaskId(), - task.getOcrStatus(), - task.getOcrGuestName(), - StringUtils.firstNonBlank(task.getOhipConfirmationNo(), task.getOcrConfirmationNo()), - task.getOhipReservationId(), - task.getArchivedAt()); - printerQueueNotifier.notifyChangedAfterCommit(); - - // Profile 解析与 OPERA 上传均异步,避免阻塞平板「归档」等待 - String profileId = trim(request.getProfileId(), 64); - String profileType = trim(request.getProfileType(), 64); - signedSubmitAsyncScheduler.scheduleAfterSignedSubmit( - task.getTaskId(), - confirmationFromRequest, - request.getSignFieldsJson(), - profileId, - profileType, - reservationIdFromRequest, - StringUtils.defaultIfBlank(trim(request.getHotelId(), 64), task.getHotelId()), - scheduleSignedArchiveOcr); - - EsignIosCaptureResponse.EsignIosCaptureData data = new EsignIosCaptureResponse.EsignIosCaptureData(); - data.setTaskId(task.getTaskId()); - data.setArchived(true); - applyNextSurveyAfterPdfSign(data, task.getHotelId()); - resp.setSuccess(true); - resp.setCode("200"); - resp.setMessage("签名已归档"); - resp.setData(data); - return resp; - } - - @Transactional(readOnly = true) - public EsignPrinterPdfArchiveResponse listArchive(int page, int size, EsignPrinterPdfArchiveFilter filter) { - int safePage = Math.max(page, 0); - int safeSize = size > 0 ? Math.min(size, 100) : ARCHIVE_PAGE_SIZE; - PageRequest pageable = PageRequest.of(safePage, safeSize); - EsignPrinterPdfArchiveFilter safeFilter = - filter != null ? filter : EsignPrinterPdfArchiveFilter.builder().build(); - - long total = archiveListQuery.count(safeFilter); - List rows = archiveListQuery.findPage(safeFilter, pageable); - Map userNameById = resolveSignedByUserNames(rows); - - List items = new ArrayList<>(); - for (EsignPrinterPdfArchiveListRow row : rows) { - items.add(toArchiveItem(row, userNameById)); - } - - EsignPrinterPdfArchiveResponse resp = new EsignPrinterPdfArchiveResponse(); - EsignPrinterPdfArchiveResponse.EsignPrinterPdfArchiveData data = - new EsignPrinterPdfArchiveResponse.EsignPrinterPdfArchiveData(); - data.setTotal(total); - data.setPage(safePage); - data.setSize(safeSize); - data.setItems(items); - resp.setSuccess(true); - resp.setCode("200"); - resp.setMessage("ok"); - resp.setData(data); - return resp; - } - - private EsignPrinterPdfArchiveResponse.EsignPrinterPdfArchiveItem toArchiveItem( - EsignPrinterPdfArchiveListRow row, Map userNameById) { - EsignPrinterPdfArchiveResponse.EsignPrinterPdfArchiveItem item = - new EsignPrinterPdfArchiveResponse.EsignPrinterPdfArchiveItem(); - item.setTaskId(row.taskId()); - item.setExternalId(row.externalId()); - item.setFileName(row.fileName()); - item.setFileSize(row.fileSize()); - item.setHotelId(row.hotelId()); - item.setStatus(row.status()); - item.setCreatedAt(row.createdAt() != null ? row.createdAt().toString() : null); - item.setCompletedAt(row.completedAt() != null ? row.completedAt().toString() : null); - item.setSignedAt(row.signedAt() != null ? row.signedAt().toString() : null); - item.setArchivedAt(row.archivedAt() != null ? row.archivedAt().toString() : null); - item.setAgentSource(row.agentSource()); - item.setSignedByUserId(row.signedByUserId()); - item.setSignedClientIp(row.signedClientIp()); - item.setSignedDeviceId(row.signedDeviceId()); - item.setSignedDeviceName(row.signedDeviceName()); - String signedByUserName = - row.signedByUserId() != null ? userNameById.get(row.signedByUserId()) : null; - item.setSignedByUserName(signedByUserName); - item.setSubmittedDeviceLabel(buildSubmittedDeviceLabel(row, signedByUserName)); - item.setHasOriginalPdf(pdfFileStorage.hasOriginalFile(row.taskId())); - boolean signed = row.signedAt() != null; - item.setHasSignedPdf(signed || pdfFileStorage.hasSignedFile(row.taskId())); - item.setHasSignedMeta(signed); - item.setOcrGuestName(row.ocrGuestName()); - item.setOcrConfirmationNo( - StringUtils.firstNonBlank(row.ohipConfirmationNo(), row.ocrConfirmationNo())); - item.setOcrPhone(row.ocrPhone()); - item.setOcrEmail(row.ocrEmail()); - item.setOcrRoomNo(row.ocrRoomNo()); - item.setOcrCheckInDate(row.ocrCheckInDate()); - item.setOcrCheckOutDate(row.ocrCheckOutDate()); - item.setOcrStatus(resolveArchiveOcrDisplayStatus(row)); - item.setOcrExtractedAt(row.ocrExtractedAt() != null ? row.ocrExtractedAt().toString() : null); - item.setOhipStatus(row.ohipStatus()); - item.setOhipLastError(row.ohipLastError()); - item.setOhipUploadedAt(row.ohipUploadedAt() != null ? row.ohipUploadedAt().toString() : null); - item.setOhipConfirmationNo(row.ohipConfirmationNo()); - item.setOhipReservationId(row.ohipReservationId()); - item.setOhipRetryCount(row.ohipRetryCount()); - return item; - } - - private Map resolveSignedByUserNames(List rows) { - if (rows == null || rows.isEmpty()) { - return Map.of(); - } - Set userIds = new LinkedHashSet<>(); - for (EsignPrinterPdfArchiveListRow row : rows) { - if (row.signedByUserId() != null) { - userIds.add(row.signedByUserId()); - } - } - if (userIds.isEmpty()) { - return Map.of(); - } - Map map = new HashMap<>(); - for (Long userId : userIds) { - esignUserRepository.findById(userId).ifPresent(user -> { - if (StringUtils.isNotBlank(user.getUserName())) { - map.put(userId, user.getUserName().trim()); - } - }); - } - return map; - } - - private static String buildSubmittedDeviceLabel( - EsignPrinterPdfArchiveListRow row, String signedByUserName) { - List parts = new ArrayList<>(); - if (StringUtils.isNotBlank(signedByUserName)) { - parts.add(signedByUserName.trim()); - } - if (StringUtils.isNotBlank(row.signedDeviceName())) { - parts.add(row.signedDeviceName().trim()); - } else if (StringUtils.isNotBlank(row.signedDeviceId())) { - parts.add(row.signedDeviceId().trim()); - } - String clientIp = EsignUserPresenceService.normalizeClientIp(row.signedClientIp()); - if (StringUtils.isNotBlank(clientIp)) { - parts.add(clientIp); - } - return String.join(" · ", parts); - } - - /** 兼容旧调用:无筛选条件 */ - @Transactional(readOnly = true) - public EsignPrinterPdfArchiveResponse listArchive(int page, int size) { - return listArchive(page, size, EsignPrinterPdfArchiveFilter.builder().build()); - } - - @Transactional - public EsignPrinterQueueResponse getQueue(String agentSource, String displayStatus, int page, int size) { - Instant cutoff = taskWindowCutoff(); - int safePage = Math.max(page, 0); - int safeSize = size > 0 ? Math.min(size, 100) : 10; - String normalizedAgent = trim(agentSource, 256); - if (StringUtils.isBlank(normalizedAgent)) { - normalizedAgent = null; - } - - QueueSqlFilter sqlFilter = resolveQueueSqlFilter(displayStatus); - String agentHostIp = normalizedAgent != null - ? EsignPrinterDeviceLinkService.extractHostIp(normalizedAgent) - : null; - Map registryByIp = buildPcRegistryByIpMap(); - PageRequest pageable = PageRequest.of(safePage, safeSize); - List rows = printerPdfTaskRepository.findQueueListRowsFiltered( - cutoff, - normalizedAgent, - agentHostIp, - sqlFilter.taskStatus(), - sqlFilter.signFilter(), - sqlFilter.pushFilter(), - pageable); - long total = printerPdfTaskRepository.countQueueRowsFiltered( - cutoff, - normalizedAgent, - agentHostIp, - sqlFilter.taskStatus(), - sqlFilter.signFilter(), - sqlFilter.pushFilter()); - - List items = new ArrayList<>(); - List taskIds = rows.stream() - .map(EsignPrinterPdfTaskListRow::getTaskId) - .filter(StringUtils::isNotBlank) - .toList(); - Map ohipByTaskId = loadOhipStatusByTaskId(taskIds); - for (EsignPrinterPdfTaskListRow row : rows) { - if (printerPushService.isAutoMode() - && STATUS_PENDING.equals(row.getStatus()) - && row.getSignedAt() == null - && !printerPushService.isTaskPushConfigured(row.getTaskId())) { - printerPdfTaskRepository.findById(row.getTaskId()).ifPresent(printerPushService::autoAssignOnSubmit); - } - String ds = resolveDisplayStatus(row, cutoff); - if (!matchesDisplayStatusFilter(displayStatus, ds)) { - continue; - } - items.add(toQueueItem(row, cutoff, registryByIp, ohipByTaskId.get(row.getTaskId()))); - } - - List agentSourceOptions = listDistinctAgentSources24h(registryByIp); - - EsignPrinterQueueResponse resp = EsignPrinterQueueResponse.ok("ok"); - EsignPrinterQueueResponse.EsignPrinterQueueData data = - new EsignPrinterQueueResponse.EsignPrinterQueueData(); - data.setGeneratedAt(Instant.now().toString()); - data.setTaskWindowHours(TASK_WINDOW_HOURS); - data.setTotal(total); - data.setPage(safePage); - data.setSize(safeSize); - data.setAgentSourceOptions(agentSourceOptions); - data.setPushMode(printerPushService.getPushMode()); - data.setItems(items); - resp.setData(data); - return resp; - } - - @Transactional(readOnly = true) - public EsignPrinterQueueWatchResponse getQueueWatch() { - Instant cutoff = taskWindowCutoff(); - long total24h = printerPdfTaskRepository.countByCreatedAtGreaterThanEqual(cutoff); - long pending = printerPdfTaskRepository.countByStatusAndCreatedAtGreaterThanEqual(STATUS_PENDING, cutoff); - long signed = printerPdfTaskRepository.countByStatusAndCreatedAtGreaterThanEqual(STATUS_COMPLETED, cutoff); - Instant latestCreated = printerPdfTaskRepository.findMaxCreatedAtSince(cutoff); - Instant latestCompleted = printerPdfTaskRepository.findMaxCompletedAtSince(cutoff); - long pushedCount = printerPdfTaskPushRepository.countDistinctPushedTasksSince(cutoff); - Instant latestPushAssigned = printerPdfTaskPushRepository.findMaxAssignedAtForTasksSince(cutoff); - - String revision = total24h - + "|" - + pending - + "|" - + signed - + "|" - + pushedCount - + "|" - + (latestCreated != null ? latestCreated.toEpochMilli() : 0) - + "|" - + (latestCompleted != null ? latestCompleted.toEpochMilli() : 0) - + "|" - + (latestPushAssigned != null ? latestPushAssigned.toEpochMilli() : 0); - - EsignPrinterQueueWatchResponse resp = EsignPrinterQueueWatchResponse.ok("ok"); - EsignPrinterQueueWatchResponse.EsignPrinterQueueWatchData data = - new EsignPrinterQueueWatchResponse.EsignPrinterQueueWatchData(); - data.setRevision(revision); - data.setTotal24h(total24h); - data.setPendingCount(pending); - data.setSignedCount(signed); - data.setLatestCreatedAt(latestCreated != null ? latestCreated.toString() : null); - data.setLatestCompletedAt(latestCompleted != null ? latestCompleted.toString() : null); - data.setTaskWindowHours(TASK_WINDOW_HOURS); - resp.setData(data); - return resp; - } - - @Transactional(readOnly = true) - public List listDistinctAgentSources24h() { - return listDistinctAgentSources24h(buildPcRegistryByIpMap()); - } - - private List listDistinctAgentSources24h(Map registryByIp) { - Instant cutoff = taskWindowCutoff(); - LinkedHashSet canonical = new LinkedHashSet<>(); - for (EsignPrinterPcSourceRow pc : printerPdfTaskRepository.aggregatePcSourcesSince(cutoff)) { - if (StringUtils.isNotBlank(pc.getAgentSource())) { - String key = normalizeAgentSource(pc.getAgentSource()); - canonical.add(resolveCanonicalPcAgentKey(key, registryByIp)); - } - } - for (String regSource : printerPcRegistryService.listEnabledAgentSources()) { - if (StringUtils.isNotBlank(regSource)) { - canonical.add(normalizeAgentSource(regSource)); - } - } - return new ArrayList<>(canonical); - } - - private Map buildPcRegistryByIpMap() { - Map registryByIp = new LinkedHashMap<>(); - for (EsignPrinterPcRegistry reg : printerPcRegistryService.listAllOrdered()) { - if (StringUtils.isNotBlank(reg.getHostIp())) { - registryByIp.putIfAbsent(reg.getHostIp().trim(), reg); - } - } - return registryByIp; - } - - @Transactional(readOnly = true) - public EsignPrinterDevicesResponse getDevices() { - Instant cutoff = taskWindowCutoff(); - List recentRows = - printerPdfTaskRepository.findQueueListRows(cutoff, null, PageRequest.of(0, 200)); - Map lastFileByAgent = new HashMap<>(); - for (EsignPrinterPdfTaskListRow row : recentRows) { - String key = normalizeAgentSource(row.getAgentSource()); - lastFileByAgent.putIfAbsent(key, row.getFileName()); - } - - List pcDevices = new ArrayList<>(); - Map> - linksByAgent = printerDeviceLinkService.approvedLinksByAgent(); - Map registryByIp = new LinkedHashMap<>(); - for (EsignPrinterPcRegistry reg : printerPcRegistryService.listAllOrdered()) { - if (StringUtils.isNotBlank(reg.getHostIp())) { - registryByIp.putIfAbsent(reg.getHostIp().trim(), reg); - } - } - Map pcByAgent = new LinkedHashMap<>(); - for (EsignPrinterPcSourceRow row : printerPdfTaskRepository.aggregatePcSourcesSince(cutoff)) { - String reportedKey = normalizeAgentSource(row.getAgentSource()); - String canonicalKey = resolveCanonicalPcAgentKey(reportedKey, registryByIp); - EsignPrinterDevicesResponse.EsignPrinterPcDevice pc = pcByAgent.get(canonicalKey); - if (pc == null) { - pc = buildPcDeviceRow(row, canonicalKey, linksByAgent, lastFileByAgent); - pcByAgent.put(canonicalKey, pc); - } else { - mergeReportingStatsIntoPc(pc, row, lastFileByAgent, reportedKey); - } - } - for (EsignPrinterPcRegistry reg : printerPcRegistryService.listAllOrdered()) { - String key = normalizeAgentSource(reg.getAgentSource()); - EsignPrinterDevicesResponse.EsignPrinterPcDevice existing = pcByAgent.get(key); - if (existing != null) { - applyRegistryMeta(existing, reg); - existing.setLinkedTabletCount(countLinkedTablets(linksByAgent, key)); - } else { - EsignPrinterDevicesResponse.EsignPrinterPcDevice pc = - buildRegisteredPcDeviceRow(reg, linksByAgent); - pcByAgent.put(key, pc); - } - } - pcDevices.addAll(pcByAgent.values()); - - Map> - linksByUser = printerDeviceLinkService.approvedLinksByUser(); - Map tabletByKey = new LinkedHashMap<>(); - List activeUsers = - userPresenceService.dedupeUsersByClientIp( - esignUserRepository.findByStatusOrderByLastLoginAtDesc("ACTIVE")); - for (EsignUser user : activeUsers) { - String ip = EsignUserPresenceService.normalizeClientIp(user.getLastLoginIp()); - String key = tabletDeviceKey(user.getId(), EsignPrinterTabletRegistryService.boundClientIpOrEmpty(ip)); - tabletByKey.put(key, buildTabletDeviceRow(user, linksByUser)); - } - for (EsignPrinterTabletRegistry reg : printerTabletRegistryService.listAllOrdered()) { - if (reg.getUserId() == null) { - continue; - } - List prelinked = printerTabletRegistryService.listAgentSourcesForRegistry(reg.getRegistryId()); - String bound = EsignPrinterTabletRegistryService.boundClientIpOrEmpty(reg.getClientIp()); - String key = tabletDeviceKey(reg.getUserId(), bound); - EsignPrinterDevicesResponse.EsignPrinterTabletDevice existing = tabletByKey.get(key); - if (existing != null) { - applyTabletRegistryMeta(existing, reg, prelinked); - } else { - esignUserRepository.findById(reg.getUserId()).ifPresent(user -> { - EsignPrinterDevicesResponse.EsignPrinterTabletDevice tab = - buildRegisteredTabletDeviceRow(user, reg, linksByUser, prelinked); - if (StringUtils.isNotBlank(bound)) { - tab.setLastLoginIp(bound); - } - tabletByKey.put(key, tab); - }); - } - } - List tabletDevices = - new ArrayList<>(tabletByKey.values()); - tabletDevices.sort(Comparator - .comparing( - (EsignPrinterDevicesResponse.EsignPrinterTabletDevice t) -> - Boolean.TRUE.equals(t.getOnline()), - Comparator.reverseOrder()) - .thenComparing(EsignPrinterDevicesResponse.EsignPrinterTabletDevice::isPushEnabled) - .reversed() - .thenComparing( - EsignPrinterDevicesResponse.EsignPrinterTabletDevice::getLastLoginAt, - Comparator.nullsLast(Comparator.reverseOrder()))); - - EsignPrinterDevicesResponse resp = EsignPrinterDevicesResponse.ok("ok"); - EsignPrinterDevicesResponse.EsignPrinterDevicesData data = - new EsignPrinterDevicesResponse.EsignPrinterDevicesData(); - data.setGeneratedAt(Instant.now().toString()); - data.setTaskWindowHours(TASK_WINDOW_HOURS); - data.setPcDevices(pcDevices); - data.setTabletDevices(tabletDevices); - List allLinks = - printerDeviceLinkService.listForCms().getItems(); - if (allLinks != null) { - List pending = new ArrayList<>(); - List approved = new ArrayList<>(); - for (var link : allLinks) { - EsignPrinterDevicesResponse.EsignPrinterDeviceLinkItem item = - copyLinkItem(link); - if (EsignPrinterDeviceLinkService.STATUS_PENDING.equals(link.getStatus())) { - pending.add(item); - } else if (EsignPrinterDeviceLinkService.STATUS_APPROVED.equals(link.getStatus())) { - approved.add(item); - } - } - data.setLinkRequests(pending); - data.setApprovedLinks(approved); - } - resp.setData(data); - return resp; - } - - private EsignPrinterDevicesResponse.EsignPrinterPcDevice buildPcDeviceRow( - EsignPrinterPcSourceRow row, - String canonicalAgentKey, - Map> - linksByAgent, - Map lastFileByAgent) { - String reportedKey = normalizeAgentSource(row.getAgentSource()); - EsignPrinterDevicesResponse.EsignPrinterPcDevice pc = - new EsignPrinterDevicesResponse.EsignPrinterPcDevice(); - pc.setAgentSource(canonicalAgentKey); - pc.setSubmitted24h(row.getSubmitted24h()); - pc.setPendingCount(row.getPendingCount()); - pc.setSigned24h(row.getSigned24h()); - pc.setLastSubmitAt(row.getLastSubmitAt() != null ? row.getLastSubmitAt().toString() : null); - pc.setLastFileName(lastFileByAgent.get(reportedKey)); - pc.setLinkedTabletCount(countLinkedTablets(linksByAgent, canonicalAgentKey)); - pc.setRegistered(false); - return pc; - } - - private static String resolveCanonicalPcAgentKey( - String reportedKey, Map registryByIp) { - String ip = EsignPrinterDeviceLinkService.extractHostIp(reportedKey); - if (ip != null) { - EsignPrinterPcRegistry reg = registryByIp.get(ip); - if (reg != null && StringUtils.isNotBlank(reg.getAgentSource())) { - return normalizeAgentSource(reg.getAgentSource()); - } - } - return reportedKey; - } - - private void mergeReportingStatsIntoPc( - EsignPrinterDevicesResponse.EsignPrinterPcDevice pc, - EsignPrinterPcSourceRow row, - Map lastFileByAgent, - String reportedKey) { - pc.setSubmitted24h(pc.getSubmitted24h() + row.getSubmitted24h()); - pc.setPendingCount(pc.getPendingCount() + row.getPendingCount()); - pc.setSigned24h(pc.getSigned24h() + row.getSigned24h()); - String latestFile = lastFileByAgent.get(reportedKey); - if (StringUtils.isNotBlank(latestFile)) { - pc.setLastFileName(latestFile); - } - if (row.getLastSubmitAt() != null) { - String existing = pc.getLastSubmitAt(); - if (existing == null || row.getLastSubmitAt().isAfter(Instant.parse(existing))) { - pc.setLastSubmitAt(row.getLastSubmitAt().toString()); - } - } - } - - private EsignPrinterDevicesResponse.EsignPrinterPcDevice buildRegisteredPcDeviceRow( - EsignPrinterPcRegistry reg, - Map> - linksByAgent) { - String key = normalizeAgentSource(reg.getAgentSource()); - EsignPrinterDevicesResponse.EsignPrinterPcDevice pc = - new EsignPrinterDevicesResponse.EsignPrinterPcDevice(); - pc.setAgentSource(key); - pc.setSubmitted24h(0); - pc.setPendingCount(0); - pc.setSigned24h(0); - pc.setLinkedTabletCount(countLinkedTablets(linksByAgent, key)); - applyRegistryMeta(pc, reg); - return pc; - } - - private static void applyRegistryMeta( - EsignPrinterDevicesResponse.EsignPrinterPcDevice pc, EsignPrinterPcRegistry reg) { - pc.setRegistered(true); - pc.setRegistryId(reg.getPcId()); - pc.setDisplayName(reg.getDisplayName()); - pc.setHostIp(reg.getHostIp()); - pc.setEnabled(reg.isEnabled()); - pc.setNotes(reg.getNotes()); - } - - private int countLinkedTablets( - Map> - linksByAgent, - String agentKey) { - Set linkedUserIds = new LinkedHashSet<>(); - for (Map.Entry> - entry : linksByAgent.entrySet()) { - if (!EsignPrinterDeviceLinkService.agentSourcesEquivalent(entry.getKey(), agentKey)) { - continue; - } - for (com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterDeviceLinkResponse.EsignPrinterDeviceLinkItem - link : entry.getValue()) { - if (link.getUserId() != null) { - linkedUserIds.add(link.getUserId()); - } - } - } - return userPresenceService.countUniqueClientIps(new ArrayList<>(linkedUserIds)); - } - - private EsignPrinterDevicesResponse.EsignPrinterTabletDevice buildTabletDeviceRow( - EsignUser user, - Map> - linksByUser) { - EsignPrinterDevicesResponse.EsignPrinterTabletDevice tab = - new EsignPrinterDevicesResponse.EsignPrinterTabletDevice(); - tab.setUserId(user.getId()); - tab.setUserName(user.getUserName()); - tab.setStatus(user.getStatus()); - boolean pushEnabled = StringUtils.isNotBlank(user.getDeviceToken()); - tab.setPushEnabled(pushEnabled); - tab.setDeliveryMode(pushEnabled ? "FCM 推送" : "仅轮询"); - tab.setLastLoginAt(user.getLastLoginAt() != null ? user.getLastLoginAt().toString() : null); - tab.setLastLoginIp(user.getLastLoginIp()); - boolean wsConnected = printerWebSocketHub.isUserConnected(user.getId()); - tab.setWsConnected(wsConnected); - tab.setOnline(wsConnected || isTabletRecentlyActive(user.getLastLoginAt())); - applyTabletLinks(tab, linksByUser.getOrDefault(user.getId(), List.of())); - tab.setRegistered(false); - return tab; - } - - private EsignPrinterDevicesResponse.EsignPrinterTabletDevice buildRegisteredTabletDeviceRow( - EsignUser user, - EsignPrinterTabletRegistry reg, - Map> - linksByUser, - List prelinked) { - EsignPrinterDevicesResponse.EsignPrinterTabletDevice tab = buildTabletDeviceRow(user, linksByUser); - applyTabletRegistryMeta(tab, reg, prelinked); - return tab; - } - - private static void applyTabletLinks( - EsignPrinterDevicesResponse.EsignPrinterTabletDevice tab, - List - userLinks) { - tab.setLinkedPcCount(userLinks.size()); - tab.setLinkedAgentSources( - userLinks.stream() - .map( - com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterDeviceLinkResponse - .EsignPrinterDeviceLinkItem::getAgentSource) - .toList()); - } - - private static void applyTabletRegistryMeta( - EsignPrinterDevicesResponse.EsignPrinterTabletDevice tab, - EsignPrinterTabletRegistry reg, - List prelinked) { - tab.setRegistered(true); - tab.setRegistryId(reg.getRegistryId()); - tab.setDisplayName(reg.getDisplayName()); - tab.setEnabled(reg.isEnabled()); - tab.setNotes(reg.getNotes()); - tab.setBoundClientIp(StringUtils.isBlank(reg.getClientIp()) ? null : reg.getClientIp()); - tab.setPrelinkedAgentSources(prelinked != null ? prelinked : List.of()); - } - - private static String tabletDeviceKey(Long userId, String clientIp) { - return userId + "#" + (clientIp != null ? clientIp : ""); - } - - private static EsignPrinterDevicesResponse.EsignPrinterDeviceLinkItem copyLinkItem( - com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterDeviceLinkResponse.EsignPrinterDeviceLinkItem src) { - EsignPrinterDevicesResponse.EsignPrinterDeviceLinkItem item = - new EsignPrinterDevicesResponse.EsignPrinterDeviceLinkItem(); - item.setLinkId(src.getLinkId()); - item.setUserId(src.getUserId()); - item.setUserName(src.getUserName()); - item.setAgentSource(src.getAgentSource()); - item.setAppLabel(src.getAppLabel()); - item.setStatus(src.getStatus()); - item.setRequestedAt(src.getRequestedAt()); - item.setReviewedAt(src.getReviewedAt()); - item.setPushEnabled(src.getPushEnabled()); - item.setDeliveryMode(src.getDeliveryMode()); - item.setLastLoginAt(src.getLastLoginAt()); - item.setLastLoginIp(src.getLastLoginIp()); - return item; - } - - private Optional resolveTask(String taskId, String externalId) { - if (StringUtils.isNotBlank(taskId)) { - return printerPdfTaskRepository.findById(taskId.trim()); - } - String ext = trim(externalId, 64); - if (StringUtils.isBlank(ext)) { - return Optional.empty(); - } - return printerPdfTaskRepository.findFirstByExternalIdAndStatus(ext, STATUS_PENDING) - .or(() -> printerPdfTaskRepository.findFirstByExternalId(ext)); - } - - private EsignPrinterPdfTaskDetailResponse toDetailResponse( - EsignPrinterPdfTask task, EsignPrinterPdfTaskDetailResponse resp) { - EsignPrinterPdfTaskDetailResponse.EsignPrinterPdfTaskDetailData data = - new EsignPrinterPdfTaskDetailResponse.EsignPrinterPdfTaskDetailData(); - data.setTaskId(task.getTaskId()); - data.setExternalId(task.getExternalId()); - data.setFileName(task.getFileName()); - data.setFilePath(task.getFilePath()); - data.setFileSize(task.getFileSize()); - data.setHotelId(task.getHotelId()); - data.setHasPdf(pdfFileStorage.hasOriginal(task)); - data.setHasSignedPdf(pdfFileStorage.hasSigned(task)); - data.setHasSignedMeta(StringUtils.isNotBlank(task.getSignedMetaJson())); - data.setCreatedAt(task.getCreatedAt() != null ? task.getCreatedAt().toString() : null); - data.setStatus(task.getStatus()); - resp.setSuccess(true); - resp.setCode("200"); - resp.setMessage("ok"); - resp.setData(data); - return resp; - } - - private EsignPendingListResponse.EsignPendingItem toPendingItem( - EsignPrinterPdfTaskListRow row, boolean completedBucket) { - EsignPendingListResponse.EsignPendingItem it = new EsignPendingListResponse.EsignPendingItem(); - it.setTaskType(TASK_TYPE); - it.setTaskId(row.getTaskId()); - it.setExternalId(row.getExternalId()); - it.setFileName(row.getFileName()); - it.setFileSize(row.getFileSize()); - it.setHotelId(row.getHotelId()); - it.setCreatedAt(row.getCreatedAt() != null ? row.getCreatedAt().toString() : null); - it.setDocTitle(row.getFileName()); - it.setAgentSource(row.getAgentSource()); - if (row.getSignedAt() != null) { - it.setSignedAt(row.getSignedAt().toString()); - } else if (completedBucket && row.getCompletedAt() != null) { - it.setSignedAt(row.getCompletedAt().toString()); - } - return it; - } - - private EsignPrinterDashboardResponse.EsignPrinterDashboardRecentTask toDashboardRecentTask( - EsignPrinterPdfTaskListRow row, - Instant cutoff, - Map registryByIp) { - EsignPrinterDashboardResponse.EsignPrinterDashboardRecentTask it = - new EsignPrinterDashboardResponse.EsignPrinterDashboardRecentTask(); - it.setTaskId(row.getTaskId()); - it.setExternalId(row.getExternalId()); - it.setFileName(row.getFileName()); - it.setFileSize(row.getFileSize()); - it.setStatus(row.getStatus()); - it.setHotelId(row.getHotelId()); - it.setCreatedAt(row.getCreatedAt() != null ? row.getCreatedAt().toString() : null); - it.setCompletedAt(row.getCompletedAt() != null ? row.getCompletedAt().toString() : null); - String reported = normalizeAgentSource(row.getAgentSource()); - it.setAgentSource(resolveCanonicalPcAgentKey(reported, registryByIp)); - it.setDisplayStatus(resolveDisplayStatus(row, cutoff)); - return it; - } - - private EsignPrinterDashboardResponse.EsignPrinterPcSourceStat toPcSourceStat( - EsignPrinterPcSourceRow row, - Map registryByIp) { - EsignPrinterDashboardResponse.EsignPrinterPcSourceStat stat = - new EsignPrinterDashboardResponse.EsignPrinterPcSourceStat(); - String reported = normalizeAgentSource(row.getAgentSource()); - stat.setAgentSource(resolveCanonicalPcAgentKey(reported, registryByIp)); - stat.setSubmitted24h(row.getSubmitted24h()); - stat.setPendingCount(row.getPendingCount()); - stat.setSigned24h(row.getSigned24h()); - stat.setLastSubmitAt(row.getLastSubmitAt() != null ? row.getLastSubmitAt().toString() : null); - return stat; - } - - private EsignPrinterDashboardResponse.EsignPrinterTabletDeviceStat toTabletDeviceStat( - EsignUser user) { - EsignPrinterDashboardResponse.EsignPrinterTabletDeviceStat stat = - new EsignPrinterDashboardResponse.EsignPrinterTabletDeviceStat(); - stat.setUserId(user.getId()); - stat.setUserName(user.getUserName()); - stat.setHotelId(user.getHotelId()); - stat.setStatus(user.getStatus()); - stat.setPushEnabled(StringUtils.isNotBlank(user.getDeviceToken())); - stat.setLastLoginAt(user.getLastLoginAt() != null ? user.getLastLoginAt().toString() : null); - boolean wsConnected = printerWebSocketHub.isUserConnected(user.getId()); - stat.setWsConnected(wsConnected); - stat.setOnline(wsConnected || isTabletRecentlyActive(user.getLastLoginAt())); - return stat; - } - - private boolean isTabletRecentlyActive(Instant lastLoginAt) { - if (lastLoginAt == null) { - return false; - } - return lastLoginAt.isAfter(Instant.now().minus(TABLET_ONLINE_MINUTES, ChronoUnit.MINUTES)); - } - - private Map loadOhipStatusByTaskId(List taskIds) { - if (taskIds == null || taskIds.isEmpty()) { - return Map.of(); - } - Map map = new HashMap<>(); - for (EsignPrinterOhipStatusRow row : printerPdfTaskRepository.findOhipStatusByTaskIdIn(taskIds)) { - if (row != null && StringUtils.isNotBlank(row.getTaskId())) { - map.put(row.getTaskId(), row); - } - } - return map; - } - - private EsignPrinterQueueResponse.EsignPrinterQueueItem toQueueItem( - EsignPrinterPdfTaskListRow row, - Instant cutoff, - Map registryByIp, - EsignPrinterOhipStatusRow ohipRow) { - EsignPrinterQueueResponse.EsignPrinterQueueItem item = - new EsignPrinterQueueResponse.EsignPrinterQueueItem(); - item.setTaskId(row.getTaskId()); - item.setExternalId(row.getExternalId()); - item.setFileName(row.getFileName()); - item.setFileSize(row.getFileSize()); - item.setHotelId(row.getHotelId()); - item.setStatus(row.getStatus()); - item.setDisplayStatus(resolveDisplayStatus(row, cutoff)); - String reported = normalizeAgentSource(row.getAgentSource()); - item.setAgentSource(resolveCanonicalPcAgentKey(reported, registryByIp)); - item.setCreatedAt(row.getCreatedAt() != null ? row.getCreatedAt().toString() : null); - item.setCompletedAt(row.getCompletedAt() != null ? row.getCompletedAt().toString() : null); - List assigned = - printerPushService.resolveAssignedPushTargets(row.getTaskId()); - item.setPushConfigured(!assigned.isEmpty()); - item.setPushTargets(assigned); - if (ohipRow != null) { - item.setOhipStatus(ohipRow.getOhipStatus()); - item.setOhipLastError(ohipRow.getOhipLastError()); - item.setOhipUploadedAt( - ohipRow.getOhipUploadedAt() != null ? ohipRow.getOhipUploadedAt().toString() : null); - } - return item; - } - - private static boolean matchesDisplayStatusFilter(String filter, String displayStatus) { - if (StringUtils.isBlank(filter) || "all".equalsIgnoreCase(filter.trim())) { - return true; - } - String f = filter.trim().toLowerCase(); - if ("awaiting_push".equals(f) || "await_push".equals(f)) { - return "待推送".equals(displayStatus); - } - if ("pending".equals(f)) { - return "待簽收".equals(displayStatus); - } - if ("signed".equals(f)) { - return "已簽收".equals(displayStatus); - } - return true; - } - - private record QueueSqlFilter(String taskStatus, String signFilter, String pushFilter) {} - - private QueueSqlFilter resolveQueueSqlFilter(String displayStatus) { - if (StringUtils.isBlank(displayStatus) || "all".equalsIgnoreCase(displayStatus.trim())) { - return new QueueSqlFilter(null, null, null); - } - String f = displayStatus.trim().toLowerCase(); - if ("signed".equals(f)) { - return new QueueSqlFilter(null, "SIGNED", null); - } - if ("awaiting_push".equals(f) || "await_push".equals(f)) { - return new QueueSqlFilter(null, "UNSIGNED", "NOT_PUSHED"); - } - if ("pending".equals(f)) { - if (printerPushService.isAutoMode()) { - return new QueueSqlFilter(null, "UNSIGNED", null); - } - return new QueueSqlFilter(null, "UNSIGNED", "PUSHED"); - } - return new QueueSqlFilter(null, null, null); - } - - private static String normalizeAgentSource(String agentSource) { - if (StringUtils.isBlank(agentSource)) { - return "printer-agent"; - } - String t = agentSource.trim(); - return t.isEmpty() ? "printer-agent" : t; - } - - private String resolveDisplayStatus(EsignPrinterPdfTaskListRow row, Instant cutoff) { - if (row.getSignedAt() != null) { - return "已簽收"; - } - if (row.getCreatedAt() != null && row.getCreatedAt().isBefore(cutoff)) { - return "超時未簽"; - } - if (!printerPushService.isAutoMode() - && !printerPushService.isTaskPushConfigured(row.getTaskId())) { - return "待推送"; - } - return "待簽收"; - } - - private static String normalizePdfBase64(String raw) { - if (StringUtils.isBlank(raw)) { - return null; - } - String t = raw.trim(); - int comma = t.indexOf(','); - if (t.regionMatches(true, 0, "data:", 0, 5) && comma > 0) { - t = t.substring(comma + 1).trim(); - } - return StringUtils.isBlank(t) ? null : t; - } - - private void applyNextSurveyAfterPdfSign( - EsignIosCaptureResponse.EsignIosCaptureData data, String hotelId) { - if (data == null) { - return; - } - surveyPushService.resolveAutoSurveyAfterPdfSign(hotelId).ifPresent(survey -> { - data.setNextSurveyId(survey.getSurveyId()); - data.setNextSurveyTitle(survey.getTitle()); - }); - } - - 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/printer/services/EsignPrinterPushRuleService.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/services/EsignPrinterPushRuleService.java deleted file mode 100644 index f885262..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/services/EsignPrinterPushRuleService.java +++ /dev/null @@ -1,406 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.services; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterPushRuleResponse; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterPushRuleSaveRequest; -import com.sjmresorts.opsapp.esign.printer.models.EsignPrinterPushRuleConfig; -import com.sjmresorts.opsapp.esign.printer.models.EsignPrinterPushRuleMapping; -import com.sjmresorts.opsapp.esign.printer.repositories.EsignPrinterPushRuleConfigRepository; -import com.sjmresorts.opsapp.esign.printer.repositories.EsignPrinterPushRuleMappingRepository; -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.util.ArrayList; -import java.util.Collections; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import java.util.UUID; -import java.util.stream.Collectors; - -@Slf4j -@Service -public class EsignPrinterPushRuleService { - - @Resource - private EsignPrinterPushRuleConfigRepository configRepository; - - @Resource - private EsignPrinterPushRuleMappingRepository mappingRepository; - - @Resource - private ObjectMapper objectMapper; - - @Transactional(readOnly = true) - public boolean isAutoMode() { - return EsignPrinterPushRuleConfig.MODE_AUTO.equalsIgnoreCase(loadConfig().getPushMode()); - } - - @Transactional(readOnly = true) - public boolean isAutoSendFcm() { - return loadConfig().isAutoSendFcm(); - } - - @Transactional(readOnly = true) - public String getPushMode() { - return loadConfig().getPushMode(); - } - - @Transactional(readOnly = true) - public EsignPrinterPushRuleResponse getRules(List knownAgentSources) { - EsignPrinterPushRuleConfig config = loadConfig(); - EsignPrinterPushRuleResponse resp = EsignPrinterPushRuleResponse.ok("ok"); - EsignPrinterPushRuleResponse.EsignPrinterPushRuleData data = - new EsignPrinterPushRuleResponse.EsignPrinterPushRuleData(); - data.setPushMode(config.getPushMode()); - data.setAutoSendFcm(config.isAutoSendFcm()); - data.setPerAgentEnabled(config.isPerAgentEnabled()); - data.setUnknownAgentEnabled(config.isUnknownAgentEnabled()); - data.setFallbackEnabled(config.isFallbackEnabled()); - data.setAllAgents(toMappingItem(findSingleton(EsignPrinterPushRuleMapping.SCOPE_ALL_AGENTS))); - data.setUnknownAgent(toMappingItem(findSingleton(EsignPrinterPushRuleMapping.SCOPE_UNKNOWN_AGENT))); - data.setManualDefault(toMappingItem(findSingleton(EsignPrinterPushRuleMapping.SCOPE_MANUAL_DEFAULT))); - - List perAgentRows = - mappingRepository.findByScopeTypeAndAgentSourceIsNotNullOrderByAgentSourceAsc( - EsignPrinterPushRuleMapping.SCOPE_PER_AGENT); - List perAgent = new ArrayList<>(); - for (EsignPrinterPushRuleMapping row : perAgentRows) { - EsignPrinterPushRuleResponse.EsignPrinterPushRulePerAgentItem item = - new EsignPrinterPushRuleResponse.EsignPrinterPushRulePerAgentItem(); - item.setAgentSource(row.getAgentSource()); - item.setUserIds(readUserIds(row)); - item.setGroupIds(readGroupIds(row)); - perAgent.add(item); - } - data.setPerAgentMappings(perAgent); - data.setKnownAgentSources(knownAgentSources != null ? knownAgentSources : List.of()); - resp.setData(data); - return resp; - } - - @Transactional - public EsignPrinterPushRuleResponse saveRules(EsignPrinterPushRuleSaveRequest request) { - if (request == null) { - return EsignPrinterPushRuleResponse.fail("400", "request body required"); - } - String mode = StringUtils.trimToEmpty(request.getPushMode()).toUpperCase(); - if (!EsignPrinterPushRuleConfig.MODE_AUTO.equals(mode) - && !EsignPrinterPushRuleConfig.MODE_MANUAL.equals(mode)) { - return EsignPrinterPushRuleResponse.fail("400", "pushMode 须为 AUTO 或 MANUAL"); - } - - EsignPrinterPushRuleConfig config = loadConfig(); - config.setPushMode(mode); - if (request.getAutoSendFcm() != null) { - config.setAutoSendFcm(request.getAutoSendFcm()); - } - if (request.getPerAgentEnabled() != null) { - config.setPerAgentEnabled(request.getPerAgentEnabled()); - } - if (request.getUnknownAgentEnabled() != null) { - config.setUnknownAgentEnabled(request.getUnknownAgentEnabled()); - } - if (request.getFallbackEnabled() != null) { - config.setFallbackEnabled(request.getFallbackEnabled()); - } - configRepository.save(config); - - saveSingleton(EsignPrinterPushRuleMapping.SCOPE_ALL_AGENTS, request.getAllAgents()); - saveSingleton(EsignPrinterPushRuleMapping.SCOPE_UNKNOWN_AGENT, request.getUnknownAgent()); - saveSingleton(EsignPrinterPushRuleMapping.SCOPE_MANUAL_DEFAULT, request.getManualDefault()); - - List existingPerAgent = - mappingRepository.findByScopeTypeAndAgentSourceIsNotNullOrderByAgentSourceAsc( - EsignPrinterPushRuleMapping.SCOPE_PER_AGENT); - for (EsignPrinterPushRuleMapping row : existingPerAgent) { - mappingRepository.delete(row); - } - if (request.getPerAgentMappings() != null) { - Set seen = new LinkedHashSet<>(); - for (EsignPrinterPushRuleResponse.EsignPrinterPushRulePerAgentItem item : - request.getPerAgentMappings()) { - if (item == null || StringUtils.isBlank(item.getAgentSource())) { - continue; - } - String agentKey = EsignPrinterDeviceLinkService.normalizeAgentSource(item.getAgentSource()); - if (!seen.add(agentKey)) { - continue; - } - EsignPrinterPushRuleMapping row = new EsignPrinterPushRuleMapping(); - row.setMappingId(UUID.randomUUID().toString()); - row.setScopeType(EsignPrinterPushRuleMapping.SCOPE_PER_AGENT); - row.setAgentSource(agentKey); - writeTargets(row, item.getUserIds(), item.getGroupIds()); - mappingRepository.save(row); - } - } - - return getRules(List.of()); - } - - /** - * 自动模式解析推送目标:PER_AGENT → UNKNOWN_AGENT → ALL_AGENTS - */ - @Transactional(readOnly = true) - public ResolvedPushTargets resolveAutoTargets(String agentSource) { - String key = EsignPrinterDeviceLinkService.normalizeAgentSource(agentSource); - EsignPrinterPushRuleConfig config = loadConfig(); - - if (config.isPerAgentEnabled()) { - Optional perAgent = findPerAgentMapping(key); - if (perAgent.isPresent() && hasTargets(perAgent.get())) { - return toResolved(perAgent.get()); - } - } - - if (config.isUnknownAgentEnabled() && "printer-agent".equals(key)) { - Optional unknown = - findSingleton(EsignPrinterPushRuleMapping.SCOPE_UNKNOWN_AGENT); - if (unknown.isPresent() && hasTargets(unknown.get())) { - return toResolved(unknown.get()); - } - } - - if (config.isFallbackEnabled()) { - Optional all = - findSingleton(EsignPrinterPushRuleMapping.SCOPE_ALL_AGENTS); - if (all.isPresent() && hasTargets(all.get())) { - return toResolved(all.get()); - } - } - - return ResolvedPushTargets.empty(); - } - - /** - * 手动模式预填:按上报 PC 映射 → 未标识 PC 统一规则 → 全局默认 - */ - @Transactional(readOnly = true) - public ResolvedPushTargets resolveManualPrefill(String agentSource) { - String key = EsignPrinterDeviceLinkService.normalizeAgentSource(agentSource); - EsignPrinterPushRuleConfig config = loadConfig(); - - if (config.isPerAgentEnabled()) { - Optional perAgent = findPerAgentMapping(key); - if (perAgent.isPresent() && hasTargets(perAgent.get())) { - return toResolved(perAgent.get()); - } - } - - if (config.isUnknownAgentEnabled() && EsignPrinterDeviceLinkService.isUnidentifiedAgent(key)) { - Optional unknown = - findSingleton(EsignPrinterPushRuleMapping.SCOPE_UNKNOWN_AGENT); - if (unknown.isPresent() && hasTargets(unknown.get())) { - return toResolved(unknown.get()); - } - } - - if (config.isFallbackEnabled()) { - Optional manualDefault = - findSingleton(EsignPrinterPushRuleMapping.SCOPE_MANUAL_DEFAULT); - if (manualDefault.isPresent() && hasTargets(manualDefault.get())) { - return toResolved(manualDefault.get()); - } - } - - return ResolvedPushTargets.empty(); - } - - @Transactional(readOnly = true) - public ResolvedPushTargets resolveManualDefault() { - EsignPrinterPushRuleConfig config = loadConfig(); - if (!config.isFallbackEnabled()) { - return ResolvedPushTargets.empty(); - } - Optional row = - findSingleton(EsignPrinterPushRuleMapping.SCOPE_MANUAL_DEFAULT); - if (row.isEmpty() || !hasTargets(row.get())) { - return ResolvedPushTargets.empty(); - } - return toResolved(row.get()); - } - - private EsignPrinterPushRuleConfig loadConfig() { - return configRepository.findById(EsignPrinterPushRuleConfig.DEFAULT_ID).orElseGet(() -> { - EsignPrinterPushRuleConfig c = new EsignPrinterPushRuleConfig(); - c.setConfigId(EsignPrinterPushRuleConfig.DEFAULT_ID); - c.setPushMode(EsignPrinterPushRuleConfig.MODE_MANUAL); - c.setAutoSendFcm(false); - c.setPerAgentEnabled(true); - c.setUnknownAgentEnabled(true); - c.setFallbackEnabled(true); - return configRepository.save(c); - }); - } - - private Optional findSingleton(String scopeType) { - List rows = - mappingRepository.findByScopeTypeOrderByUpdatedAtDesc(scopeType); - return rows.isEmpty() ? Optional.empty() : Optional.of(rows.get(0)); - } - - /** 按 PC 标识查规则:先精确匹配,再按 IP 等价匹配(Electron 上报名与 CMS 登记名可不同)。 */ - private Optional findPerAgentMapping(String agentSource) { - String key = EsignPrinterDeviceLinkService.normalizeAgentSource(agentSource); - Optional exact = mappingRepository.findFirstByScopeTypeAndAgentSource( - EsignPrinterPushRuleMapping.SCOPE_PER_AGENT, key); - if (exact.isPresent()) { - return exact; - } - for (EsignPrinterPushRuleMapping row : - mappingRepository.findByScopeTypeAndAgentSourceIsNotNullOrderByAgentSourceAsc( - EsignPrinterPushRuleMapping.SCOPE_PER_AGENT)) { - if (EsignPrinterDeviceLinkService.agentSourcesEquivalent(row.getAgentSource(), key)) { - return Optional.of(row); - } - } - return Optional.empty(); - } - - private void saveSingleton( - String scopeType, EsignPrinterPushRuleResponse.EsignPrinterPushRuleMappingItem item) { - List existing = - mappingRepository.findByScopeTypeOrderByUpdatedAtDesc(scopeType); - for (int i = 1; i < existing.size(); i++) { - mappingRepository.delete(existing.get(i)); - } - EsignPrinterPushRuleMapping row; - if (existing.isEmpty()) { - row = new EsignPrinterPushRuleMapping(); - row.setMappingId(UUID.randomUUID().toString()); - row.setScopeType(scopeType); - } else { - row = existing.get(0); - } - if (item == null) { - row.setUserIdsJson(null); - row.setGroupIdsJson(null); - } else { - writeTargets(row, item.getUserIds(), item.getGroupIds()); - } - mappingRepository.save(row); - } - - private void writeTargets(EsignPrinterPushRuleMapping row, List userIds, List groupIds) { - row.setUserIdsJson(writeJson(normalizeUserIds(userIds))); - row.setGroupIdsJson(writeJson(normalizeGroupIds(groupIds))); - } - - private List normalizeUserIds(List userIds) { - if (userIds == null) { - return List.of(); - } - return userIds.stream().filter(id -> id != null && id > 0).distinct().collect(Collectors.toList()); - } - - private List normalizeGroupIds(List groupIds) { - if (groupIds == null) { - return List.of(); - } - return groupIds.stream() - .filter(StringUtils::isNotBlank) - .map(String::trim) - .distinct() - .collect(Collectors.toList()); - } - - private EsignPrinterPushRuleResponse.EsignPrinterPushRuleMappingItem toMappingItem( - Optional rowOpt) { - EsignPrinterPushRuleResponse.EsignPrinterPushRuleMappingItem item = - new EsignPrinterPushRuleResponse.EsignPrinterPushRuleMappingItem(); - if (rowOpt.isEmpty()) { - item.setUserIds(List.of()); - item.setGroupIds(List.of()); - return item; - } - item.setUserIds(readUserIds(rowOpt.get())); - item.setGroupIds(readGroupIds(rowOpt.get())); - return item; - } - - private ResolvedPushTargets toResolved(EsignPrinterPushRuleMapping row) { - return new ResolvedPushTargets(readUserIds(row), readGroupIds(row)); - } - - private boolean hasTargets(EsignPrinterPushRuleMapping row) { - return !readUserIds(row).isEmpty() || !readGroupIds(row).isEmpty(); - } - - private List readUserIds(EsignPrinterPushRuleMapping row) { - return readLongList(row.getUserIdsJson()); - } - - private List readGroupIds(EsignPrinterPushRuleMapping row) { - return readStringList(row.getGroupIdsJson()); - } - - private List readLongList(String json) { - if (StringUtils.isBlank(json)) { - return List.of(); - } - try { - List list = objectMapper.readValue(json, new TypeReference>() {}); - return list != null ? normalizeUserIds(list) : List.of(); - } catch (Exception e) { - log.warn("Invalid userIdsJson: {}", json, e); - return List.of(); - } - } - - private List readStringList(String json) { - if (StringUtils.isBlank(json)) { - return List.of(); - } - try { - List list = objectMapper.readValue(json, new TypeReference>() {}); - return list != null ? normalizeGroupIds(list) : List.of(); - } catch (Exception e) { - log.warn("Invalid groupIdsJson: {}", json, e); - return List.of(); - } - } - - private String writeJson(List list) { - if (list == null || list.isEmpty()) { - return null; - } - try { - return objectMapper.writeValueAsString(list); - } catch (Exception e) { - log.warn("Failed to serialize json list", e); - return null; - } - } - - public static final class ResolvedPushTargets { - private final List userIds; - private final List groupIds; - - public ResolvedPushTargets(List userIds, List groupIds) { - this.userIds = userIds != null ? List.copyOf(userIds) : List.of(); - this.groupIds = groupIds != null ? List.copyOf(groupIds) : List.of(); - } - - public static ResolvedPushTargets empty() { - return new ResolvedPushTargets(Collections.emptyList(), Collections.emptyList()); - } - - public boolean isEmpty() { - return userIds.isEmpty() && groupIds.isEmpty(); - } - - public List getUserIds() { - return userIds; - } - - public List getGroupIds() { - return groupIds; - } - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/services/EsignPrinterPushService.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/services/EsignPrinterPushService.java deleted file mode 100644 index 102f0f1..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/services/EsignPrinterPushService.java +++ /dev/null @@ -1,688 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.services; - -import com.sjmresorts.opsapp.esign.capture.dtos.EsignIosCaptureResponse; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterPushAssignRequest; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterPushCandidatesResponse; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterPushGroupListResponse; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterPushGroupSaveRequest; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterQueueResponse; -import com.sjmresorts.opsapp.esign.printer.models.EsignPrinterDeviceLink; -import com.sjmresorts.opsapp.esign.printer.models.EsignPrinterPdfTask; -import com.sjmresorts.opsapp.esign.printer.models.EsignPrinterPdfTaskPush; -import com.sjmresorts.opsapp.esign.printer.models.EsignPrinterPushGroup; -import com.sjmresorts.opsapp.esign.printer.models.EsignPrinterPushGroupMember; -import com.sjmresorts.opsapp.esign.auth.models.EsignUser; -import com.sjmresorts.opsapp.esign.printer.repositories.EsignPrinterPdfTaskPushRepository; -import com.sjmresorts.opsapp.esign.printer.repositories.EsignPrinterPdfTaskRepository; -import com.sjmresorts.opsapp.esign.printer.repositories.EsignPrinterPushGroupMemberRepository; -import com.sjmresorts.opsapp.esign.printer.repositories.EsignPrinterPushGroupRepository; -import com.sjmresorts.opsapp.esign.auth.repositories.EsignUserRepository; -import com.sjmresorts.opsapp.esign.auth.services.EsignUserPresenceService; -import com.sjmresorts.opsapp.esign.printer.support.EsignPrinterQueueNotifier; -import com.sjmresorts.opsapp.notifications.dtos.SendNotificationData; -import com.sjmresorts.opsapp.notifications.dtos.SendNotificationResponse; -import com.sjmresorts.opsapp.notifications.enums.NotificationTargetType; -import com.sjmresorts.opsapp.notifications.services.NotificationService; -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.util.ArrayList; -import java.util.Collection; -import java.util.Comparator; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.UUID; -import java.util.stream.Collectors; - -@Slf4j -@Service -public class EsignPrinterPushService { - - private static final String ACTIVE = "ACTIVE"; - private static final String TRIGGER_TASK = "PRINTER_PDF_PUSH"; - private static final String CLICK_ACTION = "OPEN_PRINTER_PDF"; - - @Resource - private EsignPrinterPdfTaskRepository printerPdfTaskRepository; - - @Resource - private EsignPrinterPdfTaskPushRepository taskPushRepository; - - @Resource - private EsignPrinterPushGroupRepository pushGroupRepository; - - @Resource - private EsignPrinterPushGroupMemberRepository pushGroupMemberRepository; - - @Resource - private EsignPrinterDeviceLinkService printerDeviceLinkService; - - @Resource - private EsignUserRepository esignUserRepository; - - @Resource - private NotificationService notificationService; - - @Resource - private EsignPrinterEventLogService printerEventLogService; - - @Resource - private EsignPrinterPushRuleService pushRuleService; - - @Resource - private com.sjmresorts.opsapp.esign.printer.websocket.EsignPrinterWebSocketHub printerWebSocketHub; - - @Resource - private EsignUserPresenceService userPresenceService; - - @Resource - private EsignPrinterTabletRegistryService tabletRegistryService; - - @Resource - private EsignPrinterQueueNotifier printerQueueNotifier; - - @Transactional(readOnly = true) - public EsignPrinterPushCandidatesResponse getPushCandidates(String taskId) { - if (StringUtils.isBlank(taskId)) { - return EsignPrinterPushCandidatesResponse.fail("400", "taskId required"); - } - Optional taskOpt = printerPdfTaskRepository.findById(taskId.trim()); - if (taskOpt.isEmpty()) { - return EsignPrinterPushCandidatesResponse.fail("404", "任务不存在"); - } - EsignPrinterPdfTask task = taskOpt.get(); - String agentSource = EsignPrinterDeviceLinkService.normalizeAgentSource(task.getAgentSource()); - - Set eligibleUserIds = EsignPrinterDeviceLinkService.isUnidentifiedAgent(agentSource) - ? printerDeviceLinkService.approvedUserIdsWithAnyApprovedLink() - : printerDeviceLinkService.approvedUserIdsForPushTarget(agentSource); - Map linkByUser = new HashMap<>(); - for (EsignPrinterDeviceLink link : printerDeviceLinkService.approvedLinksForPushTarget(agentSource)) { - if (link.getUserId() != null) { - linkByUser.putIfAbsent(link.getUserId(), link); - } - } - - List activeUsers = esignUserRepository.findByStatusOrderByLastLoginAtDesc(ACTIVE); - List devices = new ArrayList<>(); - for (Long userId : userPresenceService.dedupeUserIdsByClientIp(new ArrayList<>(eligibleUserIds))) { - EsignUser user = activeUsers.stream().filter(u -> userId.equals(u.getId())).findFirst().orElse(null); - if (user == null) { - continue; - } - EsignPrinterDeviceLink link = linkByUser.get(userId); - EsignPrinterPushCandidatesResponse.EsignPrinterPushDeviceOption opt = - new EsignPrinterPushCandidatesResponse.EsignPrinterPushDeviceOption(); - opt.setUserId(user.getId()); - opt.setUserName(user.getUserName()); - opt.setAppLabel(link != null ? link.getAppLabel() : null); - boolean pushEnabled = StringUtils.isNotBlank(user.getDeviceToken()); - opt.setPushEnabled(pushEnabled); - opt.setDeliveryMode(pushEnabled ? "FCM 推送" : "仅轮询"); - devices.add(opt); - } - - List groups = groupsForAgent(agentSource); - List groupOptions = new ArrayList<>(); - for (EsignPrinterPushGroup group : groups) { - List members = pushGroupMemberRepository.findByGroupId(group.getGroupId()); - List memberIds = members.stream().map(EsignPrinterPushGroupMember::getUserId).toList(); - EsignPrinterPushCandidatesResponse.EsignPrinterPushGroupOption g = - new EsignPrinterPushCandidatesResponse.EsignPrinterPushGroupOption(); - g.setGroupId(group.getGroupId()); - g.setGroupName(group.getGroupName()); - g.setMemberCount(memberIds.size()); - g.setMemberUserIds(memberIds); - groupOptions.add(g); - } - - List selectedUserIds = taskPushRepository.findByTaskId(task.getTaskId()).stream() - .map(EsignPrinterPdfTaskPush::getUserId) - .toList(); - List selectedGroupIds = List.of(); - if (selectedUserIds.isEmpty() && !pushRuleService.isAutoMode()) { - EsignPrinterPushRuleService.ResolvedPushTargets defaults = - pushRuleService.resolveManualPrefill(agentSource); - if (!defaults.isEmpty()) { - selectedUserIds = defaults.getUserIds(); - selectedGroupIds = defaults.getGroupIds(); - } - } - Set availableUserIds = devices.stream() - .map(EsignPrinterPushCandidatesResponse.EsignPrinterPushDeviceOption::getUserId) - .filter(Objects::nonNull) - .collect(Collectors.toSet()); - selectedUserIds = selectedUserIds.stream() - .filter(availableUserIds::contains) - .toList(); - Set availableGroupIds = groupOptions.stream() - .map(EsignPrinterPushCandidatesResponse.EsignPrinterPushGroupOption::getGroupId) - .filter(StringUtils::isNotBlank) - .collect(Collectors.toSet()); - selectedGroupIds = selectedGroupIds.stream() - .filter(availableGroupIds::contains) - .toList(); - - EsignPrinterPushCandidatesResponse resp = EsignPrinterPushCandidatesResponse.ok("ok"); - EsignPrinterPushCandidatesResponse.EsignPrinterPushCandidatesData data = - new EsignPrinterPushCandidatesResponse.EsignPrinterPushCandidatesData(); - data.setTaskId(task.getTaskId()); - data.setAgentSource(agentSource); - data.setDevices(devices); - data.setGroups(groupOptions); - data.setSelectedUserIds(selectedUserIds); - data.setSelectedGroupIds(selectedGroupIds); - data.setPushMode(pushRuleService.getPushMode()); - resp.setData(data); - return resp; - } - - @Transactional(readOnly = true) - public String getPushMode() { - return pushRuleService.getPushMode(); - } - - @Transactional(readOnly = true) - public boolean isAutoMode() { - return pushRuleService.isAutoMode(); - } - - @Transactional - public void autoAssignOnSubmit(EsignPrinterPdfTask task) { - if (task == null || !pushRuleService.isAutoMode()) { - return; - } - if (!EsignPrinterPdfTaskService.STATUS_PENDING.equals(task.getStatus())) { - return; - } - if (taskPushRepository.existsByTaskId(task.getTaskId())) { - return; - } - EsignPrinterPushRuleService.ResolvedPushTargets targets = - pushRuleService.resolveAutoTargets(task.getAgentSource()); - if (targets.isEmpty()) { - log.info("Auto push skipped taskId={}: no rule mapping", task.getTaskId()); - return; - } - Set assigned = applyPushTargetsInternal( - task, - targets.getUserIds(), - targets.getGroupIds(), - "AUTO"); - if (assigned.isEmpty()) { - log.info("Auto push skipped taskId={}: no valid targets after filter", task.getTaskId()); - return; - } - printerEventLogService.append( - EsignPrinterEventLogService.EVENT_PDF_PUSH_ASSIGN, - task, - "自动推送 " + assigned.size() + " 台", - "AUTO"); - if (pushRuleService.isAutoSendFcm()) { - sendFcmForTask(task, new ArrayList<>(assigned)); - } - notifyPushTargetsViaWebSocket(task, assigned); - printerQueueNotifier.notifyChangedAfterCommit(); - } - - @Transactional(readOnly = true) - public boolean isTaskPushConfigured(String taskId) { - return taskPushRepository.existsByTaskId(taskId); - } - - @Transactional - public EsignIosCaptureResponse quickPushWithRules(String taskId, boolean sendFcm) { - EsignIosCaptureResponse resp = new EsignIosCaptureResponse(); - if (StringUtils.isBlank(taskId)) { - resp.setSuccess(false); - resp.setCode("400"); - resp.setMessage("taskId required"); - return resp; - } - Optional taskOpt = printerPdfTaskRepository.findById(taskId.trim()); - if (taskOpt.isEmpty()) { - resp.setSuccess(false); - resp.setCode("404"); - resp.setMessage("任务不存在"); - return resp; - } - EsignPrinterPdfTask task = taskOpt.get(); - if (!EsignPrinterPdfTaskService.STATUS_PENDING.equals(task.getStatus())) { - resp.setSuccess(false); - resp.setCode("409"); - resp.setMessage("仅待签收任务可推送"); - return resp; - } - - if (pushRuleService.isAutoMode() && !taskPushRepository.existsByTaskId(task.getTaskId())) { - autoAssignOnSubmit(task); - } - - if (taskPushRepository.existsByTaskId(task.getTaskId())) { - resp.setSuccess(true); - resp.setCode("200"); - resp.setMessage("已推送"); - EsignIosCaptureResponse.EsignIosCaptureData data = new EsignIosCaptureResponse.EsignIosCaptureData(); - data.setTaskId(task.getTaskId()); - resp.setData(data); - return resp; - } - - String agentSource = EsignPrinterDeviceLinkService.normalizeAgentSource(task.getAgentSource()); - EsignPrinterPushRuleService.ResolvedPushTargets targets = - pushRuleService.resolveManualPrefill(agentSource); - if (targets.isEmpty()) { - resp.setSuccess(false); - resp.setCode("400"); - resp.setMessage( - "请先在推送规则中配置 PC「" + agentSource + "」的平板映射,或配置全局默认映射"); - return resp; - } - - Set targetUserIds = applyPushTargetsInternal( - task, targets.getUserIds(), targets.getGroupIds(), "CMS_QUICK"); - if (targetUserIds.isEmpty()) { - resp.setSuccess(false); - resp.setCode("400"); - resp.setMessage( - "推送规则中的平板与 PDF 上报 PC「" + agentSource + "」未建立已批准连接。" - + "常见原因:App 连接标识与 Electron :39201 上报标识不一致。" - + "请修正连接后重试,或使用「调整」手动选择设备。"); - return resp; - } - - printerEventLogService.append( - EsignPrinterEventLogService.EVENT_PDF_PUSH_ASSIGN, - task, - "快捷推送 " + targetUserIds.size() + " 台", - "CMS_QUICK"); - - if (shouldSendFcm(sendFcm)) { - sendFcmForTask(task, new ArrayList<>(targetUserIds)); - } - notifyPushTargetsViaWebSocket(task, targetUserIds); - printerQueueNotifier.notifyChangedAfterCommit(); - - resp.setSuccess(true); - resp.setCode("200"); - resp.setMessage("已推送(" + targetUserIds.size() + " 台,平板将通过轮询拉取)"); - EsignIosCaptureResponse.EsignIosCaptureData data = new EsignIosCaptureResponse.EsignIosCaptureData(); - data.setTaskId(task.getTaskId()); - resp.setData(data); - return resp; - } - - @Transactional - public EsignIosCaptureResponse assignPushTargets(EsignPrinterPushAssignRequest request) { - EsignIosCaptureResponse resp = new EsignIosCaptureResponse(); - if (request == null || StringUtils.isBlank(request.getTaskId())) { - resp.setSuccess(false); - resp.setCode("400"); - resp.setMessage("taskId required"); - return resp; - } - Optional taskOpt = printerPdfTaskRepository.findById(request.getTaskId().trim()); - if (taskOpt.isEmpty()) { - resp.setSuccess(false); - resp.setCode("404"); - resp.setMessage("任务不存在"); - return resp; - } - EsignPrinterPdfTask task = taskOpt.get(); - if (!EsignPrinterPdfTaskService.STATUS_PENDING.equals(task.getStatus())) { - resp.setSuccess(false); - resp.setCode("409"); - resp.setMessage("仅待签收任务可设置推送目标"); - return resp; - } - - String assignedBy = trim(request.getAssignedBy(), 64); - Set targetUserIds = applyPushTargetsInternal( - task, request.getUserIds(), request.getGroupIds(), assignedBy); - - if (targetUserIds.isEmpty()) { - resp.setSuccess(false); - resp.setCode("400"); - resp.setMessage("请至少选择一个已连接的设备或组"); - return resp; - } - - printerEventLogService.append( - EsignPrinterEventLogService.EVENT_PDF_PUSH_ASSIGN, - task, - "CMS 设置推送目标 " + targetUserIds.size() + " 台", - assignedBy); - - if (shouldSendFcm(request.isSendFcm())) { - sendFcmForTask(task, new ArrayList<>(targetUserIds)); - } - notifyPushTargetsViaWebSocket(task, targetUserIds); - printerQueueNotifier.notifyChangedAfterCommit(); - - resp.setSuccess(true); - resp.setCode("200"); - resp.setMessage(pushRuleService.isAutoMode() - ? "已设置推送目标(" + targetUserIds.size() + " 台)" - : "已设置推送目标(" + targetUserIds.size() + " 台,平板将通过轮询拉取)"); - EsignIosCaptureResponse.EsignIosCaptureData data = new EsignIosCaptureResponse.EsignIosCaptureData(); - data.setTaskId(task.getTaskId()); - resp.setData(data); - return resp; - } - - private Set applyPushTargetsInternal( - EsignPrinterPdfTask task, - List requestUserIds, - List requestGroupIds, - String assignedBy) { - String agentSource = EsignPrinterDeviceLinkService.normalizeAgentSource(task.getAgentSource()); - Set allowedUserIds = pushEligibleUserIds(); - - Set targetUserIds = new LinkedHashSet<>(); - if (requestUserIds != null) { - for (Long uid : requestUserIds) { - if (uid != null && allowedUserIds.contains(uid)) { - targetUserIds.add(uid); - } - } - } - if (requestGroupIds != null && !requestGroupIds.isEmpty()) { - List fromGroups = pushGroupMemberRepository.findUserIdsByGroupIdIn(requestGroupIds); - for (Long uid : fromGroups) { - if (allowedUserIds.contains(uid)) { - targetUserIds.add(uid); - } - } - } - if (targetUserIds.isEmpty()) { - return targetUserIds; - } - targetUserIds = userPresenceService.dedupeUserIdsByClientIp(targetUserIds); - - taskPushRepository.deleteByTaskId(task.getTaskId()); - Instant now = Instant.now(); - String by = trim(assignedBy, 64); - for (Long userId : targetUserIds) { - Set clientIps = tabletRegistryService.resolvePushTargetClientIpsForUser(userId); - if (clientIps.isEmpty()) { - clientIps = Set.of(""); - } - for (String clientIp : clientIps) { - EsignPrinterPdfTaskPush row = new EsignPrinterPdfTaskPush(); - row.setTaskId(task.getTaskId()); - row.setUserId(userId); - row.setClientIp(clientIp != null ? clientIp : ""); - row.setAssignedAt(now); - row.setAssignedBy(by); - taskPushRepository.save(row); - } - } - return targetUserIds; - } - - @Transactional(readOnly = true) - public List resolveAssignedPushTargets(String taskId) { - List rows = taskPushRepository.findByTaskId(taskId); - if (rows.isEmpty()) { - return List.of(); - } - Optional taskOpt = printerPdfTaskRepository.findById(taskId); - String agentSource = taskOpt - .map(t -> EsignPrinterDeviceLinkService.normalizeAgentSource(t.getAgentSource())) - .orElse("printer-agent"); - - Set userIds = rows.stream().map(EsignPrinterPdfTaskPush::getUserId).collect(Collectors.toSet()); - Map users = new HashMap<>(); - for (Long uid : userIds) { - esignUserRepository.findById(uid).ifPresent(u -> users.put(uid, u)); - } - - List links = printerDeviceLinkService.approvedLinksForPushTarget(agentSource); - Map linkByUser = new HashMap<>(); - for (EsignPrinterDeviceLink link : links) { - linkByUser.put(link.getUserId(), link); - } - - List targets = new ArrayList<>(); - for (Long uid : userIds) { - EsignUser user = users.get(uid); - if (user == null || !ACTIVE.equalsIgnoreCase(user.getStatus())) { - continue; - } - EsignPrinterDeviceLink link = linkByUser.get(uid); - EsignPrinterQueueResponse.EsignPrinterPushTarget target = - new EsignPrinterQueueResponse.EsignPrinterPushTarget(); - target.setUserId(user.getId()); - target.setUserName(user.getUserName()); - target.setAgentSource(agentSource); - target.setAppLabel(link != null ? link.getAppLabel() : null); - target.setLinkStatus(EsignPrinterDeviceLinkService.STATUS_APPROVED); - boolean pushEnabled = StringUtils.isNotBlank(user.getDeviceToken()); - target.setPushEnabled(pushEnabled); - target.setDeliveryMode(pushEnabled ? "FCM 推送" : "仅轮询"); - targets.add(target); - } - return targets; - } - - @Transactional(readOnly = true) - public boolean isTaskVisibleToUser(String taskId, Long userId, String clientIp) { - if (userId == null) { - return false; - } - if (!taskPushRepository.existsByTaskId(taskId)) { - return false; - } - String ip = EsignUserPresenceService.normalizeClientIp(clientIp); - return taskPushRepository.findByTaskId(taskId).stream() - .anyMatch(p -> userId.equals(p.getUserId()) && clientIpMatchesPushTarget(p.getClientIp(), ip)); - } - - static boolean clientIpMatchesPushTarget(String storedClientIp, String requestClientIp) { - if (StringUtils.isBlank(storedClientIp)) { - return true; - } - return requestClientIp != null && storedClientIp.equals(requestClientIp); - } - - @Transactional(readOnly = true) - public Set assignedTaskIdsForUser(Long userId, String clientIp) { - if (userId == null) { - return Set.of(); - } - String ip = EsignUserPresenceService.normalizeClientIp(clientIp); - if (ip == null) { - return taskPushRepository.findTaskIdsByUserIdWildcardOnly(userId); - } - return taskPushRepository.findTaskIdsByUserIdAndClientIp(userId, ip); - } - - /** @deprecated 使用 {@link #assignedTaskIdsForUser(Long, String)} */ - @Deprecated - @Transactional(readOnly = true) - public Set assignedTaskIdsForUser(Long userId) { - if (userId == null) { - return Set.of(); - } - return taskPushRepository.findTaskIdsByUserId(userId); - } - - @Transactional(readOnly = true) - public EsignPrinterPushGroupListResponse listGroups(String agentSource) { - if (StringUtils.isBlank(agentSource)) { - return EsignPrinterPushGroupListResponse.fail("400", "agentSource required"); - } - String key = EsignPrinterDeviceLinkService.normalizeAgentSource(agentSource); - List groups = groupsForAgent(key); - List items = new ArrayList<>(); - for (EsignPrinterPushGroup group : groups) { - List memberIds = pushGroupMemberRepository.findByGroupId(group.getGroupId()).stream() - .map(EsignPrinterPushGroupMember::getUserId) - .toList(); - EsignPrinterPushGroupListResponse.EsignPrinterPushGroupItem item = - new EsignPrinterPushGroupListResponse.EsignPrinterPushGroupItem(); - item.setGroupId(group.getGroupId()); - item.setGroupName(group.getGroupName()); - item.setAgentSource(group.getAgentSource()); - item.setMemberCount(memberIds.size()); - item.setMemberUserIds(memberIds); - item.setCreatedAt(group.getCreatedAt() != null ? group.getCreatedAt().toString() : null); - item.setUpdatedAt(group.getUpdatedAt() != null ? group.getUpdatedAt().toString() : null); - items.add(item); - } - EsignPrinterPushGroupListResponse resp = EsignPrinterPushGroupListResponse.ok("ok"); - resp.setData(items); - return resp; - } - - @Transactional - public EsignPrinterPushGroupListResponse saveGroup(EsignPrinterPushGroupSaveRequest request) { - if (request == null || StringUtils.isBlank(request.getGroupName()) || StringUtils.isBlank(request.getAgentSource())) { - return EsignPrinterPushGroupListResponse.fail("400", "组名与 agentSource 必填"); - } - String agentSource = EsignPrinterDeviceLinkService.normalizeAgentSource(request.getAgentSource()); - String groupName = trim(request.getGroupName(), 128); - - Set allowedUserIds = pushEligibleUserIds(); - List memberIds = request.getMemberUserIds() != null ? request.getMemberUserIds() : List.of(); - List validMembers = memberIds.stream() - .filter(id -> id != null && allowedUserIds.contains(id)) - .distinct() - .toList(); - - EsignPrinterPushGroup group; - if (StringUtils.isNotBlank(request.getGroupId())) { - Optional opt = pushGroupRepository.findById(request.getGroupId().trim()); - if (opt.isEmpty()) { - return EsignPrinterPushGroupListResponse.fail("404", "推送组不存在"); - } - group = opt.get(); - group.setGroupName(groupName); - } else { - group = new EsignPrinterPushGroup(); - group.setGroupId(UUID.randomUUID().toString()); - group.setGroupName(groupName); - group.setAgentSource(agentSource); - } - pushGroupRepository.save(group); - - pushGroupMemberRepository.deleteByGroupId(group.getGroupId()); - for (Long uid : validMembers) { - EsignPrinterPushGroupMember member = new EsignPrinterPushGroupMember(); - member.setGroupId(group.getGroupId()); - member.setUserId(uid); - pushGroupMemberRepository.save(member); - } - - return listGroups(agentSource); - } - - @Transactional - public EsignPrinterPushGroupListResponse deleteGroup(String groupId, String agentSource) { - if (StringUtils.isBlank(groupId)) { - return EsignPrinterPushGroupListResponse.fail("400", "groupId required"); - } - pushGroupMemberRepository.deleteByGroupId(groupId.trim()); - pushGroupRepository.deleteById(groupId.trim()); - return listGroups(agentSource); - } - - /** - * 手动模式:仅写入推送目标,App 通过 WebSocket 即时通知 + 轮询兜底拉取,不发 FCM。 - * 自动模式:仅在规则开启 autoSendFcm 且调用方请求 sendFcm 时发送可选提醒。 - */ - private boolean shouldSendFcm(boolean requestedSend) { - if (!pushRuleService.isAutoMode()) { - return false; - } - return requestedSend && pushRuleService.isAutoSendFcm(); - } - - private void notifyPushTargetsViaWebSocket(EsignPrinterPdfTask task, Collection userIds) { - if (task == null || userIds == null || userIds.isEmpty()) { - return; - } - printerWebSocketHub.notifyPrinterPdfAssigned( - userIds, - task.getTaskId(), - task.getFileName(), - task.getAgentSource(), - tabletRegistryService.resolvePushClientIpFilters(userIds)); - } - - private void sendFcmForTask(EsignPrinterPdfTask task, List userIds) { - List targets = userIds.stream().map(String::valueOf).toList(); - if (targets.isEmpty()) { - return; - } - SendNotificationData data = new SendNotificationData(); - data.setTargetType(NotificationTargetType.ESIGN_USER); - data.setTargets(targets); - data.setTitle("新的登记卡 PDF"); - data.setBody(StringUtils.defaultIfBlank(task.getFileName(), "请打开 App 签收")); - data.setTriggerTask(TRIGGER_TASK); - data.setClickAction(CLICK_ACTION); - Map payload = new HashMap<>(); - payload.put("action", CLICK_ACTION); - payload.put("taskId", task.getTaskId()); - payload.put("taskType", EsignPrinterPdfTaskService.TASK_TYPE); - payload.put("fileName", task.getFileName()); - data.setData(payload); - try { - SendNotificationResponse response = notificationService.send(data, TRIGGER_TASK); - log.info( - "Printer PDF FCM taskId={}, targets={}, status={}", - task.getTaskId(), - targets.size(), - response.getData() != null ? response.getData().getResultStatus() : null); - } catch (Exception e) { - log.warn("Printer PDF FCM failed taskId={}", task.getTaskId(), e); - } - } - - /** 推送规则/手动推送可选的平板账号:启用中的 App 用户,不要求设备连接已审批。 */ - private Set pushEligibleUserIds() { - List ids = esignUserRepository.findByStatusOrderByLastLoginAtDesc(ACTIVE).stream() - .map(EsignUser::getId) - .filter(Objects::nonNull) - .toList(); - return new LinkedHashSet<>(userPresenceService.dedupeUserIdsByClientIp(ids)); - } - - private List groupsForAgent(String agentSource) { - String key = EsignPrinterDeviceLinkService.normalizeAgentSource(agentSource); - List direct = pushGroupRepository.findByAgentSourceOrderByGroupNameAsc(key); - if (!direct.isEmpty() || EsignPrinterDeviceLinkService.isUnidentifiedAgent(key)) { - return direct; - } - String ip = EsignPrinterDeviceLinkService.extractHostIp(key); - if (ip == null) { - return direct; - } - return pushGroupRepository.findAll().stream() - .filter(group -> EsignPrinterDeviceLinkService.sameHostIp(group.getAgentSource(), key)) - .sorted(Comparator.comparing(EsignPrinterPushGroup::getGroupName)) - .toList(); - } - - 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/printer/services/EsignPrinterTabletRegistryService.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/services/EsignPrinterTabletRegistryService.java deleted file mode 100644 index 9a81fa8..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/services/EsignPrinterTabletRegistryService.java +++ /dev/null @@ -1,404 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.services; - -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterTabletRegistryDeleteRequest; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterTabletRegistryResponse; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterTabletRegistrySaveRequest; -import com.sjmresorts.opsapp.esign.printer.models.EsignPrinterDeviceLink; -import com.sjmresorts.opsapp.esign.printer.models.EsignPrinterTabletRegistry; -import com.sjmresorts.opsapp.esign.printer.models.EsignPrinterTabletRegistryPc; -import com.sjmresorts.opsapp.esign.auth.models.EsignUser; -import com.sjmresorts.opsapp.esign.printer.repositories.EsignPrinterDeviceLinkRepository; -import com.sjmresorts.opsapp.esign.printer.repositories.EsignPrinterTabletRegistryPcRepository; -import com.sjmresorts.opsapp.esign.printer.repositories.EsignPrinterTabletRegistryRepository; -import com.sjmresorts.opsapp.esign.auth.repositories.EsignUserRepository; -import com.sjmresorts.opsapp.esign.auth.services.EsignUserPresenceService; -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.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.UUID; - -@Slf4j -@Service -public class EsignPrinterTabletRegistryService { - - public static final String CMS_REVIEWED_BY = "cms-tablet-registry"; - - @Resource - private EsignPrinterTabletRegistryRepository registryRepository; - - @Resource - private EsignPrinterTabletRegistryPcRepository bindingRepository; - - @Resource - private EsignPrinterDeviceLinkRepository linkRepository; - - @Resource - private EsignUserRepository esignUserRepository; - - @Transactional(readOnly = true) - public EsignPrinterTabletRegistryResponse listAll() { - EsignPrinterTabletRegistryResponse resp = EsignPrinterTabletRegistryResponse.ok("ok"); - resp.setItems(toItems(registryRepository.findAllByOrderByUserNameAsc())); - return resp; - } - - @Transactional(readOnly = true) - public List listAllOrdered() { - return registryRepository.findAllByOrderByUserNameAsc(); - } - - @Transactional(readOnly = true) - public List findAllByUserId(Long userId) { - if (userId == null) { - return List.of(); - } - return registryRepository.findAllByUserIdOrderByClientIpAsc(userId); - } - - @Transactional(readOnly = true) - public Optional matchRegistry(Long userId, String clientIp) { - if (userId == null) { - return Optional.empty(); - } - String ip = normalizeBoundClientIp(clientIp); - List enabled = - registryRepository.findAllByUserIdAndEnabledTrueOrderByClientIpAsc(userId); - if (enabled.isEmpty()) { - return Optional.empty(); - } - if (ip != null) { - for (EsignPrinterTabletRegistry reg : enabled) { - String bound = normalizeBoundClientIp(reg.getClientIp()); - if (bound != null && bound.equals(ip)) { - return Optional.of(reg); - } - } - } - List wildcards = enabled.stream() - .filter(reg -> StringUtils.isBlank(normalizeBoundClientIp(reg.getClientIp()))) - .toList(); - if (wildcards.size() == 1) { - return Optional.of(wildcards.get(0)); - } - return Optional.empty(); - } - - @Transactional(readOnly = true) - public boolean isRegisteredAndEnabled(Long userId, String clientIp) { - return matchRegistry(userId, clientIp).isPresent(); - } - - /** 若账号登记了具体 IP,推送 WS 时仅通知匹配 IP 的会话 */ - @Transactional(readOnly = true) - public Map> resolvePushClientIpFilters(Collection userIds) { - Map> map = new HashMap<>(); - if (userIds == null) { - return map; - } - for (Long userId : userIds) { - if (userId == null) { - continue; - } - List regs = - registryRepository.findAllByUserIdAndEnabledTrueOrderByClientIpAsc(userId); - Set ips = new LinkedHashSet<>(); - boolean hasWildcard = false; - for (EsignPrinterTabletRegistry reg : regs) { - String bound = normalizeBoundClientIp(reg.getClientIp()); - if (StringUtils.isBlank(bound)) { - hasWildcard = true; - } else { - ips.add(bound); - } - } - if (!hasWildcard && !ips.isEmpty()) { - map.put(userId, ips); - } - } - return map; - } - - /** - * 写入推送记录时展开目标 IP:有绑定 IP 登记则逐 IP 写入;有「不限 IP」登记则仅 wildcard; - * 无登记时优先用用户最近登录 IP。 - */ - @Transactional(readOnly = true) - public Set resolvePushTargetClientIpsForUser(Long userId) { - if (userId == null) { - return Set.of(); - } - List regs = - registryRepository.findAllByUserIdAndEnabledTrueOrderByClientIpAsc(userId); - if (regs.isEmpty()) { - return esignUserRepository.findById(userId) - .map(user -> { - String ip = normalizeBoundClientIp(user.getLastLoginIp()); - return ip != null ? Set.of(ip) : Set.of(""); - }) - .orElse(Set.of("")); - } - boolean hasWildcard = false; - Set ips = new LinkedHashSet<>(); - for (EsignPrinterTabletRegistry reg : regs) { - String bound = normalizeBoundClientIp(reg.getClientIp()); - if (StringUtils.isBlank(bound)) { - hasWildcard = true; - } else { - ips.add(bound); - } - } - if (hasWildcard) { - return Set.of(""); - } - return ips.isEmpty() ? Set.of("") : ips; - } - - public static String normalizeBoundClientIp(String clientIp) { - return EsignUserPresenceService.normalizeClientIp(clientIp); - } - - public static String boundClientIpOrEmpty(String clientIp) { - String ip = normalizeBoundClientIp(clientIp); - return ip != null ? ip : ""; - } - - @Transactional(readOnly = true) - public List listAgentSourcesForRegistry(String registryId) { - if (StringUtils.isBlank(registryId)) { - return List.of(); - } - return bindingRepository.findByRegistryIdOrderByAgentSourceAsc(registryId.trim()).stream() - .map(EsignPrinterTabletRegistryPc::getAgentSource) - .toList(); - } - - @Transactional - public EsignPrinterTabletRegistryResponse save(EsignPrinterTabletRegistrySaveRequest request) { - if (request == null) { - return EsignPrinterTabletRegistryResponse.fail("400", "请求体不能为空"); - } - String userName = trim(request.getUserName(), 50); - if (StringUtils.isBlank(userName)) { - return EsignPrinterTabletRegistryResponse.fail("400", "请填写 App 登录账号"); - } - - Optional userOpt = esignUserRepository.findByUserName(userName); - if (userOpt.isEmpty()) { - return EsignPrinterTabletRegistryResponse.fail( - "400", "App 账号不存在,请先在系统中创建该用户后再登记"); - } - EsignUser user = userOpt.get(); - if (!"ACTIVE".equalsIgnoreCase(user.getStatus())) { - return EsignPrinterTabletRegistryResponse.fail("403", "App 账号未启用"); - } - - String clientIp = boundClientIpOrEmpty(request.getClientIp()); - List agentSources = normalizeAgentSources(request.getAgentSources()); - boolean creating = StringUtils.isBlank(request.getRegistryId()); - EsignPrinterTabletRegistry row; - if (creating) { - if (registryRepository.existsByUserIdAndClientIp(user.getId(), clientIp)) { - return EsignPrinterTabletRegistryResponse.fail( - "409", StringUtils.isBlank(clientIp) ? "该账号已存在「不限 IP」登记" : "该账号在此 IP 下已登记"); - } - row = new EsignPrinterTabletRegistry(); - row.setRegistryId(UUID.randomUUID().toString()); - row.setCreatedBy(trim(request.getCreatedBy(), 64)); - } else { - Optional opt = registryRepository.findById(request.getRegistryId().trim()); - if (opt.isEmpty()) { - return EsignPrinterTabletRegistryResponse.fail("404", "登记记录不存在"); - } - row = opt.get(); - if (registryRepository.existsByUserIdAndClientIpAndRegistryIdNot(user.getId(), clientIp, row.getRegistryId())) { - return EsignPrinterTabletRegistryResponse.fail("409", "该账号在此 IP 下已有其他登记"); - } - } - - row.setUserName(userName); - row.setUserId(user.getId()); - row.setClientIp(clientIp); - row.setDisplayName(trim(request.getDisplayName(), 128)); - if (request.getEnabled() != null) { - row.setEnabled(request.getEnabled()); - } else if (creating) { - row.setEnabled(true); - } - row.setNotes(trim(request.getNotes(), 256)); - registryRepository.save(row); - - bindingRepository.deleteByRegistryId(row.getRegistryId()); - for (String agentSource : agentSources) { - EsignPrinterTabletRegistryPc binding = new EsignPrinterTabletRegistryPc(); - binding.setBindingId(UUID.randomUUID().toString()); - binding.setRegistryId(row.getRegistryId()); - binding.setAgentSource(agentSource); - bindingRepository.save(binding); - } - - syncDeviceLinksForUser(user.getId()); - - log.info( - "Printer tablet registry {} registryId={}, userName={}, clientIp={}, pcs={}", - creating ? "created" : "updated", - row.getRegistryId(), - userName, - StringUtils.isBlank(clientIp) ? "*" : clientIp, - agentSources.size()); - - EsignPrinterTabletRegistryResponse resp = - EsignPrinterTabletRegistryResponse.ok(creating ? "已添加平板登记" : "已更新平板登记"); - resp.setData(toItem(row, agentSources)); - return resp; - } - - @Transactional - public EsignPrinterTabletRegistryResponse delete(EsignPrinterTabletRegistryDeleteRequest request) { - if (request == null || StringUtils.isBlank(request.getRegistryId())) { - return EsignPrinterTabletRegistryResponse.fail("400", "registryId required"); - } - Optional opt = registryRepository.findById(request.getRegistryId().trim()); - if (opt.isEmpty()) { - return EsignPrinterTabletRegistryResponse.fail("404", "登记记录不存在"); - } - EsignPrinterTabletRegistry row = opt.get(); - Long userId = row.getUserId(); - bindingRepository.deleteByRegistryId(row.getRegistryId()); - registryRepository.delete(row); - if (userId != null) { - syncDeviceLinksForUser(userId); - } - log.info( - "Printer tablet registry deleted registryId={}, userName={}, clientIp={}", - row.getRegistryId(), - row.getUserName(), - row.getClientIp()); - return EsignPrinterTabletRegistryResponse.ok("已删除平板登记"); - } - - private void syncDeviceLinksForUser(Long userId) { - if (userId == null) { - return; - } - Optional userOpt = esignUserRepository.findById(userId); - if (userOpt.isEmpty()) { - return; - } - EsignUser user = userOpt.get(); - Set desired = new LinkedHashSet<>(); - for (EsignPrinterTabletRegistry reg : - registryRepository.findAllByUserIdAndEnabledTrueOrderByClientIpAsc(userId)) { - desired.addAll(listAgentSourcesForRegistry(reg.getRegistryId())); - } - List approved = - linkRepository.findByUserIdAndStatus(userId, EsignPrinterDeviceLinkService.STATUS_APPROVED); - for (String agentSource : desired) { - Optional existing = linkRepository.findFirstByUserIdAndAgentSourceAndStatusIn( - userId, - agentSource, - List.of( - EsignPrinterDeviceLinkService.STATUS_PENDING, - EsignPrinterDeviceLinkService.STATUS_APPROVED)); - if (existing.isPresent()) { - EsignPrinterDeviceLink link = existing.get(); - if (EsignPrinterDeviceLinkService.STATUS_PENDING.equals(link.getStatus())) { - link.setStatus(EsignPrinterDeviceLinkService.STATUS_APPROVED); - link.setReviewedAt(Instant.now()); - link.setReviewedBy(CMS_REVIEWED_BY); - linkRepository.save(link); - } - continue; - } - EsignPrinterDeviceLink link = new EsignPrinterDeviceLink(); - link.setLinkId(UUID.randomUUID().toString()); - link.setUserId(userId); - link.setAgentSource(agentSource); - link.setAppLabel(user.getUserName()); - link.setStatus(EsignPrinterDeviceLinkService.STATUS_APPROVED); - link.setRequestedAt(Instant.now()); - link.setReviewedAt(Instant.now()); - link.setReviewedBy(CMS_REVIEWED_BY); - linkRepository.save(link); - } - for (EsignPrinterDeviceLink link : approved) { - if (CMS_REVIEWED_BY.equals(link.getReviewedBy()) && !desired.contains(link.getAgentSource())) { - link.setStatus(EsignPrinterDeviceLinkService.STATUS_REVOKED); - link.setReviewedAt(Instant.now()); - linkRepository.save(link); - } - } - } - - private void syncDeviceLinks( - EsignPrinterTabletRegistry registry, EsignUser user, List agentSources) { - syncDeviceLinksForUser(user != null ? user.getId() : null); - } - - private void revokeCmsManagedLinks(Long userId) { - syncDeviceLinksForUser(userId); - } - - private List normalizeAgentSources(List raw) { - if (raw == null) { - return List.of(); - } - Set set = new LinkedHashSet<>(); - for (String item : raw) { - if (StringUtils.isBlank(item)) { - continue; - } - set.add(EsignPrinterDeviceLinkService.normalizeAgentSource(item)); - } - return new ArrayList<>(set); - } - - private List toItems( - List rows) { - List items = new ArrayList<>(); - for (EsignPrinterTabletRegistry row : rows) { - items.add(toItem(row, listAgentSourcesForRegistry(row.getRegistryId()))); - } - return items; - } - - private EsignPrinterTabletRegistryResponse.EsignPrinterTabletRegistryItem toItem( - EsignPrinterTabletRegistry row, List agentSources) { - EsignPrinterTabletRegistryResponse.EsignPrinterTabletRegistryItem item = - new EsignPrinterTabletRegistryResponse.EsignPrinterTabletRegistryItem(); - item.setRegistryId(row.getRegistryId()); - item.setUserName(row.getUserName()); - item.setUserId(row.getUserId()); - item.setClientIp(StringUtils.isBlank(row.getClientIp()) ? null : row.getClientIp()); - item.setDisplayName(row.getDisplayName()); - item.setEnabled(row.isEnabled()); - item.setNotes(row.getNotes()); - item.setCreatedBy(row.getCreatedBy()); - item.setCreatedAt(row.getCreatedAt() != null ? row.getCreatedAt().toString() : null); - item.setUpdatedAt(row.getUpdatedAt() != null ? row.getUpdatedAt().toString() : null); - item.setAgentSources(agentSources); - return item; - } - - 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/printer/services/LocalExeLaunchService.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/services/LocalExeLaunchService.java deleted file mode 100644 index 5222585..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/services/LocalExeLaunchService.java +++ /dev/null @@ -1,294 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.services; - -import com.sjmresorts.opsapp.esign.printer.dtos.LocalExeLaunchResult; -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.io.File; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; - -/** - * 在本机(运行 Spring Boot 的 Windows 机器)启动 exe,并尝试将窗口拉到前台。 - */ -@Slf4j -@Service -public class LocalExeLaunchService { - - private static final String ACTIVATE_PS1 = """ - param( - [string]$ProcessName = "auto-check-in", - [int]$InitialWaitMs = 2000, - [int]$PollTimeoutMs = 15000 - ) - Start-Sleep -Milliseconds $InitialWaitMs - $deadline = (Get-Date).AddMilliseconds($PollTimeoutMs) - $proc = $null - while ((Get-Date) -lt $deadline) { - $proc = Get-Process -Name $ProcessName -ErrorAction SilentlyContinue | - Sort-Object StartTime -Descending | Select-Object -First 1 - if ($proc -and $proc.MainWindowHandle -ne [IntPtr]::Zero) { break } - Start-Sleep -Milliseconds 300 - } - if (-not $proc) { Write-Error "process not found: $ProcessName"; exit 1 } - if ($proc.MainWindowHandle -eq [IntPtr]::Zero) { Write-Error "no main window"; exit 2 } - Add-Type @' - using System; - using System.Runtime.InteropServices; - public class Win32Foreground { - [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); - [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd); - [DllImport("user32.dll")] public static extern bool BringWindowToTop(IntPtr hWnd); - [DllImport("user32.dll")] public static extern bool AllowSetForegroundWindow(int processId); - } - '@ - [void][Win32Foreground]::AllowSetForegroundWindow($proc.Id) - [void][Win32Foreground]::ShowWindow($proc.MainWindowHandle, 9) - [void][Win32Foreground]::BringWindowToTop($proc.MainWindowHandle) - [void][Win32Foreground]::SetForegroundWindow($proc.MainWindowHandle) - exit 0 - """; - - @Value("${ops-app.deeplink.local-exe.enabled:false}") - private boolean enabled; - - @Value("${ops-app.deeplink.local-exe.path:}") - private String exePath; - - @Value("${ops-app.deeplink.local-exe.working-dir:}") - private String workingDir; - - @Value("${ops-app.deeplink.local-exe.args:}") - private String extraArgs; - - @Value("${ops-app.deeplink.local-exe.launch-via:auto}") - private String launchVia; - - @Value("${ops-app.deeplink.local-exe.activate-window:true}") - private boolean activateWindow; - - @Value("${ops-app.deeplink.local-exe.activate-wait-ms:2000}") - private int activateWaitMs; - - @Value("${ops-app.deeplink.local-exe.activate-poll-ms:15000}") - private int activatePollMs; - - public LocalExeLaunchResult launchIfEnabled() { - if (!enabled) { - return LocalExeLaunchResult.skipped("local-exe.enabled=false,使用浏览器 myapp://"); - } - if (StringUtils.isBlank(exePath)) { - return LocalExeLaunchResult.failed("未配置 ops-app.deeplink.local-exe.path"); - } - - File exe = new File(exePath.trim()); - if (!exe.isFile()) { - return LocalExeLaunchResult.failed("exe 不存在: " + exe.getAbsolutePath()); - } - - File workDir = resolveWorkingDir(exe); - List args = parseArgs(); - String via = StringUtils.defaultIfBlank(launchVia, "auto").trim().toLowerCase(); - - try { - String detail = launchWithMode(via, exe, workDir, args); - if (activateWindow) { - activateWindowToForeground(exe); - detail = detail + "+activate"; - } - log.info("本机 exe 已启动 ({}): {}", detail, exe.getAbsolutePath()); - return LocalExeLaunchResult.success(exe.getAbsolutePath() + " (" + detail + ")"); - } catch (Exception e) { - log.error("本机 exe 启动失败: {}", exe.getAbsolutePath(), e); - return LocalExeLaunchResult.failed(e.getMessage()); - } - } - - private String launchWithMode(String via, File exe, File workDir, List args) throws Exception { - return switch (via) { - case "direct" -> { - startDirect(exe, workDir, args); - yield "direct"; - } - case "windows-start" -> { - startViaWindowsCmd(exe, workDir, args, false); - yield "windows-start"; - } - case "windows-start-max" -> { - startViaWindowsCmd(exe, workDir, args, true); - yield "windows-start-max"; - } - case "powershell" -> { - startViaPowerShell(exe, workDir, args); - yield "powershell"; - } - case "explorer" -> { - startViaExplorer(exe); - yield "explorer"; - } - default -> launchAuto(exe, workDir, args); - }; - } - - private String launchAuto(File exe, File workDir, List args) throws Exception { - Exception last = null; - for (String mode : List.of("explorer", "windows-start-max", "direct")) { - try { - return launchWithMode(mode, exe, workDir, args); - } catch (Exception e) { - last = e; - log.warn("launch-via={} 失败,尝试下一种: {}", mode, e.getMessage()); - } - } - throw last != null ? last : new IOException("auto 启动全部失败"); - } - - private void activateWindowToForeground(File exe) { - String processName = exe.getName().replaceAll("(?i)\\.exe$", ""); - Thread activator = new Thread(() -> { - File scriptFile = null; - try { - scriptFile = Files.createTempFile("auto-check-in-activate-", ".ps1").toFile(); - Files.writeString(scriptFile.toPath(), ACTIVATE_PS1, StandardCharsets.UTF_8); - ProcessBuilder builder = new ProcessBuilder( - "powershell.exe", - "-NoProfile", - "-ExecutionPolicy", - "Bypass", - "-File", - scriptFile.getAbsolutePath(), - "-ProcessName", - processName, - "-InitialWaitMs", - String.valueOf(activateWaitMs), - "-PollTimeoutMs", - String.valueOf(activatePollMs)); - builder.redirectErrorStream(true); - Process process = builder.start(); - String output = new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8); - if (!process.waitFor(activatePollMs + activateWaitMs + 5000L, TimeUnit.MILLISECONDS)) { - process.destroyForcibly(); - log.warn("前台激活超时: {}", processName); - return; - } - if (process.exitValue() == 0) { - log.info("前台激活成功: {}", processName); - } else { - log.warn("前台激活失败 exit={} output={}", process.exitValue(), output.trim()); - } - } catch (Exception e) { - log.warn("前台激活异常: {}", e.getMessage()); - } finally { - if (scriptFile != null && scriptFile.exists() && !scriptFile.delete()) { - scriptFile.deleteOnExit(); - } - } - }, "exe-window-activator"); - activator.setDaemon(true); - activator.start(); - } - - private void startViaPowerShell(File exe, File workDir, List args) throws Exception { - StringBuilder ps = new StringBuilder(); - ps.append("$p=@{FilePath='").append(escapePowerShell(exe.getAbsolutePath())).append("';WindowStyle='Normal'};"); - if (workDir != null) { - ps.append("$p.WorkingDirectory='").append(escapePowerShell(workDir.getAbsolutePath())).append("';"); - } - if (!args.isEmpty()) { - ps.append("$p.ArgumentList=").append(toPowerShellStringArray(args)).append(";"); - } - ps.append("Start-Process @p"); - runAndCheck(List.of( - "powershell.exe", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", ps.toString())); - } - - private void startViaExplorer(File exe) throws IOException { - new ProcessBuilder("explorer.exe", exe.getAbsolutePath()).start(); - } - - private void startViaWindowsCmd(File exe, File workDir, List args, boolean maximized) throws IOException { - List command = new ArrayList<>(); - command.add("cmd.exe"); - command.add("/c"); - command.add("start"); - if (maximized) { - command.add("/MAX"); - } - command.add(""); - if (workDir != null) { - command.add("/D"); - command.add(workDir.getAbsolutePath()); - } - command.add(exe.getAbsolutePath()); - command.addAll(args); - new ProcessBuilder(command).start(); - } - - private void startDirect(File exe, File workDir, List args) throws IOException { - List command = new ArrayList<>(); - command.add(exe.getAbsolutePath()); - command.addAll(args); - ProcessBuilder builder = new ProcessBuilder(command); - if (workDir != null) { - builder.directory(workDir); - } - builder.start(); - } - - private void runAndCheck(List command) throws Exception { - ProcessBuilder builder = new ProcessBuilder(command); - builder.redirectErrorStream(true); - Process process = builder.start(); - String output = new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8); - if (!process.waitFor(15, TimeUnit.SECONDS)) { - process.destroyForcibly(); - throw new IOException("启动命令超时: " + String.join(" ", command)); - } - if (process.exitValue() != 0) { - throw new IOException("启动命令失败 exit=" + process.exitValue() + " output=" + output.trim()); - } - } - - private File resolveWorkingDir(File exe) { - if (StringUtils.isNotBlank(workingDir)) { - File dir = new File(workingDir.trim()); - if (dir.isDirectory()) { - return dir; - } - log.warn("local-exe working-dir 无效,忽略: {}", workingDir); - } - File parent = exe.getParentFile(); - return parent != null && parent.isDirectory() ? parent : null; - } - - private List parseArgs() { - List args = new ArrayList<>(); - if (StringUtils.isNotBlank(extraArgs)) { - Arrays.stream(extraArgs.split("\\s+")) - .filter(StringUtils::isNotBlank) - .forEach(args::add); - } - return args; - } - - private static String escapePowerShell(String value) { - return String.valueOf(value).replace("'", "''"); - } - - private static String toPowerShellStringArray(List args) { - if (args.isEmpty()) { - return "@()"; - } - return "@(" + args.stream() - .map(a -> "'" + escapePowerShell(a) + "'") - .collect(Collectors.joining(",")) + ")"; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/services/RegistrationCardMetaExtractService.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/services/RegistrationCardMetaExtractService.java deleted file mode 100644 index 0fabdf9..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/services/RegistrationCardMetaExtractService.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.services; - -import com.sjmresorts.opsapp.esign.printer.dtos.RegistrationCardMeta; -import com.sjmresorts.opsapp.esign.printer.dtos.RegistrationCardMetaExtractor; -import com.sjmresorts.opsapp.esign.printer.support.RegistrationConfirmationOcrExtractor; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.apache.pdfbox.Loader; -import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.text.PDFTextStripper; -import org.springframework.stereotype.Service; - -import java.util.Base64; -import java.util.Optional; - -@Slf4j -@Service -@RequiredArgsConstructor -public class RegistrationCardMetaExtractService { - - private final RegistrationConfirmationOcrExtractor ocrExtractor; - - public RegistrationCardMeta extractFromPdfBase64(String pdfBase64) { - if (StringUtils.isBlank(pdfBase64)) { - return RegistrationCardMeta.builder().build(); - } - try { - byte[] bytes = Base64.getDecoder().decode(normalizeBase64(pdfBase64)); - return extractFromPdfBytes(bytes); - } catch (IllegalArgumentException e) { - return RegistrationCardMeta.builder().build(); - } - } - - public RegistrationCardMeta extractFromPdfBytes(byte[] pdfBytes) { - if (pdfBytes == null || pdfBytes.length == 0) { - return RegistrationCardMeta.builder().build(); - } - RegistrationCardMeta textMeta = extractPdfTextLayer(pdfBytes) - .map(RegistrationCardMetaExtractor::extractFromText) - .orElseGet(() -> RegistrationCardMeta.builder().build()); - - if (!ocrExtractor.isOcrEnabled()) { - log.info("registration meta OCR disabled, textLayerFields={}", textMeta.hasAnyField()); - return textMeta; - } - if (!ocrExtractor.isOcrReady()) { - log.warn("registration meta OCR unavailable: {}", ocrExtractor.getUnavailableReason()); - return textMeta; - } - - Optional ocrText = ocrExtractor.extractFullTextFromPdfBytes(pdfBytes); - RegistrationCardMeta ocrMeta = ocrText - .map(RegistrationCardMetaExtractor::extractFromText) - .orElseGet(() -> RegistrationCardMeta.builder().build()); - RegistrationCardMeta merged = textMeta.mergedWith(ocrMeta); - log.info( - "registration meta merged guest={}, conf={}, room={}, email={}, checkIn={}, checkOut={}", - merged.getGuestName(), - merged.getConfirmationNo(), - merged.getRoomNo(), - merged.getEmail(), - merged.getCheckInDate(), - merged.getCheckOutDate()); - return merged; - } - - private static Optional extractPdfTextLayer(byte[] pdfBytes) { - try (PDDocument doc = Loader.loadPDF(pdfBytes)) { - PDFTextStripper stripper = new PDFTextStripper(); - stripper.setStartPage(1); - stripper.setEndPage(Math.min(doc.getNumberOfPages(), 2)); - return Optional.ofNullable(stripper.getText(doc)).filter(StringUtils::isNotBlank); - } catch (Exception e) { - 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/printer/support/EsignOhipUploadRetryScheduler.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/support/EsignOhipUploadRetryScheduler.java deleted file mode 100644 index 33cfefa..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/support/EsignOhipUploadRetryScheduler.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.support; - -import com.sjmresorts.opsapp.esign.printer.models.EsignPrinterPdfTask; -import com.sjmresorts.opsapp.esign.printer.repositories.EsignPrinterPdfTaskRepository; -import com.sjmresorts.opsapp.esign.printer.services.EsignPrinterPdfOhipUploadService; -import com.sjmresorts.opsapp.esign.reservation.dtos.SignedRegistrationCardUploadResult; -import jakarta.annotation.Resource; -import lombok.extern.slf4j.Slf4j; -import org.springframework.data.domain.PageRequest; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Component; - -import java.util.List; - -/** 扫描 PENDING/FAILED 的已签名任务,从归档 signed.pdf 重试 OPERA 上传。 */ -@Slf4j -@Component -public class EsignOhipUploadRetryScheduler { - - private static final int BATCH_SIZE = 20; - - @Resource - private EsignPrinterPdfTaskRepository printerPdfTaskRepository; - - @Resource - private EsignPrinterPdfOhipUploadService ohipUploadService; - - @Scheduled(fixedDelayString = "${ops-app.esign.ohip-retry.fixed-delay-ms:300000}") - public void retryPendingUploads() { - List tasks = printerPdfTaskRepository.findOhipRetryCandidates( - PageRequest.of(0, BATCH_SIZE)); - if (tasks.isEmpty()) { - return; - } - log.info("OHIP retry scheduler processing {} task(s)", tasks.size()); - for (EsignPrinterPdfTask task : tasks) { - if (task == null || task.getTaskId() == null) { - continue; - } - try { - SignedRegistrationCardUploadResult result = ohipUploadService.uploadFromArchive( - task.getTaskId(), - task.getOcrConfirmationNo(), - task.getSignFieldsJson(), - null, - null, - true, - false); - log.info( - "OHIP retry taskId={}, success={}, statusCode={}, retryCount={}", - task.getTaskId(), - result.isSuccess(), - result.getStatusCode(), - task.getOhipRetryCount()); - if (!result.isSuccess() - && !EsignPrinterPdfOhipUploadService.isRetryable(result)) { - log.info("OHIP retry stop (non-retryable) taskId={}", task.getTaskId()); - } - } catch (Exception e) { - log.warn("OHIP retry failed taskId={}", task.getTaskId(), e); - } - } - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/support/EsignPrinterOhipStatus.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/support/EsignPrinterOhipStatus.java deleted file mode 100644 index 0ff4358..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/support/EsignPrinterOhipStatus.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.support; - -/** OPERA/OHIP 上传状态(持久化于 esign_printer_pdf_tasks.ohip_status)。 */ -public final class EsignPrinterOhipStatus { - - public static final String PENDING = "PENDING"; - public static final String SUCCESS = "SUCCESS"; - public static final String FAILED = "FAILED"; - public static final String SKIPPED = "SKIPPED"; - - public static final int MAX_RETRY = 5; - - private EsignPrinterOhipStatus() {} -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/support/EsignPrinterPdfArchiveListQuery.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/support/EsignPrinterPdfArchiveListQuery.java deleted file mode 100644 index 550de0f..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/support/EsignPrinterPdfArchiveListQuery.java +++ /dev/null @@ -1,130 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.support; - -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterPdfArchiveListRow; -import com.sjmresorts.opsapp.esign.printer.filters.EsignPrinterPdfArchiveFilter; -import com.sjmresorts.opsapp.esign.printer.models.EsignPrinterPdfTask; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterPdfArchiveSpecs; -import jakarta.persistence.EntityManager; -import jakarta.persistence.Tuple; -import jakarta.persistence.TypedQuery; -import jakarta.persistence.criteria.CriteriaBuilder; -import jakarta.persistence.criteria.CriteriaQuery; -import jakarta.persistence.criteria.Predicate; -import jakarta.persistence.criteria.Root; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.domain.Specification; -import org.springframework.stereotype.Component; - -import java.time.Instant; -import java.util.List; - -/** - * 归档列表查询:只 SELECT 元数据列,避免加载 pdf_base64 / signed_pdf_base64 等大字段。 - */ -@Component -@RequiredArgsConstructor -public class EsignPrinterPdfArchiveListQuery { - - private final EntityManager entityManager; - - public long count(EsignPrinterPdfArchiveFilter filter) { - CriteriaBuilder cb = entityManager.getCriteriaBuilder(); - CriteriaQuery cq = cb.createQuery(Long.class); - Root root = cq.from(EsignPrinterPdfTask.class); - cq.select(cb.count(root)); - applyWhere(filter, root, cq, cb); - return entityManager.createQuery(cq).getSingleResult(); - } - - public List findPage( - EsignPrinterPdfArchiveFilter filter, Pageable pageable) { - CriteriaBuilder cb = entityManager.getCriteriaBuilder(); - CriteriaQuery cq = cb.createTupleQuery(); - Root root = cq.from(EsignPrinterPdfTask.class); - cq.multiselect( - root.get("taskId").alias("taskId"), - root.get("externalId").alias("externalId"), - root.get("fileName").alias("fileName"), - root.get("fileSize").alias("fileSize"), - root.get("hotelId").alias("hotelId"), - root.get("status").alias("status"), - root.get("createdAt").alias("createdAt"), - root.get("completedAt").alias("completedAt"), - root.get("signedAt").alias("signedAt"), - root.get("archivedAt").alias("archivedAt"), - root.get("agentSource").alias("agentSource"), - root.get("signedByUserId").alias("signedByUserId"), - root.get("signedClientIp").alias("signedClientIp"), - root.get("signedDeviceId").alias("signedDeviceId"), - root.get("signedDeviceName").alias("signedDeviceName"), - root.get("ocrGuestName").alias("ocrGuestName"), - root.get("ocrConfirmationNo").alias("ocrConfirmationNo"), - root.get("ocrPhone").alias("ocrPhone"), - root.get("ocrEmail").alias("ocrEmail"), - root.get("ocrRoomNo").alias("ocrRoomNo"), - root.get("ocrCheckInDate").alias("ocrCheckInDate"), - root.get("ocrCheckOutDate").alias("ocrCheckOutDate"), - root.get("ocrStatus").alias("ocrStatus"), - root.get("ocrExtractedAt").alias("ocrExtractedAt"), - root.get("ohipStatus").alias("ohipStatus"), - root.get("ohipLastError").alias("ohipLastError"), - root.get("ohipUploadedAt").alias("ohipUploadedAt"), - root.get("ohipConfirmationNo").alias("ohipConfirmationNo"), - root.get("ohipReservationId").alias("ohipReservationId"), - root.get("ohipRetryCount").alias("ohipRetryCount")); - applyWhere(filter, root, cq, cb); - cq.orderBy(cb.desc(EsignPrinterPdfArchiveSpecs.archiveSortTime(root, cb))); - - TypedQuery query = entityManager.createQuery(cq); - query.setFirstResult((int) pageable.getOffset()); - query.setMaxResults(pageable.getPageSize()); - return query.getResultList().stream().map(this::mapRow).toList(); - } - - private static void applyWhere( - EsignPrinterPdfArchiveFilter filter, - Root root, - CriteriaQuery cq, - CriteriaBuilder cb) { - Specification spec = EsignPrinterPdfArchiveSpecs.archived(filter); - Predicate predicate = spec.toPredicate(root, cq, cb); - if (predicate != null) { - cq.where(predicate); - } - } - - private EsignPrinterPdfArchiveListRow mapRow(Tuple tuple) { - return new EsignPrinterPdfArchiveListRow( - tuple.get("taskId", String.class), - tuple.get("externalId", String.class), - tuple.get("fileName", String.class), - tuple.get("fileSize", Long.class), - tuple.get("hotelId", String.class), - tuple.get("status", String.class), - tuple.get("createdAt", Instant.class), - tuple.get("completedAt", Instant.class), - tuple.get("signedAt", Instant.class), - tuple.get("archivedAt", Instant.class), - tuple.get("agentSource", String.class), - tuple.get("signedByUserId", Long.class), - tuple.get("signedClientIp", String.class), - tuple.get("signedDeviceId", String.class), - tuple.get("signedDeviceName", String.class), - tuple.get("ocrGuestName", String.class), - tuple.get("ocrConfirmationNo", String.class), - tuple.get("ocrPhone", String.class), - tuple.get("ocrEmail", String.class), - tuple.get("ocrRoomNo", String.class), - tuple.get("ocrCheckInDate", String.class), - tuple.get("ocrCheckOutDate", String.class), - tuple.get("ocrStatus", String.class), - tuple.get("ocrExtractedAt", Instant.class), - tuple.get("ohipStatus", String.class), - tuple.get("ohipLastError", String.class), - tuple.get("ohipUploadedAt", Instant.class), - tuple.get("ohipConfirmationNo", String.class), - tuple.get("ohipReservationId", String.class), - tuple.get("ohipRetryCount", Integer.class)); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/support/EsignPrinterPdfArchiveOcrBatchQuery.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/support/EsignPrinterPdfArchiveOcrBatchQuery.java deleted file mode 100644 index d52ae67..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/support/EsignPrinterPdfArchiveOcrBatchQuery.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.support; - -import com.sjmresorts.opsapp.esign.printer.filters.EsignPrinterPdfArchiveFilter; -import com.sjmresorts.opsapp.esign.printer.models.EsignPrinterPdfTask; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterPdfArchiveSpecs; -import com.sjmresorts.opsapp.esign.printer.services.EsignPrinterPdfArchiveOcrService; -import jakarta.persistence.EntityManager; -import jakarta.persistence.TypedQuery; -import jakarta.persistence.criteria.CriteriaBuilder; -import jakarta.persistence.criteria.CriteriaQuery; -import jakarta.persistence.criteria.Predicate; -import jakarta.persistence.criteria.Root; -import lombok.RequiredArgsConstructor; -import org.springframework.data.jpa.domain.Specification; -import org.springframework.stereotype.Component; - -import java.util.List; - -@Component -@RequiredArgsConstructor -public class EsignPrinterPdfArchiveOcrBatchQuery { - - private final EntityManager entityManager; - - public long countTaskIds(EsignPrinterPdfArchiveFilter filter, boolean failedOnly) { - CriteriaBuilder cb = entityManager.getCriteriaBuilder(); - CriteriaQuery cq = cb.createQuery(Long.class); - Root root = cq.from(EsignPrinterPdfTask.class); - cq.select(cb.count(root)); - Predicate predicate = buildPredicate(filter, failedOnly, root, cq, cb); - if (predicate != null) { - cq.where(predicate); - } - return entityManager.createQuery(cq).getSingleResult(); - } - - public List findTaskIds( - EsignPrinterPdfArchiveFilter filter, boolean failedOnly, int page, int size) { - CriteriaBuilder cb = entityManager.getCriteriaBuilder(); - CriteriaQuery cq = cb.createQuery(String.class); - Root root = cq.from(EsignPrinterPdfTask.class); - cq.select(root.get("taskId")); - Predicate predicate = buildPredicate(filter, failedOnly, root, cq, cb); - if (predicate != null) { - cq.where(predicate); - } - cq.orderBy(cb.desc(EsignPrinterPdfArchiveSpecs.archiveSortTime(root, cb))); - TypedQuery query = entityManager.createQuery(cq); - query.setFirstResult(Math.max(page, 0) * size); - query.setMaxResults(size); - return query.getResultList(); - } - - private static Predicate buildPredicate( - EsignPrinterPdfArchiveFilter filter, - boolean failedOnly, - Root root, - CriteriaQuery cq, - CriteriaBuilder cb) { - Specification spec = EsignPrinterPdfArchiveSpecs.archived(filter); - if (failedOnly) { - spec = spec.and(ocrNeedsRetry()); - } - return spec.toPredicate(root, cq, cb); - } - - private static Specification ocrNeedsRetry() { - return (root, query, cb) -> cb.or( - cb.isNull(root.get("ocrStatus")), - cb.equal(root.get("ocrStatus"), EsignPrinterPdfArchiveOcrService.OCR_FAILED), - cb.equal(root.get("ocrStatus"), EsignPrinterPdfArchiveOcrService.OCR_SKIPPED), - cb.equal(root.get("ocrStatus"), EsignPrinterPdfArchiveOcrService.OCR_PENDING)); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/support/EsignPrinterPdfArchiveOcrBatchRunner.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/support/EsignPrinterPdfArchiveOcrBatchRunner.java deleted file mode 100644 index 882e55a..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/support/EsignPrinterPdfArchiveOcrBatchRunner.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.support; - -import com.sjmresorts.opsapp.esign.printer.filters.EsignPrinterPdfArchiveFilter; -import com.sjmresorts.opsapp.esign.printer.services.EsignPrinterPdfArchiveOcrBatchService; -import jakarta.annotation.Resource; -import org.springframework.scheduling.annotation.Async; -import org.springframework.stereotype.Component; - -@Component -public class EsignPrinterPdfArchiveOcrBatchRunner { - - @Resource - private EsignPrinterPdfArchiveOcrBatchService batchService; - - @Async - public void runAsync(String jobId, EsignPrinterPdfArchiveFilter filter, boolean failedOnly) { - batchService.executeBatch(jobId, filter, failedOnly); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/support/EsignPrinterPdfArchiveOcrEventListener.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/support/EsignPrinterPdfArchiveOcrEventListener.java deleted file mode 100644 index b921aa0..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/support/EsignPrinterPdfArchiveOcrEventListener.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.support; - -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterPdfArchiveOcrEvent; -import com.sjmresorts.opsapp.esign.printer.services.EsignPrinterPdfArchiveOcrService; -import jakarta.annotation.Resource; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.springframework.scheduling.annotation.Async; -import org.springframework.stereotype.Component; -import org.springframework.transaction.event.TransactionPhase; -import org.springframework.transaction.event.TransactionalEventListener; - -/** - * 异步 OCR 监听器:独立 Bean,避免与 {@link EsignPrinterPdfArchiveOcrService} 循环依赖。 - */ -@Slf4j -@Component -public class EsignPrinterPdfArchiveOcrEventListener { - - @Resource - private EsignPrinterPdfArchiveOcrService archiveOcrService; - - @Async - @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT, fallbackExecution = true) - public void onArchiveOcrRequested(EsignPrinterPdfArchiveOcrEvent event) { - if (event == null || StringUtils.isBlank(event.taskId())) { - return; - } - try { - archiveOcrService.extractAndPersist(event.taskId().trim()); - } catch (Exception e) { - log.warn("archive OCR async failed taskId={}", event.taskId(), e); - } - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/support/EsignPrinterPdfFileStorage.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/support/EsignPrinterPdfFileStorage.java deleted file mode 100644 index e071271..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/support/EsignPrinterPdfFileStorage.java +++ /dev/null @@ -1,283 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.support; - -import com.sjmresorts.opsapp.esign.printer.models.EsignPrinterPdfTask; -import jakarta.annotation.PostConstruct; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Base64; -import java.util.Locale; -import java.util.Optional; - -/** - * Printer PDF 本地磁盘存储。DB 字段存 {@code stored://{taskId}/original|signed} 引用,兼容遗留 Base64。 - */ -@Slf4j -@Component -public class EsignPrinterPdfFileStorage { - - public static final String STORED_PREFIX = "stored://"; - private static final String ORIGINAL_SUFFIX = "/original"; - private static final String SIGNED_SUFFIX = "/signed"; - - @Value("${ops-app.esign.printer-pdf.storage-dir:C:/auto-check-in/printer-pdf}") - private String storageDir; - - @Value("${ops-app.esign.printer-pdf.store-on-disk:true}") - private boolean storeOnDisk; - - private Path storageRoot; - - @PostConstruct - void init() throws IOException { - storageRoot = Paths.get(storageDir).toAbsolutePath().normalize(); - Files.createDirectories(storageRoot); - log.info("Printer PDF storage root={}, onDisk={}", storageRoot, storeOnDisk); - } - - public Path getStorageRoot() { - return storageRoot; - } - - public boolean isStoreOnDisk() { - return storeOnDisk; - } - - public static boolean isStoredReference(String value) { - return StringUtils.isNotBlank(value) && value.trim().startsWith(STORED_PREFIX); - } - - public String originalRef(String taskId) { - return STORED_PREFIX + sanitizeTaskId(taskId) + ORIGINAL_SUFFIX; - } - - public String signedRef(String taskId) { - return STORED_PREFIX + sanitizeTaskId(taskId) + SIGNED_SUFFIX; - } - - /** 写入原始 PDF,返回 DB 引用;关闭落盘时退回 Base64 字符串。 */ - public String persistOriginal(String taskId, byte[] pdfBytes) throws IOException { - if (pdfBytes == null || pdfBytes.length == 0) { - throw new IOException("empty PDF"); - } - if (!storeOnDisk) { - return Base64.getEncoder().encodeToString(pdfBytes); - } - writeOriginal(sanitizeTaskId(taskId), pdfBytes); - return originalRef(taskId); - } - - /** 写入签名 PDF,返回 DB 引用。 */ - public String persistSigned(String taskId, byte[] pdfBytes) throws IOException { - if (pdfBytes == null || pdfBytes.length == 0) { - throw new IOException("empty signed PDF"); - } - if (!storeOnDisk) { - return Base64.getEncoder().encodeToString(pdfBytes); - } - writeSigned(sanitizeTaskId(taskId), pdfBytes); - return signedRef(taskId); - } - - /** 从 normalized Base64 落盘并返回引用(新任务提交用)。 */ - public String persistOriginalFromBase64(String taskId, String normalizedBase64) throws IOException { - byte[] bytes = decodeBase64(normalizedBase64); - if (bytes == null || bytes.length == 0) { - throw new IOException("invalid PDF base64"); - } - return persistOriginal(taskId, bytes); - } - - public String persistSignedFromBase64(String taskId, String normalizedBase64) throws IOException { - byte[] bytes = decodeBase64(normalizedBase64); - if (bytes == null || bytes.length == 0) { - throw new IOException("invalid signed PDF base64"); - } - return persistSigned(taskId, bytes); - } - - public Optional readOriginalBytes(EsignPrinterPdfTask task) { - if (task == null) { - return Optional.empty(); - } - return readPdfField(task.getTaskId(), task.getPdfBase64(), PdfKind.ORIGINAL); - } - - public Optional readSignedBytes(EsignPrinterPdfTask task) { - if (task == null) { - return Optional.empty(); - } - return readPdfField(task.getTaskId(), task.getSignedPdfBase64(), PdfKind.SIGNED); - } - - /** 供合成签名 PDF / OHIP 上传:优先读磁盘,兼容 DB 内 Base64。 */ - public String materializeOriginalBase64(EsignPrinterPdfTask task) { - return readOriginalBytes(task) - .map(bytes -> Base64.getEncoder().encodeToString(bytes)) - .orElse(null); - } - - public String materializeSignedBase64(EsignPrinterPdfTask task) { - return readSignedBytes(task) - .map(bytes -> Base64.getEncoder().encodeToString(bytes)) - .orElse(null); - } - - public boolean hasOriginal(EsignPrinterPdfTask task) { - if (task == null) { - return false; - } - if (StringUtils.isNotBlank(task.getPdfBase64())) { - if (isStoredReference(task.getPdfBase64())) { - return hasOriginalFile(task.getTaskId()); - } - return true; - } - return hasOriginalFile(task.getTaskId()); - } - - public boolean hasOriginalFile(String taskId) { - if (StringUtils.isBlank(taskId)) { - return false; - } - try { - return Files.isRegularFile(taskDir(sanitizeTaskId(taskId)).resolve("original.pdf")); - } catch (IllegalArgumentException e) { - return false; - } - } - - public boolean hasSignedFile(String taskId) { - if (StringUtils.isBlank(taskId)) { - return false; - } - try { - return Files.isRegularFile(taskDir(sanitizeTaskId(taskId)).resolve("signed.pdf")); - } catch (IllegalArgumentException e) { - return false; - } - } - - public boolean hasSigned(EsignPrinterPdfTask task) { - if (task == null) { - return false; - } - if (StringUtils.isNotBlank(task.getSignedPdfBase64())) { - if (isStoredReference(task.getSignedPdfBase64())) { - return hasSignedFile(task.getTaskId()); - } - return true; - } - // 仅有签名时间/图层元数据不算已归档 PDF(OPERA 必须能读到 signed.pdf) - return hasSignedFile(task.getTaskId()); - } - - /** OCR 用:优先签名 PDF,否则原始 PDF。 */ - public Optional readArchivePdfBytes(EsignPrinterPdfTask task) { - Optional signed = readSignedBytes(task); - if (signed.isPresent()) { - return signed; - } - return readOriginalBytes(task); - } - - private void writeOriginal(String taskId, byte[] bytes) throws IOException { - Path dir = taskDir(taskId); - Files.createDirectories(dir); - Path target = dir.resolve("original.pdf"); - Files.write(target, bytes); - } - - private void writeSigned(String taskId, byte[] bytes) throws IOException { - Path dir = taskDir(taskId); - Files.createDirectories(dir); - Path target = dir.resolve("signed.pdf"); - Files.write(target, bytes); - } - - private Optional readPdfField(String taskId, String fieldValue, PdfKind kind) { - if (StringUtils.isBlank(fieldValue)) { - return Optional.empty(); - } - String trimmed = fieldValue.trim(); - if (isStoredReference(trimmed)) { - Optional path = resolveStoredPath(trimmed); - if (path.isEmpty()) { - return Optional.empty(); - } - try { - byte[] bytes = Files.readAllBytes(path.get()); - return bytes.length > 0 ? Optional.of(bytes) : Optional.empty(); - } catch (IOException e) { - log.warn("read stored PDF failed taskId={}, ref={}", taskId, trimmed, e); - return Optional.empty(); - } - } - byte[] decoded = decodeBase64(trimmed); - return decoded != null && decoded.length > 0 ? Optional.of(decoded) : Optional.empty(); - } - - private Optional resolveStoredPath(String ref) { - if (!isStoredReference(ref)) { - return Optional.empty(); - } - String rest = ref.substring(STORED_PREFIX.length()); - int slash = rest.lastIndexOf('/'); - if (slash <= 0 || slash >= rest.length() - 1) { - return Optional.empty(); - } - String taskId = rest.substring(0, slash); - String kind = rest.substring(slash + 1).toLowerCase(Locale.ROOT); - try { - sanitizeTaskId(taskId); - } catch (IllegalArgumentException e) { - return Optional.empty(); - } - Path file = "signed".equals(kind) - ? taskDir(taskId).resolve("signed.pdf") - : taskDir(taskId).resolve("original.pdf"); - return Optional.of(file); - } - - private Path taskDir(String taskId) { - return storageRoot.resolve(sanitizeTaskId(taskId)); - } - - private static String sanitizeTaskId(String taskId) { - if (StringUtils.isBlank(taskId)) { - throw new IllegalArgumentException("taskId required"); - } - String id = taskId.trim(); - if (id.contains("..") || id.contains("/") || id.contains("\\")) { - throw new IllegalArgumentException("invalid taskId"); - } - return id; - } - - private static byte[] decodeBase64(String normalized) { - if (StringUtils.isBlank(normalized)) { - return null; - } - try { - String t = normalized.trim(); - int comma = t.indexOf(','); - if (t.regionMatches(true, 0, "data:", 0, 5) && comma > 0) { - t = t.substring(comma + 1).trim(); - } - return Base64.getDecoder().decode(t.replaceAll("\\s", "")); - } catch (IllegalArgumentException e) { - return null; - } - } - - private enum PdfKind { - ORIGINAL, - SIGNED - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/support/EsignPrinterPdfSignedOhipEventListener.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/support/EsignPrinterPdfSignedOhipEventListener.java deleted file mode 100644 index 8557c77..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/support/EsignPrinterPdfSignedOhipEventListener.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.support; - -import com.sjmresorts.opsapp.common.config.AsyncConfig; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterPdfSignedOhipEvent; -import com.sjmresorts.opsapp.esign.printer.services.EsignPrinterPdfOhipUploadService; -import com.sjmresorts.opsapp.esign.reservation.dtos.SignedRegistrationCardUploadResult; -import jakarta.annotation.Resource; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.springframework.scheduling.annotation.Async; -import org.springframework.stereotype.Component; -import org.springframework.transaction.event.TransactionPhase; -import org.springframework.transaction.event.TransactionalEventListener; - -/** 平板签名提交后异步上传 OPERA(仅读归档 signed.pdf)。 */ -@Slf4j -@Component -public class EsignPrinterPdfSignedOhipEventListener { - - @Resource - private EsignPrinterPdfOhipUploadService ohipUploadService; - - @Async(AsyncConfig.OHIP_UPLOAD_EXECUTOR) - @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT, fallbackExecution = true) - public void onSignedOhipUpload(EsignPrinterPdfSignedOhipEvent event) { - if (event == null || StringUtils.isBlank(event.taskId())) { - return; - } - String taskId = event.taskId().trim(); - log.info( - "async OHIP upload triggered taskId={}, hotelId={}, reservationId={}", - taskId, - event.hotelId(), - event.reservationId()); - try { - SignedRegistrationCardUploadResult result = ohipUploadService.uploadFromArchive( - taskId, - event.confirmationNumberFromRequest(), - event.signFieldsJson(), - event.profileId(), - event.profileType(), - false, - false, - event.reservationId()); - log.info( - "async OHIP upload taskId={}, invoked={}, success={}, statusCode={}, confirmation={}, reservationId={}, error={}", - taskId, - result.isInvoked(), - result.isSuccess(), - result.getStatusCode(), - result.getConfirmationNumber(), - result.getReservationId(), - result.getErrorMessage()); - } catch (Exception e) { - log.warn("async OHIP upload failed taskId={}", taskId, e); - } - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/support/EsignPrinterPdfSignedSubmitAsyncScheduler.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/support/EsignPrinterPdfSignedSubmitAsyncScheduler.java deleted file mode 100644 index 626fa4c..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/support/EsignPrinterPdfSignedSubmitAsyncScheduler.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.support; - -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterPdfSignedOhipEvent; -import com.sjmresorts.opsapp.esign.printer.services.EsignPrinterPdfArchiveOcrService; -import jakarta.annotation.Resource; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.stereotype.Component; - -/** 平板签名归档后的异步后处理(OCR / OPERA)。 */ -@Slf4j -@Component -public class EsignPrinterPdfSignedSubmitAsyncScheduler { - - @Resource - private ApplicationEventPublisher applicationEventPublisher; - - @Resource - private EsignPrinterPdfArchiveOcrService archiveOcrService; - - public void scheduleAfterSignedSubmit( - String taskId, - String confirmationNumberFromRequest, - String signFieldsJson, - String profileId, - String profileType, - String reservationId, - String hotelId, - boolean scheduleOcr) { - if (StringUtils.isBlank(taskId)) { - return; - } - String id = taskId.trim(); - if (scheduleOcr) { - archiveOcrService.scheduleAfterArchive(id); - } - EsignPrinterPdfSignedOhipEvent ohipEvent = new EsignPrinterPdfSignedOhipEvent( - id, - confirmationNumberFromRequest, - signFieldsJson, - profileId, - profileType, - reservationId, - hotelId); - // 在事务内发布,由 @TransactionalEventListener(AFTER_COMMIT) 在 commit 后触发 - applicationEventPublisher.publishEvent(ohipEvent); - log.info( - "signed submit async scheduled taskId={}, hotelId={}, reservationId={}, hasConfirmation={}, scheduleOcr={}", - id, - hotelId, - reservationId, - StringUtils.isNotBlank(confirmationNumberFromRequest), - scheduleOcr); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/support/EsignPrinterQueueNotifier.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/support/EsignPrinterQueueNotifier.java deleted file mode 100644 index 22bf998..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/support/EsignPrinterQueueNotifier.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.support; - -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterQueueWatchResponse; -import com.sjmresorts.opsapp.esign.printer.services.EsignPrinterPdfTaskService; -import com.sjmresorts.opsapp.esign.printer.support.EsignPrinterQueueSseHub; -import jakarta.annotation.Resource; -import lombok.extern.slf4j.Slf4j; -import org.springframework.context.annotation.Lazy; -import org.springframework.stereotype.Component; -import org.springframework.transaction.support.TransactionSynchronization; -import org.springframework.transaction.support.TransactionSynchronizationManager; - -/** 在事务提交后向 CMS SSE 订阅者广播 PDF 队列变更。 */ -@Slf4j -@Component -public class EsignPrinterQueueNotifier { - - @Resource - private EsignPrinterQueueSseHub queueSseHub; - - @Resource - @Lazy - private EsignPrinterPdfTaskService printerPdfTaskService; - - public void notifyChangedAfterCommit() { - if (TransactionSynchronizationManager.isSynchronizationActive()) { - TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { - @Override - public void afterCommit() { - publishNow(); - } - }); - return; - } - publishNow(); - } - - private void publishNow() { - try { - EsignPrinterQueueWatchResponse watch = printerPdfTaskService.getQueueWatch(); - String revision = watch != null && watch.getData() != null ? watch.getData().getRevision() : null; - queueSseHub.publishQueueChanged(revision); - } catch (Exception e) { - log.warn("CMS queue SSE notify failed: {}", e.getMessage()); - } - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/support/EsignPrinterQueueSseHub.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/support/EsignPrinterQueueSseHub.java deleted file mode 100644 index e5f2595..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/support/EsignPrinterQueueSseHub.java +++ /dev/null @@ -1,103 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.support; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.annotation.PreDestroy; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.springframework.stereotype.Component; -import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; - -import java.io.IOException; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; - -/** CMS PDF 队列 SSE:任务上报/推送/签收后即时通知前端刷新。 */ -@Slf4j -@Component -public class EsignPrinterQueueSseHub { - - private static final long EMITTER_TIMEOUT_MS = 30 * 60 * 1000L; - private static final long HEARTBEAT_MS = 25_000L; - - private final ObjectMapper objectMapper; - private final ConcurrentHashMap emitters = new ConcurrentHashMap<>(); - private final ScheduledExecutorService heartbeatScheduler = Executors.newSingleThreadScheduledExecutor(r -> { - Thread t = new Thread(r, "printer-queue-sse-heartbeat"); - t.setDaemon(true); - return t; - }); - - public EsignPrinterQueueSseHub(ObjectMapper objectMapper) { - this.objectMapper = objectMapper; - heartbeatScheduler.scheduleAtFixedRate(this::sendHeartbeats, HEARTBEAT_MS, HEARTBEAT_MS, TimeUnit.MILLISECONDS); - } - - @PreDestroy - void shutdown() { - heartbeatScheduler.shutdownNow(); - emitters.values().forEach(SseEmitter::complete); - emitters.clear(); - } - - public SseEmitter subscribe() { - String id = UUID.randomUUID().toString(); - SseEmitter emitter = new SseEmitter(EMITTER_TIMEOUT_MS); - emitters.put(id, emitter); - Runnable remove = () -> emitters.remove(id, emitter); - emitter.onCompletion(remove); - emitter.onTimeout(() -> { - remove.run(); - emitter.complete(); - }); - emitter.onError(e -> remove.run()); - try { - emitter.send(SseEmitter.event().name("connected").data("{\"ok\":true}")); - } catch (IOException e) { - emitters.remove(id, emitter); - throw new IllegalStateException("SSE connect failed", e); - } - log.debug("CMS queue SSE subscribed id={}, active={}", id, emitters.size()); - return emitter; - } - - public void publishQueueChanged(String revision) { - if (emitters.isEmpty()) { - return; - } - String payload = toJson(Map.of("revision", StringUtils.defaultString(revision))); - emitters.forEach((id, emitter) -> { - try { - emitter.send(SseEmitter.event().name("queue-changed").data(payload)); - } catch (IOException e) { - emitters.remove(id, emitter); - } - }); - log.debug("CMS queue SSE published revision={}, active={}", revision, emitters.size()); - } - - private void sendHeartbeats() { - if (emitters.isEmpty()) { - return; - } - emitters.forEach((id, emitter) -> { - try { - emitter.send(SseEmitter.event().comment("keepalive")); - } catch (IOException e) { - emitters.remove(id, emitter); - } - }); - } - - private String toJson(Map payload) { - try { - return objectMapper.writeValueAsString(payload); - } catch (JsonProcessingException e) { - return "{}"; - } - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/support/PdfSignFieldTextExtractor.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/support/PdfSignFieldTextExtractor.java deleted file mode 100644 index 6c019c8..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/support/PdfSignFieldTextExtractor.java +++ /dev/null @@ -1,366 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.support; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.apache.pdfbox.Loader; -import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.pdmodel.PDPage; -import org.apache.pdfbox.pdmodel.common.PDRectangle; -import org.apache.pdfbox.text.PDFTextStripperByArea; -import org.springframework.stereotype.Component; - -import java.awt.Rectangle; -import java.util.Optional; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** 从登记卡 PDF(含扫描件)按热区比例提取文字字段(PDF 文本层 → 热区 OCR)。 */ -@Slf4j -@Component -@RequiredArgsConstructor -public class PdfSignFieldTextExtractor { - - private static final Pattern EMAIL = - Pattern.compile("[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"); - private static final Pattern PHONE_STRICT = Pattern.compile( - "\\+(?:852|853|86|1|44|61|65|81|82|886)\\s*[\\d\\s().-]{5,}\\d" - + "|\\+[\\d\\s().-]{8,}\\d" - + "|\\b\\d{2,4}[\\s().-]?\\d{4}[\\s().-]?\\d{3,4}\\b"); - private static final Pattern PHONE_LETTER_GAP = - Pattern.compile("(?<=\\d)\\s+[A-Za-z]{2,6}\\s+(?=\\d)|\\s+[A-Za-z]{2,6}\\s+(?=\\d)"); - - private final RegistrationConfirmationOcrExtractor ocrExtractor; - - public Optional extractFromCropImage(String cropImageBase64, String inputFormat) { - if (StringUtils.isBlank(cropImageBase64)) { - return Optional.empty(); - } - // 裁剪图 OCR 返回原文,由平板端按 inputFormat 统一清洗(避免后端 phone 清洗失败导致 404 空弹窗) - return ocrExtractor - .ocrCropImageBase64(cropImageBase64, inputFormat) - .map(StringUtils::trimToEmpty) - .filter(StringUtils::isNotBlank); - } - - public Optional extract( - byte[] pdfBytes, - int pageIndex, - double left, - double top, - double width, - double height, - String inputFormat) { - if (pdfBytes == null || pdfBytes.length == 0) { - return Optional.empty(); - } - String fmt = normalizeInputFormat(inputFormat); - - try (PDDocument doc = Loader.loadPDF(pdfBytes)) { - if (pageIndex < 0 || pageIndex >= doc.getNumberOfPages()) { - return Optional.empty(); - } - PDPage page = doc.getPage(pageIndex); - PDRectangle media = page.getMediaBox(); - float pageW = media.getWidth(); - float pageH = media.getHeight(); - - String fromArea = extractTextInArea(page, pageW, pageH, left, top, width, height); - String cleaned = cleanFieldText(fromArea, fmt); - if (StringUtils.isNotBlank(cleaned)) { - return Optional.of(cleaned); - } - } catch (Exception e) { - log.warn("PdfSignFieldTextExtractor pdf text failed page={}", pageIndex, e); - } - - Optional ocrRegion = ocrExtractor.ocrFieldRegionFromPdfBytes( - pdfBytes, pageIndex, left, top, width, height); - if (ocrRegion.isPresent()) { - String cleaned = cleanFieldText(ocrRegion.get(), fmt); - if (StringUtils.isNotBlank(cleaned)) { - return Optional.of(cleaned); - } - } - return Optional.empty(); - } - - private static String extractTextInArea( - PDPage page, - float pageW, - float pageH, - double left, - double top, - double width, - double height) - throws Exception { - double l = clamp01(left); - double t = clamp01(top); - double w = clamp01(width); - double h = clamp01(height); - if (w <= 0 || h <= 0) { - return ""; - } - int x = (int) Math.max(0, Math.round(l * pageW) - 2); - int stripW = (int) Math.max(8, Math.round(w * pageW) + 4); - int stripH = (int) Math.max(8, Math.round(h * pageH) + 4); - int y = (int) Math.max(0, Math.round(pageH - (t + h) * pageH) - 2); - if (y + stripH > pageH) { - stripH = (int) pageH - y; - } - if (x + stripW > pageW) { - stripW = (int) pageW - x; - } - PDFTextStripperByArea stripper = new PDFTextStripperByArea(); - stripper.setSortByPosition(true); - stripper.addRegion("field", new Rectangle(x, y, stripW, stripH)); - stripper.extractRegions(page); - return StringUtils.trimToEmpty(stripper.getTextForRegion("field")); - } - - static String normalizeOcrEmail(String raw) { - if (StringUtils.isBlank(raw)) { - return ""; - } - String s = raw.trim() - .replaceAll("[\\(\\[\\{<「『【]+@", "@") - .replaceAll("@[\\)\\]\\}>」』】]+", "@") - .replaceAll("\\s+", ""); - if (!s.contains("@")) { - // PDF/OCR 常把 @ 识成 dz、at、©(画面仍是 @) - s = s.replaceAll("(?i)\\(at\\)", "@") - .replaceAll("(?i)\\[at\\]", "@") - .replaceAll("(?i)\\{at\\}", "@") - .replaceAll("(?i)([A-Za-z0-9._%+-])(?:dz|©|®|Ⓐ|@)([A-Za-z0-9.-]+\\.[A-Za-z]{2,})", "$1@$2") - .replaceAll("(?i)([A-Za-z0-9._%+-])at(?=[A-Za-z0-9.-]+\\.[A-Za-z]{2,})", "$1@"); - } - return s; - } - - static String extractEmailFromOcr(String raw) { - String normalized = normalizeOcrEmail(raw); - Matcher m = EMAIL.matcher(normalized); - return m.find() ? m.group() : ""; - } - - static String stripPhoneLabel(String raw) { - if (StringUtils.isBlank(raw)) { - return ""; - } - return raw.trim() - .replaceAll("(?i)^(phone\\s*電話|phone|電話|电话)\\s*[::]?\\s*", "") - .trim(); - } - - static String fixOcrPhoneChars(String raw) { - if (StringUtils.isBlank(raw)) { - return ""; - } - return raw - .replace('O', '0') - .replace('o', '0') - .replace('Q', '0') - .replace('q', '0') - .replace('D', '0') - .replace('I', '1') - .replace('l', '1') - .replace('|', '1') - .replace('!', '1') - .replace('Z', '2') - .replace('z', '2') - .replace('S', '5') - .replace('s', '5') - .replace('$', '5') - .replace('B', '8') - .replace('b', '8') - .replace('G', '9') - .replace('g', '9') - .replace('A', '4') - .replace('a', '4'); - } - - static int countDigits(String raw) { - if (StringUtils.isBlank(raw)) { - return 0; - } - return raw.replaceAll("\\D", "").length(); - } - - static boolean looksLikePhoneText(String raw) { - String s = stripPhoneLabel(raw); - if (StringUtils.isBlank(s) || s.contains("@")) { - return false; - } - int digits = countDigits(s); - return digits >= 7 && digits <= 15 && s.matches(".*[\\d+].*"); - } - - static String formatPhoneFromDigits(String digits, boolean preferPlus) { - String d = digits.replaceAll("\\D", ""); - if (d.length() < 7 || d.length() > 15) { - return ""; - } - if (d.length() == 11 && d.startsWith("86")) { - return "+86 " + d.substring(2, 5) + " " + d.substring(5, 9) + " " + d.substring(9); - } - if (d.length() == 11 && d.startsWith("852")) { - return "+852 " + d.substring(3, 7) + " " + d.substring(7); - } - if (d.length() == 11 && d.startsWith("853")) { - return "+853 " + d.substring(3, 7) + " " + d.substring(7); - } - if (d.length() == 11 && d.startsWith("1")) { - return "+1 " + d.substring(1, 4) + " " + d.substring(4, 7) + " " + d.substring(7); - } - if (d.length() == 10 && d.startsWith("1")) { - return "+1 " + d.substring(0, 3) + " " + d.substring(3, 6) + " " + d.substring(6); - } - if (d.length() == 8) { - return d.substring(0, 4) + " " + d.substring(4); - } - if (preferPlus || d.length() > 10) { - return "+" + d; - } - return d; - } - - static String extractPhoneFromOcr(String raw) { - String s = stripPhoneLabel(raw); - if (StringUtils.isBlank(s)) { - return ""; - } - // 常见 OCR:+1 误识为 +41,优先尝试修正后再提取 - if (s.matches("(?i).*\\+41\\s*.*")) { - String us = extractPhoneFromOcrOnce(s.replaceFirst("(?i)\\+41", "+1")); - if (StringUtils.isNotBlank(us)) { - return us; - } - } - return extractPhoneFromOcrOnce(s); - } - - private static String extractPhoneFromOcrOnce(String raw) { - String s = stripPhoneLabel(raw); - if (StringUtils.isBlank(s)) { - return ""; - } - s = PHONE_LETTER_GAP.matcher(s).replaceAll(" "); - s = fixOcrPhoneChars(s); - s = StringUtils.normalizeSpace(s); - - Matcher strict = PHONE_STRICT.matcher(s); - if (strict.find()) { - return strict.group().trim(); - } - - boolean hasPlus = s.contains("+"); - String digits = s.replaceAll("\\D", ""); - if (digits.length() >= 7 && digits.length() <= 15) { - return formatPhoneFromDigits(digits, hasPlus); - } - return ""; - } - - static String cleanFieldText(String raw, String inputFormat) { - String fmt = normalizeInputFormat(inputFormat); - String rawText = StringUtils.trimToEmpty(raw); - if ("email".equals(fmt) || rawText.contains("@")) { - String email = extractEmailFromOcr(rawText); - if (StringUtils.isNotBlank(email)) { - return email; - } - if ("email".equals(fmt)) { - return ""; - } - } - if ("phone".equals(fmt) || looksLikePhoneText(rawText)) { - String phone = extractPhoneFromOcr(rawText); - if (StringUtils.isNotBlank(phone)) { - return phone; - } - if ("phone".equals(fmt)) { - String fallback = stripPhoneLabel(rawText); - fallback = StringUtils.normalizeSpace(fallback); - return StringUtils.isNotBlank(fallback) ? fallback : ""; - } - } - String t = StringUtils.normalizeSpace(rawText); - if (StringUtils.isBlank(t)) { - t = pickFormatFromText(raw, fmt); - } else { - t = stripPhoneLabel(t); - t = t.replaceAll("(?i)^(email\\s*電子郵件|email)\\s*[::]?\\s*", "").trim(); - if (t.contains("@")) { - String email = extractEmailFromOcr(t); - t = StringUtils.isNotBlank(email) ? email : ""; - } else if (looksLikePhoneText(t)) { - String phone = extractPhoneFromOcr(t); - t = StringUtils.isNotBlank(phone) ? phone : ""; - } - } - if (!isPlausibleFieldText(t, fmt)) { - return ""; - } - return t; - } - - static String pickFormatFromText(String text, String inputFormat) { - String fmt = normalizeInputFormat(inputFormat); - String page = StringUtils.trimToEmpty(text); - if (StringUtils.isBlank(page)) { - return ""; - } - if ("email".equals(fmt) || page.contains("@")) { - String email = extractEmailFromOcr(page); - return StringUtils.isNotBlank(email) ? email : ""; - } - if ("phone".equals(fmt) || looksLikePhoneText(page)) { - String phone = extractPhoneFromOcr(page); - return StringUtils.isNotBlank(phone) ? phone : ""; - } - return isPlausibleFieldText(page, fmt) ? page : ""; - } - - static boolean isPlausibleFieldText(String text, String fmt) { - if (StringUtils.isBlank(text)) { - return false; - } - String t = text.trim(); - if ("email".equals(fmt) || t.contains("@")) { - return StringUtils.isNotBlank(extractEmailFromOcr(t)); - } - if ("phone".equals(fmt) || looksLikePhoneText(t)) { - int digits = countDigits(t); - return digits >= 7 && digits <= 15; - } - String compact = t.replaceAll("\\s", ""); - if (compact.length() <= 3 && compact.matches("[a-zA-Z]+")) { - return false; - } - if (!t.matches(".*[A-Za-z0-9\\u4e00-\\u9fff].*")) { - return false; - } - return compact.length() >= 2; - } - - private static double clamp01(double v) { - if (Double.isNaN(v) || Double.isInfinite(v)) { - return 0; - } - return Math.max(0, Math.min(1, v)); - } - - static String normalizeInputFormat(String raw) { - String s = StringUtils.trimToEmpty(raw).toLowerCase(); - 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; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/support/RegistrationConfirmationOcrExtractor.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/support/RegistrationConfirmationOcrExtractor.java deleted file mode 100644 index 5dc7823..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/support/RegistrationConfirmationOcrExtractor.java +++ /dev/null @@ -1,628 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.support; - -import com.sjmresorts.opsapp.esign.common.support.EsignLogMaskUtil; -import com.sjmresorts.opsapp.esign.reservation.support.RegistrationConfirmationExtractor; -import lombok.extern.slf4j.Slf4j; -import net.sourceforge.tess4j.ITesseract; -import net.sourceforge.tess4j.Tesseract; -import org.apache.commons.lang3.StringUtils; -import org.apache.pdfbox.Loader; -import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.rendering.PDFRenderer; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; - -import jakarta.annotation.PostConstruct; -import javax.imageio.ImageIO; -import java.awt.Graphics2D; -import java.awt.RenderingHints; -import java.awt.image.BufferedImage; -import java.io.ByteArrayInputStream; -import java.util.Base64; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** PDF 首页渲染 + Tesseract OCR,用于扫描件登记卡提取确认号。 */ -@Slf4j -@Component -public class RegistrationConfirmationOcrExtractor { - - private static final String[] WINDOWS_TESSDATA_CANDIDATES = { - "C:/Program Files/Tesseract-OCR/tessdata", - "C:/Program Files (x86)/Tesseract-OCR/tessdata" - }; - - private static final String[] LINUX_TESSDATA_CANDIDATES = { - "/usr/share/tesseract/tessdata", - "/usr/share/tessdata", - "/usr/local/share/tessdata" - }; - - @Value("${ops-app.esign.confirmation-extract.ocr-enabled:true}") - private boolean ocrEnabled; - - @Value("${ops-app.esign.confirmation-extract.tessdata-path:}") - private String tessdataPath; - - @Value("${ops-app.esign.confirmation-extract.ocr-language:eng}") - private String ocrLanguage; - - @Value("${ops-app.esign.confirmation-extract.ocr-dpi:260}") - private int ocrDpi; - - /** 热区字段 OCR 分辨率(低于整页 OCR,仅裁剪框内识别) */ - @Value("${ops-app.esign.confirmation-extract.field-ocr-dpi:180}") - private int fieldOcrDpi; - - @Value("${ops-app.esign.confirmation-extract.ocr-max-pages:2}") - private int ocrMaxPages; - - @Value("${ops-app.esign.confirmation-extract.ocr-header-ratio:0.38}") - private double ocrHeaderRatio; - - private volatile String resolvedTessdataPath; - private volatile boolean nativeLibAvailable; - private volatile String nativeLibError = ""; - private volatile String lastOcrHint = ""; - - private static final Pattern DIGIT_CONFIRMATION = Pattern.compile("(\\d{6,12})"); - private static final double[][] ROYAL_PARK_CONFIRMATION_REGIONS = { - {0.46, 0.10, 0.22, 0.12}, - {0.50, 0.12, 0.16, 0.08}, - {0.43, 0.08, 0.28, 0.16}, - {0.38, 0.08, 0.36, 0.18} - }; - - @PostConstruct - void init() { - resolvedTessdataPath = resolveTessdataPath(); - if (!ocrEnabled) { - return; - } - if (StringUtils.isBlank(resolvedTessdataPath)) { - log.warn( - "confirmation OCR enabled but tessdata not found; run scripts/download-tessdata.ps1 " - + "or install tesseract-ocr on server, or set ops-app.esign.confirmation-extract.tessdata-path"); - return; - } - nativeLibAvailable = probeNativeLibrary(); - if (nativeLibAvailable) { - log.info( - "confirmation OCR ready tessdata={}, language={}, dpi={}, maxPages={}, headerRatio={}", - resolvedTessdataPath, - ocrLanguage, - ocrDpi, - ocrMaxPages, - ocrHeaderRatio); - } else { - log.warn( - "confirmation OCR tessdata={} but native libtesseract unavailable: {}. " - + "Install tesseract-ocr on Linux or set ops-app.esign.confirmation-extract.ocr-enabled=false", - resolvedTessdataPath, - nativeLibError); - } - } - - public boolean isOcrEnabled() { - return ocrEnabled; - } - - public boolean isOcrReady() { - return ocrEnabled && StringUtils.isNotBlank(resolvedTessdataPath) && nativeLibAvailable; - } - - public String getUnavailableReason() { - if (!ocrEnabled) { - return "OCR 已关闭"; - } - if (StringUtils.isBlank(resolvedTessdataPath)) { - return "未找到 tessdata(请运行 scripts/download-tessdata.ps1 或安装 Tesseract)"; - } - if (!nativeLibAvailable) { - if (StringUtils.isNotBlank(nativeLibError)) { - return "未安装 Tesseract 原生库(libtesseract):" + nativeLibError; - } - return "未安装 Tesseract 原生库(libtesseract),请在服务器安装 tesseract-ocr 或关闭 OCR"; - } - return ""; - } - - public String getLastOcrHint() { - return lastOcrHint; - } - - public Optional extractFromPdfBytes(byte[] pdfBytes) { - lastOcrHint = ""; - if (!ocrEnabled || pdfBytes == null || pdfBytes.length == 0) { - return Optional.empty(); - } - if (!isOcrReady()) { - log.warn("confirmation OCR skip: {}", getUnavailableReason()); - return Optional.empty(); - } - Optional regionFound = extractConfirmationFromKnownRegions(pdfBytes); - if (regionFound.isPresent()) { - return regionFound; - } - Optional fullText = extractFullTextFromPdfBytes(pdfBytes); - if (fullText.isEmpty() || StringUtils.isBlank(fullText.get())) { - lastOcrHint = "OCR 未识别到文字"; - return Optional.empty(); - } - Optional found = RegistrationConfirmationExtractor.extractFromText(fullText.get()); - if (found.isPresent()) { - log.info( - "confirmation OCR hit via full text confirmation={} sample={}", - found.get(), - EsignLogMaskUtil.truncate(fullText.get())); - return found; - } - lastOcrHint = "OCR样例 " + EsignLogMaskUtil.truncate(fullText.get()); - return Optional.empty(); - } - - /** 渲染 PDF 前若干页并 OCR 合并为纯文本(供登记卡字段解析)。 */ - public Optional extractFullTextFromPdfBytes(byte[] pdfBytes) { - lastOcrHint = ""; - if (!ocrEnabled || pdfBytes == null || pdfBytes.length == 0) { - return Optional.empty(); - } - if (!isOcrReady()) { - return Optional.empty(); - } - try (PDDocument doc = Loader.loadPDF(pdfBytes)) { - int pages = Math.min(doc.getNumberOfPages(), Math.max(ocrMaxPages, 1)); - PDFRenderer renderer = new PDFRenderer(doc); - ITesseract tesseract = createTesseract(); - StringBuilder sb = new StringBuilder(); - for (int page = 0; page < pages; page++) { - BufferedImage full = renderer.renderImageWithDPI(page, ocrDpi); - String headerText = tesseract.doOCR(cropHeader(full)); - if (StringUtils.isNotBlank(headerText)) { - sb.append(headerText).append('\n'); - } - String pageText = tesseract.doOCR(full); - if (StringUtils.isNotBlank(pageText)) { - sb.append(pageText).append('\n'); - } - } - String merged = sb.toString().trim(); - return StringUtils.isNotBlank(merged) ? Optional.of(merged) : Optional.empty(); - } catch (Throwable e) { - lastOcrHint = "OCR 异常: " + e.getMessage(); - log.warn("registration card OCR full text failed", e); - return Optional.empty(); - } - } - - private Optional ocrImage(ITesseract tesseract, BufferedImage image, int page, String region) - throws Exception { - String pageText = tesseract.doOCR(image); - if (StringUtils.isBlank(pageText)) { - return Optional.empty(); - } - log.info( - "confirmation OCR page={} region={} textLen={}", - page + 1, - region, - pageText.length()); - Optional found = RegistrationConfirmationExtractor.extractFromText(pageText); - if (found.isPresent()) { - log.info( - "confirmation OCR hit page={} region={} confirmation={}", - page + 1, - region, - found.get()); - return found; - } - lastOcrHint = "OCR样例(" + region + ") " + EsignLogMaskUtil.truncate(pageText); - log.info("confirmation OCR page={} region={} sample={}", page + 1, region, lastOcrHint); - return Optional.empty(); - } - - /** - * 直接 OCR 平板 renderjs 裁剪的热区 PNG(与屏幕所见一致)。 - */ - public Optional ocrCropImageBase64(String base64) { - return ocrCropImageBase64(base64, null); - } - - public Optional ocrCropImageBase64(String base64, String inputFormat) { - lastOcrHint = ""; - String fmt = StringUtils.defaultIfBlank(inputFormat, "text"); - if (!ocrEnabled || !isOcrReady() || StringUtils.isBlank(base64)) { - lastOcrHint = getUnavailableReason(); - log.info( - "crop OCR skip format={} enabled={} ready={} hasImage={} reason={}", - fmt, - ocrEnabled, - isOcrReady(), - StringUtils.isNotBlank(base64), - lastOcrHint); - return Optional.empty(); - } - try { - String raw = base64.trim(); - int comma = raw.indexOf(','); - if (comma >= 0) { - raw = raw.substring(comma + 1); - } - byte[] bytes = Base64.getDecoder().decode(raw); - BufferedImage image = ImageIO.read(new ByteArrayInputStream(bytes)); - if (image == null) { - lastOcrHint = "crop image decode failed"; - log.warn("crop OCR decode failed format={} base64Len={}", fmt, raw.length()); - return Optional.empty(); - } - if (!cropHasMeaningfulInk(image)) { - lastOcrHint = "crop region empty"; - log.info( - "crop OCR empty ink format={} size={}x{}", - fmt, - image.getWidth(), - image.getHeight()); - return Optional.empty(); - } - image = prepareCropForOcr(image); - String text = ocrCropTextWithFallback(image, inputFormat); - if (StringUtils.isBlank(text)) { - lastOcrHint = "crop OCR empty"; - log.info( - "crop OCR no text format={} size={}x{}", - fmt, - image.getWidth(), - image.getHeight()); - return Optional.empty(); - } - log.info( - "crop OCR ok format={} size={}x{} textLen={} sample={}", - fmt, - image.getWidth(), - image.getHeight(), - text.length(), - EsignLogMaskUtil.truncate(text)); - return Optional.of(text.trim()); - } catch (Throwable e) { - markNativeUnavailable(e); - lastOcrHint = "crop OCR failed: " + summarizeNativeLibError(e); - log.warn("crop image OCR failed format={}", fmt, e); - return Optional.empty(); - } - } - - /** - * 按热区比例(页面左上原点,0~1)裁剪并 OCR,用于扫描件登记卡文字字段预填。 - */ - public Optional ocrFieldRegionFromPdfBytes( - byte[] pdfBytes, - int pageIndex, - double left, - double top, - double width, - double height) { - lastOcrHint = ""; - if (!ocrEnabled || pdfBytes == null || pdfBytes.length == 0 || !isOcrReady()) { - return Optional.empty(); - } - try (PDDocument doc = Loader.loadPDF(pdfBytes)) { - if (pageIndex < 0 || pageIndex >= doc.getNumberOfPages()) { - return Optional.empty(); - } - int dpi = Math.max(120, Math.min(fieldOcrDpi, 240)); - PDFRenderer renderer = new PDFRenderer(doc); - BufferedImage full = renderer.renderImageWithDPI(pageIndex, dpi); - BufferedImage crop = cropFieldRegion(full, left, top, width, height); - if (crop == null) { - return Optional.empty(); - } - ITesseract tesseract = createFieldTesseract(); - String text = tesseract.doOCR(crop); - if (StringUtils.isBlank(text)) { - lastOcrHint = "field OCR empty"; - return Optional.empty(); - } - log.info( - "field OCR page={} dpi={} crop={}x{} textLen={} sample={}", - pageIndex + 1, - dpi, - crop.getWidth(), - crop.getHeight(), - text.length(), - EsignLogMaskUtil.truncate(text)); - return Optional.of(text.trim()); - } catch (Throwable e) { - markNativeUnavailable(e); - lastOcrHint = "field OCR failed: " + summarizeNativeLibError(e); - log.warn("field region OCR failed page={}", pageIndex + 1, e); - return Optional.empty(); - } - } - - private static BufferedImage cropFieldRegion( - BufferedImage full, - double left, - double top, - double width, - double height) { - if (full == null) { - return null; - } - double l = clampRatio(left); - double t = clampRatio(top); - double w = clampRatio(width); - double h = clampRatio(height); - if (w <= 0 || h <= 0) { - return null; - } - int imgW = full.getWidth(); - int imgH = full.getHeight(); - int x = (int) Math.max(0, Math.round(l * imgW) - 4); - int y = (int) Math.max(0, Math.round(t * imgH) - 4); - int cropW = (int) Math.max(8, Math.round(w * imgW) + 8); - int cropH = (int) Math.max(8, Math.round(h * imgH) + 8); - if (x + cropW > imgW) { - cropW = imgW - x; - } - if (y + cropH > imgH) { - cropH = imgH - y; - } - if (cropW <= 0 || cropH <= 0) { - return null; - } - return full.getSubimage(x, y, cropW, cropH); - } - - private Optional extractConfirmationFromKnownRegions(byte[] pdfBytes) { - try (PDDocument doc = Loader.loadPDF(pdfBytes)) { - if (doc.getNumberOfPages() <= 0) { - return Optional.empty(); - } - PDFRenderer renderer = new PDFRenderer(doc); - BufferedImage full = renderer.renderImageWithDPI(0, Math.max(ocrDpi, 320)); - for (int i = 0; i < ROYAL_PARK_CONFIRMATION_REGIONS.length; i++) { - double[] region = ROYAL_PARK_CONFIRMATION_REGIONS[i]; - BufferedImage crop = cropFieldRegion(full, region[0], region[1], region[2], region[3]); - Optional found = ocrDigitsFromRegion(crop, i, region); - if (found.isPresent()) { - log.info("confirmation OCR hit known region index={} confirmation={}", i, found.get()); - return found; - } - } - return Optional.empty(); - } catch (Throwable e) { - lastOcrHint = "known region OCR failed: " + summarizeNativeLibError(e); - log.warn("confirmation known region OCR failed", e); - return Optional.empty(); - } - } - - private Optional ocrDigitsFromRegion(BufferedImage image, int index, double[] region) throws Exception { - if (image == null || !cropHasMeaningfulInk(image)) { - log.info("confirmation known region OCR skip index={} emptyInk={}", index, image != null); - return Optional.empty(); - } - BufferedImage prepared = prepareCropForOcr(image); - ITesseract tesseract = createTesseract(); - tesseract.setPageSegMode(7); - tesseract.setTessVariable("tessedit_char_whitelist", "0123456789"); - String text = StringUtils.trimToEmpty(tesseract.doOCR(prepared)); - String compact = text.replaceAll("\\s+", ""); - log.info( - "confirmation known region OCR index={} region=[{},{},{},{}] psm=7 crop={}x{} textRaw={} textCompact={}", - index, - region[0], - region[1], - region[2], - region[3], - prepared.getWidth(), - prepared.getHeight(), - EsignLogMaskUtil.truncate(text), - EsignLogMaskUtil.truncate(compact)); - Optional token = extractDigitToken(text); - if (token.isPresent()) { - return token; - } - ITesseract retry = createTesseract(); - retry.setPageSegMode(6); - retry.setTessVariable("tessedit_char_whitelist", "0123456789"); - text = StringUtils.trimToEmpty(retry.doOCR(prepared)); - compact = text.replaceAll("\\s+", ""); - log.info( - "confirmation known region OCR index={} region=[{},{},{},{}] psm=6 crop={}x{} textRaw={} textCompact={}", - index, - region[0], - region[1], - region[2], - region[3], - prepared.getWidth(), - prepared.getHeight(), - EsignLogMaskUtil.truncate(text), - EsignLogMaskUtil.truncate(compact)); - return extractDigitToken(text); - } - - private Optional extractDigitToken(String text) { - Matcher matcher = DIGIT_CONFIRMATION.matcher(StringUtils.defaultString(text)); - return matcher.find() ? Optional.of(matcher.group(1)) : Optional.empty(); - } - - private static double clampRatio(double v) { - if (Double.isNaN(v) || Double.isInfinite(v)) { - return 0; - } - return Math.max(0, Math.min(1, v)); - } - - private BufferedImage cropHeader(BufferedImage full) { - if (full == null || ocrHeaderRatio <= 0 || ocrHeaderRatio >= 1) { - return full; - } - int h = Math.max(1, (int) Math.round(full.getHeight() * ocrHeaderRatio)); - return full.getSubimage(0, 0, full.getWidth(), h); - } - - private ITesseract createFieldTesseract() { - return createFieldTesseract(null); - } - - private ITesseract createFieldTesseract(String inputFormat) { - ITesseract tesseract = createTesseract(); - tesseract.setPageSegMode(7); - return tesseract; - } - - private String ocrCropTextWithFallback(BufferedImage image, String inputFormat) throws Exception { - ITesseract tesseract = createFieldTesseract(inputFormat); - String text = StringUtils.trimToEmpty(tesseract.doOCR(image)); - if (StringUtils.isNotBlank(text)) { - return text; - } - String fmt = PdfSignFieldTextExtractor.normalizeInputFormat(inputFormat); - if (!"phone".equals(fmt)) { - return text; - } - ITesseract retry = createTesseract(); - retry.setPageSegMode(7); - return StringUtils.trimToEmpty(retry.doOCR(image)); - } - - /** 放大过小裁剪图并二值化,提升单行 email/phone OCR 准确率。 */ - private static BufferedImage prepareCropForOcr(BufferedImage source) { - if (source == null) { - return null; - } - BufferedImage image = source; - int minHeight = 96; - if (image.getHeight() < minHeight) { - double scale = (double) minHeight / Math.max(1, image.getHeight()); - int nw = Math.max(1, (int) Math.round(image.getWidth() * scale)); - int nh = Math.max(minHeight, (int) Math.round(image.getHeight() * scale)); - BufferedImage scaled = new BufferedImage(nw, nh, BufferedImage.TYPE_INT_RGB); - Graphics2D g = scaled.createGraphics(); - g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); - g.drawImage(image, 0, 0, nw, nh, null); - g.dispose(); - image = scaled; - } - BufferedImage gray = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_BYTE_GRAY); - Graphics2D g2 = gray.createGraphics(); - g2.drawImage(image, 0, 0, null); - g2.dispose(); - for (int y = 0; y < gray.getHeight(); y++) { - for (int x = 0; x < gray.getWidth(); x++) { - int rgb = gray.getRGB(x, y) & 0xff; - int v = rgb >= 145 ? 255 : rgb <= 95 ? 0 : rgb >= 120 ? 255 : 0; - int out = (v << 16) | (v << 8) | v; - gray.setRGB(x, y, out); - } - } - return gray; - } - - private static boolean cropHasMeaningfulInk(BufferedImage image) { - if (image == null || image.getWidth() <= 0 || image.getHeight() <= 0) { - return false; - } - int dark = 0; - int total = image.getWidth() * image.getHeight(); - for (int y = 0; y < image.getHeight(); y++) { - for (int x = 0; x < image.getWidth(); x++) { - int argb = image.getRGB(x, y); - int r = (argb >> 16) & 0xff; - int g = (argb >> 8) & 0xff; - int b = argb & 0xff; - int gray = (int) (r * 0.299 + g * 0.587 + b * 0.114); - if (gray < 140) { - dark++; - } - } - } - return (double) dark / total >= 0.004; - } - - private ITesseract createTesseract() { - try { - Tesseract tesseract = new Tesseract(); - tesseract.setDatapath(resolvedTessdataPath); - tesseract.setLanguage(StringUtils.defaultIfBlank(ocrLanguage, "eng")); - tesseract.setPageSegMode(6); - return tesseract; - } catch (Throwable e) { - markNativeUnavailable(e); - throw new IllegalStateException( - "Tesseract native library unavailable: " + summarizeNativeLibError(e), e); - } - } - - private boolean probeNativeLibrary() { - try { - ITesseract tesseract = createTesseract(); - BufferedImage tiny = new BufferedImage(8, 8, BufferedImage.TYPE_INT_RGB); - tesseract.doOCR(tiny); - nativeLibError = ""; - return true; - } catch (Throwable e) { - markNativeUnavailable(e); - return false; - } - } - - private void markNativeUnavailable(Throwable e) { - nativeLibAvailable = false; - nativeLibError = summarizeNativeLibError(e); - } - - private static String summarizeNativeLibError(Throwable e) { - Throwable root = e; - while (root.getCause() != null && root.getCause() != root) { - root = root.getCause(); - } - String msg = StringUtils.defaultIfBlank(root.getMessage(), root.getClass().getSimpleName()); - String combined = msg + " " + e.getClass().getSimpleName(); - if (combined.contains("Could not initialize class") - || combined.contains("UnsatisfiedLinkError") - || combined.contains("no tessdata") - || combined.contains("libtesseract") - || combined.contains("Tesseract")) { - return "未安装 Tesseract 原生库(请在服务器安装 tesseract-ocr 后重启后端)"; - } - return StringUtils.defaultIfBlank(msg, "OCR 初始化失败"); - } - - private String resolveTessdataPath() { - List candidates = new ArrayList<>(); - if (StringUtils.isNotBlank(tessdataPath)) { - candidates.add(tessdataPath.trim()); - } - String userDir = System.getProperty("user.dir"); - if (StringUtils.isNotBlank(userDir)) { - candidates.add(Path.of(userDir, "tessdata").toString()); - } - for (String c : WINDOWS_TESSDATA_CANDIDATES) { - candidates.add(c); - } - for (String c : LINUX_TESSDATA_CANDIDATES) { - candidates.add(c); - } - candidates.add("tessdata"); - for (String candidate : candidates) { - if (isValidTessdataDir(candidate)) { - return Path.of(candidate).toAbsolutePath().normalize().toString(); - } - } - return null; - } - - private static boolean isValidTessdataDir(String path) { - if (StringUtils.isBlank(path)) { - return false; - } - Path dir = Path.of(path); - return Files.isDirectory(dir) && Files.exists(dir.resolve("eng.traineddata")); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/support/SignedPdfArchiveReader.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/support/SignedPdfArchiveReader.java deleted file mode 100644 index 0ac2cf6..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/support/SignedPdfArchiveReader.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.support; - -import com.sjmresorts.opsapp.esign.printer.dtos.RegistrationCardSignFieldParser; -import com.sjmresorts.opsapp.esign.printer.dtos.SignedRegistrationPdfComposer; -import com.sjmresorts.opsapp.esign.printer.models.EsignPrinterPdfTask; -import jakarta.annotation.Resource; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.springframework.stereotype.Component; - -import java.util.Base64; -import java.util.Optional; - -/** - * 读取已归档的签名 PDF。OPERA 上传优先读磁盘/DB 归档; - * 若归档丢失但仍有签名图层 + 原件,则按归档热区重合成并回写(与预览下载逻辑一致)。 - */ -@Slf4j -@Component -public class SignedPdfArchiveReader { - - @Resource - private EsignPrinterPdfFileStorage pdfFileStorage; - - public Optional readSignedBase64(EsignPrinterPdfTask task) { - if (task == null) { - return Optional.empty(); - } - String signedBase64 = pdfFileStorage.materializeSignedBase64(task); - if (StringUtils.isNotBlank(signedBase64)) { - return Optional.of(signedBase64); - } - log.warn( - "archived signed PDF missing taskId={}, hasSignedRef={}, hasSignedFile={}, hasMeta={}, hasOriginal={}", - task.getTaskId(), - StringUtils.isNotBlank(task.getSignedPdfBase64()), - pdfFileStorage.hasSignedFile(task.getTaskId()), - StringUtils.isNotBlank(task.getSignedMetaJson()), - pdfFileStorage.hasOriginal(task)); - return recoverFromMeta(task); - } - - /** - * 归档文件丢失时的兜底:用原件 + signedMetaJson + signFieldsJson 重合成并落盘。 - */ - private Optional recoverFromMeta(EsignPrinterPdfTask task) { - if (StringUtils.isBlank(task.getSignedMetaJson()) || !pdfFileStorage.hasOriginal(task)) { - return Optional.empty(); - } - try { - var signFields = RegistrationCardSignFieldParser.parse(task.getSignFieldsJson()); - String composed = SignedRegistrationPdfComposer.composeBase64( - pdfFileStorage.materializeOriginalBase64(task), - task.getSignedMetaJson(), - signFields); - if (StringUtils.isBlank(composed)) { - return Optional.empty(); - } - byte[] bytes = Base64.getDecoder().decode(composed); - if (bytes.length == 0) { - return Optional.empty(); - } - String ref = pdfFileStorage.persistSigned(task.getTaskId(), bytes); - task.setSignedPdfBase64(ref); - log.info( - "recovered signed PDF from meta taskId={}, signedPdfLen={}, ref={}", - task.getTaskId(), - composed.length(), - ref); - return Optional.of(composed); - } catch (Exception e) { - log.warn("recover signed PDF from meta failed taskId={}", task.getTaskId(), e); - return Optional.empty(); - } - } - - public String requireSignedBase64(EsignPrinterPdfTask task) { - return readSignedBase64(task) - .orElseThrow(() -> new IllegalStateException( - "归档签名 PDF 不存在,无法上传 OPERA(taskId=" - + (task != null ? task.getTaskId() : "null") - + ")")); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/support/SignedPdfComposeValidator.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/support/SignedPdfComposeValidator.java deleted file mode 100644 index 478a49b..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/support/SignedPdfComposeValidator.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.support; - -import com.alibaba.fastjson2.JSON; -import com.alibaba.fastjson2.JSONObject; -import com.sjmresorts.opsapp.esign.printer.dtos.RegistrationCardSignFieldParser; -import com.sjmresorts.opsapp.esign.printer.dtos.SignedRegistrationPdfComposer; -import org.apache.commons.lang3.StringUtils; - -import java.util.List; -import java.util.Map; - -/** 归档前校验:必填热区均有签名图层。 */ -public final class SignedPdfComposeValidator { - - private SignedPdfComposeValidator() {} - - public static void assertRequiredSignaturesPresent(String signaturesJson, String signFieldsJson) { - if (StringUtils.isBlank(signaturesJson)) { - throw new IllegalStateException("signaturesJson required"); - } - Map signatures = parseSignatures(signaturesJson); - List fields = - RegistrationCardSignFieldParser.parse(signFieldsJson); - for (SignedRegistrationPdfComposer.SignFieldRect field : fields) { - if (field.isTextField()) { - continue; - } - String value = signatures.get(field.id()); - if (StringUtils.isBlank(value)) { - throw new IllegalStateException("必填签名热区未填写: " + field.id()); - } - if (!SignedRegistrationPdfComposer.isImageSignatureValue(value) - && StringUtils.length(StringUtils.trim(value)) < 1) { - throw new IllegalStateException("签名热区内容无效: " + field.id()); - } - } - } - - private static Map parseSignatures(String signaturesJson) { - try { - JSONObject obj = JSON.parseObject(signaturesJson); - if (obj == null) { - return Map.of(); - } - return obj.toJavaObject(Map.class); - } catch (Exception e) { - throw new IllegalStateException("signaturesJson 格式无效"); - } - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/websocket/EsignPrinterWebSocketAuthInterceptor.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/websocket/EsignPrinterWebSocketAuthInterceptor.java deleted file mode 100644 index b569e3a..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/websocket/EsignPrinterWebSocketAuthInterceptor.java +++ /dev/null @@ -1,116 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.websocket; - -import com.auth0.jwt.exceptions.JWTVerificationException; -import com.sjmresorts.opsapp.authentication.dtos.LoginUserInfo; -import com.sjmresorts.opsapp.common.utils.JwtTokenUtil; -import com.sjmresorts.opsapp.common.utils.NetworkUtil; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.springframework.http.HttpStatus; -import org.springframework.http.server.ServerHttpRequest; -import org.springframework.http.server.ServerHttpResponse; -import org.springframework.http.server.ServletServerHttpRequest; -import org.springframework.http.server.ServletServerHttpResponse; -import org.springframework.stereotype.Component; -import org.springframework.web.socket.WebSocketHandler; -import org.springframework.web.socket.server.HandshakeInterceptor; - -import java.util.Map; - -@Slf4j -@Component -public class EsignPrinterWebSocketAuthInterceptor implements HandshakeInterceptor { - - @Override - public boolean beforeHandshake( - ServerHttpRequest request, - ServerHttpResponse response, - WebSocketHandler wsHandler, - Map attributes) { - if (!(request instanceof ServletServerHttpRequest servletRequest)) { - reject(response, "not a servlet request"); - return false; - } - HttpServletRequest httpRequest = servletRequest.getServletRequest(); - String token = resolveToken(httpRequest); - if (StringUtils.isBlank(token)) { - reject(response, "missing token"); - return false; - } - try { - JwtTokenUtil.getJwtVerifier().verify(token); - } catch (JWTVerificationException e) { - reject(response, "invalid or expired token"); - return false; - } - LoginUserInfo user = JwtTokenUtil.parseUserFromToken(token); - if (user == null || StringUtils.isBlank(user.getEmployeeId())) { - reject(response, "no employeeId in token"); - return false; - } - try { - attributes.put( - EsignPrinterWebSocketHub.SESSION_USER_ID, - Long.parseLong(user.getEmployeeId().trim())); - attributes.put( - EsignPrinterWebSocketHub.SESSION_CLIENT_IP, - NetworkUtil.getClientIpByRequest(httpRequest)); - log.info("Printer WS handshake ok userId={}", user.getEmployeeId()); - return true; - } catch (NumberFormatException e) { - reject(response, "employeeId not numeric"); - return false; - } - } - - private static String resolveToken(HttpServletRequest request) { - String token = request.getParameter("token"); - if (StringUtils.isBlank(token)) { - token = extractBearerToken(request.getHeader("Authorization")); - } - if (StringUtils.isBlank(token)) { - return null; - } - token = token.trim(); - if (token.regionMatches(true, 0, "Bearer ", 0, 7)) { - token = token.substring(7).trim(); - } - return token; - } - - private static String extractBearerToken(String authorizationHeader) { - if (StringUtils.isBlank(authorizationHeader)) { - return null; - } - String h = authorizationHeader.trim(); - if (h.regionMatches(true, 0, "Bearer ", 0, 7)) { - String t = h.substring(7).trim(); - return StringUtils.isNotBlank(t) ? t : null; - } - return null; - } - - private void reject(ServerHttpResponse response, String reason) { - log.warn("Printer WS handshake rejected: {}", reason); - if (response instanceof ServletServerHttpResponse servletResponse) { - servletResponse.setStatusCode(HttpStatus.FORBIDDEN); - HttpServletResponse raw = servletResponse.getServletResponse(); - if (raw != null && !raw.isCommitted()) { - raw.setStatus(HttpServletResponse.SC_FORBIDDEN); - } - } - } - - @Override - public void afterHandshake( - ServerHttpRequest request, - ServerHttpResponse response, - WebSocketHandler wsHandler, - Exception exception) { - if (exception != null) { - log.warn("Printer WS afterHandshake error: {}", exception.getMessage()); - } - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/websocket/EsignPrinterWebSocketHandler.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/websocket/EsignPrinterWebSocketHandler.java deleted file mode 100644 index 985e790..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/websocket/EsignPrinterWebSocketHandler.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.websocket; - -import com.sjmresorts.opsapp.esign.auth.services.EsignUserPresenceService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import org.springframework.web.socket.CloseStatus; -import org.springframework.web.socket.TextMessage; -import org.springframework.web.socket.WebSocketSession; -import org.springframework.web.socket.handler.TextWebSocketHandler; - -@Slf4j -@Component -@RequiredArgsConstructor -public class EsignPrinterWebSocketHandler extends TextWebSocketHandler { - - private final EsignPrinterWebSocketHub hub; - private final EsignUserPresenceService userPresenceService; - - @Override - public void afterConnectionEstablished(WebSocketSession session) throws Exception { - Long userId = hub.sessionUserId(session); - if (userId == null) { - session.close(CloseStatus.NOT_ACCEPTABLE); - return; - } - userPresenceService.touchClientIp(userId, hub.sessionClientIp(session)); - hub.register(userId, session); - session.sendMessage(new TextMessage("{\"type\":\"connected\"}")); - } - - @Override - protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { - String payload = message.getPayload(); - if (payload != null && payload.trim().equalsIgnoreCase("ping")) { - session.sendMessage(new TextMessage("{\"type\":\"pong\"}")); - } - } - - @Override - public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { - hub.unregister(session); - } - - @Override - public void handleTransportError(WebSocketSession session, Throwable exception) { - log.debug("Printer WS transport error sessionId={}", session.getId(), exception); - hub.unregister(session); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/printer/websocket/EsignPrinterWebSocketHub.java b/src/main/java/com/sjmresorts/opsapp/esign/printer/websocket/EsignPrinterWebSocketHub.java deleted file mode 100644 index e860de1..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/printer/websocket/EsignPrinterWebSocketHub.java +++ /dev/null @@ -1,227 +0,0 @@ -package com.sjmresorts.opsapp.esign.printer.websocket; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.sjmresorts.opsapp.esign.audit.services.EsignAuditLogService; -import jakarta.annotation.Resource; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import org.springframework.web.socket.TextMessage; -import org.springframework.web.socket.WebSocketSession; -import org.apache.commons.lang3.StringUtils; - -import java.io.IOException; -import java.util.Collection; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArraySet; - -@Slf4j -@Component -public class EsignPrinterWebSocketHub { - - public static final String SESSION_USER_ID = "userId"; - public static final String SESSION_CLIENT_IP = "clientIp"; - - private final ObjectMapper objectMapper; - private final ConcurrentHashMap> sessionsByUser = new ConcurrentHashMap<>(); - - @Resource(name = "esignAuditLogService") - private EsignAuditLogService auditLogService; - - public EsignPrinterWebSocketHub(ObjectMapper objectMapper) { - this.objectMapper = objectMapper; - } - - public void register(Long userId, WebSocketSession session) { - if (userId == null || session == null) { - return; - } - sessionsByUser.computeIfAbsent(userId, k -> new CopyOnWriteArraySet<>()).add(session); - log.info("Printer WS connected userId={}, sessionId={}", userId, session.getId()); - auditConnection(EsignAuditLogService.EVT_WS_CONNECTED, userId, session, "WebSocket 已連接"); - } - - public void unregister(WebSocketSession session) { - if (session == null) { - return; - } - Long userId = sessionUserId(session); - if (userId != null) { - Set set = sessionsByUser.get(userId); - if (set != null) { - set.remove(session); - if (set.isEmpty()) { - sessionsByUser.remove(userId, set); - } - } - } - log.info("Printer WS disconnected userId={}, sessionId={}", userId, session.getId()); - auditConnection(EsignAuditLogService.EVT_WS_DISCONNECTED, userId, session, "WebSocket 已斷開"); - } - - public Long sessionUserId(WebSocketSession session) { - Object v = session.getAttributes().get(SESSION_USER_ID); - if (v instanceof Long l) { - return l; - } - if (v instanceof Number n) { - return n.longValue(); - } - return null; - } - - public String sessionClientIp(WebSocketSession session) { - Object v = session != null ? session.getAttributes().get(SESSION_CLIENT_IP) : null; - return v != null ? String.valueOf(v) : null; - } - - public boolean isUserConnected(Long userId) { - if (userId == null) { - return false; - } - Set sessions = sessionsByUser.get(userId); - if (sessions == null || sessions.isEmpty()) { - return false; - } - for (WebSocketSession session : sessions) { - if (session != null && session.isOpen()) { - return true; - } - } - return false; - } - - public void notifyPrinterPdfAssigned( - Collection userIds, - String taskId, - String fileName, - String agentSource) { - notifyPrinterPdfAssigned(userIds, taskId, fileName, agentSource, null); - } - - public void notifyPrinterPdfAssigned( - Collection userIds, - String taskId, - String fileName, - String agentSource, - Map> allowedClientIpsByUser) { - if (userIds == null || userIds.isEmpty() || taskId == null || taskId.isBlank()) { - return; - } - String payload; - try { - payload = objectMapper.writeValueAsString(Map.of( - "type", "PRINTER_PDF", - "taskId", taskId.trim(), - "fileName", fileName != null ? fileName : "", - "agentSource", agentSource != null ? agentSource : "", - "taskType", "PRINTER_PDF")); - } catch (Exception e) { - log.warn("Printer WS notify serialize failed taskId={}", taskId, e); - return; - } - TextMessage message = new TextMessage(payload); - for (Long userId : userIds) { - if (userId == null) { - continue; - } - Set sessions = sessionsByUser.get(userId); - if (sessions == null || sessions.isEmpty()) { - continue; - } - for (WebSocketSession session : sessions) { - if (allowedClientIpsByUser != null) { - Set allowed = allowedClientIpsByUser.get(userId); - if (allowed != null && !allowed.isEmpty()) { - String sessionIp = - com.sjmresorts.opsapp.esign.auth.services.EsignUserPresenceService.normalizeClientIp( - sessionClientIp(session)); - if (sessionIp == null || !allowed.contains(sessionIp)) { - continue; - } - } - } - sendSafe(session, message); - } - } - } - - public void notifySurveyAssigned( - Collection userIds, - String surveyId, - String title, - Map> allowedClientIpsByUser) { - if (userIds == null || userIds.isEmpty() || StringUtils.isBlank(surveyId)) { - return; - } - String payload; - try { - payload = objectMapper.writeValueAsString(Map.of( - "type", "SURVEY", - "taskId", surveyId.trim(), - "surveyId", surveyId.trim(), - "fileName", title != null ? title : "", - "taskType", "SURVEY")); - } catch (Exception e) { - log.warn("Printer WS survey notify serialize failed surveyId={}", surveyId, e); - return; - } - TextMessage message = new TextMessage(payload); - for (Long userId : userIds) { - if (userId == null) { - continue; - } - Set sessions = sessionsByUser.get(userId); - if (sessions == null || sessions.isEmpty()) { - continue; - } - for (WebSocketSession session : sessions) { - if (allowedClientIpsByUser != null) { - Set allowed = allowedClientIpsByUser.get(userId); - if (allowed != null && !allowed.isEmpty()) { - String sessionIp = - com.sjmresorts.opsapp.esign.auth.services.EsignUserPresenceService.normalizeClientIp( - sessionClientIp(session)); - if (sessionIp == null || !allowed.contains(sessionIp)) { - continue; - } - } - } - sendSafe(session, message); - } - } - } - - private void sendSafe(WebSocketSession session, TextMessage message) { - if (session == null || !session.isOpen()) { - return; - } - try { - synchronized (session) { - if (session.isOpen()) { - session.sendMessage(message); - } - } - } catch (IOException e) { - log.debug("Printer WS send failed sessionId={}", session.getId(), e); - } - } - - private void auditConnection(String eventType, Long userId, WebSocketSession session, String message) { - if (auditLogService == null) { - return; - } - try { - auditLogService.appendConnection( - eventType, - userId, - message, - "websocket", - sessionClientIp(session), - session != null ? "{\"sessionId\":\"" + session.getId() + "\"}" : null); - } catch (Exception e) { - log.debug("audit connection log failed", e); - } - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/reservation/controllers/EsignReservationController.java b/src/main/java/com/sjmresorts/opsapp/esign/reservation/controllers/EsignReservationController.java deleted file mode 100644 index 6ace206..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/reservation/controllers/EsignReservationController.java +++ /dev/null @@ -1,204 +0,0 @@ -package com.sjmresorts.opsapp.esign.reservation.controllers; - -import com.sjmresorts.opsapp.esign.reservation.dtos.GuestProfileIdResponse; -import com.sjmresorts.opsapp.esign.reservation.dtos.RegistrationCardResponse; -import com.sjmresorts.opsapp.esign.reservation.dtos.SignedRegistrationCardSubmitRequest; -import com.sjmresorts.opsapp.esign.capture.dtos.EsignIosCaptureResponse; -import com.sjmresorts.opsapp.esign.reservation.dtos.EsignReservationNotifyResponse; -import com.sjmresorts.opsapp.esign.reservation.services.EsignReservationService; -import com.sjmresorts.opsapp.esign.reservation.support.OperaFlexLinkPageBuilder; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.CrossOrigin; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -@Tag(name = "Esign Reservation", description = "预订相关(前端上报 reservationId + hotelId)") -@RestController -@RequestMapping({"/api/v1/esign/reservations", "/api/v1/sinosecu/reservations"}) -@CrossOrigin(origins = "*") -@RequiredArgsConstructor -@Slf4j -public class EsignReservationController { - - private final EsignReservationService reservationService; - - @Value("${ops-app.deeplink.opera.close-popup:true}") - private boolean operaClosePopup; - - @Value("${ops-app.deeplink.opera.parent-origin:}") - private String operaParentOrigin; - - @Value("${ops-app.deeplink.opera.flex-link-origin:http://127.0.0.1:8066}") - private String operaFlexLinkOrigin; - - @Value("${ops-app.deeplink.opera.close-delay-ms:100}") - private int operaCloseDelayMs; - - @Value("${ops-app.deeplink.opera.open-delay-ms:800}") - private int operaOpenDelayMs; - - @Value("${ops-app.deeplink.opera.ext-app-id:0}") - private long operaExtAppId; - - @Value("${ops-app.deeplink.opera.debug-page:true}") - private boolean operaDebugPage; - - @Value("${ops-app.deeplink.opera.popup-width:100%}") - private String operaPopupWidth; - - @Value("${ops-app.deeplink.opera.popup-height:800px}") - private String operaPopupHeight; - - @Value("${ops-app.deeplink.opera.launch-mode:new-window}") - private String operaLaunchMode; - - @Value("${ops-app.deeplink.opera.close-modal-message:true}") - private boolean operaCloseModalMessage; - - @Operation( - summary = "接收预订参数并转发第三方", - description = "免 JWT。GET 查询参数:reservationId、hotelId。先取 OHIP OAuth Token,再请求 registrationCard 接口;成功后向同酒店 Esign 用户推送 FCM(data 含 reservationId)。" - ) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "已接收且第三方调用成功(或未配置第三方 URL)"), - @ApiResponse(responseCode = "400", description = "参数缺失"), - @ApiResponse(responseCode = "502", description = "第三方接口调用失败") - }) - @GetMapping(value = "/notify", produces = "application/json;charset=UTF-8") - public ResponseEntity notifyReservation( - @Parameter(description = "预订 ID", required = true) - @RequestParam("reservationId") String reservationId, - @Parameter(description = "酒店 ID", required = true) - @RequestParam("hotelId") String hotelId) { - EsignReservationNotifyResponse body = reservationService.acceptReservation(reservationId, hotelId); - HttpStatus status = HttpStatus.OK; - if (!body.isSuccess()) { - if ("502".equals(body.getCode())) { - status = HttpStatus.BAD_GATEWAY; - } else { - status = HttpStatus.BAD_REQUEST; - } - } - return ResponseEntity.status(status).body(body); - } - - @Operation( - summary = "Opera Flex Link 用:发通知并返回 HTML 页尝试关 Pop-up", - description = "免 JWT。服务端先执行 notify(FCM),再返回 HTML 页发送 DO_RETURN_EVENT。" - + " Destination 请用此路径,勿用 /launch。" - ) - @GetMapping(value = "/notify/page", produces = "text/html;charset=UTF-8") - public ResponseEntity notifyReservationPage( - @Parameter(description = "预订 ID", required = true) - @RequestParam("reservationId") String reservationId, - @Parameter(description = "酒店 ID", required = true) - @RequestParam("hotelId") String hotelId) { - EsignReservationNotifyResponse body = reservationService.acceptReservation(reservationId, hotelId); - String html = OperaFlexLinkPageBuilder.buildNotifyPage( - body.isSuccess(), - body.getMessage(), - body.getReservationId(), - operaLaunchMode, - operaClosePopup, - operaCloseModalMessage, - operaParentOrigin, - operaFlexLinkOrigin, - operaExtAppId, - operaOpenDelayMs, - operaCloseDelayMs, - operaDebugPage, - operaPopupWidth, - operaPopupHeight); - HttpStatus status = body.isSuccess() ? HttpStatus.OK : HttpStatus.BAD_REQUEST; - return ResponseEntity.status(status) - .contentType(MediaType.parseMediaType("text/html;charset=UTF-8")) - .header(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate") - .body(html); - } - - @Operation( - summary = "获取预订登记卡 PDF", - description = "免 JWT。点击推送通知后调用:先取 OHIP OAuth Token,再请求 registrationCard,返回 PDF Base64。" - ) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "获取成功"), - @ApiResponse(responseCode = "400", description = "参数缺失"), - @ApiResponse(responseCode = "502", description = "第三方接口失败或未解析到 PDF"), - @ApiResponse(responseCode = "503", description = "第三方未配置") - }) - @GetMapping(value = "/registration-card", produces = "application/json;charset=UTF-8") - public ResponseEntity getRegistrationCard( - @Parameter(description = "预订 ID", required = true) - @RequestParam("reservationId") String reservationId, - @Parameter(description = "酒店 ID", required = true) - @RequestParam("hotelId") String hotelId) { - RegistrationCardResponse body = reservationService.fetchRegistrationCard(reservationId, hotelId); - HttpStatus status = HttpStatus.OK; - if (!body.isSuccess()) { - status = switch (body.getCode()) { - case "502" -> HttpStatus.BAD_GATEWAY; - case "503" -> HttpStatus.SERVICE_UNAVAILABLE; - default -> HttpStatus.BAD_REQUEST; - }; - } - return ResponseEntity.status(status).body(body); - } - - @Operation( - summary = "获取预订关联的 Guest Profile ID", - description = "免 JWT。代理 OHIP GET /rsv/v1/hotels/{hotelId}/reservations/{reservationId}?fetchInstructions=Reservation," - + "从 reservationGuests.profileInfo.profileIdList 解析 type=Profile 的 id。") - @GetMapping(value = "/guest-profile-id", produces = "application/json;charset=UTF-8") - public ResponseEntity getGuestProfileId( - @Parameter(description = "预订 ID", required = true) - @RequestParam("reservationId") String reservationId, - @Parameter(description = "酒店 ID", required = true) - @RequestParam("hotelId") String hotelId) { - GuestProfileIdResponse body = reservationService.fetchGuestProfileId(reservationId, hotelId); - HttpStatus status = body.isSuccess() ? HttpStatus.OK : HttpStatus.BAD_REQUEST; - if (!body.isSuccess() && "404".equals(body.getCode())) { - status = HttpStatus.NOT_FOUND; - } - return ResponseEntity.status(status).body(body); - } - - @Operation( - summary = "提交签名登记卡至 OPERA", - description = "免 JWT。通知/预订登记卡页签名后:合成 PDF → fileAttachments。" - ) - @PostMapping(value = "/signed-registration-card", consumes = "application/json;charset=UTF-8", produces = "application/json;charset=UTF-8") - public ResponseEntity submitSignedRegistrationCard( - @RequestBody SignedRegistrationCardSubmitRequest request) { - log.info( - "POST /reservations/signed-registration-card reservationId={}, hotelId={}, signatureCount={}", - request != null ? request.getReservationId() : null, - request != null ? request.getHotelId() : null, - request != null ? request.getSignatureCount() : null); - EsignIosCaptureResponse body = reservationService.submitSignedRegistrationCard(request); - log.info( - "POST /reservations/signed-registration-card done success={} code={} message={}", - body.isSuccess(), - body.getCode(), - body.getMessage()); - HttpStatus status = body.isSuccess() ? HttpStatus.OK : HttpStatus.BAD_REQUEST; - if (!body.isSuccess() && "502".equals(body.getCode())) { - status = HttpStatus.BAD_GATEWAY; - } - return ResponseEntity.status(status).body(body); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/reservation/controllers/EsignSignFieldOhipMappingController.java b/src/main/java/com/sjmresorts/opsapp/esign/reservation/controllers/EsignSignFieldOhipMappingController.java deleted file mode 100644 index 2329398..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/reservation/controllers/EsignSignFieldOhipMappingController.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.sjmresorts.opsapp.esign.reservation.controllers; - -import com.sjmresorts.opsapp.esign.common.dtos.OhipProfileFieldCatalogResponse; -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.services.EsignSignFieldOhipMappingService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.annotation.Resource; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.CrossOrigin; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -@Tag(name = "Esign Sign Field OHIP Mapping", description = "签名字段 → OHIP 字段映射(CMS 配置)") -@RestController -@RequestMapping({"/api/v1/esign/sign-field-ohip-mapping", "/api/v1/sinosecu/sign-field-ohip-mapping"}) -@CrossOrigin(origins = "*") -public class EsignSignFieldOhipMappingController { - - @Resource - private EsignSignFieldOhipMappingService mappingService; - - @Operation(summary = "管理端:读取签名字段 OHIP 映射") - @GetMapping(produces = "application/json;charset=UTF-8") - public ResponseEntity getForAdmin( - @RequestParam(value = "hotelId", required = false) String hotelId) { - if (hotelId != null && !hotelId.isBlank()) { - return ResponseEntity.ok(mappingService.getForAdmin(hotelId)); - } - return ResponseEntity.ok(mappingService.getForAdmin()); - } - - @Operation(summary = "管理端:OHIP profileDetails 固定字段目录") - @GetMapping(value = "/fields", produces = "application/json;charset=UTF-8") - public ResponseEntity listFields() { - return ResponseEntity.ok(mappingService.listProfileFields()); - } - - @Operation(summary = "管理端:保存签名字段 OHIP 映射") - @PutMapping(produces = "application/json;charset=UTF-8") - public ResponseEntity save( - @RequestBody EsignSignFieldOhipMappingSaveRequest request) { - EsignSignFieldOhipMappingResponse body = mappingService.save(request); - if (!body.isSuccess()) { - return ResponseEntity.badRequest().body(body); - } - return ResponseEntity.ok(body); - } - - @Operation(summary = "管理端:预览映射组装后的 profileDetails JSON(与提交 OHIP 时结构一致)") - @PostMapping(value = "/preview", produces = "application/json;charset=UTF-8") - public ResponseEntity preview( - @RequestBody EsignSignFieldOhipPreviewRequest request) { - return ResponseEntity.ok(mappingService.previewProfileBody(request)); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/EsignReservationNotifyResponse.java b/src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/EsignReservationNotifyResponse.java deleted file mode 100644 index 395d70b..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/EsignReservationNotifyResponse.java +++ /dev/null @@ -1,121 +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 EsignReservationNotifyResponse { - - private boolean success; - private String code; - private String message; - - @Schema(description = "回显预订 ID") - private String reservationId; - - @Schema(description = "回显酒店 ID") - private String hotelId; - - @Schema(description = "是否已调用第三方接口") - private Boolean thirdPartyInvoked; - - @Schema(description = "第三方 HTTP 状态码(未调用时为 null)") - private Integer thirdPartyStatusCode; - - @Schema(description = "第三方响应摘要(可能截断)") - private String thirdPartyResponse; - - @Schema(description = "是否已尝试发送 FCM 通知") - private Boolean notificationInvoked; - - @Schema(description = "FCM 通知目标用户数") - private Integer notificationTargetCount; - - @Schema(description = "FCM 通知成功数") - private Integer notificationSuccessCount; - - @Schema(description = "FCM 通知失败数") - private Integer notificationFailureCount; - - @Schema(description = "FCM 通知结果状态") - private String notificationResultStatus; - - @Schema(description = "FCM 通知说明(含跳过原因)") - private String notificationMessage; - - public static EsignReservationNotifyResponse ok( - String reservationId, - String hotelId, - String message, - ReservationThirdPartyResult thirdParty) { - return ok(reservationId, hotelId, message, thirdParty, null); - } - - public static EsignReservationNotifyResponse ok( - String reservationId, - String hotelId, - String message, - ReservationThirdPartyResult thirdParty, - ReservationNotificationResult notification) { - EsignReservationNotifyResponse r = new EsignReservationNotifyResponse(); - r.setSuccess(true); - r.setCode("200"); - r.setMessage(message != null ? message : "Reservation accepted"); - r.setReservationId(reservationId); - r.setHotelId(hotelId); - applyThirdParty(r, thirdParty); - applyNotification(r, notification); - return r; - } - - public static EsignReservationNotifyResponse fail(String code, String message) { - EsignReservationNotifyResponse r = new EsignReservationNotifyResponse(); - r.setSuccess(false); - r.setCode(code); - r.setMessage(message); - return r; - } - - public static EsignReservationNotifyResponse fail( - String code, - String message, - String reservationId, - String hotelId, - ReservationThirdPartyResult thirdParty) { - EsignReservationNotifyResponse r = fail(code, message); - r.setReservationId(reservationId); - r.setHotelId(hotelId); - applyThirdParty(r, thirdParty); - return r; - } - - private static void applyThirdParty(EsignReservationNotifyResponse r, ReservationThirdPartyResult thirdParty) { - if (thirdParty == null) { - return; - } - r.setThirdPartyInvoked(thirdParty.isInvoked()); - if (thirdParty.isInvoked()) { - r.setThirdPartyStatusCode(thirdParty.getStatusCode()); - r.setThirdPartyResponse(thirdParty.getResponseBody()); - } - } - - private static void applyNotification( - EsignReservationNotifyResponse r, - ReservationNotificationResult notification) { - if (notification == null) { - return; - } - r.setNotificationInvoked(notification.isInvoked()); - r.setNotificationMessage(notification.getMessage()); - if (notification.isInvoked()) { - r.setNotificationTargetCount(notification.getTargetCount()); - r.setNotificationSuccessCount(notification.getSuccessCount()); - r.setNotificationFailureCount(notification.getFailureCount()); - r.setNotificationResultStatus(notification.getResultStatus()); - } - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/RegistrationCardResponse.java b/src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/RegistrationCardResponse.java deleted file mode 100644 index ec8d7b2..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/RegistrationCardResponse.java +++ /dev/null @@ -1,99 +0,0 @@ -package com.sjmresorts.opsapp.esign.reservation.dtos; - -import com.sjmresorts.opsapp.esign.printer.dtos.RegistrationCardResult; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@Schema(description = "预订登记卡 PDF 响应") -public class RegistrationCardResponse { - - private boolean success; - private String code; - private String message; - - @Schema(description = "预订 ID") - private String reservationId; - - @Schema(description = "酒店 ID") - private String hotelId; - - @Schema(description = "文件名") - private String fileName; - - @Schema(description = "文件类型,通常为 PDF") - private String fileType; - - @Schema(description = "PDF Base64(不含 data: 前缀)") - private String pdfBase64; - - @Schema(description = "旅客 Profile ID(OHIP reservationGuests)") - private String profileId; - - @Schema(description = "旅客 Profile Type(OHIP profile.profileType)") - private String profileType; - - @Schema(description = "第三方 HTTP 状态码") - private Integer thirdPartyStatusCode; - - public static RegistrationCardResponse ok( - String reservationId, - String hotelId, - RegistrationCardResult result) { - return ok(reservationId, hotelId, result, null, null); - } - - public static RegistrationCardResponse ok( - String reservationId, - String hotelId, - RegistrationCardResult result, - String profileId) { - return ok(reservationId, hotelId, result, profileId, null); - } - - public static RegistrationCardResponse ok( - String reservationId, - String hotelId, - RegistrationCardResult result, - String profileId, - String profileType) { - RegistrationCardResponse r = new RegistrationCardResponse(); - r.setSuccess(true); - r.setCode("200"); - r.setMessage("Registration card fetched"); - r.setReservationId(reservationId); - r.setHotelId(hotelId); - r.setFileName(result.getFileName()); - r.setFileType(result.getFileType()); - r.setPdfBase64(result.getPdfBase64()); - r.setProfileId(profileId); - r.setProfileType(profileType); - r.setThirdPartyStatusCode(result.getStatusCode()); - return r; - } - - public static RegistrationCardResponse fail(String code, String message) { - RegistrationCardResponse r = new RegistrationCardResponse(); - r.setSuccess(false); - r.setCode(code); - r.setMessage(message); - return r; - } - - public static RegistrationCardResponse fail( - String code, - String message, - String reservationId, - String hotelId, - RegistrationCardResult result) { - RegistrationCardResponse r = fail(code, message); - r.setReservationId(reservationId); - r.setHotelId(hotelId); - if (result != null) { - r.setThirdPartyStatusCode(result.getStatusCode()); - } - return r; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/ReservationNotificationResult.java b/src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/ReservationNotificationResult.java deleted file mode 100644 index a2fa68e..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/reservation/dtos/ReservationNotificationResult.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.sjmresorts.opsapp.esign.reservation.dtos; - -import com.sjmresorts.opsapp.notifications.dtos.SendNotificationResponse; -import com.sjmresorts.opsapp.notifications.dtos.SendNotificationResultData; -import lombok.Getter; - -@Getter -public class ReservationNotificationResult { - - private final boolean invoked; - private final boolean success; - private final int targetCount; - private final int successCount; - private final int failureCount; - private final String resultStatus; - private final String message; - - private ReservationNotificationResult( - boolean invoked, - boolean success, - int targetCount, - int successCount, - int failureCount, - String resultStatus, - String message) { - this.invoked = invoked; - this.success = success; - this.targetCount = targetCount; - this.successCount = successCount; - this.failureCount = failureCount; - this.resultStatus = resultStatus; - this.message = message; - } - - public static ReservationNotificationResult skipped(String message) { - return new ReservationNotificationResult(false, false, 0, 0, 0, null, message); - } - - public static ReservationNotificationResult from(SendNotificationResponse response, int targetCount) { - SendNotificationResultData data = response != null ? response.getData() : null; - int successCount = data != null ? data.getSuccessCount() : 0; - int failureCount = data != null ? data.getFailureCount() : 0; - String resultStatus = data != null ? data.getResultStatus() : null; - boolean success = response != null && response.isSuccess() && successCount > 0; - String message = response != null ? response.getMessage() : null; - return new ReservationNotificationResult(true, success, targetCount, successCount, failureCount, resultStatus, message); - } - - public static ReservationNotificationResult failure(String message) { - return new ReservationNotificationResult(true, false, 0, 0, 0, "FAILED", message); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/reservation/services/EsignReservationNotificationService.java b/src/main/java/com/sjmresorts/opsapp/esign/reservation/services/EsignReservationNotificationService.java deleted file mode 100644 index fcbb5c0..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/reservation/services/EsignReservationNotificationService.java +++ /dev/null @@ -1,170 +0,0 @@ -package com.sjmresorts.opsapp.esign.reservation.services; - -import com.sjmresorts.opsapp.esign.common.support.EsignReservationParamUtil; -import com.sjmresorts.opsapp.esign.reservation.dtos.ReservationNotificationResult; -import com.sjmresorts.opsapp.esign.auth.models.EsignUser; -import com.sjmresorts.opsapp.esign.auth.repositories.EsignUserRepository; -import com.sjmresorts.opsapp.notifications.dtos.SendNotificationData; -import com.sjmresorts.opsapp.notifications.dtos.SendNotificationResponse; -import com.sjmresorts.opsapp.notifications.dtos.SendNotificationResultData; -import com.sjmresorts.opsapp.notifications.enums.NotificationTargetType; -import com.sjmresorts.opsapp.notifications.services.NotificationService; -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.Service; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -@Slf4j -@Service -@RequiredArgsConstructor -public class EsignReservationNotificationService { - - private static final String ACTIVE_STATUS = "ACTIVE"; - - private final NotificationService notificationService; - private final EsignUserRepository esignUserRepository; - private final EsignReservationTaskService reservationTaskService; - - @Value("${ops-app.esign.reservation.notification.enabled:true}") - private boolean enabled; - - @Value("${ops-app.esign.reservation.notification.broadcast-if-no-hotel-match:true}") - private boolean broadcastIfNoHotelMatch; - - @Value("${ops-app.esign.reservation.notification.title:新预订待处理}") - private String title; - - @Value("${ops-app.esign.reservation.notification.body-template:预订号 {reservationId} 已就绪,请点击查看}") - private String bodyTemplate; - - @Value("${ops-app.esign.reservation.notification.trigger-task:RESERVATION_NOTIFY}") - private String triggerTask; - - @Value("${ops-app.esign.reservation.notification.action:OPEN_RESERVATION}") - private String action; - - public ReservationNotificationResult notifyReservationReady(String reservationId, String hotelId) { - log.info( - "Reservation notification start reservationId={}, hotelId={}, enabled={}, broadcastIfNoHotelMatch={}", - reservationId, hotelId, enabled, broadcastIfNoHotelMatch); - - if (StringUtils.isAnyBlank(reservationId, hotelId)) { - return ReservationNotificationResult.skipped("reservationId or hotelId is blank"); - } - - String rid = reservationId.trim(); - String hid = hotelId.trim(); - String notifyTitle = title; - String notifyBody = formatBody(rid, hid); - String taskId = reservationTaskService.createOrRefreshPendingTask(rid, hid, notifyTitle, notifyBody); - if (taskId == null) { - log.warn("Reservation task persist failed reservationId={}, hotelId={}", rid, hid); - } - - if (!enabled) { - return ReservationNotificationResult.skipped("Reservation notification disabled"); - } - - List recipients = resolveRecipients(hid); - if (recipients.isEmpty()) { - log.warn( - "No active Esign users with device token for hotelId={} (broadcastIfNoHotelMatch={}); task persisted for offline pull taskId={}", - hid, - broadcastIfNoHotelMatch, - taskId); - return ReservationNotificationResult.skipped("No push recipients with device token"); - } - - List targets = recipients.stream() - .map(user -> String.valueOf(user.getId())) - .collect(Collectors.toList()); - - log.info( - "Reservation notification recipients reservationId={}, hotelId={}, userIds={}", - reservationId, - hotelId, - targets); - - SendNotificationData data = new SendNotificationData(); - data.setTargetType(NotificationTargetType.ESIGN_USER); - data.setTargets(targets); - data.setTitle(notifyTitle); - data.setBody(notifyBody); - data.setTriggerTask(triggerTask); - data.setClickAction(action); - - Map payload = new HashMap<>(); - payload.put("action", action); - payload.put("reservationId", rid); - payload.put("hotelId", hid); - if (StringUtils.isNotBlank(taskId)) { - payload.put("taskId", taskId); - payload.put("taskType", EsignReservationTaskService.TASK_TYPE); - } - data.setData(payload); - - try { - SendNotificationResponse response = notificationService.send(data, triggerTask); - SendNotificationResultData resultData = response.getData(); - log.info( - "Reservation notification finished reservationId={}, hotelId={}, targets={}, success={}, failure={}, status={}", - reservationId, - hotelId, - targets.size(), - resultData != null ? resultData.getSuccessCount() : 0, - resultData != null ? resultData.getFailureCount() : 0, - resultData != null ? resultData.getResultStatus() : null); - return ReservationNotificationResult.from(response, targets.size()); - } catch (Exception e) { - log.error("Reservation notification failed reservationId={}, hotelId={}", reservationId, hotelId, e); - return ReservationNotificationResult.failure(e.getMessage()); - } - } - - private List resolveRecipients(String hotelId) { - String normalizedHotelId = EsignReservationParamUtil.normalizeParam(hotelId); - List byHotel = esignUserRepository.findPushRecipientsByHotel(normalizedHotelId, ACTIVE_STATUS); - if (!byHotel.isEmpty()) { - log.info("Found {} push recipient(s) for hotelId={}", byHotel.size(), normalizedHotelId); - return byHotel; - } - - List allActive = esignUserRepository.findActivePushRecipients(ACTIVE_STATUS); - if (!allActive.isEmpty()) { - log.warn( - "No esign_user matched hotelId={} (query uses esign_user.hotel_id, not esign_hotels). " - + "Active users with device token:", - normalizedHotelId); - for (EsignUser user : allActive) { - log.warn( - " userId={}, hotelId=[{}], userName={}", - user.getId(), - user.getHotelId(), - user.getUserName()); - } - } - - if (!broadcastIfNoHotelMatch) { - return List.of(); - } - - if (!allActive.isEmpty()) { - log.warn( - "Fallback broadcast to all {} active user(s) with device token", - allActive.size()); - } - return allActive; - } - - private String formatBody(String reservationId, String hotelId) { - return bodyTemplate - .replace("{reservationId}", reservationId) - .replace("{hotelId}", hotelId); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/reservation/services/EsignReservationService.java b/src/main/java/com/sjmresorts/opsapp/esign/reservation/services/EsignReservationService.java deleted file mode 100644 index 2924637..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/reservation/services/EsignReservationService.java +++ /dev/null @@ -1,221 +0,0 @@ -package com.sjmresorts.opsapp.esign.reservation.services; - -import com.sjmresorts.opsapp.esign.common.support.EsignReservationParamUtil; -import com.sjmresorts.opsapp.esign.reservation.dtos.GuestProfileIdResponse; -import com.sjmresorts.opsapp.esign.reservation.dtos.RegistrationCardResponse; -import com.sjmresorts.opsapp.esign.printer.dtos.RegistrationCardResult; -import com.sjmresorts.opsapp.esign.reservation.dtos.ReservationNotificationResult; -import com.sjmresorts.opsapp.esign.reservation.dtos.ReservationThirdPartyResult; -import com.sjmresorts.opsapp.esign.reservation.dtos.SignedRegistrationCardSubmitRequest; -import com.sjmresorts.opsapp.esign.reservation.dtos.SignedRegistrationCardUploadResult; -import com.sjmresorts.opsapp.esign.capture.dtos.EsignIosCaptureResponse; -import com.sjmresorts.opsapp.esign.reservation.dtos.EsignReservationNotifyResponse; -import com.sjmresorts.opsapp.esign.reservation.dtos.GuestProfileRef; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.springframework.stereotype.Service; - -@Slf4j -@Service -@RequiredArgsConstructor -public class EsignReservationService { - - private final ReservationThirdPartyClient thirdPartyClient; - private final EsignReservationNotificationService reservationNotificationService; - private final SignedRegistrationCardOhipService signedRegistrationCardOhipService; - - public EsignReservationNotifyResponse acceptReservation(String reservationId, String hotelId) { - if (StringUtils.isBlank(reservationId)) { - return EsignReservationNotifyResponse.fail("400", "reservationId must not be blank"); - } - if (StringUtils.isBlank(hotelId)) { - return EsignReservationNotifyResponse.fail("400", "hotelId must not be blank"); - } - - String rid = EsignReservationParamUtil.normalizeParam(reservationId); - String hid = EsignReservationParamUtil.normalizeParam(hotelId); - log.info("Reservation accepted reservationId={}, hotelId={}", rid, hid); - - // TODO: 临时关闭 OHIP 第三方调用(① OAuth Token ② registrationCard),仅保留推送通知 - /* - ReservationThirdPartyResult thirdPartyResult = thirdPartyClient.notifyReservation(rid, hid); - if (!thirdPartyResult.isInvoked()) { - return EsignReservationNotifyResponse.ok( - rid, - hid, - thirdPartyResult.getErrorMessage(), - thirdPartyResult); - } - if (thirdPartyResult.isHttpSuccess()) { - log.info("Registration card OK, triggering push notification reservationId={}, hotelId={}", rid, hid); - ReservationNotificationResult notificationResult = - reservationNotificationService.notifyReservationReady(rid, hid); - return EsignReservationNotifyResponse.ok( - rid, - hid, - "Registration card fetched", - thirdPartyResult, - notificationResult); - } - - return EsignReservationNotifyResponse.fail( - "502", - StringUtils.defaultIfBlank(thirdPartyResult.getErrorMessage(), "Third party request failed"), - rid, - hid, - thirdPartyResult); - */ - - ReservationThirdPartyResult thirdPartyResult = - ReservationThirdPartyResult.skipped("Third party temporarily disabled"); - log.info("Third party skipped, triggering push notification reservationId={}, hotelId={}", rid, hid); - ReservationNotificationResult notificationResult = - reservationNotificationService.notifyReservationReady(rid, hid); - return EsignReservationNotifyResponse.ok( - rid, - hid, - "Notification sent (third party disabled)", - thirdPartyResult, - notificationResult); - } - - public RegistrationCardResponse fetchRegistrationCard(String reservationId, String hotelId) { - if (StringUtils.isBlank(reservationId)) { - return RegistrationCardResponse.fail("400", "reservationId must not be blank"); - } - if (StringUtils.isBlank(hotelId)) { - return RegistrationCardResponse.fail("400", "hotelId must not be blank"); - } - - String rid = EsignReservationParamUtil.normalizeParam(reservationId); - String hid = EsignReservationParamUtil.normalizeParam(hotelId); - log.info("Fetch registration card PDF reservationId={}, hotelId={}", rid, hid); - - RegistrationCardResult result = thirdPartyClient.fetchRegistrationCard(rid, hid); - if (!result.isInvoked()) { - return RegistrationCardResponse.fail("503", result.getErrorMessage(), rid, hid, result); - } - if (result.isSuccess()) { - GuestProfileRef profile = thirdPartyClient.fetchGuestProfile(hid, rid).orElse(null); - return RegistrationCardResponse.ok( - rid, - hid, - result, - profile != null ? profile.profileId() : null, - profile != null ? profile.profileType() : null); - } - return RegistrationCardResponse.fail( - "502", - StringUtils.defaultIfBlank(result.getErrorMessage(), "Third party request failed"), - rid, - hid, - result); - } - - public GuestProfileIdResponse fetchGuestProfileId(String reservationId, String hotelId) { - if (StringUtils.isBlank(reservationId)) { - return GuestProfileIdResponse.fail("400", "reservationId must not be blank", null, hotelId); - } - if (StringUtils.isBlank(hotelId)) { - return GuestProfileIdResponse.fail("400", "hotelId must not be blank", reservationId, null); - } - String rid = EsignReservationParamUtil.normalizeParam(reservationId); - String hid = EsignReservationParamUtil.normalizeParam(hotelId); - return thirdPartyClient.fetchGuestProfile(hid, rid) - .map(profile -> GuestProfileIdResponse.ok( - rid, hid, profile.profileId(), profile.profileType())) - .orElseGet(() -> GuestProfileIdResponse.fail( - "404", "Profile not found in reservation", rid, hid)); - } - - public EsignIosCaptureResponse submitSignedRegistrationCard(SignedRegistrationCardSubmitRequest request) { - EsignIosCaptureResponse resp = new EsignIosCaptureResponse(); - if (request == null) { - resp.setSuccess(false); - resp.setCode("400"); - resp.setMessage("request body required"); - return resp; - } - String rid = EsignReservationParamUtil.normalizeParam(request.getReservationId()); - String hid = EsignReservationParamUtil.normalizeParam(request.getHotelId()); - String signaturesJson = StringUtils.trimToEmpty(request.getSignaturesJson()); - String signedBase64 = StringUtils.trimToEmpty(request.getBase64()); - String originalPdf = StringUtils.trimToEmpty(request.getPdfBase64()); - if (StringUtils.isBlank(rid)) { - resp.setSuccess(false); - resp.setCode("400"); - resp.setMessage("reservationId required"); - return resp; - } - if (StringUtils.isAllBlank(signedBase64, signaturesJson, originalPdf)) { - resp.setSuccess(false); - resp.setCode("400"); - resp.setMessage("pdfBase64 or signaturesJson required"); - return resp; - } - - log.info( - "Submit signed registration card reservationId={}, hotelId={}, signatureCount={}", - rid, - hid, - request.getSignatureCount()); - - GuestProfileRef profile = resolveGuestProfile( - request.getProfileId(), request.getProfileType(), hid, rid); - - SignedRegistrationCardUploadResult ohipResult = signedRegistrationCardOhipService.uploadForReservation( - rid, - hid, - originalPdf, - signedBase64, - signaturesJson, - request.getFileName(), - request.getConfirmationNumber(), - request.getSignFieldsJson(), - profile.profileId(), - profile.profileType()); - - EsignIosCaptureResponse.EsignIosCaptureData data = new EsignIosCaptureResponse.EsignIosCaptureData(); - data.setReservationId(rid); - if (ohipResult.isInvoked()) { - data.setConfirmationNumber(ohipResult.getConfirmationNumber()); - data.setOhipUploaded(ohipResult.isSuccess()); - if (!ohipResult.isSuccess()) { - resp.setSuccess(false); - resp.setCode(ohipResult.getStatusCode() > 0 ? String.valueOf(ohipResult.getStatusCode()) : "502"); - resp.setMessage(StringUtils.defaultIfBlank( - ohipResult.getErrorMessage(), "提交 OPERA 失败")); - resp.setData(data); - return resp; - } - resp.setSuccess(true); - resp.setCode("200"); - resp.setMessage("签名已提交至 OPERA(reservation " + rid + ")"); - resp.setData(data); - return resp; - } - - resp.setSuccess(true); - resp.setCode("200"); - resp.setMessage(StringUtils.defaultIfBlank( - ohipResult.getErrorMessage(), - "签名已处理(未上传 OPERA,请检查 signed-upload / OAuth 配置)")); - resp.setData(data); - return resp; - } - - private GuestProfileRef resolveGuestProfile( - String profileIdFromRequest, String profileTypeFromRequest, String hotelId, String reservationId) { - String profileId = StringUtils.isNotBlank(profileIdFromRequest) - ? EsignReservationParamUtil.normalizeParam(profileIdFromRequest) - : null; - String profileType = StringUtils.isNotBlank(profileTypeFromRequest) - ? EsignReservationParamUtil.normalizeParam(profileTypeFromRequest) - : null; - if (StringUtils.isNotBlank(profileId)) { - return new GuestProfileRef(profileId, profileType); - } - return thirdPartyClient.fetchGuestProfile(hotelId, reservationId).orElse(new GuestProfileRef(null, null)); - } -} 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 index 0899782..9ef128e 100644 --- a/src/main/java/com/sjmresorts/opsapp/esign/reservation/services/EsignReservationTaskService.java +++ b/src/main/java/com/sjmresorts/opsapp/esign/reservation/services/EsignReservationTaskService.java @@ -1,7 +1,5 @@ package com.sjmresorts.opsapp.esign.reservation.services; -import com.sjmresorts.opsapp.esign.capture.dtos.EsignIosCaptureResponse; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPendingListResponse; import com.sjmresorts.opsapp.esign.common.support.EsignReservationParamUtil; import com.sjmresorts.opsapp.esign.reservation.models.EsignReservationTask; import com.sjmresorts.opsapp.esign.reservation.repositories.EsignReservationTaskRepository; @@ -80,64 +78,6 @@ public class EsignReservationTaskService { return task.getTaskId(); } - @Transactional(readOnly = true) - public List listItemsForBucket(String bucket, String hotelId) { - String b = StringUtils.isBlank(bucket) ? "pending" : bucket.trim().toLowerCase(); - Instant cutoff = taskWindowCutoff(); - List rows; - if ("signed".equals(b)) { - rows = reservationTaskRepository.findTop100ByStatusOrderByCompletedAtDesc(STATUS_COMPLETED); - } else if ("voided".equals(b)) { - rows = reservationTaskRepository.findTop100ByStatusAndCreatedAtLessThanOrderByCreatedAtDesc( - STATUS_PENDING, cutoff); - } else { - rows = reservationTaskRepository.findTop100ByStatusAndCreatedAtGreaterThanEqualOrderByCreatedAtDesc( - STATUS_PENDING, cutoff); - } - - boolean filterByHotel = StringUtils.isNotBlank(hotelId); - String normalizedHotelId = filterByHotel ? EsignReservationParamUtil.normalizeParam(hotelId) : null; - List items = new ArrayList<>(); - for (EsignReservationTask row : rows) { - if (filterByHotel && !normalizedHotelId.equals(row.getHotelId())) { - continue; - } - items.add(toPendingItem(row, "signed".equals(b))); - } - return items; - } - - @Transactional - public EsignIosCaptureResponse completeTask(String taskId, String reservationId, String hotelId) { - EsignIosCaptureResponse resp = new EsignIosCaptureResponse(); - Optional opt = resolveTask(taskId, reservationId, hotelId); - if (opt.isEmpty()) { - resp.setSuccess(false); - resp.setCode("404"); - resp.setMessage("Reservation task not found"); - return resp; - } - EsignReservationTask t = opt.get(); - if (STATUS_COMPLETED.equals(t.getStatus())) { - resp.setSuccess(true); - resp.setCode("200"); - resp.setMessage("Already completed"); - EsignIosCaptureResponse.EsignIosCaptureData data = new EsignIosCaptureResponse.EsignIosCaptureData(); - data.setTaskId(t.getTaskId()); - resp.setData(data); - return resp; - } - t.setStatus(STATUS_COMPLETED); - t.setCompletedAt(Instant.now()); - reservationTaskRepository.save(t); - resp.setSuccess(true); - resp.setCode("200"); - resp.setMessage("Completed"); - EsignIosCaptureResponse.EsignIosCaptureData data = new EsignIosCaptureResponse.EsignIosCaptureData(); - data.setTaskId(t.getTaskId()); - resp.setData(data); - return resp; - } private Optional resolveTask(String taskId, String reservationId, String hotelId) { if (StringUtils.isNotBlank(taskId)) { @@ -152,48 +92,6 @@ public class EsignReservationTaskService { rid, hid, STATUS_PENDING); } - private EsignPendingListResponse.EsignPendingItem toPendingItem(EsignReservationTask row, boolean completedBucket) { - EsignPendingListResponse.EsignPendingItem it = new EsignPendingListResponse.EsignPendingItem(); - it.setTaskType(TASK_TYPE); - it.setTaskId(row.getTaskId()); - it.setReservationId(row.getReservationId()); - it.setHotelId(row.getHotelId()); - it.setCreatedAt(row.getCreatedAt() != null ? row.getCreatedAt().toString() : null); - it.setDocTitle(StringUtils.defaultIfBlank(row.getTitle(), "新预订待处理")); - it.setNotificationTitle(row.getTitle()); - it.setNotificationBody(row.getBody()); - if (completedBucket && row.getCompletedAt() != null) { - it.setSignedAt(row.getCompletedAt().toString()); - } - return it; - } - - public static void mergeSortedByCreatedAt( - List target, - List extra) { - if (extra == null || extra.isEmpty()) { - return; - } - target.addAll(extra); - target.sort(Comparator.comparing( - EsignReservationTaskService::readCreatedInstant, - Comparator.nullsLast(Comparator.reverseOrder()))); - if (target.size() > 100) { - target.subList(100, target.size()).clear(); - } - } - - private static Instant readCreatedInstant(EsignPendingListResponse.EsignPendingItem item) { - if (item == null || StringUtils.isBlank(item.getCreatedAt())) { - return null; - } - try { - return Instant.parse(item.getCreatedAt()); - } catch (Exception e) { - return null; - } - } - private static String trim(String s, int max) { if (s == null) { return null; diff --git a/src/main/java/com/sjmresorts/opsapp/esign/reservation/services/RegistrationConfirmationExtractService.java b/src/main/java/com/sjmresorts/opsapp/esign/reservation/services/RegistrationConfirmationExtractService.java deleted file mode 100644 index e7df52a..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/reservation/services/RegistrationConfirmationExtractService.java +++ /dev/null @@ -1,96 +0,0 @@ -package com.sjmresorts.opsapp.esign.reservation.services; - -import com.sjmresorts.opsapp.esign.reservation.support.RegistrationConfirmationExtractor; -import com.sjmresorts.opsapp.esign.printer.support.RegistrationConfirmationOcrExtractor; -import com.sjmresorts.opsapp.esign.common.support.EsignLogMaskUtil; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.springframework.stereotype.Service; - -import java.util.Base64; -import java.util.Optional; - -@Slf4j -@Service -@RequiredArgsConstructor -public class RegistrationConfirmationExtractService { - - private final RegistrationConfirmationOcrExtractor ocrExtractor; - - @Getter - private volatile String lastFailureDetail = ""; - - public Optional extractFromPdfBase64(String pdfBase64) { - if (StringUtils.isBlank(pdfBase64)) { - lastFailureDetail = "PDF Base64 为空"; - return Optional.empty(); - } - byte[] bytes = decodePdf(pdfBase64); - if (bytes == null) { - lastFailureDetail = "PDF Base64 解码失败"; - return Optional.empty(); - } - return extractFromPdfBytes(bytes); - } - - public Optional extractFromPdfBytes(byte[] pdfBytes) { - lastFailureDetail = ""; - if (pdfBytes == null || pdfBytes.length == 0) { - lastFailureDetail = "PDF 字节为空"; - return Optional.empty(); - } - Optional fromText = RegistrationConfirmationExtractor.extractFromPdfBytes(pdfBytes); - if (fromText.isPresent()) { - log.info("confirmation extract hit via PDF text: {}", fromText.get()); - return fromText; - } - log.info("confirmation extract PDF text miss, trying OCR pdfBytes={}", pdfBytes.length); - if (!ocrExtractor.isOcrEnabled()) { - lastFailureDetail = "PDF 无文本层且 OCR 已关闭"; - return Optional.empty(); - } - if (!ocrExtractor.isOcrReady()) { - lastFailureDetail = ocrExtractor.getUnavailableReason(); - log.warn("confirmation extract OCR unavailable: {}", lastFailureDetail); - return Optional.empty(); - } - Optional fromOcr = ocrExtractor.extractFromPdfBytes(pdfBytes); - if (fromOcr.isPresent()) { - log.info("confirmation extract hit via OCR: {}", fromOcr.get()); - return fromOcr; - } - String ocrHint = ocrExtractor.getLastOcrHint(); - lastFailureDetail = StringUtils.isNotBlank(ocrHint) - ? "PDF 无文本层,OCR 已执行但未匹配确认号;" + ocrHint - : "PDF 无文本层,OCR 已执行但未匹配确认号"; - log.warn("confirmation extract miss after PDF text + OCR: {}", lastFailureDetail); - return Optional.empty(); - } - - public String buildUserFacingFailureMessage() { - if (StringUtils.isNotBlank(lastFailureDetail)) { - return "无法在 PDF 中找到确认号(Conf. No / 確認號碼)。" + lastFailureDetail - + "。可安装 Tesseract 或执行 scripts/download-tessdata.ps1 后重启后端。"; - } - return "无法在 PDF 中找到确认号(Conf. No / 確認號碼)"; - } - - private static byte[] decodePdf(String pdfBase64) { - try { - return Base64.getDecoder().decode(normalizeBase64(pdfBase64)); - } catch (IllegalArgumentException e) { - return null; - } - } - - 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/services/ReservationThirdPartyClient.java b/src/main/java/com/sjmresorts/opsapp/esign/reservation/services/ReservationThirdPartyClient.java index a43a4ae..907c453 100644 --- a/src/main/java/com/sjmresorts/opsapp/esign/reservation/services/ReservationThirdPartyClient.java +++ b/src/main/java/com/sjmresorts/opsapp/esign/reservation/services/ReservationThirdPartyClient.java @@ -4,8 +4,6 @@ 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.printer.dtos.RegistrationCardResponseParser; -import com.sjmresorts.opsapp.esign.printer.dtos.RegistrationCardResult; import com.sjmresorts.opsapp.esign.reservation.dtos.ReservationThirdPartyResult; import com.sjmresorts.opsapp.esign.reservation.dtos.SignedRegistrationCardUploadResult; import com.sjmresorts.opsapp.esign.reservation.support.HospitalityOAuthTokenSingleton; @@ -103,53 +101,6 @@ public class ReservationThirdPartyClient { return ReservationThirdPartyResult.skipped("Third party disabled in notify flow"); } - /** 点击通知后:OAuth + registrationCard,解析 PDF */ - public RegistrationCardResult fetchRegistrationCard(String reservationId, String hotelId) { - if (StringUtils.isBlank(baseUrl)) { - return RegistrationCardResult.skipped("Third party base-url not configured"); - } - if (!oauthTokenSingleton.isConfigured()) { - return RegistrationCardResult.skipped("OHIP OAuth is not configured"); - } - - String ohipHotelId = resolveOhipHotelId(hotelId); - String path = buildRegistrationCardPath(ohipHotelId, reservationId.trim()); - String url = SimpleHttpClient.joinUrl(baseUrl, path); - - try { - SimpleHttpClient.RequestBuilder request = SimpleHttpClient.get(url) - .timeoutMs(timeoutMs) - .queryParam("regenerate", String.valueOf(regenerate)) - .queryParam("signedOnly", String.valueOf(signedOnly)) - .queryParam("reservationIdContext", reservationIdContext) - .queryParam("reservationIdType", reservationIdType) - .queryParam("language", language); - - applyBusinessAuth(request, ohipHotelId); - - log.info( - "RegistrationCard fetch: method=GET, url={}, pathHotelId={}, reservationId={}, requestHotelId={}", - request.getRequestUrlWithQuery(), ohipHotelId, reservationId, hotelId); - log.info( - "RegistrationCard fetch headers={}", - SimpleHttpClient.maskHeadersForLog(request.getHeaders())); - - long start = System.currentTimeMillis(); - HttpResponseResult response = request.execute(); - auditThirdParty( - EsignAuditLogService.EVT_OHIP_GET_REGISTRATION, - request, - response, - start, - ohipHotelId, - "GET registrationCard reservationId=" + reservationId); - return toRegistrationCardResult(response); - } catch (IllegalStateException e) { - log.error("RegistrationCard fetch failed", e); - return RegistrationCardResult.failure(-1, null, e.getMessage()); - } - } - /** * 签名登记卡提交 OPERA:确认号 → reservation id → fileAttachments。 */ @@ -719,48 +670,6 @@ public class ReservationThirdPartyClient { return trimmed.replaceAll("\\s", ""); } - private RegistrationCardResult toRegistrationCardResult(HttpResponseResult httpResult) { - int statusCode = httpResult.getStatusCode(); - String body = httpResult.getResponseContent(); - String summary = truncate(body); - - if (httpResult.isTransportError()) { - String error = StringUtils.defaultIfBlank(httpResult.getErrorMessage(), "Third party request failed"); - log.error("RegistrationCard API failed: {}", error); - return RegistrationCardResult.failure(statusCode, summary, error); - } - - if (!httpResult.isSuccessful()) { - String error = "Third party returned HTTP " + statusCode; - log.warn("RegistrationCard API non-success status={}, body={}", statusCode, summary); - return RegistrationCardResult.failure(statusCode, summary, error); - } - - Optional pdf = - RegistrationCardResponseParser.extract(body); - if (pdf.isEmpty()) { - log.warn("RegistrationCard API success but PDF not found in response, status={}", statusCode); - return RegistrationCardResult.failure( - statusCode, - summary, - "PDF not found in registrationCard response"); - } - - RegistrationCardResponseParser.PdfPayload payload = pdf.get(); - log.info( - "RegistrationCard PDF extracted status={}, fileName={}, fileType={}, base64Len={}", - statusCode, - payload.fileName(), - payload.fileType(), - payload.pdfBase64() != null ? payload.pdfBase64().length() : 0); - return RegistrationCardResult.success( - statusCode, - payload.fileName(), - payload.fileType(), - payload.pdfBase64(), - summary); - } - private String resolveOhipHotelId(String requestHotelId) { String configured = oauthTokenSingleton.getHotelId(); if (StringUtils.isNotBlank(configured)) { diff --git a/src/main/java/com/sjmresorts/opsapp/esign/reservation/services/SignedRegistrationCardOhipService.java b/src/main/java/com/sjmresorts/opsapp/esign/reservation/services/SignedRegistrationCardOhipService.java deleted file mode 100644 index efee2e3..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/reservation/services/SignedRegistrationCardOhipService.java +++ /dev/null @@ -1,258 +0,0 @@ -package com.sjmresorts.opsapp.esign.reservation.services; - -import com.sjmresorts.opsapp.esign.reservation.dtos.SignedRegistrationCardUploadResult; -import com.sjmresorts.opsapp.esign.printer.models.EsignPrinterPdfTask; -import com.sjmresorts.opsapp.esign.printer.support.EsignPrinterPdfFileStorage; -import com.sjmresorts.opsapp.esign.printer.dtos.RegistrationCardSignFieldParser; -import com.sjmresorts.opsapp.esign.printer.dtos.SignedRegistrationPdfComposer; -import com.sjmresorts.opsapp.esign.reservation.services.EsignSignFieldOhipMappingService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.springframework.stereotype.Service; - -import java.util.Map; - -@Slf4j -@Service -@RequiredArgsConstructor -public class SignedRegistrationCardOhipService { - - private final ReservationThirdPartyClient thirdPartyClient; - private final RegistrationConfirmationExtractService confirmationExtractService; - private final EsignSignFieldOhipMappingService signFieldOhipMappingService; - private final EsignPrinterPdfFileStorage pdfFileStorage; - - public SignedRegistrationCardUploadResult uploadAfterSign( - EsignPrinterPdfTask task, - String signedPdfBase64FromRequest, - String signaturesJson, - String confirmationNumberFromRequest, - String signFieldsJson) { - return uploadAfterSign( - task, - signedPdfBase64FromRequest, - signaturesJson, - confirmationNumberFromRequest, - signFieldsJson, - null, - null); - } - - public SignedRegistrationCardUploadResult uploadAfterSign( - EsignPrinterPdfTask task, - String signedPdfBase64FromRequest, - String signaturesJson, - String confirmationNumberFromRequest, - String signFieldsJson, - String profileId) { - return uploadAfterSign( - task, - signedPdfBase64FromRequest, - signaturesJson, - confirmationNumberFromRequest, - signFieldsJson, - profileId, - null); - } - - public SignedRegistrationCardUploadResult uploadAfterSign( - EsignPrinterPdfTask task, - String signedPdfBase64FromRequest, - String signaturesJson, - String confirmationNumberFromRequest, - String signFieldsJson, - String profileId, - String profileType) { - String taskId = task != null ? task.getTaskId() : null; - String originalPdf = task != null ? pdfFileStorage.materializeOriginalBase64(task) : null; - log.info( - "OHIP uploadAfterSign start taskId={}, hasOriginalPdf={}, hasSignedFromRequest={}, signaturesJsonLen={}, confirmationFromRequest={}", - taskId, - task != null && pdfFileStorage.hasOriginal(task), - StringUtils.isNotBlank(signedPdfBase64FromRequest), - StringUtils.length(signaturesJson), - StringUtils.isNotBlank(confirmationNumberFromRequest)); - if ((task == null || !pdfFileStorage.hasOriginal(task)) && StringUtils.isBlank(signedPdfBase64FromRequest)) { - log.warn("OHIP uploadAfterSign skip taskId={}: no PDF", taskId); - return SignedRegistrationCardUploadResult.skipped("No PDF available for OHIP upload"); - } - - String signedPdfBase64 = StringUtils.isNotBlank(signedPdfBase64FromRequest) - ? signedPdfBase64FromRequest - : composeSignedPdf(originalPdf, signaturesJson, signFieldsJson); - log.info("OHIP uploadAfterSign composed taskId={}, signedPdfLen={}", taskId, StringUtils.length(signedPdfBase64)); - - String confirmationNumber = StringUtils.isNotBlank(confirmationNumberFromRequest) - ? confirmationNumberFromRequest.trim() - : confirmationExtractService.extractFromPdfBase64(signedPdfBase64) - .or(() -> confirmationExtractService.extractFromPdfBase64(originalPdf)) - .orElse(null); - if (StringUtils.isBlank(confirmationNumber)) { - log.warn("OHIP uploadAfterSign fail taskId={}: confirmation number not found in PDF", taskId); - return SignedRegistrationCardUploadResult.failure( - 400, null, confirmationExtractService.buildUserFacingFailureMessage()); - } - - String hotelId = task != null ? task.getHotelId() : null; - String fileName = task != null ? task.getFileName() : null; - log.info( - "OHIP uploadAfterSign lookup OPERA confirmation={}, hotelId={}, taskId={}, fileName={}", - confirmationNumber, - hotelId, - taskId, - fileName); - - return submitWithMappedFields( - thirdPartyClient.uploadSignedRegistrationCard( - hotelId, - confirmationNumber, - signedPdfBase64, - fileName), - hotelId, - signaturesJson, - signFieldsJson, - profileId, - profileType); - } - - /** - * 登记卡通知页:已有 reservationId,合成 PDF 后直接上传 OPERA。 - */ - public SignedRegistrationCardUploadResult uploadForReservation( - String reservationId, - String hotelId, - String originalPdfBase64, - String signedPdfBase64FromRequest, - String signaturesJson, - String fileName, - String confirmationNumberFromRequest, - String signFieldsJson, - String profileId, - String profileType) { - if (StringUtils.isBlank(reservationId)) { - return SignedRegistrationCardUploadResult.failure(400, null, "reservationId required"); - } - String originalPdf = originalPdfBase64; - if (StringUtils.isBlank(originalPdf) && StringUtils.isBlank(signedPdfBase64FromRequest)) { - return SignedRegistrationCardUploadResult.failure( - 400, null, "pdfBase64 or signed base64 required"); - } - - String signedPdfBase64 = StringUtils.isNotBlank(signedPdfBase64FromRequest) - ? signedPdfBase64FromRequest - : composeSignedPdf(originalPdf, signaturesJson, signFieldsJson); - - String confirmationNumber = StringUtils.isNotBlank(confirmationNumberFromRequest) - ? confirmationNumberFromRequest.trim() - : confirmationExtractService.extractFromPdfBase64(signedPdfBase64) - .or(() -> confirmationExtractService.extractFromPdfBase64(originalPdf)) - .orElse(reservationId.trim()); - - log.info( - "OHIP signed upload (reservation flow) reservationId={}, hotelId={}, confirmation={}, profileId={}, profileType={}", - reservationId, - hotelId, - confirmationNumber, - profileId, - profileType); - - return submitWithMappedFields( - thirdPartyClient.uploadSignedRegistrationCardByReservationId( - hotelId, - reservationId.trim(), - confirmationNumber, - signedPdfBase64, - fileName), - hotelId, - signaturesJson, - signFieldsJson, - profileId, - profileType); - } - - private SignedRegistrationCardUploadResult submitWithMappedFields( - SignedRegistrationCardUploadResult pdfResult, - String hotelId, - String signaturesJson, - String signFieldsJson) { - return submitWithMappedFields(pdfResult, hotelId, signaturesJson, signFieldsJson, null, null); - } - - private SignedRegistrationCardUploadResult submitWithMappedFields( - SignedRegistrationCardUploadResult pdfResult, - String hotelId, - String signaturesJson, - String signFieldsJson, - String profileId) { - return submitWithMappedFields(pdfResult, hotelId, signaturesJson, signFieldsJson, profileId, null); - } - - private SignedRegistrationCardUploadResult submitWithMappedFields( - SignedRegistrationCardUploadResult pdfResult, - String hotelId, - String signaturesJson, - String signFieldsJson, - String profileId, - String profileType) { - if (pdfResult == null || !pdfResult.isSuccess() || StringUtils.isBlank(pdfResult.getReservationId())) { - return pdfResult; - } - Map mapped = - signFieldOhipMappingService.mapSignatureValues(hotelId, signaturesJson, signFieldsJson); - if (mapped.isEmpty()) { - log.warn( - "OHIP profile update skipped: no mapped text fields hotelId={}, reservationId={}, profileId={}, signFieldsJsonLen={}", - hotelId, - pdfResult.getReservationId(), - profileId, - StringUtils.length(signFieldsJson)); - return pdfResult; - } - log.info( - "OHIP CRM profile update reservationId={}, profileId={}, profileType={}, count={}, keys={}", - pdfResult.getReservationId(), - StringUtils.firstNonBlank(profileId, pdfResult.getProfileId()), - StringUtils.firstNonBlank(profileType, pdfResult.getProfileType()), - mapped.size(), - mapped.keySet()); - SignedRegistrationCardUploadResult profileResult = thirdPartyClient.submitMappedProfileFields( - hotelId, - pdfResult.getReservationId(), - mapped, - StringUtils.firstNonBlank(profileId, pdfResult.getProfileId()), - StringUtils.firstNonBlank(profileType, pdfResult.getProfileType())); - if (!profileResult.isInvoked()) { - log.warn( - "OHIP profile update not invoked reservationId={}, reason={}", - pdfResult.getReservationId(), - profileResult.getErrorMessage()); - return pdfResult; - } - if (!profileResult.isSuccess()) { - log.error( - "OHIP profile update failed reservationId={}, statusCode={}, error={}", - pdfResult.getReservationId(), - profileResult.getStatusCode(), - profileResult.getErrorMessage()); - // PDF 已上传 OPERA,不因 Profile 字段更新失败把整体签收标为失败 - return pdfResult; - } - return pdfResult; - } - - private String composeSignedPdf(String originalPdfBase64, String signaturesJson, String signFieldsJson) { - if (StringUtils.isBlank(signaturesJson)) { - return originalPdfBase64; - } - try { - return SignedRegistrationPdfComposer.composeBase64( - originalPdfBase64, - signaturesJson, - RegistrationCardSignFieldParser.parse(signFieldsJson)); - } catch (Exception e) { - log.warn("Compose signed PDF failed, fallback to original PDF", e); - return originalPdfBase64; - } - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/survey/controllers/EsignSurveyController.java b/src/main/java/com/sjmresorts/opsapp/esign/survey/controllers/EsignSurveyController.java deleted file mode 100644 index 4cbf2e9..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/survey/controllers/EsignSurveyController.java +++ /dev/null @@ -1,309 +0,0 @@ -package com.sjmresorts.opsapp.esign.survey.controllers; - -import com.auth0.jwt.JWTVerifier; -import com.sjmresorts.opsapp.authentication.dtos.LoginUserInfo; -import com.sjmresorts.opsapp.common.utils.JwtTokenUtil; -import com.sjmresorts.opsapp.common.utils.NetworkUtil; -import com.sjmresorts.opsapp.esign.survey.dtos.ExternalSurveyUrlParseResponse; -import com.sjmresorts.opsapp.esign.capture.dtos.EsignIosCaptureResponse; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterPushCandidatesResponse; -import com.sjmresorts.opsapp.esign.survey.dtos.EsignSurveyExternalWebhookRequest; -import com.sjmresorts.opsapp.esign.survey.dtos.EsignSurveyPushAssignRequest; -import com.sjmresorts.opsapp.esign.survey.dtos.EsignSurveyResponse; -import com.sjmresorts.opsapp.esign.survey.dtos.EsignSurveySaveRequest; -import com.sjmresorts.opsapp.esign.survey.dtos.EsignSurveySubmitRequest; -import com.sjmresorts.opsapp.esign.survey.services.EsignSurveyMediaService; -import com.sjmresorts.opsapp.esign.survey.services.EsignSurveyPushService; -import com.sjmresorts.opsapp.esign.survey.services.EsignSurveyService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.annotation.Resource; -import jakarta.servlet.http.HttpServletRequest; -import org.apache.commons.lang3.StringUtils; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.http.CacheControl; -import org.springframework.http.MediaType; -import org.springframework.web.bind.annotation.CrossOrigin; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.multipart.MultipartFile; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Optional; -import java.util.concurrent.TimeUnit; - -@Tag(name = "Esign Survey", description = "调查问卷(Electron 配置 + App 填写)") -@RestController -@RequestMapping({"/api/v1/esign/surveys", "/api/v1/sinosecu/surveys"}) -@CrossOrigin(origins = "*") -public class EsignSurveyController { - - @Resource - private EsignSurveyService surveyService; - - @Resource - private EsignSurveyPushService surveyPushService; - - @Resource - private EsignSurveyMediaService surveyMediaService; - - @Operation(summary = "管理端:问卷列表", description = "免 JWT。Electron 问卷配置管理。") - @GetMapping(produces = "application/json;charset=UTF-8") - public ResponseEntity listAll() { - return ResponseEntity.ok(surveyService.listAllForAdmin()); - } - - @Operation(summary = "App:已发布问卷列表") - @GetMapping(value = "/published", produces = "application/json;charset=UTF-8") - public ResponseEntity listPublished( - @RequestParam(value = "hotelId", required = false) String hotelId) { - return ResponseEntity.ok(surveyService.listPublished(hotelId)); - } - - @Operation(summary = "问卷详情(含题目)") - @GetMapping(value = "/{surveyId}", produces = "application/json;charset=UTF-8") - public ResponseEntity getDetail(@PathVariable("surveyId") String surveyId) { - EsignSurveyResponse body = surveyService.getDetail(surveyId, true); - if (!body.isSuccess()) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(body); - } - return ResponseEntity.ok(body); - } - - @Operation(summary = "问卷 Logo 图片(顶栏固定样式)") - @GetMapping(value = "/{surveyId}/media") - public ResponseEntity getMedia(@PathVariable("surveyId") String surveyId) { - Optional path = surveyMediaService.resolveMediaFile(surveyId); - if (path.isEmpty()) { - return ResponseEntity.notFound().build(); - } - try { - Path file = path.get(); - String contentType = surveyMediaService.probeMimeForPath(file); - org.springframework.core.io.FileSystemResource body = new org.springframework.core.io.FileSystemResource(file); - return ResponseEntity.ok() - .contentType(MediaType.parseMediaType(contentType)) - .cacheControl(CacheControl.maxAge(7, TimeUnit.DAYS).cachePublic()) - .contentLength(Files.size(file)) - .body(body); - } catch (IOException e) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); - } - } - - @Operation(summary = "上传问卷顶栏 Logo(multipart,推荐)") - @PostMapping(value = "/{surveyId}/media", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public ResponseEntity uploadMedia( - @PathVariable("surveyId") String surveyId, - @RequestParam("file") MultipartFile file) { - if (file == null || file.isEmpty()) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST) - .body(EsignSurveyResponse.fail("400", "file required")); - } - try { - EsignSurveyResponse body = surveyService.uploadLogo( - surveyId, - file.getBytes(), - StringUtils.defaultIfBlank(file.getContentType(), MediaType.APPLICATION_OCTET_STREAM_VALUE)); - if (!body.isSuccess()) { - HttpStatus status = "404".equals(body.getCode()) ? HttpStatus.NOT_FOUND : HttpStatus.BAD_REQUEST; - return ResponseEntity.status(status).body(body); - } - return ResponseEntity.ok(body); - } catch (Exception e) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(EsignSurveyResponse.fail("500", "upload failed")); - } - } - - @Operation(summary = "创建或更新问卷") - @PostMapping(consumes = "application/json;charset=UTF-8", produces = "application/json;charset=UTF-8") - public ResponseEntity save(@RequestBody EsignSurveySaveRequest request) { - EsignSurveyResponse body = surveyService.save(request); - if (!body.isSuccess()) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(body); - } - return ResponseEntity.ok(body); - } - - @Operation(summary = "更新问卷") - @PutMapping(value = "/{surveyId}", consumes = "application/json;charset=UTF-8", produces = "application/json;charset=UTF-8") - public ResponseEntity update( - @PathVariable("surveyId") String surveyId, - @RequestBody EsignSurveySaveRequest request) { - if (request != null) { - request.setSurveyId(surveyId); - } - EsignSurveyResponse body = surveyService.save(request); - if (!body.isSuccess()) { - HttpStatus status = "404".equals(body.getCode()) ? HttpStatus.NOT_FOUND : HttpStatus.BAD_REQUEST; - return ResponseEntity.status(status).body(body); - } - return ResponseEntity.ok(body); - } - - @Operation(summary = "发布问卷") - @PostMapping(value = "/{surveyId}/publish", produces = "application/json;charset=UTF-8") - public ResponseEntity publish(@PathVariable("surveyId") String surveyId) { - EsignSurveyResponse body = surveyService.publish(surveyId); - if (!body.isSuccess()) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(body); - } - return ResponseEntity.ok(body); - } - - @Operation(summary = "归档问卷") - @PostMapping(value = "/{surveyId}/archive", produces = "application/json;charset=UTF-8") - public ResponseEntity archive(@PathVariable("surveyId") String surveyId) { - EsignSurveyResponse body = surveyService.archive(surveyId); - if (!body.isSuccess()) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(body); - } - return ResponseEntity.ok(body); - } - - @Operation(summary = "删除问卷") - @DeleteMapping(value = "/{surveyId}", produces = "application/json;charset=UTF-8") - public ResponseEntity delete(@PathVariable("surveyId") String surveyId) { - EsignSurveyResponse body = surveyService.delete(surveyId); - if (!body.isSuccess()) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(body); - } - return ResponseEntity.ok(body); - } - - @Operation(summary = "提交问卷答案") - @PostMapping(value = "/{surveyId}/submit", consumes = "application/json;charset=UTF-8", produces = "application/json;charset=UTF-8") - public ResponseEntity submit( - HttpServletRequest request, - @PathVariable("surveyId") String surveyId, - @RequestBody EsignSurveySubmitRequest body) { - EsignSurveyResponse resp = surveyService.submit(surveyId, body); - if (!resp.isSuccess()) { - HttpStatus status = switch (resp.getCode()) { - case "404" -> HttpStatus.NOT_FOUND; - case "409" -> HttpStatus.CONFLICT; - default -> HttpStatus.BAD_REQUEST; - }; - return ResponseEntity.status(status).body(resp); - } - Long userId = resolveUserIdIfVerifiedJwt(request); - if (userId != null) { - surveyPushService.completePushForUser( - surveyId, userId, NetworkUtil.getClientIpByRequest(request)); - } - return ResponseEntity.ok(resp); - } - - @Operation(summary = "App:完成问卷推送(从待签列表移除)") - @PostMapping(value = "/{surveyId}/push/complete", produces = "application/json;charset=UTF-8") - public ResponseEntity completeSurveyPush( - HttpServletRequest request, - @PathVariable("surveyId") String surveyId) { - Long userId = resolveUserIdIfVerifiedJwt(request); - EsignIosCaptureResponse body = surveyPushService.completePushForUser( - surveyId, userId, NetworkUtil.getClientIpByRequest(request)); - return ResponseEntity.status(httpStatus(body.isSuccess(), body.getCode())).body(body); - } - - private static Long resolveUserIdIfVerifiedJwt(HttpServletRequest request) { - String auth = request.getHeader("Authorization"); - if (StringUtils.isBlank(auth) || !auth.startsWith("Bearer ")) { - return null; - } - String token = auth.substring(7).trim(); - if (StringUtils.isBlank(token)) { - return null; - } - try { - JWTVerifier jwtVerifier = JwtTokenUtil.getJwtVerifier(); - jwtVerifier.verify(token); - LoginUserInfo user = JwtTokenUtil.parseUserFromToken(token); - if (user == null || StringUtils.isBlank(user.getEmployeeId())) { - return null; - } - return Long.parseLong(user.getEmployeeId()); - } catch (Exception e) { - return null; - } - } - - @Operation(summary = "解析第三方问卷链接(Microsoft Forms 等)") - @GetMapping(value = "/external/parse-url", produces = "application/json;charset=UTF-8") - public ResponseEntity parseExternalUrl( - @RequestParam(value = "url", required = false) String url) { - if (StringUtils.isBlank(url)) { - return ResponseEntity.badRequest() - .body(ExternalSurveyUrlParseResponse.fail("400", "url query parameter required")); - } - ExternalSurveyUrlParseResponse body = surveyService.parseExternalUrl(url); - HttpStatus status = body.isSuccess() ? HttpStatus.OK : HttpStatus.BAD_REQUEST; - return ResponseEntity.status(status).body(body); - } - - @Operation(summary = "第三方问卷 Webhook 回传(Power Automate 等)") - @PostMapping(value = "/external/webhook", consumes = "application/json;charset=UTF-8", produces = "application/json;charset=UTF-8") - public ResponseEntity externalWebhook( - @RequestBody EsignSurveyExternalWebhookRequest request) { - EsignSurveyResponse body = surveyService.ingestExternalWebhook(request); - if (!body.isSuccess()) { - HttpStatus status = switch (body.getCode()) { - case "404" -> HttpStatus.NOT_FOUND; - case "403" -> HttpStatus.FORBIDDEN; - case "409" -> HttpStatus.CONFLICT; - default -> HttpStatus.BAD_REQUEST; - }; - return ResponseEntity.status(status).body(body); - } - return ResponseEntity.ok(body); - } - - @Operation(summary = "管理端:问卷提交记录") - @GetMapping(value = "/{surveyId}/submissions", produces = "application/json;charset=UTF-8") - public ResponseEntity listSubmissions(@PathVariable("surveyId") String surveyId) { - EsignSurveyResponse body = surveyService.listSubmissions(surveyId); - if (!body.isSuccess()) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(body); - } - return ResponseEntity.ok(body); - } - - @Operation(summary = "问卷推送候选(设备+组)", description = "免 JWT。仅 MANUAL_PUSH 模式可用。") - @GetMapping(value = "/survey-push-candidates", produces = "application/json;charset=UTF-8") - public ResponseEntity getSurveyPushCandidates( - @RequestParam("surveyId") String surveyId) { - EsignPrinterPushCandidatesResponse body = surveyPushService.getPushCandidates(surveyId); - return ResponseEntity.status(httpStatus(body.isSuccess(), body.getCode())).body(body); - } - - @Operation(summary = "设置问卷推送目标", description = "免 JWT。选择设备 userIds 和/或 groupIds。") - @PostMapping(value = "/survey-push", produces = "application/json;charset=UTF-8") - public ResponseEntity assignSurveyPush( - @RequestBody EsignSurveyPushAssignRequest body) { - EsignIosCaptureResponse resp = surveyPushService.assignPushTargets(body); - return ResponseEntity.status(httpStatus(resp.isSuccess(), resp.getCode())).body(resp); - } - - private static HttpStatus httpStatus(boolean success, String code) { - if (success) { - return HttpStatus.OK; - } - return switch (StringUtils.defaultString(code)) { - case "400" -> HttpStatus.BAD_REQUEST; - case "401" -> HttpStatus.UNAUTHORIZED; - case "404" -> HttpStatus.NOT_FOUND; - case "409" -> HttpStatus.CONFLICT; - default -> HttpStatus.INTERNAL_SERVER_ERROR; - }; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/survey/dtos/EsignSurveyExternalWebhookRequest.java b/src/main/java/com/sjmresorts/opsapp/esign/survey/dtos/EsignSurveyExternalWebhookRequest.java deleted file mode 100644 index 83572cb..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/survey/dtos/EsignSurveyExternalWebhookRequest.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.sjmresorts.opsapp.esign.survey.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 = "第三方问卷 Webhook 回传(Power Automate / 自定义集成)") -public class EsignSurveyExternalWebhookRequest { - - /** 可选;与 webhook secret 二选一用于定位问卷 */ - private String surveyId; - /** Microsoft Forms form id;可与 surveyId 二选一 */ - private String externalFormId; - /** 与 esign_surveys.webhook_secret 匹配 */ - private String secret; - /** 外部平台响应 ID,用于去重 */ - private String externalResponseId; - private String respondentEmail; - private String respondentName; - private String deviceId; - private String hotelId; - /** 题目 → 答案 */ - private Map answers; - /** 原始 payload(answers 为空时使用) */ - private Map raw; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/survey/dtos/EsignSurveyPushAssignRequest.java b/src/main/java/com/sjmresorts/opsapp/esign/survey/dtos/EsignSurveyPushAssignRequest.java deleted file mode 100644 index 902bdae..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/survey/dtos/EsignSurveyPushAssignRequest.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.sjmresorts.opsapp.esign.survey.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 EsignSurveyPushAssignRequest { - - private String surveyId; - private List userIds; - private List groupIds; - /** 按平板登记推送(与 CMS 设备页 hotel-1 等条目一一对应) */ - private List registryIds; - private boolean sendFcm = false; - private String assignedBy; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/survey/dtos/EsignSurveyResponse.java b/src/main/java/com/sjmresorts/opsapp/esign/survey/dtos/EsignSurveyResponse.java deleted file mode 100644 index a0374fb..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/survey/dtos/EsignSurveyResponse.java +++ /dev/null @@ -1,93 +0,0 @@ -package com.sjmresorts.opsapp.esign.survey.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; -import java.util.Map; - -@Data -@NoArgsConstructor -@JsonInclude(JsonInclude.Include.NON_NULL) -@Schema(description = "问卷 API 通用响应") -public class EsignSurveyResponse { - - private boolean success = true; - private String code = "200"; - private String message; - private EsignSurveyData data; - - @Data - @NoArgsConstructor - public static class EsignSurveyData { - private String surveyId; - private String submissionId; - private List surveys; - private EsignSurveyItem survey; - private List submissions; - } - - @Data - @NoArgsConstructor - public static class EsignSurveyItem { - private String surveyId; - private String title; - private String description; - private String rewardText; - private String displayType; - private String imageUrl; - private String headerHotelName; - private String hotelId; - private String status; - private Integer sortOrder; - private Integer questionCount; - private String createdAt; - private String updatedAt; - private String publishedAt; - /** NATIVE | EXTERNAL */ - private String sourceType; - /** MANUAL_PUSH | AUTO_AFTER_PDF_SIGN */ - private String deliveryMode; - private String externalUrl; - private String externalProvider; - private String externalFormId; - /** App WebView 打开地址 */ - private String externalEmbedUrl; - /** CMS 展示 Webhook 地址用(只读) */ - private String webhookUrl; - /** 管理端保存后展示,用于配置 Power Automate */ - private String webhookSecret; - private Integer submissionCount; - private List> questions; - } - - @Data - @NoArgsConstructor - public static class EsignSurveySubmissionItem { - private String submissionId; - private String surveyId; - private String sourceType; - private String externalResponseId; - private String deviceId; - private String hotelId; - private String submittedAt; - private Map answers; - private Map respondentMeta; - } - - public static EsignSurveyResponse ok(String message) { - EsignSurveyResponse r = new EsignSurveyResponse(); - r.setMessage(message); - return r; - } - - public static EsignSurveyResponse fail(String code, String message) { - EsignSurveyResponse r = new EsignSurveyResponse(); - r.setSuccess(false); - r.setCode(code); - r.setMessage(message); - return r; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/survey/dtos/EsignSurveySaveRequest.java b/src/main/java/com/sjmresorts/opsapp/esign/survey/dtos/EsignSurveySaveRequest.java deleted file mode 100644 index 98b07ed..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/survey/dtos/EsignSurveySaveRequest.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.sjmresorts.opsapp.esign.survey.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; -import java.util.Map; - -@Data -@NoArgsConstructor -@JsonInclude(JsonInclude.Include.NON_NULL) -@Schema(description = "问卷保存请求(Electron 配置)") -public class EsignSurveySaveRequest { - - private String surveyId; - private String title; - private String description; - private String rewardText; - /** CARD | IMAGE */ - private String displayType; - private String imageUrl; - /** Forms 顶栏酒店名称 */ - private String headerHotelName; - private String hotelId; - private Integer sortOrder; - /** NATIVE | EXTERNAL */ - private String sourceType; - /** MANUAL_PUSH | AUTO_AFTER_PDF_SIGN */ - private String deliveryMode; - /** 第三方问卷链接(Microsoft Forms 等) */ - private String externalUrl; - /** 题目列表,见文档:SINGLE | MULTI | TEXT | RATING */ - private List> questions; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/survey/dtos/EsignSurveySubmitRequest.java b/src/main/java/com/sjmresorts/opsapp/esign/survey/dtos/EsignSurveySubmitRequest.java deleted file mode 100644 index 03a1773..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/survey/dtos/EsignSurveySubmitRequest.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.sjmresorts.opsapp.esign.survey.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; -import java.util.Map; - -@Data -@NoArgsConstructor -@JsonInclude(JsonInclude.Include.NON_NULL) -@Schema(description = "问卷提交答案") -public class EsignSurveySubmitRequest { - - private String deviceId; - private String hotelId; - /** questionId -> answer(单选为 string,多选为 array,文本为 string,评分为 number) */ - private Map answers; - private List> answerList; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/survey/dtos/ExternalSurveyUrlParseResponse.java b/src/main/java/com/sjmresorts/opsapp/esign/survey/dtos/ExternalSurveyUrlParseResponse.java deleted file mode 100644 index 24b3d6c..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/survey/dtos/ExternalSurveyUrlParseResponse.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.sjmresorts.opsapp.esign.survey.dtos; - -import com.fasterxml.jackson.annotation.JsonInclude; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@JsonInclude(JsonInclude.Include.NON_NULL) -@Schema(description = "第三方问卷链接解析结果") -public class ExternalSurveyUrlParseResponse { - - private boolean success = true; - private String code = "200"; - private String message; - private ParseData data; - - @Data - @NoArgsConstructor - public static class ParseData { - private String provider; - private String formId; - private String normalizedUrl; - private String embedUrl; - } - - public static ExternalSurveyUrlParseResponse ok(ParseData data) { - ExternalSurveyUrlParseResponse r = new ExternalSurveyUrlParseResponse(); - r.setData(data); - return r; - } - - public static ExternalSurveyUrlParseResponse fail(String code, String message) { - ExternalSurveyUrlParseResponse r = new ExternalSurveyUrlParseResponse(); - r.setSuccess(false); - r.setCode(code); - r.setMessage(message); - return r; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/survey/dtos/ExternalSurveyUrlParser.java b/src/main/java/com/sjmresorts/opsapp/esign/survey/dtos/ExternalSurveyUrlParser.java deleted file mode 100644 index 4e89437..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/survey/dtos/ExternalSurveyUrlParser.java +++ /dev/null @@ -1,123 +0,0 @@ -package com.sjmresorts.opsapp.esign.survey.dtos; - -import org.apache.commons.lang3.StringUtils; - -import java.net.URI; -import java.net.URISyntaxException; -import java.util.Locale; -import java.util.Optional; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** 识别第三方问卷链接(Microsoft Forms 等)并提取 formId、规范化 embed URL。 */ -public final class ExternalSurveyUrlParser { - - public static final String PROVIDER_MICROSOFT_FORMS = "MICROSOFT_FORMS"; - public static final String PROVIDER_GOOGLE_FORMS = "GOOGLE_FORMS"; - public static final String PROVIDER_GENERIC = "GENERIC"; - - private static final Pattern MS_FORMS_OFFICE_R = - Pattern.compile("forms\\.office\\.com/r/([A-Za-z0-9_-]+)", Pattern.CASE_INSENSITIVE); - private static final Pattern GOOGLE_FORMS = - Pattern.compile("docs\\.google\\.com/forms/d/(?:e/)?([A-Za-z0-9_-]+)", Pattern.CASE_INSENSITIVE); - - private ExternalSurveyUrlParser() { - } - - public record ParseResult( - String provider, - String formId, - String normalizedUrl, - String embedUrl) { - } - - public static Optional parse(String rawUrl) { - String trimmed = StringUtils.trimToNull(rawUrl); - if (trimmed == null) { - return Optional.empty(); - } - String url = trimmed; - if (!url.contains("://")) { - url = "https://" + url; - } - URI uri; - try { - uri = new URI(url); - } catch (URISyntaxException e) { - return Optional.empty(); - } - String host = StringUtils.defaultString(uri.getHost()).toLowerCase(Locale.ROOT); - String path = StringUtils.defaultString(uri.getPath()); - String query = StringUtils.defaultString(uri.getRawQuery()); - - if (host.contains("forms.cloud.microsoft") || host.contains("forms.office.com")) { - return parseMicrosoftForms(url, host, path, query); - } - if (host.contains("docs.google.com")) { - return parseGoogleForms(url); - } - return Optional.of(new ParseResult(PROVIDER_GENERIC, null, url, url)); - } - - private static Optional parseMicrosoftForms( - String url, String host, String path, String query) { - String formId = extractQueryParam(query, "id"); - if (StringUtils.isBlank(formId)) { - Matcher m = MS_FORMS_OFFICE_R.matcher(url); - if (m.find()) { - formId = m.group(1); - } - } - if (StringUtils.isBlank(formId)) { - return Optional.empty(); - } - String embedUrl = buildMicrosoftFormsEmbedUrl(formId, host, path, query, url); - return Optional.of(new ParseResult(PROVIDER_MICROSOFT_FORMS, formId.trim(), url.trim(), embedUrl)); - } - - private static String buildMicrosoftFormsEmbedUrl( - String formId, String host, String path, String query, String fallbackUrl) { - if (host.contains("forms.cloud.microsoft")) { - return "https://forms.cloud.microsoft/pages/responsepage.aspx?id=" - + formId - + "&origin=lprLink&route=shorturl"; - } - if (path.toLowerCase(Locale.ROOT).contains("/r/")) { - return fallbackUrl; - } - if (StringUtils.isNotBlank(query) && query.contains("id=")) { - return "https://forms.office.com/Pages/ResponsePage.aspx?id=" + formId; - } - return fallbackUrl; - } - - private static Optional parseGoogleForms(String url) { - Matcher m = GOOGLE_FORMS.matcher(url); - if (!m.find()) { - return Optional.empty(); - } - String formId = m.group(1); - String embedUrl = url.contains("/viewform") - ? url - : "https://docs.google.com/forms/d/" + formId + "/viewform"; - return Optional.of(new ParseResult(PROVIDER_GOOGLE_FORMS, formId, url.trim(), embedUrl)); - } - - private static String extractQueryParam(String query, String name) { - if (StringUtils.isBlank(query) || StringUtils.isBlank(name)) { - return null; - } - for (String part : query.split("&")) { - int eq = part.indexOf('='); - if (eq <= 0) { - continue; - } - String key = part.substring(0, eq); - if (!name.equalsIgnoreCase(key)) { - continue; - } - return part.substring(eq + 1); - } - return null; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/survey/models/EsignSurvey.java b/src/main/java/com/sjmresorts/opsapp/esign/survey/models/EsignSurvey.java deleted file mode 100644 index 3fba9ff..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/survey/models/EsignSurvey.java +++ /dev/null @@ -1,89 +0,0 @@ -package com.sjmresorts.opsapp.esign.survey.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_surveys") -public class EsignSurvey { - - @Id - @Column(name = "survey_id", nullable = false, length = 36) - private String surveyId; - - @Column(name = "title", nullable = false, length = 256) - private String title; - - @Column(name = "description", length = 1000) - private String description; - - @Column(name = "reward_text", length = 256) - private String rewardText; - - @Column(name = "display_type", nullable = false, length = 16) - private String displayType; - - @Lob - @Column(name = "image_url") - private String imageUrl; - - /** Forms 顶栏酒店名称(Logo 使用 imageUrl) */ - @Column(name = "header_hotel_name", length = 256) - private String headerHotelName; - - @Column(name = "hotel_id", length = 64) - private String hotelId; - - @Column(name = "status", nullable = false, length = 32) - private String status; - - @Lob - @Column(name = "questions_json", nullable = false) - private String questionsJson; - - @Column(name = "sort_order", nullable = false) - private Integer sortOrder; - - @Column(name = "created_at", nullable = false) - private Instant createdAt; - - @Column(name = "updated_at", nullable = false) - private Instant updatedAt; - - @Column(name = "published_at") - private Instant publishedAt; - - /** NATIVE | EXTERNAL */ - @Column(name = "source_type", nullable = false, length = 16) - private String sourceType; - - @Column(name = "external_url", length = 2000) - private String externalUrl; - - @Column(name = "external_provider", length = 32) - private String externalProvider; - - @Column(name = "external_form_id", length = 512) - private String externalFormId; - - @Column(name = "external_embed_url", length = 2000) - private String externalEmbedUrl; - - @Column(name = "webhook_secret", length = 64) - private String webhookSecret; - - /** MANUAL_PUSH | AUTO_AFTER_PDF_SIGN */ - @Column(name = "delivery_mode", nullable = false, length = 32) - private String deliveryMode; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/survey/models/EsignSurveyPush.java b/src/main/java/com/sjmresorts/opsapp/esign/survey/models/EsignSurveyPush.java deleted file mode 100644 index cc6c70f..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/survey/models/EsignSurveyPush.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.sjmresorts.opsapp.esign.survey.models; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; -import jakarta.persistence.IdClass; -import jakarta.persistence.Table; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -import java.io.Serializable; -import java.time.Instant; - -@Getter -@Setter -@NoArgsConstructor -@Entity -@Table(name = "esign_survey_push") -@IdClass(EsignSurveyPush.SurveyPushId.class) -public class EsignSurveyPush { - - @Id - @Column(name = "survey_id", nullable = false, length = 36) - private String surveyId; - - @Id - @Column(name = "user_id", nullable = false) - private Long userId; - - @Id - @Column(name = "client_ip", nullable = false, length = 64) - private String clientIp = ""; - - @Column(name = "assigned_at", nullable = false) - private Instant assignedAt; - - @Column(name = "assigned_by", length = 64) - private String assignedBy; - - @Column(name = "group_id", length = 36) - private String groupId; - - @Getter - @Setter - @NoArgsConstructor - public static class SurveyPushId implements Serializable { - private String surveyId; - private Long userId; - private String clientIp; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/survey/models/EsignSurveySubmission.java b/src/main/java/com/sjmresorts/opsapp/esign/survey/models/EsignSurveySubmission.java deleted file mode 100644 index 395c67a..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/survey/models/EsignSurveySubmission.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.sjmresorts.opsapp.esign.survey.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_survey_submissions") -public class EsignSurveySubmission { - - @Id - @Column(name = "submission_id", nullable = false, length = 36) - private String submissionId; - - @Column(name = "survey_id", nullable = false, length = 36) - private String surveyId; - - @Lob - @Column(name = "answers_json", nullable = false) - private String answersJson; - - @Column(name = "device_id", length = 128) - private String deviceId; - - @Column(name = "hotel_id", length = 64) - private String hotelId; - - @Column(name = "submitted_at", nullable = false) - private Instant submittedAt; - - /** NATIVE | EXTERNAL */ - @Column(name = "source_type", nullable = false, length = 16) - private String sourceType; - - @Column(name = "external_response_id", length = 256) - private String externalResponseId; - - @Lob - @Column(name = "respondent_meta_json") - private String respondentMetaJson; -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/survey/repositories/EsignSurveyPushRepository.java b/src/main/java/com/sjmresorts/opsapp/esign/survey/repositories/EsignSurveyPushRepository.java deleted file mode 100644 index 5c65262..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/survey/repositories/EsignSurveyPushRepository.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.sjmresorts.opsapp.esign.survey.repositories; - -import com.sjmresorts.opsapp.esign.survey.models.EsignSurveyPush; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.stereotype.Repository; - -import java.util.Collection; -import java.util.List; - -@Repository -public interface EsignSurveyPushRepository extends JpaRepository { - - List findBySurveyId(String surveyId); - - void deleteBySurveyId(String surveyId); - - @Query("SELECT p.surveyId FROM EsignSurveyPush p WHERE p.userId = :userId") - List findSurveyIdsByUserId(Long userId); - - @Query( - """ - SELECT p FROM EsignSurveyPush p - WHERE p.userId = :userId - AND p.surveyId IN :surveyIds - """) - List findByUserIdAndSurveyIdIn(Long userId, Collection surveyIds); - - @Query("SELECT COUNT(p) > 0 FROM EsignSurveyPush p WHERE p.surveyId = :surveyId") - boolean existsBySurveyId(String surveyId); -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/survey/repositories/EsignSurveyRepository.java b/src/main/java/com/sjmresorts/opsapp/esign/survey/repositories/EsignSurveyRepository.java deleted file mode 100644 index fec5f4a..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/survey/repositories/EsignSurveyRepository.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.sjmresorts.opsapp.esign.survey.repositories; - -import com.sjmresorts.opsapp.esign.survey.models.EsignSurvey; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.List; - -@Repository -public interface EsignSurveyRepository extends JpaRepository { - - List findAllByOrderBySortOrderAscUpdatedAtDesc(); - - List findByStatusOrderBySortOrderAscUpdatedAtDesc(String status); -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/survey/repositories/EsignSurveySubmissionRepository.java b/src/main/java/com/sjmresorts/opsapp/esign/survey/repositories/EsignSurveySubmissionRepository.java deleted file mode 100644 index 393cfed..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/survey/repositories/EsignSurveySubmissionRepository.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.sjmresorts.opsapp.esign.survey.repositories; - -import com.sjmresorts.opsapp.esign.survey.models.EsignSurveySubmission; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.List; -import java.util.Optional; - -@Repository -public interface EsignSurveySubmissionRepository extends JpaRepository { - - List findBySurveyIdOrderBySubmittedAtDesc(String surveyId); - - long countBySurveyId(String surveyId); - - Optional findFirstBySurveyIdAndExternalResponseId( - String surveyId, String externalResponseId); -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/survey/services/EsignSurveyMediaService.java b/src/main/java/com/sjmresorts/opsapp/esign/survey/services/EsignSurveyMediaService.java deleted file mode 100644 index 43d2448..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/survey/services/EsignSurveyMediaService.java +++ /dev/null @@ -1,227 +0,0 @@ -package com.sjmresorts.opsapp.esign.survey.services; - -import com.sjmresorts.opsapp.esign.survey.models.EsignSurvey; -import com.sjmresorts.opsapp.esign.survey.repositories.EsignSurveyRepository; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.Resource; -import org.apache.commons.lang3.StringUtils; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Base64; -import java.util.Locale; -import java.util.Optional; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -@Service -public class EsignSurveyMediaService { - - public static final String STORED_PREFIX = "stored://"; - private static final Pattern DATA_URL = - Pattern.compile("^data:([^;]+);base64,(.+)$", Pattern.CASE_INSENSITIVE | Pattern.DOTALL); - - @Value("${ops-app.survey.media-dir:C:/auto-check-in/survey-logos}") - private String mediaDir; - - @Resource - private EsignSurveyRepository surveyRepository; - - private Path mediaRoot; - - @PostConstruct - void init() throws IOException { - mediaRoot = Paths.get(mediaDir).toAbsolutePath().normalize(); - Files.createDirectories(mediaRoot); - } - - public String saveBytes(String surveyId, byte[] bytes, String contentType) throws IOException { - if (StringUtils.isBlank(surveyId) || bytes == null || bytes.length == 0) { - throw new IOException("empty logo"); - } - String ext = extensionForMime(StringUtils.defaultString(contentType)); - Path target = mediaRoot.resolve(surveyId.trim() + ext); - Files.write(target, bytes); - deleteSiblingFiles(surveyId.trim(), target); - return STORED_PREFIX + surveyId.trim(); - } - - public String persistIfNeeded(String surveyId, String imageUrl) throws IOException { - if (StringUtils.isBlank(imageUrl)) { - deleteStoredFile(surveyId); - return null; - } - String trimmed = imageUrl.trim(); - if (trimmed.startsWith(STORED_PREFIX)) { - return trimmed; - } - if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) { - deleteStoredFile(surveyId); - return trimmed; - } - if (isSurveyMediaPath(trimmed)) { - return STORED_PREFIX + surveyId; - } - if (!trimmed.startsWith("data:")) { - return trimmed; - } - ParsedDataUrl parsed = parseDataUrl(trimmed); - if (parsed == null) { - throw new IOException("invalid data URL"); - } - String ext = extensionForMime(parsed.mimeType()); - Path target = mediaRoot.resolve(surveyId + ext); - Files.write(target, parsed.bytes()); - deleteSiblingFiles(surveyId, target); - return STORED_PREFIX + surveyId; - } - - @Transactional - public Optional resolveMediaFile(String surveyId) { - if (StringUtils.isBlank(surveyId)) { - return Optional.empty(); - } - Optional opt = surveyRepository.findById(surveyId.trim()); - return opt.flatMap(this::resolveMediaFile); - } - - @Transactional - public Optional resolveMediaFile(EsignSurvey row) { - if (row == null) { - return Optional.empty(); - } - String imageUrl = StringUtils.trimToEmpty(row.getImageUrl()); - if (imageUrl.startsWith("data:")) { - try { - String stored = persistIfNeeded(row.getSurveyId(), imageUrl); - row.setImageUrl(stored); - surveyRepository.save(row); - imageUrl = StringUtils.trimToEmpty(stored); - } catch (IOException ignored) { - return Optional.empty(); - } - } - String fileId = resolveStoredFileId(row.getSurveyId(), imageUrl); - return findStoredFilePath(fileId); - } - - public String probeMimeForPath(Path path) throws IOException { - String probed = Files.probeContentType(path); - if (StringUtils.isNotBlank(probed)) { - return probed; - } - String name = path.getFileName().toString().toLowerCase(Locale.ROOT); - if (name.endsWith(".png")) return "image/png"; - if (name.endsWith(".webp")) return "image/webp"; - if (name.endsWith(".gif")) return "image/gif"; - return "image/jpeg"; - } - - public boolean shouldUseMediaEndpoint(EsignSurvey row) { - if (row == null || StringUtils.isBlank(row.getImageUrl())) { - return false; - } - String imageUrl = row.getImageUrl().trim(); - return imageUrl.startsWith(STORED_PREFIX) - || imageUrl.startsWith("data:") - || isSurveyMediaPath(imageUrl); - } - - public String toRelativeMediaPath(String surveyId) { - return "/esign/surveys/" + surveyId + "/media"; - } - - private static String resolveStoredFileId(String surveyId, String imageUrl) { - if (StringUtils.isBlank(imageUrl)) { - return surveyId; - } - if (imageUrl.startsWith(STORED_PREFIX)) { - return imageUrl.substring(STORED_PREFIX.length()); - } - if (isSurveyMediaPath(imageUrl)) { - return surveyId; - } - return surveyId; - } - - static boolean isSurveyMediaPath(String url) { - return StringUtils.isNotBlank(url) - && url.contains("/esign/surveys/") - && url.endsWith("/media"); - } - - private Optional findStoredFilePath(String surveyId) { - if (StringUtils.isBlank(surveyId)) { - return Optional.empty(); - } - for (Path path : candidatePaths(surveyId.trim())) { - if (Files.isRegularFile(path)) { - return Optional.of(path); - } - } - return Optional.empty(); - } - - private Path[] candidatePaths(String surveyId) { - return new Path[] { - mediaRoot.resolve(surveyId + ".jpg"), - mediaRoot.resolve(surveyId + ".jpeg"), - mediaRoot.resolve(surveyId + ".png"), - mediaRoot.resolve(surveyId + ".webp"), - mediaRoot.resolve(surveyId + ".gif") - }; - } - - private void deleteStoredFile(String surveyId) throws IOException { - if (StringUtils.isBlank(surveyId)) { - return; - } - deleteSiblingFiles(surveyId, null); - } - - private void deleteSiblingFiles(String surveyId, Path keep) throws IOException { - String prefix = surveyId + "."; - if (!Files.isDirectory(mediaRoot)) { - return; - } - try (var stream = Files.list(mediaRoot)) { - stream.filter(p -> p.getFileName().toString().startsWith(prefix)) - .filter(p -> keep == null || !p.equals(keep)) - .forEach(p -> { - try { - Files.deleteIfExists(p); - } catch (IOException ignored) { - } - }); - } - } - - private static String extensionForMime(String mimeType) { - String mime = StringUtils.defaultString(mimeType).toLowerCase(Locale.ROOT); - if (mime.contains("png")) return ".png"; - if (mime.contains("webp")) return ".webp"; - if (mime.contains("gif")) return ".gif"; - return ".jpg"; - } - - private static ParsedDataUrl parseDataUrl(String dataUrl) { - Matcher m = DATA_URL.matcher(dataUrl); - if (!m.matches()) { - return null; - } - try { - byte[] bytes = Base64.getDecoder().decode(m.group(2).replaceAll("\\s", "")); - return new ParsedDataUrl(StringUtils.defaultString(m.group(1)), bytes); - } catch (IllegalArgumentException e) { - return null; - } - } - - private record ParsedDataUrl(String mimeType, byte[] bytes) { - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/esign/survey/services/EsignSurveyPushService.java b/src/main/java/com/sjmresorts/opsapp/esign/survey/services/EsignSurveyPushService.java deleted file mode 100644 index 6930a18..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/survey/services/EsignSurveyPushService.java +++ /dev/null @@ -1,487 +0,0 @@ -package com.sjmresorts.opsapp.esign.survey.services; - -import com.sjmresorts.opsapp.esign.capture.dtos.EsignIosCaptureResponse; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPendingListResponse; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterDevicesResponse; -import com.sjmresorts.opsapp.esign.printer.dtos.EsignPrinterPushCandidatesResponse; -import com.sjmresorts.opsapp.esign.printer.services.EsignPrinterPdfTaskService; -import com.sjmresorts.opsapp.esign.printer.services.EsignPrinterTabletRegistryService; -import com.sjmresorts.opsapp.esign.printer.support.EsignPrinterQueueNotifier; -import com.sjmresorts.opsapp.esign.survey.dtos.EsignSurveyPushAssignRequest; -import com.sjmresorts.opsapp.esign.printer.models.EsignPrinterPushGroup; -import com.sjmresorts.opsapp.esign.printer.models.EsignPrinterPushGroupMember; -import com.sjmresorts.opsapp.esign.printer.models.EsignPrinterTabletRegistry; -import com.sjmresorts.opsapp.esign.survey.models.EsignSurvey; -import com.sjmresorts.opsapp.esign.survey.models.EsignSurveyPush; -import com.sjmresorts.opsapp.esign.printer.repositories.EsignPrinterPushGroupMemberRepository; -import com.sjmresorts.opsapp.esign.printer.repositories.EsignPrinterPushGroupRepository; -import com.sjmresorts.opsapp.esign.survey.repositories.EsignSurveyPushRepository; -import com.sjmresorts.opsapp.esign.survey.repositories.EsignSurveyRepository; -import com.sjmresorts.opsapp.esign.auth.repositories.EsignUserRepository; -import com.sjmresorts.opsapp.esign.common.support.EsignReservationParamUtil; -import com.sjmresorts.opsapp.esign.auth.services.EsignUserPresenceService; -import com.sjmresorts.opsapp.esign.printer.websocket.EsignPrinterWebSocketHub; -import jakarta.annotation.Resource; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.springframework.context.annotation.Lazy; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.Instant; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Comparator; -import java.util.HashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; - -@Slf4j -@Service -public class EsignSurveyPushService { - - public static final String DELIVERY_MANUAL_PUSH = "MANUAL_PUSH"; - public static final String DELIVERY_AUTO_AFTER_PDF_SIGN = "AUTO_AFTER_PDF_SIGN"; - private static final String DEFAULT_AGENT_SOURCE = "printer-agent"; - - @Resource - private EsignSurveyRepository surveyRepository; - - @Resource - private EsignSurveyPushRepository surveyPushRepository; - - @Resource - private EsignUserRepository esignUserRepository; - - @Resource - private EsignPrinterPushGroupRepository pushGroupRepository; - - @Resource - private EsignPrinterPushGroupMemberRepository pushGroupMemberRepository; - - @Resource - private EsignPrinterTabletRegistryService tabletRegistryService; - - @Resource - private EsignPrinterWebSocketHub printerWebSocketHub; - - @Resource - private EsignPrinterQueueNotifier printerQueueNotifier; - - @Resource - @Lazy - private EsignPrinterPdfTaskService printerPdfTaskService; - - @Transactional(readOnly = true) - public EsignPrinterPushCandidatesResponse getPushCandidates(String surveyId) { - if (StringUtils.isBlank(surveyId)) { - return EsignPrinterPushCandidatesResponse.fail("400", "surveyId required"); - } - Optional surveyOpt = surveyRepository.findById(surveyId.trim()); - if (surveyOpt.isEmpty()) { - return EsignPrinterPushCandidatesResponse.fail("404", "Survey not found"); - } - EsignSurvey survey = surveyOpt.get(); - if (!DELIVERY_MANUAL_PUSH.equals(normalizeDeliveryMode(survey.getDeliveryMode()))) { - return EsignPrinterPushCandidatesResponse.fail("409", "Survey delivery mode is not MANUAL_PUSH"); - } - String agentSource = DEFAULT_AGENT_SOURCE; - List devices = - buildPushDeviceOptionsFromDevicesApi(); - List groups = pushGroupRepository.findByAgentSourceOrderByGroupNameAsc(agentSource); - List groupOptions = new ArrayList<>(); - for (EsignPrinterPushGroup group : groups) { - List members = pushGroupMemberRepository.findByGroupId(group.getGroupId()); - EsignPrinterPushCandidatesResponse.EsignPrinterPushGroupOption g = - new EsignPrinterPushCandidatesResponse.EsignPrinterPushGroupOption(); - g.setGroupId(group.getGroupId()); - g.setGroupName(group.getGroupName()); - g.setMemberCount(members.size()); - g.setMemberUserIds(members.stream().map(EsignPrinterPushGroupMember::getUserId).toList()); - groupOptions.add(g); - } - List selectedUserIds = surveyPushRepository.findBySurveyId(survey.getSurveyId()).stream() - .map(EsignSurveyPush::getUserId) - .distinct() - .toList(); - List selectedRegistryIds = resolveSelectedRegistryIds(survey.getSurveyId(), devices); - - EsignPrinterPushCandidatesResponse resp = EsignPrinterPushCandidatesResponse.ok("ok"); - EsignPrinterPushCandidatesResponse.EsignPrinterPushCandidatesData data = - new EsignPrinterPushCandidatesResponse.EsignPrinterPushCandidatesData(); - data.setTaskId(survey.getSurveyId()); - data.setAgentSource(agentSource); - data.setDevices(devices); - data.setGroups(groupOptions); - data.setSelectedUserIds(selectedUserIds); - data.setSelectedRegistryIds(selectedRegistryIds); - data.setSelectedGroupIds(List.of()); - data.setPushMode("MANUAL"); - resp.setData(data); - return resp; - } - - @Transactional - public EsignIosCaptureResponse assignPushTargets(EsignSurveyPushAssignRequest request) { - EsignIosCaptureResponse resp = new EsignIosCaptureResponse(); - if (request == null || StringUtils.isBlank(request.getSurveyId())) { - resp.setSuccess(false); - resp.setCode("400"); - resp.setMessage("surveyId required"); - return resp; - } - Optional surveyOpt = surveyRepository.findById(request.getSurveyId().trim()); - if (surveyOpt.isEmpty()) { - resp.setSuccess(false); - resp.setCode("404"); - resp.setMessage("Survey not found"); - return resp; - } - EsignSurvey survey = surveyOpt.get(); - if (!EsignSurveyService.STATUS_PUBLISHED.equals(survey.getStatus())) { - resp.setSuccess(false); - resp.setCode("409"); - resp.setMessage("Survey not published"); - return resp; - } - if (!DELIVERY_MANUAL_PUSH.equals(normalizeDeliveryMode(survey.getDeliveryMode()))) { - resp.setSuccess(false); - resp.setCode("409"); - resp.setMessage("Survey delivery mode is not MANUAL_PUSH"); - return resp; - } - Set targetUserIds = resolveTargetUserIds(request); - List pushTargets = resolvePushTargetRows(request, targetUserIds); - if (pushTargets.isEmpty()) { - resp.setSuccess(false); - resp.setCode("400"); - resp.setMessage("No push targets selected"); - return resp; - } - surveyPushRepository.deleteBySurveyId(survey.getSurveyId()); - Instant now = Instant.now(); - String by = trim(request.getAssignedBy(), 64); - Set notifyUserIds = new LinkedHashSet<>(); - for (PushTargetRow target : pushTargets) { - EsignSurveyPush row = new EsignSurveyPush(); - row.setSurveyId(survey.getSurveyId()); - row.setUserId(target.userId()); - row.setClientIp(target.clientIp() != null ? target.clientIp() : ""); - row.setAssignedAt(now); - row.setAssignedBy(by); - surveyPushRepository.save(row); - notifyUserIds.add(target.userId()); - } - notifyPushTargets(survey, notifyUserIds); - printerQueueNotifier.notifyChangedAfterCommit(); - resp.setSuccess(true); - resp.setCode("200"); - resp.setMessage("Survey push assigned"); - EsignIosCaptureResponse.EsignIosCaptureData data = new EsignIosCaptureResponse.EsignIosCaptureData(); - data.setTaskId(survey.getSurveyId()); - resp.setData(data); - log.info("Survey push assigned surveyId={}, targets={}", survey.getSurveyId(), targetUserIds.size()); - return resp; - } - - @Transactional - public EsignIosCaptureResponse completePushForUser(String surveyId, Long userId, String clientIp) { - EsignIosCaptureResponse resp = new EsignIosCaptureResponse(); - if (StringUtils.isBlank(surveyId)) { - resp.setSuccess(false); - resp.setCode("400"); - resp.setMessage("surveyId required"); - return resp; - } - if (userId == null) { - resp.setSuccess(false); - resp.setCode("401"); - resp.setMessage("login required"); - return resp; - } - String normalizedIp = EsignUserPresenceService.normalizeClientIp(clientIp); - List rows = surveyPushRepository.findBySurveyId(surveyId.trim()); - boolean removed = false; - for (EsignSurveyPush row : rows) { - if (!userId.equals(row.getUserId())) { - continue; - } - String rowIp = StringUtils.defaultString(row.getClientIp()); - if (rowIp.isEmpty() || normalizedIp == null || rowIp.equals(normalizedIp)) { - surveyPushRepository.delete(row); - removed = true; - } - } - if (!removed) { - resp.setSuccess(true); - resp.setCode("200"); - resp.setMessage("Already completed"); - EsignIosCaptureResponse.EsignIosCaptureData data = new EsignIosCaptureResponse.EsignIosCaptureData(); - data.setTaskId(surveyId.trim()); - resp.setData(data); - return resp; - } - printerQueueNotifier.notifyChangedAfterCommit(); - resp.setSuccess(true); - resp.setCode("200"); - resp.setMessage("Survey push completed"); - EsignIosCaptureResponse.EsignIosCaptureData data = new EsignIosCaptureResponse.EsignIosCaptureData(); - data.setTaskId(surveyId.trim()); - resp.setData(data); - log.info("Survey push completed surveyId={}, userId={}", surveyId.trim(), userId); - return resp; - } - - @Transactional(readOnly = true) - public List listPendingItemsForUser( - Long userId, String clientIp, String hotelId) { - if (userId == null) { - return List.of(); - } - String normalizedHotelId = StringUtils.isNotBlank(hotelId) - ? EsignReservationParamUtil.normalizeParam(hotelId) - : null; - String normalizedClientIp = EsignUserPresenceService.normalizeClientIp(clientIp); - List pushedSurveyIds = surveyPushRepository.findSurveyIdsByUserId(userId); - if (pushedSurveyIds.isEmpty()) { - return List.of(); - } - List surveys = surveyRepository.findAllById(pushedSurveyIds).stream() - .filter(s -> EsignSurveyService.STATUS_PUBLISHED.equals(s.getStatus())) - .filter(s -> DELIVERY_MANUAL_PUSH.equals(normalizeDeliveryMode(s.getDeliveryMode()))) - .filter(s -> normalizedHotelId == null - || StringUtils.isBlank(s.getHotelId()) - || normalizedHotelId.equals(s.getHotelId())) - .sorted(Comparator.comparing(EsignSurvey::getSortOrder, Comparator.nullsLast(Integer::compareTo)) - .thenComparing(EsignSurvey::getUpdatedAt, Comparator.nullsLast(Comparator.reverseOrder()))) - .toList(); - List items = new ArrayList<>(); - for (EsignSurvey survey : surveys) { - if (!isVisibleToUser(survey.getSurveyId(), userId, normalizedClientIp)) { - continue; - } - EsignPendingListResponse.EsignPendingItem item = - new EsignPendingListResponse.EsignPendingItem(); - item.setTaskId(survey.getSurveyId()); - item.setTaskType("SURVEY"); - item.setDocTitle(survey.getTitle()); - item.setGuestName(survey.getRewardText()); - item.setCreatedAt(survey.getPublishedAt() != null ? survey.getPublishedAt().toString() : null); - items.add(item); - } - return items; - } - - @Transactional(readOnly = true) - public Optional resolveAutoSurveyAfterPdfSign(String hotelId) { - String normalizedHotelId = StringUtils.isNotBlank(hotelId) - ? EsignReservationParamUtil.normalizeParam(hotelId) - : null; - return surveyRepository.findByStatusOrderBySortOrderAscUpdatedAtDesc(EsignSurveyService.STATUS_PUBLISHED) - .stream() - .filter(s -> DELIVERY_AUTO_AFTER_PDF_SIGN.equals(normalizeDeliveryMode(s.getDeliveryMode()))) - .filter(s -> normalizedHotelId == null - || StringUtils.isBlank(s.getHotelId()) - || normalizedHotelId.equals(s.getHotelId())) - .findFirst(); - } - - @Transactional(readOnly = true) - public boolean isVisibleToUser(String surveyId, Long userId, String clientIp) { - if (userId == null || StringUtils.isBlank(surveyId)) { - return false; - } - List rows = surveyPushRepository.findBySurveyId(surveyId.trim()); - if (rows.isEmpty()) { - return false; - } - String ip = EsignUserPresenceService.normalizeClientIp(clientIp); - for (EsignSurveyPush row : rows) { - if (!userId.equals(row.getUserId())) { - continue; - } - String rowIp = StringUtils.defaultString(row.getClientIp()); - if (rowIp.isEmpty() || ip == null || rowIp.equals(ip)) { - return true; - } - } - return false; - } - - public static String normalizeDeliveryMode(String mode) { - if (StringUtils.isBlank(mode)) { - return DELIVERY_MANUAL_PUSH; - } - String t = mode.trim().toUpperCase(); - if (DELIVERY_AUTO_AFTER_PDF_SIGN.equals(t)) { - return DELIVERY_AUTO_AFTER_PDF_SIGN; - } - return DELIVERY_MANUAL_PUSH; - } - - private Set resolveTargetUserIds(EsignSurveyPushAssignRequest request) { - Set targetUserIds = new LinkedHashSet<>(); - if (request.getRegistryIds() != null && !request.getRegistryIds().isEmpty()) { - for (String registryId : request.getRegistryIds()) { - if (StringUtils.isBlank(registryId)) { - continue; - } - tabletRegistryService.listAllOrdered().stream() - .filter(reg -> registryId.trim().equals(reg.getRegistryId())) - .findFirst() - .ifPresent(reg -> { - if (reg.getUserId() != null && isPushableUserId(reg.getUserId())) { - targetUserIds.add(reg.getUserId()); - } - }); - } - } - if (request.getUserIds() != null) { - for (Long uid : request.getUserIds()) { - if (uid != null && isPushableUserId(uid)) { - targetUserIds.add(uid); - } - } - } - if (request.getGroupIds() != null && !request.getGroupIds().isEmpty()) { - for (Long uid : pushGroupMemberRepository.findUserIdsByGroupIdIn(request.getGroupIds())) { - if (isPushableUserId(uid)) { - targetUserIds.add(uid); - } - } - } - return targetUserIds; - } - - private List resolvePushTargetRows( - EsignSurveyPushAssignRequest request, Set targetUserIds) { - List rows = new ArrayList<>(); - Set seen = new LinkedHashSet<>(); - if (request.getRegistryIds() != null && !request.getRegistryIds().isEmpty()) { - Map registryById = tabletRegistryService.listAllOrdered().stream() - .collect(Collectors.toMap(EsignPrinterTabletRegistry::getRegistryId, r -> r, (a, b) -> a)); - for (String registryId : request.getRegistryIds()) { - if (StringUtils.isBlank(registryId)) { - continue; - } - EsignPrinterTabletRegistry reg = registryById.get(registryId.trim()); - if (reg == null || reg.getUserId() == null || !isPushableUserId(reg.getUserId())) { - continue; - } - String ip = EsignPrinterTabletRegistryService.boundClientIpOrEmpty(reg.getClientIp()); - String key = reg.getUserId() + "#" + ip; - if (seen.add(key)) { - rows.add(new PushTargetRow(reg.getUserId(), ip)); - } - } - if (!rows.isEmpty()) { - return rows; - } - } - for (Long userId : targetUserIds) { - Set clientIps = tabletRegistryService.resolvePushTargetClientIpsForUser(userId); - if (clientIps.isEmpty()) { - clientIps = Set.of(""); - } - for (String clientIp : clientIps) { - String ip = clientIp != null ? clientIp : ""; - String key = userId + "#" + ip; - if (seen.add(key)) { - rows.add(new PushTargetRow(userId, ip)); - } - } - } - return rows; - } - - private boolean isPushableUserId(Long userId) { - if (userId == null) { - return false; - } - return esignUserRepository.findById(userId).isPresent(); - } - - /** 与 CMS「设备管理」平板列表同源,展示全部平板(不要求设备连接审批)。 */ - private List buildPushDeviceOptionsFromDevicesApi() { - EsignPrinterDevicesResponse devicesResponse = printerPdfTaskService.getDevices(); - List tablets = - devicesResponse.getData() != null && devicesResponse.getData().getTabletDevices() != null - ? devicesResponse.getData().getTabletDevices() - : List.of(); - List devices = new ArrayList<>(); - for (EsignPrinterDevicesResponse.EsignPrinterTabletDevice tab : tablets) { - if (tab.getUserId() == null) { - continue; - } - EsignPrinterPushCandidatesResponse.EsignPrinterPushDeviceOption opt = - new EsignPrinterPushCandidatesResponse.EsignPrinterPushDeviceOption(); - opt.setUserId(tab.getUserId()); - opt.setUserName(tab.getUserName()); - opt.setDisplayName(tab.getDisplayName()); - opt.setRegistryId(tab.getRegistryId()); - String boundIp = StringUtils.isNotBlank(tab.getBoundClientIp()) - ? tab.getBoundClientIp() - : EsignUserPresenceService.normalizeClientIp(tab.getLastLoginIp()); - opt.setClientIp(boundIp != null ? boundIp : ""); - opt.setPushEnabled(tab.isPushEnabled()); - opt.setDeliveryMode( - StringUtils.defaultIfBlank(tab.getDeliveryMode(), tab.isPushEnabled() ? "FCM 推送" : "仅轮询")); - devices.add(opt); - } - return devices; - } - - private List resolveSelectedRegistryIds( - String surveyId, - List devices) { - List pushes = surveyPushRepository.findBySurveyId(surveyId); - if (pushes.isEmpty() || devices == null || devices.isEmpty()) { - return List.of(); - } - Map registryIdByKey = new HashMap<>(); - for (EsignPrinterPushCandidatesResponse.EsignPrinterPushDeviceOption device : devices) { - if (StringUtils.isBlank(device.getRegistryId()) || device.getUserId() == null) { - continue; - } - String ip = device.getClientIp() != null ? device.getClientIp() : ""; - registryIdByKey.put(device.getUserId() + "#" + ip, device.getRegistryId()); - } - List selected = new ArrayList<>(); - for (EsignSurveyPush push : pushes) { - String ip = push.getClientIp() != null ? push.getClientIp() : ""; - String registryId = registryIdByKey.get(push.getUserId() + "#" + ip); - if (registryId != null) { - selected.add(registryId); - } - } - return selected.stream().distinct().toList(); - } - - private record PushTargetRow(Long userId, String clientIp) {} - - private void notifyPushTargets(EsignSurvey survey, Collection userIds) { - if (survey == null || userIds == null || userIds.isEmpty()) { - return; - } - printerWebSocketHub.notifySurveyAssigned( - userIds, - survey.getSurveyId(), - survey.getTitle(), - tabletRegistryService.resolvePushClientIpFilters(userIds)); - } - - 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/survey/services/EsignSurveyService.java b/src/main/java/com/sjmresorts/opsapp/esign/survey/services/EsignSurveyService.java deleted file mode 100644 index 5eb6df4..0000000 --- a/src/main/java/com/sjmresorts/opsapp/esign/survey/services/EsignSurveyService.java +++ /dev/null @@ -1,668 +0,0 @@ -package com.sjmresorts.opsapp.esign.survey.services; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.sjmresorts.opsapp.esign.survey.dtos.ExternalSurveyUrlParseResponse; -import com.sjmresorts.opsapp.esign.survey.dtos.EsignSurveyExternalWebhookRequest; -import com.sjmresorts.opsapp.esign.survey.dtos.EsignSurveyResponse; -import com.sjmresorts.opsapp.esign.survey.dtos.EsignSurveySaveRequest; -import com.sjmresorts.opsapp.esign.survey.dtos.EsignSurveySubmitRequest; -import com.sjmresorts.opsapp.esign.survey.models.EsignSurvey; -import com.sjmresorts.opsapp.esign.survey.models.EsignSurveySubmission; -import com.sjmresorts.opsapp.esign.survey.repositories.EsignSurveyRepository; -import com.sjmresorts.opsapp.esign.survey.repositories.EsignSurveySubmissionRepository; -import com.sjmresorts.opsapp.esign.common.support.EsignReservationParamUtil; -import com.sjmresorts.opsapp.esign.survey.dtos.ExternalSurveyUrlParser; -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.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.UUID; -import java.util.stream.Collectors; - -@Slf4j -@Service -public class EsignSurveyService { - - public static final String STATUS_DRAFT = "DRAFT"; - public static final String STATUS_PUBLISHED = "PUBLISHED"; - public static final String STATUS_ARCHIVED = "ARCHIVED"; - public static final String SOURCE_NATIVE = "NATIVE"; - public static final String SOURCE_EXTERNAL = "EXTERNAL"; - - private static final Set ALLOWED_SOURCE_TYPES = Set.of(SOURCE_NATIVE, SOURCE_EXTERNAL); - private static final Set ALLOWED_DISPLAY_TYPES = Set.of("CARD", "IMAGE"); - private static final int MAX_IMAGE_URL_LENGTH = 2_000_000; - - @Resource - private EsignSurveyRepository surveyRepository; - - @Resource - private EsignSurveySubmissionRepository submissionRepository; - - @Resource - private ObjectMapper objectMapper; - - @Resource - private EsignSurveyMediaService mediaService; - - public EsignSurveyResponse listAllForAdmin() { - List rows = surveyRepository.findAllByOrderBySortOrderAscUpdatedAtDesc(); - EsignSurveyResponse r = EsignSurveyResponse.ok("ok"); - EsignSurveyResponse.EsignSurveyData data = new EsignSurveyResponse.EsignSurveyData(); - data.setSurveys(rows.stream().map(this::toListItem).collect(Collectors.toList())); - r.setData(data); - return r; - } - - public EsignSurveyResponse listPublished(String hotelId) { - List rows = surveyRepository.findByStatusOrderBySortOrderAscUpdatedAtDesc(STATUS_PUBLISHED); - String normalizedHotelId = StringUtils.isNotBlank(hotelId) - ? EsignReservationParamUtil.normalizeParam(hotelId) - : null; - List filtered = new ArrayList<>(); - for (EsignSurvey row : rows) { - if (normalizedHotelId != null && StringUtils.isNotBlank(row.getHotelId()) - && !normalizedHotelId.equals(row.getHotelId())) { - continue; - } - filtered.add(toListItem(row)); - } - EsignSurveyResponse r = EsignSurveyResponse.ok("ok"); - EsignSurveyResponse.EsignSurveyData data = new EsignSurveyResponse.EsignSurveyData(); - data.setSurveys(filtered); - r.setData(data); - return r; - } - - public EsignSurveyResponse getDetail(String surveyId, boolean includeQuestions) { - Optional opt = findSurvey(surveyId); - if (opt.isEmpty()) { - return EsignSurveyResponse.fail("404", "Survey not found"); - } - EsignSurveyResponse r = EsignSurveyResponse.ok("ok"); - EsignSurveyResponse.EsignSurveyData data = new EsignSurveyResponse.EsignSurveyData(); - data.setSurvey(toClientItem(opt.get(), includeQuestions)); - r.setData(data); - return r; - } - - @Transactional - public EsignSurveyResponse save(EsignSurveySaveRequest request) { - if (request == null || StringUtils.isBlank(request.getTitle())) { - return EsignSurveyResponse.fail("400", "title required"); - } - String sourceType = normalizeSourceType(request.getSourceType()); - boolean external = SOURCE_EXTERNAL.equals(sourceType); - - String questionsJson; - try { - questionsJson = objectMapper.writeValueAsString( - request.getQuestions() != null ? request.getQuestions() : List.of()); - } catch (JsonProcessingException e) { - return EsignSurveyResponse.fail("400", "invalid questions"); - } - - Instant now = Instant.now(); - EsignSurvey entity; - if (StringUtils.isNotBlank(request.getSurveyId())) { - Optional opt = findSurvey(request.getSurveyId()); - if (opt.isEmpty()) { - return EsignSurveyResponse.fail("404", "Survey not found"); - } - entity = opt.get(); - } else { - entity = new EsignSurvey(); - entity.setSurveyId(UUID.randomUUID().toString()); - entity.setStatus(STATUS_DRAFT); - entity.setCreatedAt(now); - entity.setWebhookSecret(UUID.randomUUID().toString().replace("-", "")); - } - - entity.setTitle(trim(request.getTitle(), 256)); - entity.setDescription(trim(request.getDescription(), 1000)); - entity.setRewardText(trim(request.getRewardText(), 256)); - entity.setDisplayType(normalizeDisplayType(request.getDisplayType())); - String imageUrl = resolveImageUrlForSave(entity, request.getImageUrl()); - if ("IMAGE".equals(entity.getDisplayType()) && StringUtils.isBlank(imageUrl)) { - return EsignSurveyResponse.fail("400", "imageUrl required for IMAGE display"); - } - entity.setImageUrl(imageUrl); - entity.setHeaderHotelName(trim(request.getHeaderHotelName(), 256)); - String hotelId = trim(request.getHotelId(), 64); - entity.setHotelId(StringUtils.isNotBlank(hotelId) ? EsignReservationParamUtil.normalizeParam(hotelId) : null); - entity.setSortOrder(request.getSortOrder() != null ? request.getSortOrder() : 0); - entity.setSourceType(sourceType); - entity.setDeliveryMode( - EsignSurveyPushService.normalizeDeliveryMode(request.getDeliveryMode())); - if (external) { - String externalUrl = trim(request.getExternalUrl(), 2000); - if (StringUtils.isBlank(externalUrl)) { - return EsignSurveyResponse.fail("400", "externalUrl required for EXTERNAL survey"); - } - ExternalSurveyUrlParser.ParseResult parsed = - ExternalSurveyUrlParser.parse(externalUrl).orElse(null); - if (parsed == null) { - return EsignSurveyResponse.fail("400", "unsupported or invalid external survey URL"); - } - entity.setExternalUrl(parsed.normalizedUrl()); - entity.setExternalProvider(parsed.provider()); - entity.setExternalFormId(parsed.formId()); - entity.setExternalEmbedUrl(parsed.embedUrl()); - entity.setQuestionsJson("[]"); - if (StringUtils.isBlank(entity.getWebhookSecret())) { - entity.setWebhookSecret(UUID.randomUUID().toString().replace("-", "")); - } - } else { - entity.setExternalUrl(null); - entity.setExternalProvider(null); - entity.setExternalFormId(null); - entity.setExternalEmbedUrl(null); - entity.setQuestionsJson(questionsJson); - } - entity.setUpdatedAt(now); - surveyRepository.save(entity); - - EsignSurveyResponse r = EsignSurveyResponse.ok("saved"); - EsignSurveyResponse.EsignSurveyData data = new EsignSurveyResponse.EsignSurveyData(); - data.setSurveyId(entity.getSurveyId()); - data.setSurvey(toClientItem(entity, true)); - r.setData(data); - return r; - } - - @Transactional - public EsignSurveyResponse uploadLogo(String surveyId, byte[] bytes, String contentType) { - if (StringUtils.isBlank(surveyId) || bytes == null || bytes.length == 0) { - return EsignSurveyResponse.fail("400", "file required"); - } - Optional opt = findSurvey(surveyId); - if (opt.isEmpty()) { - return EsignSurveyResponse.fail("404", "Survey not found"); - } - EsignSurvey entity = opt.get(); - try { - String stored = mediaService.saveBytes(entity.getSurveyId(), bytes, contentType); - entity.setImageUrl(stored); - entity.setUpdatedAt(Instant.now()); - surveyRepository.save(entity); - } catch (Exception e) { - return EsignSurveyResponse.fail("500", "failed to store survey logo"); - } - EsignSurveyResponse r = EsignSurveyResponse.ok("logo uploaded"); - EsignSurveyResponse.EsignSurveyData data = new EsignSurveyResponse.EsignSurveyData(); - data.setSurveyId(entity.getSurveyId()); - data.setSurvey(toClientItem(entity, true)); - r.setData(data); - return r; - } - - public ExternalSurveyUrlParseResponse parseExternalUrl(String url) { - return ExternalSurveyUrlParser.parse(url) - .map(parsed -> { - ExternalSurveyUrlParseResponse.ParseData data = - new ExternalSurveyUrlParseResponse.ParseData(); - data.setProvider(parsed.provider()); - data.setFormId(parsed.formId()); - data.setNormalizedUrl(parsed.normalizedUrl()); - data.setEmbedUrl(parsed.embedUrl()); - return ExternalSurveyUrlParseResponse.ok(data); - }) - .orElse(ExternalSurveyUrlParseResponse.fail("400", "unsupported or invalid external survey URL")); - } - - public EsignSurveyResponse listSubmissions(String surveyId) { - Optional opt = findSurvey(surveyId); - if (opt.isEmpty()) { - return EsignSurveyResponse.fail("404", "Survey not found"); - } - List rows = - submissionRepository.findBySurveyIdOrderBySubmittedAtDesc(surveyId.trim()); - EsignSurveyResponse r = EsignSurveyResponse.ok("ok"); - EsignSurveyResponse.EsignSurveyData data = new EsignSurveyResponse.EsignSurveyData(); - data.setSurveyId(surveyId.trim()); - data.setSubmissions(rows.stream().map(this::toSubmissionItem).collect(Collectors.toList())); - r.setData(data); - return r; - } - - @Transactional - public EsignSurveyResponse ingestExternalWebhook(EsignSurveyExternalWebhookRequest request) { - if (request == null) { - return EsignSurveyResponse.fail("400", "request required"); - } - Optional surveyOpt = resolveSurveyForWebhook(request); - if (surveyOpt.isEmpty()) { - return EsignSurveyResponse.fail("404", "Survey not found for webhook"); - } - EsignSurvey survey = surveyOpt.get(); - if (!SOURCE_EXTERNAL.equals(normalizeSourceType(survey.getSourceType()))) { - return EsignSurveyResponse.fail("409", "Survey is not external"); - } - if (!matchesWebhookSecret(survey, request.getSecret())) { - return EsignSurveyResponse.fail("403", "invalid webhook secret"); - } - String externalResponseId = trim(request.getExternalResponseId(), 256); - if (StringUtils.isNotBlank(externalResponseId)) { - Optional dup = - submissionRepository.findFirstBySurveyIdAndExternalResponseId( - survey.getSurveyId(), externalResponseId); - if (dup.isPresent()) { - EsignSurveyResponse r = EsignSurveyResponse.ok("duplicate ignored"); - EsignSurveyResponse.EsignSurveyData data = new EsignSurveyResponse.EsignSurveyData(); - data.setSubmissionId(dup.get().getSubmissionId()); - r.setData(data); - return r; - } - } - Map answers = resolveExternalAnswers(request); - if (answers.isEmpty()) { - return EsignSurveyResponse.fail("400", "answers or raw payload required"); - } - Map meta = new LinkedHashMap<>(); - if (StringUtils.isNotBlank(request.getRespondentEmail())) { - meta.put("email", request.getRespondentEmail().trim()); - } - if (StringUtils.isNotBlank(request.getRespondentName())) { - meta.put("name", request.getRespondentName().trim()); - } - if (StringUtils.isNotBlank(request.getExternalFormId())) { - meta.put("externalFormId", request.getExternalFormId().trim()); - } - return persistExternalSubmission( - survey, - answers, - meta, - externalResponseId, - trim(request.getDeviceId(), 128), - request.getHotelId()); - } - - @Transactional - public EsignSurveyResponse publish(String surveyId) { - Optional opt = findSurvey(surveyId); - if (opt.isEmpty()) { - return EsignSurveyResponse.fail("404", "Survey not found"); - } - EsignSurvey entity = opt.get(); - if (SOURCE_EXTERNAL.equals(normalizeSourceType(entity.getSourceType())) - && StringUtils.isBlank(entity.getExternalEmbedUrl())) { - return EsignSurveyResponse.fail("400", "external survey URL not configured"); - } - entity.setStatus(STATUS_PUBLISHED); - entity.setPublishedAt(Instant.now()); - entity.setUpdatedAt(Instant.now()); - surveyRepository.save(entity); - return EsignSurveyResponse.ok("published"); - } - - @Transactional - public EsignSurveyResponse archive(String surveyId) { - Optional opt = findSurvey(surveyId); - if (opt.isEmpty()) { - return EsignSurveyResponse.fail("404", "Survey not found"); - } - EsignSurvey entity = opt.get(); - entity.setStatus(STATUS_ARCHIVED); - entity.setUpdatedAt(Instant.now()); - surveyRepository.save(entity); - return EsignSurveyResponse.ok("archived"); - } - - @Transactional - public EsignSurveyResponse delete(String surveyId) { - Optional opt = findSurvey(surveyId); - if (opt.isEmpty()) { - return EsignSurveyResponse.fail("404", "Survey not found"); - } - surveyRepository.delete(opt.get()); - try { - mediaService.persistIfNeeded(surveyId, null); - } catch (Exception ignored) { - } - return EsignSurveyResponse.ok("deleted"); - } - - @Transactional - public EsignSurveyResponse submit(String surveyId, EsignSurveySubmitRequest request) { - Optional opt = findSurvey(surveyId); - if (opt.isEmpty()) { - return EsignSurveyResponse.fail("404", "Survey not found"); - } - EsignSurvey survey = opt.get(); - if (!STATUS_PUBLISHED.equals(survey.getStatus())) { - return EsignSurveyResponse.fail("409", "Survey not published"); - } - if (SOURCE_EXTERNAL.equals(normalizeSourceType(survey.getSourceType()))) { - return EsignSurveyResponse.fail("409", "External survey answers must be sent via webhook"); - } - Map answers = resolveAnswers(request); - if (answers.isEmpty()) { - return EsignSurveyResponse.fail("400", "answers required"); - } - String answersJson; - try { - answersJson = objectMapper.writeValueAsString(answers); - } catch (JsonProcessingException e) { - return EsignSurveyResponse.fail("500", "failed to serialize answers"); - } - - EsignSurveySubmission sub = new EsignSurveySubmission(); - sub.setSubmissionId(UUID.randomUUID().toString()); - sub.setSurveyId(survey.getSurveyId()); - sub.setAnswersJson(answersJson); - sub.setDeviceId(trim(request != null ? request.getDeviceId() : null, 128)); - String hotelId = request != null ? request.getHotelId() : null; - sub.setHotelId(StringUtils.isNotBlank(hotelId) ? EsignReservationParamUtil.normalizeParam(hotelId) : null); - sub.setSourceType(SOURCE_NATIVE); - sub.setSubmittedAt(Instant.now()); - submissionRepository.save(sub); - - EsignSurveyResponse r = EsignSurveyResponse.ok("submitted"); - EsignSurveyResponse.EsignSurveyData data = new EsignSurveyResponse.EsignSurveyData(); - data.setSubmissionId(sub.getSubmissionId()); - r.setData(data); - return r; - } - - private Map resolveAnswers(EsignSurveySubmitRequest request) { - if (request == null) { - return Map.of(); - } - if (request.getAnswers() != null && !request.getAnswers().isEmpty()) { - return request.getAnswers(); - } - if (request.getAnswerList() == null || request.getAnswerList().isEmpty()) { - return Map.of(); - } - Map out = new HashMap<>(); - for (Map row : request.getAnswerList()) { - if (row == null) { - continue; - } - Object qid = row.get("questionId"); - if (qid == null) { - qid = row.get("id"); - } - if (qid == null) { - continue; - } - Object ans = row.containsKey("answer") ? row.get("answer") : row.get("value"); - out.put(String.valueOf(qid), ans); - } - return out; - } - - private Optional findSurvey(String surveyId) { - if (StringUtils.isBlank(surveyId)) { - return Optional.empty(); - } - return surveyRepository.findById(surveyId.trim()); - } - - private EsignSurveyResponse.EsignSurveyItem toListItem(EsignSurvey row) { - return toClientItem(row, false); - } - - private EsignSurveyResponse.EsignSurveyItem toClientItem(EsignSurvey row, boolean includeQuestions) { - EsignSurveyResponse.EsignSurveyItem item = toDetailItem(row, includeQuestions); - item.setImageUrl(resolveClientImageUrl(row)); - return item; - } - - private EsignSurveyResponse.EsignSurveyItem toDetailItem(EsignSurvey row, boolean includeQuestions) { - EsignSurveyResponse.EsignSurveyItem item = new EsignSurveyResponse.EsignSurveyItem(); - item.setSurveyId(row.getSurveyId()); - item.setTitle(row.getTitle()); - item.setDescription(row.getDescription()); - item.setRewardText(row.getRewardText()); - item.setDisplayType(StringUtils.defaultIfBlank(row.getDisplayType(), "CARD")); - item.setImageUrl(row.getImageUrl()); - item.setHeaderHotelName(row.getHeaderHotelName()); - item.setHotelId(row.getHotelId()); - item.setStatus(row.getStatus()); - item.setSortOrder(row.getSortOrder()); - item.setSourceType(normalizeSourceType(row.getSourceType())); - item.setDeliveryMode(EsignSurveyPushService.normalizeDeliveryMode(row.getDeliveryMode())); - item.setExternalUrl(row.getExternalUrl()); - item.setExternalProvider(row.getExternalProvider()); - item.setExternalFormId(row.getExternalFormId()); - item.setExternalEmbedUrl(row.getExternalEmbedUrl()); - item.setSubmissionCount((int) submissionRepository.countBySurveyId(row.getSurveyId())); - if (SOURCE_EXTERNAL.equals(item.getSourceType()) && StringUtils.isNotBlank(row.getWebhookSecret())) { - item.setWebhookUrl("/api/v1/esign/surveys/external/webhook"); - item.setWebhookSecret(row.getWebhookSecret()); - } - item.setQuestionCount( - SOURCE_EXTERNAL.equals(item.getSourceType()) - ? 0 - : parseQuestions(row.getQuestionsJson()).size()); - item.setCreatedAt(row.getCreatedAt() != null ? row.getCreatedAt().toString() : null); - item.setUpdatedAt(row.getUpdatedAt() != null ? row.getUpdatedAt().toString() : null); - item.setPublishedAt(row.getPublishedAt() != null ? row.getPublishedAt().toString() : null); - if (includeQuestions && !SOURCE_EXTERNAL.equals(item.getSourceType())) { - item.setQuestions(parseQuestions(row.getQuestionsJson())); - } - return item; - } - - private List> parseQuestions(String json) { - if (StringUtils.isBlank(json)) { - return List.of(); - } - try { - return objectMapper.readValue(json, new TypeReference>>() {}); - } catch (Exception e) { - return List.of(); - } - } - - private static String normalizeSourceType(String sourceType) { - if (StringUtils.isBlank(sourceType)) { - return SOURCE_NATIVE; - } - String t = sourceType.trim().toUpperCase(); - return ALLOWED_SOURCE_TYPES.contains(t) ? t : SOURCE_NATIVE; - } - - private Optional resolveSurveyForWebhook(EsignSurveyExternalWebhookRequest request) { - if (StringUtils.isNotBlank(request.getSurveyId())) { - return findSurvey(request.getSurveyId()); - } - String formId = trim(request.getExternalFormId(), 512); - if (StringUtils.isBlank(formId)) { - return Optional.empty(); - } - return surveyRepository.findAllByOrderBySortOrderAscUpdatedAtDesc().stream() - .filter(row -> formId.equals(row.getExternalFormId())) - .findFirst(); - } - - private static boolean matchesWebhookSecret(EsignSurvey survey, String secret) { - if (survey == null || StringUtils.isBlank(survey.getWebhookSecret())) { - return false; - } - return survey.getWebhookSecret().equals(StringUtils.trimToEmpty(secret)); - } - - private Map resolveExternalAnswers(EsignSurveyExternalWebhookRequest request) { - if (request.getAnswers() != null && !request.getAnswers().isEmpty()) { - return request.getAnswers(); - } - if (request.getRaw() != null && !request.getRaw().isEmpty()) { - return request.getRaw(); - } - return Map.of(); - } - - private EsignSurveyResponse persistExternalSubmission( - EsignSurvey survey, - Map answers, - Map meta, - String externalResponseId, - String deviceId, - String hotelId) { - String answersJson; - String metaJson = null; - try { - answersJson = objectMapper.writeValueAsString(answers); - if (!meta.isEmpty()) { - metaJson = objectMapper.writeValueAsString(meta); - } - } catch (JsonProcessingException e) { - return EsignSurveyResponse.fail("500", "failed to serialize webhook payload"); - } - EsignSurveySubmission sub = new EsignSurveySubmission(); - sub.setSubmissionId(UUID.randomUUID().toString()); - sub.setSurveyId(survey.getSurveyId()); - sub.setAnswersJson(answersJson); - sub.setRespondentMetaJson(metaJson); - sub.setExternalResponseId(externalResponseId); - sub.setDeviceId(deviceId); - sub.setHotelId(StringUtils.isNotBlank(hotelId) ? EsignReservationParamUtil.normalizeParam(hotelId) : null); - sub.setSourceType(SOURCE_EXTERNAL); - sub.setSubmittedAt(Instant.now()); - submissionRepository.save(sub); - log.info( - "External survey webhook saved surveyId={}, submissionId={}, externalResponseId={}", - survey.getSurveyId(), - sub.getSubmissionId(), - externalResponseId); - EsignSurveyResponse r = EsignSurveyResponse.ok("webhook accepted"); - EsignSurveyResponse.EsignSurveyData data = new EsignSurveyResponse.EsignSurveyData(); - data.setSubmissionId(sub.getSubmissionId()); - r.setData(data); - return r; - } - - private EsignSurveyResponse.EsignSurveySubmissionItem toSubmissionItem(EsignSurveySubmission row) { - EsignSurveyResponse.EsignSurveySubmissionItem item = - new EsignSurveyResponse.EsignSurveySubmissionItem(); - item.setSubmissionId(row.getSubmissionId()); - item.setSurveyId(row.getSurveyId()); - item.setSourceType(StringUtils.defaultIfBlank(row.getSourceType(), SOURCE_NATIVE)); - item.setExternalResponseId(row.getExternalResponseId()); - item.setDeviceId(row.getDeviceId()); - item.setHotelId(row.getHotelId()); - item.setSubmittedAt(row.getSubmittedAt() != null ? row.getSubmittedAt().toString() : null); - item.setAnswers(parseJsonMap(row.getAnswersJson())); - item.setRespondentMeta(parseJsonMap(row.getRespondentMetaJson())); - return item; - } - - private Map parseJsonMap(String json) { - if (StringUtils.isBlank(json)) { - return Map.of(); - } - try { - return objectMapper.readValue(json, new TypeReference>() {}); - } catch (Exception e) { - return Map.of(); - } - } - - private static String normalizeDisplayType(String displayType) { - if (StringUtils.isBlank(displayType)) { - return "CARD"; - } - String t = displayType.trim().toUpperCase(); - return ALLOWED_DISPLAY_TYPES.contains(t) ? t : "CARD"; - } - - private static String normalizeImageUrl(String imageUrl) { - if (StringUtils.isBlank(imageUrl)) { - return null; - } - String t = imageUrl.trim(); - if (t.length() > MAX_IMAGE_URL_LENGTH) { - return t.substring(0, MAX_IMAGE_URL_LENGTH); - } - return t; - } - - private String resolveClientImageUrl(EsignSurvey row) { - if (row == null || StringUtils.isBlank(row.getImageUrl())) { - return null; - } - String imageUrl = row.getImageUrl().trim(); - if (imageUrl.startsWith("http://") || imageUrl.startsWith("https://")) { - return imageUrl; - } - if (imageUrl.startsWith("data:") || mediaService.shouldUseMediaEndpoint(row)) { - return mediaService.toRelativeMediaPath(row.getSurveyId()); - } - return imageUrl; - } - - private String resolveImageUrlForSave(EsignSurvey entity, String requestImageUrl) { - String surveyId = entity.getSurveyId(); - if (requestImageUrl == null) { - return normalizeExistingImageRef(entity.getImageUrl(), surveyId); - } - if (requestImageUrl.isEmpty()) { - try { - mediaService.persistIfNeeded(surveyId, null); - } catch (Exception ignored) { - } - return null; - } - return prepareImageUrl(surveyId, requestImageUrl); - } - - private String normalizeExistingImageRef(String existing, String surveyId) { - if (StringUtils.isBlank(existing)) { - return null; - } - String trimmed = existing.trim(); - if (trimmed.startsWith(EsignSurveyMediaService.STORED_PREFIX)) { - return trimmed; - } - if (trimmed.startsWith("data:")) { - return trimmed; - } - if (EsignSurveyMediaService.isSurveyMediaPath(trimmed)) { - return EsignSurveyMediaService.STORED_PREFIX + surveyId; - } - return trimmed; - } - - private String prepareImageUrl(String surveyId, String rawImageUrl) { - if (StringUtils.isBlank(rawImageUrl)) { - return null; - } - String trimmed = rawImageUrl.trim(); - if (trimmed.startsWith("data:") || trimmed.startsWith(EsignSurveyMediaService.STORED_PREFIX)) { - try { - return mediaService.persistIfNeeded(surveyId, trimmed); - } catch (Exception e) { - throw new IllegalStateException("failed to store survey logo", e); - } - } - if (EsignSurveyMediaService.isSurveyMediaPath(trimmed)) { - return EsignSurveyMediaService.STORED_PREFIX + surveyId; - } - return normalizeImageUrl(trimmed); - } - - 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/logging/controllers/ApiLogController.java b/src/main/java/com/sjmresorts/opsapp/logging/controllers/ApiLogController.java deleted file mode 100644 index 6cca5ec..0000000 --- a/src/main/java/com/sjmresorts/opsapp/logging/controllers/ApiLogController.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.sjmresorts.opsapp.logging.controllers; - -import com.sjmresorts.opsapp.common.dtos.DataTableResult; -import com.sjmresorts.opsapp.logging.dtos.ApiLogDto; -import com.sjmresorts.opsapp.logging.dtos.ApiLogPageRequest; -import com.sjmresorts.opsapp.logging.dtos.ApiLogPageResponse; -import com.sjmresorts.opsapp.logging.dtos.GetApiLogResponse; -import com.sjmresorts.opsapp.logging.services.ApiLogService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.annotation.Resource; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.validation.constraints.NotNull; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -@Tag(name = "Logging", description = "APIs for Api Log functions") -@RestController -@RequestMapping("/api/v1/logging/apiLog") -public class ApiLogController { - - @Resource - private ApiLogService apiLogService; - - @Operation( - summary = "Query api log page", - description = "API for query api log" - ) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "Api log queried successful"), - @ApiResponse(responseCode = "400", description = "Invalid request data") - }) - @PostMapping("/queryPage") - public ResponseEntity queryPage( - HttpServletRequest request, - @RequestBody ApiLogPageRequest pageRequest) { - - DataTableResult apiLogList = apiLogService.getApiLogListByPage(pageRequest); - - ApiLogPageResponse response = new ApiLogPageResponse(); - response.setCode("200"); - response.setSuccess(true); - response.setMessage("Query Successfully"); - response.setData(apiLogList); - - return ResponseEntity.ok(response); - } - - @Operation( - summary = "Get one api log data", - description = "API for get one api log data" - ) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "Api log data got successful"), - @ApiResponse(responseCode = "400", description = "Invalid request data") - }) - @GetMapping("/{apiLogId}") - public ResponseEntity getApiLogById( - @PathVariable @NotNull Long apiLogId) { - - GetApiLogResponse responseData = new GetApiLogResponse(); - - ApiLogDto apiLogDto = apiLogService.queryApiLogById(apiLogId); - responseData.setCode("200"); - responseData.setSuccess(apiLogDto != null); - responseData.setMessage(apiLogDto != null ? "Query Successfully" : "Api log does not exists"); - responseData.setData(apiLogDto); - return ResponseEntity.status(HttpStatus.OK).body(responseData); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/logging/controllers/AppLogController.java b/src/main/java/com/sjmresorts/opsapp/logging/controllers/AppLogController.java deleted file mode 100644 index 40ab920..0000000 --- a/src/main/java/com/sjmresorts/opsapp/logging/controllers/AppLogController.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.sjmresorts.opsapp.logging.controllers; - -import com.sjmresorts.opsapp.authentication.dtos.LoginUserInfo; -import com.sjmresorts.opsapp.common.utils.JwtTokenUtil; -import com.sjmresorts.opsapp.logging.dtos.CreateAppLogRequest; -import com.sjmresorts.opsapp.logging.dtos.CreateAppLogResponse; -import com.sjmresorts.opsapp.logging.mappers.AppLogMapper; -import com.sjmresorts.opsapp.logging.models.AppLog; -import com.sjmresorts.opsapp.logging.services.AppLogService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.annotation.Resource; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.validation.Valid; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -@Tag(name = "Logging", description = "APIs to store app frond-end logging ") -@RestController -@RequestMapping("/api/v1/logging") -@CrossOrigin(origins = "*") -public class AppLogController { - - @Resource - private AppLogService appLogService; - @Resource - private AppLogMapper appLogMapper; - - @Operation( - summary = "Create App log", - description = "API for App frond-end log creation" - ) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "App log created successful"), - @ApiResponse(responseCode = "400", description = "Invalid request data") - }) - @PostMapping(value="/createAppLog",produces = "application/json;charset=UTF-8") - public ResponseEntity createAppLog( - HttpServletRequest request, - @Parameter(description = "App log request info", required = true) @Valid @RequestBody CreateAppLogRequest requestData) { - CreateAppLogResponse responseData = new CreateAppLogResponse(); - LoginUserInfo requestor = JwtTokenUtil.parseUserFromRequest(request); - AppLog appLog = appLogMapper.requestToEntity(requestData); - if(requestor!=null){ - appLog.setUserId(requestor.getEmployeeId()); - } - appLogService.saveAppLog(appLog); - responseData.setSuccess(true); - responseData.setCode("200"); - responseData.setMessage("App log created success"); - - return ResponseEntity.status(HttpStatus.OK).body(responseData); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/logging/controllers/AuditLogController.java b/src/main/java/com/sjmresorts/opsapp/logging/controllers/AuditLogController.java deleted file mode 100644 index 926a60e..0000000 --- a/src/main/java/com/sjmresorts/opsapp/logging/controllers/AuditLogController.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.sjmresorts.opsapp.logging.controllers; - - -import com.sjmresorts.opsapp.common.dtos.DataTableResult; -import com.sjmresorts.opsapp.logging.dtos.AuditLogDto; -import com.sjmresorts.opsapp.logging.dtos.AuditLogPageRequest; -import com.sjmresorts.opsapp.logging.dtos.AuditLogPageResponse; -import com.sjmresorts.opsapp.logging.dtos.GetAuditLogResponse; -import com.sjmresorts.opsapp.logging.services.AuditLogService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.annotation.Resource; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.validation.constraints.NotNull; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -@Tag(name = "Logging", description = "APIs for Audit Log functions") -@RestController -@RequestMapping("/api/v1/logging/auditLog") -public class AuditLogController { - - @Resource - private AuditLogService auditLogService; - - @Operation( - summary = "Query audit log page", - description = "API for query audit log" - ) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "Audit log queried successful"), - @ApiResponse(responseCode = "400", description = "Invalid request data") - }) - @PostMapping("/queryPage") - public ResponseEntity queryPage( - HttpServletRequest request, - @RequestBody AuditLogPageRequest pageRequest) { - - DataTableResult apiLogList = auditLogService.getAuditLogListByPage(pageRequest); - - AuditLogPageResponse response = new AuditLogPageResponse(); - response.setCode("200"); - response.setSuccess(true); - response.setMessage("Query Successfully"); - response.setData(apiLogList); - - return ResponseEntity.ok(response); - } - - @Operation( - summary = "Get one audit log data", - description = "API for get one audit log data" - ) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "Audit log data got successful"), - @ApiResponse(responseCode = "400", description = "Invalid request data") - }) - @GetMapping("/{auditLogId}") - public ResponseEntity getAuditLogById( - @PathVariable @NotNull Long auditLogId) { - - GetAuditLogResponse responseData = new GetAuditLogResponse(); - - AuditLogDto auditLogDto = auditLogService.queryAuditLogById(auditLogId); - responseData.setCode("200"); - responseData.setSuccess(auditLogDto != null); - responseData.setMessage(auditLogDto != null ? "Query Successfully" : "Audit log does not exists"); - responseData.setData(auditLogDto); - return ResponseEntity.status(HttpStatus.OK).body(responseData); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/logging/dtos/ApiLogDto.java b/src/main/java/com/sjmresorts/opsapp/logging/dtos/ApiLogDto.java deleted file mode 100644 index 2701bb8..0000000 --- a/src/main/java/com/sjmresorts/opsapp/logging/dtos/ApiLogDto.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.sjmresorts.opsapp.logging.dtos; - -import lombok.Data; - -import java.time.LocalDateTime; - -@Data -public class ApiLogDto { - - private Long apiLogId; - - private String url; - - private String type; - - private LocalDateTime requestTime; - - private LocalDateTime responseTime; - - private String header = ""; - - private String requestBody = ""; - - private String requestBy = ""; - - private String responseBody = ""; - - private String debugLevel = ""; - - private String statusCode = ""; -} diff --git a/src/main/java/com/sjmresorts/opsapp/logging/dtos/ApiLogPageRequest.java b/src/main/java/com/sjmresorts/opsapp/logging/dtos/ApiLogPageRequest.java deleted file mode 100644 index 74b224c..0000000 --- a/src/main/java/com/sjmresorts/opsapp/logging/dtos/ApiLogPageRequest.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.sjmresorts.opsapp.logging.dtos; - -import com.sjmresorts.opsapp.common.dtos.BasePageRequest; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -import java.util.List; - -@Setter -@Getter -@NoArgsConstructor -public class ApiLogPageRequest extends BasePageRequest { - - private String searchType; - - private String searchDebugLevel; - - private List searchDate; -} diff --git a/src/main/java/com/sjmresorts/opsapp/logging/dtos/ApiLogPageResponse.java b/src/main/java/com/sjmresorts/opsapp/logging/dtos/ApiLogPageResponse.java deleted file mode 100644 index 8d5b6f8..0000000 --- a/src/main/java/com/sjmresorts/opsapp/logging/dtos/ApiLogPageResponse.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.sjmresorts.opsapp.logging.dtos; - -import com.sjmresorts.opsapp.common.dtos.BaseApiResponse; -import com.sjmresorts.opsapp.common.dtos.DataTableResult; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Getter -@Setter -@NoArgsConstructor -@Schema(description = "Api Log Info List") -public class ApiLogPageResponse extends BaseApiResponse { - - private DataTableResult data; -} diff --git a/src/main/java/com/sjmresorts/opsapp/logging/dtos/AuditLogDto.java b/src/main/java/com/sjmresorts/opsapp/logging/dtos/AuditLogDto.java deleted file mode 100644 index e1ac6ee..0000000 --- a/src/main/java/com/sjmresorts/opsapp/logging/dtos/AuditLogDto.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.sjmresorts.opsapp.logging.dtos; - -import lombok.Data; - -import java.time.Instant; - -@Data -public class AuditLogDto { - - private Long auditLogId; - - private String username; - - private String action; - - private String details; - - private String oldValue; - - private String newValue; - - private Instant actionTime; - - private String userId; -} diff --git a/src/main/java/com/sjmresorts/opsapp/logging/dtos/AuditLogPageRequest.java b/src/main/java/com/sjmresorts/opsapp/logging/dtos/AuditLogPageRequest.java deleted file mode 100644 index ae05948..0000000 --- a/src/main/java/com/sjmresorts/opsapp/logging/dtos/AuditLogPageRequest.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.sjmresorts.opsapp.logging.dtos; - -import com.sjmresorts.opsapp.common.dtos.BasePageRequest; -import lombok.Data; -import lombok.EqualsAndHashCode; - -import java.util.List; - -@EqualsAndHashCode(callSuper = true) -@Data -public class AuditLogPageRequest extends BasePageRequest { - - private String searchUsername; - - private String searchAction; - - private String searchAdminUserCode; - - private List searchDate; -} diff --git a/src/main/java/com/sjmresorts/opsapp/logging/dtos/AuditLogPageResponse.java b/src/main/java/com/sjmresorts/opsapp/logging/dtos/AuditLogPageResponse.java deleted file mode 100644 index be8d07d..0000000 --- a/src/main/java/com/sjmresorts/opsapp/logging/dtos/AuditLogPageResponse.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.sjmresorts.opsapp.logging.dtos; - -import com.sjmresorts.opsapp.common.dtos.BaseApiResponse; -import com.sjmresorts.opsapp.common.dtos.DataTableResult; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Setter -@Getter -@NoArgsConstructor -public class AuditLogPageResponse extends BaseApiResponse { - - private DataTableResult data; -} diff --git a/src/main/java/com/sjmresorts/opsapp/logging/dtos/CreateAppLogRequest.java b/src/main/java/com/sjmresorts/opsapp/logging/dtos/CreateAppLogRequest.java deleted file mode 100644 index 90437bd..0000000 --- a/src/main/java/com/sjmresorts/opsapp/logging/dtos/CreateAppLogRequest.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.sjmresorts.opsapp.logging.dtos; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.Column; -import jakarta.validation.constraints.NotNull; -import lombok.Data; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -import java.time.Instant; - -@Data -@Getter -@Setter -@NoArgsConstructor -@Schema(description = "Request to create app log for frond-end") -public class CreateAppLogRequest { - - @Schema(description = "Event code of log") - @NotNull(message = "Event Code must not be null") - private String eventCode; - @Schema(description = "Request time of app log") - @NotNull(message = "Request time must not be null") - private Instant requestTime; - @Schema(description = "Log description") - @NotNull(message = "Log description must not be null") - private String logDesc; - @Column(name = "tag_success") - @NotNull(message = "Tag success must not be null") - private Integer tagSuccess; - @Schema(description = "Log by information detail") - private String logBy; - @Schema(description = "User Id to request log, Optional and using token userId in first priority ") - private String userId; -} diff --git a/src/main/java/com/sjmresorts/opsapp/logging/dtos/CreateAppLogResponse.java b/src/main/java/com/sjmresorts/opsapp/logging/dtos/CreateAppLogResponse.java deleted file mode 100644 index 809da48..0000000 --- a/src/main/java/com/sjmresorts/opsapp/logging/dtos/CreateAppLogResponse.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.sjmresorts.opsapp.logging.dtos; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Data -@Getter -@Setter -@NoArgsConstructor -@Schema(description = "Response info of create app log") -public class CreateAppLogResponse { - @Schema(description = "Indicate success status") - private boolean success=false; - - @Schema(description = "Status Code of response",example="200") - private String code; - - @Schema(description = "Description of result") - private String message; -} diff --git a/src/main/java/com/sjmresorts/opsapp/logging/dtos/GetApiLogResponse.java b/src/main/java/com/sjmresorts/opsapp/logging/dtos/GetApiLogResponse.java deleted file mode 100644 index 047254a..0000000 --- a/src/main/java/com/sjmresorts/opsapp/logging/dtos/GetApiLogResponse.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.sjmresorts.opsapp.logging.dtos; - -import com.sjmresorts.opsapp.common.dtos.BaseApiResponse; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Getter -@Setter -@NoArgsConstructor -@Schema(description = "Response info of getting api log") -public class GetApiLogResponse extends BaseApiResponse { - - private ApiLogDto data; -} diff --git a/src/main/java/com/sjmresorts/opsapp/logging/dtos/GetAuditLogResponse.java b/src/main/java/com/sjmresorts/opsapp/logging/dtos/GetAuditLogResponse.java deleted file mode 100644 index 4ff5ada..0000000 --- a/src/main/java/com/sjmresorts/opsapp/logging/dtos/GetAuditLogResponse.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.sjmresorts.opsapp.logging.dtos; - -import com.sjmresorts.opsapp.common.dtos.BaseApiResponse; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Getter -@Setter -@NoArgsConstructor -@Schema(description = "Response info of getting audit log") -public class GetAuditLogResponse extends BaseApiResponse { - - private AuditLogDto data; -} diff --git a/src/main/java/com/sjmresorts/opsapp/logging/dtos/GetNotificationLogResponse.java b/src/main/java/com/sjmresorts/opsapp/logging/dtos/GetNotificationLogResponse.java deleted file mode 100644 index 2a1a44c..0000000 --- a/src/main/java/com/sjmresorts/opsapp/logging/dtos/GetNotificationLogResponse.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.sjmresorts.opsapp.logging.dtos; - -import com.sjmresorts.opsapp.common.dtos.BaseApiResponse; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Getter -@Setter -@NoArgsConstructor -@Schema(description = "Response info of getting notification log") -public class GetNotificationLogResponse extends BaseApiResponse { - - private NotificationLogDto data; -} diff --git a/src/main/java/com/sjmresorts/opsapp/logging/dtos/NotificationLogDto.java b/src/main/java/com/sjmresorts/opsapp/logging/dtos/NotificationLogDto.java deleted file mode 100644 index db79f30..0000000 --- a/src/main/java/com/sjmresorts/opsapp/logging/dtos/NotificationLogDto.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.sjmresorts.opsapp.logging.dtos; - -import lombok.Data; - -import java.time.Instant; - -@Data -public class NotificationLogDto { - - private Long id; - - private String notificationType; - - private String triggerTask; - - private String recipients; - - private String subject; - - private String notificationContent; - - private Instant requestedTime; - - private Instant finishedTime; - - private String resultStatus; - - private String errorMessage; -} diff --git a/src/main/java/com/sjmresorts/opsapp/logging/dtos/NotificationLogPageRequest.java b/src/main/java/com/sjmresorts/opsapp/logging/dtos/NotificationLogPageRequest.java deleted file mode 100644 index 20513aa..0000000 --- a/src/main/java/com/sjmresorts/opsapp/logging/dtos/NotificationLogPageRequest.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.sjmresorts.opsapp.logging.dtos; - -import com.sjmresorts.opsapp.common.dtos.BasePageRequest; -import lombok.Data; -import lombok.EqualsAndHashCode; - -import java.util.List; - -@EqualsAndHashCode(callSuper = true) -@Data -public class NotificationLogPageRequest extends BasePageRequest { - - private String searchType; - - private String searchResultStatus; - - private List searchDate; -} diff --git a/src/main/java/com/sjmresorts/opsapp/logging/dtos/NotificationLogPageResponse.java b/src/main/java/com/sjmresorts/opsapp/logging/dtos/NotificationLogPageResponse.java deleted file mode 100644 index b4d39b5..0000000 --- a/src/main/java/com/sjmresorts/opsapp/logging/dtos/NotificationLogPageResponse.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.sjmresorts.opsapp.logging.dtos; - -import com.sjmresorts.opsapp.common.dtos.BaseApiResponse; -import com.sjmresorts.opsapp.common.dtos.DataTableResult; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Setter -@Getter -@NoArgsConstructor -public class NotificationLogPageResponse extends BaseApiResponse { - - private DataTableResult data; -} diff --git a/src/main/java/com/sjmresorts/opsapp/logging/filters/ApiLoggingFilter.java b/src/main/java/com/sjmresorts/opsapp/logging/filters/ApiLoggingFilter.java deleted file mode 100644 index e77d8b6..0000000 --- a/src/main/java/com/sjmresorts/opsapp/logging/filters/ApiLoggingFilter.java +++ /dev/null @@ -1,232 +0,0 @@ -package com.sjmresorts.opsapp.logging.filters; - - -import com.alibaba.fastjson2.JSONObject; -import com.sjmresorts.opsapp.authentication.dtos.LoginUserInfo; -import com.sjmresorts.opsapp.common.utils.JwtTokenUtil; -import com.sjmresorts.opsapp.common.utils.NetworkUtil; -import com.sjmresorts.opsapp.logging.models.ApiLog; -import com.sjmresorts.opsapp.logging.services.ApiLogService; -import jakarta.annotation.PostConstruct; -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.NonNull; -import lombok.RequiredArgsConstructor; -import lombok.Setter; -import org.apache.commons.lang3.StringUtils; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.core.Ordered; -import org.springframework.core.annotation.Order; -import org.springframework.stereotype.Component; -import org.springframework.web.filter.OncePerRequestFilter; -import org.springframework.web.servlet.DispatcherServlet; -import org.springframework.web.util.ContentCachingRequestWrapper; -import org.springframework.web.util.ContentCachingResponseWrapper; - -import java.io.IOException; -import java.time.LocalDateTime; -import java.util.HashMap; -import java.util.Map; - -@Component -@Order(Ordered.HIGHEST_PRECEDENCE) -@RequiredArgsConstructor -public class ApiLoggingFilter extends OncePerRequestFilter { - - @Setter - private int debugLevelCode=500; - @Setter - private String debugLevel="ERROR"; - private final ApiLogService apiLogService; - - @Value("${ops-app.api-logging-level:ERROR}") - private String apiLogLevel; - - @PostConstruct - void applyApiLogLevel() { - if ("WARN".equalsIgnoreCase(apiLogLevel)) { - debugLevel = "WARN"; - debugLevelCode = 400; - } else if ("INFO".equalsIgnoreCase(apiLogLevel)) { - debugLevel = "INFO"; - debugLevelCode = 0; - } else { - debugLevel = "ERROR"; - debugLevelCode = 500; - } - } - - @Override - protected boolean shouldNotFilter(HttpServletRequest request) { - String uri = request.getRequestURI(); - if (uri == null) { - return false; - } - // 高频轮询 / 大体积 PDF 提交不写 API 日志,减轻 DB 与连接池压力 - return uri.contains("/esign/ios-task-list") - || uri.contains("/sinosecu/ios-task-list") - || uri.contains("/esign/ios-pending-tasks") - || uri.contains("/sinosecu/ios-pending-tasks") - || uri.contains("/esign/printer-pdf-task-submit") - || uri.contains("/sinosecu/printer-pdf-task-submit") - || uri.contains("/esign/printer-dashboard") - || uri.contains("/sinosecu/printer-dashboard") - || uri.contains("/esign/printer-queue") - || uri.contains("/sinosecu/printer-queue") - || uri.contains("/esign/printer-devices") - || uri.contains("/sinosecu/printer-devices") - || uri.contains("/esign/printer-device-link") - || uri.contains("/sinosecu/printer-device-link") - || uri.contains("/esign/printer-push") - || uri.contains("/sinosecu/printer-push") - || uri.contains("/esign/printer-event-logs") - || uri.contains("/sinosecu/printer-event-logs") - || uri.contains("/esign/printer-pdf-archive") - || uri.contains("/sinosecu/printer-pdf-archive"); - } - - @Override - protected void doFilterInternal( - @NonNull HttpServletRequest request, - @NonNull HttpServletResponse response, - @NonNull FilterChain filterChain - ) - throws IOException, ServletException { - Throwable handleException = null; - ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request); - ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper(response); - LocalDateTime startTime = LocalDateTime.now(); - try { - filterChain.doFilter(wrappedRequest, wrappedResponse); - }catch(Exception err){ - handleException = err; - throw err; - } - finally { - String errorMessage = handleException!=null?handleException.getMessage():""; - if(StringUtils.isBlank(errorMessage)){ - errorMessage = checkResponseErrors(wrappedRequest); - } - if(!"OPTIONS".equalsIgnoreCase(request.getMethod())){ - int statusCode = wrappedResponse.getStatus(); - if(statusCode>=debugLevelCode || handleException!=null){ - String debugLevel = handleException!=null?"ERROR":getDebugLevelByCode(statusCode); - ApiLog requestLog = createRequestLog(request, startTime); - requestLog.setResponseTime(LocalDateTime.now()); - if(StringUtils.isNotBlank(errorMessage)){ - requestLog.setResponseBody(errorMessage); - }else{ - requestLog.setResponseBody(getResponseContent(wrappedResponse)); - } - requestLog.setStatusCode(String.valueOf(statusCode)); - requestLog.setDebugLevel(debugLevel); - apiLogService.saveApiLog(requestLog); - } - } - wrappedResponse.copyBodyToResponse(); - } - } - - private String checkResponseErrors(HttpServletRequest request) { - String errorMessage = ""; - Object exceptionVal = request.getAttribute("AUTH_EXCEPTION"); - errorMessage = exceptionVal!=null?exceptionVal.toString():""; - if(StringUtils.isBlank(errorMessage)){ - exceptionVal = request.getAttribute(DispatcherServlet.EXCEPTION_ATTRIBUTE); - errorMessage = exceptionVal!=null?exceptionVal.toString():""; - } - return errorMessage; - } - - private String getDebugLevelByCode(int statusCode){ - String debugLevel = "INFO"; - if(statusCode>=400 && statusCode<500){ - debugLevel="WARN"; - }else if(statusCode>=500){ - debugLevel="ERROR"; - } - return debugLevel; - } - - private ApiLog createRequestLog(HttpServletRequest request, LocalDateTime startTime) { - ApiLog log = new ApiLog(); - log.setType(request.getMethod()); - log.setUrl(getFullUrl(request)); - log.setRequestTime(startTime); - log.setHeader(getHeadersAsString(request)); - log.setRequestBy(getRequestSource(request)); - if (request instanceof ContentCachingRequestWrapper wrappedRequest) { - byte[] content = wrappedRequest.getContentAsByteArray(); - if (content.length > 0) { - try { - String body = new String(content, wrappedRequest.getCharacterEncoding()); - log.setRequestBody(body); - }catch(Exception ignored){ - log.setRequestBody(new String(content)); - } - } - } - return log; - } - - private String getFullUrl(HttpServletRequest request) { - String scheme = request.getScheme(); - String serverName = request.getServerName(); - int serverPort = request.getServerPort(); - String contextPath = request.getContextPath(); - String servletPath = request.getServletPath(); - String pathInfo = request.getPathInfo(); - String queryString = request.getQueryString(); - - StringBuilder url = new StringBuilder(); - url.append(scheme).append("://").append(serverName); - if ((scheme.equals("http") && serverPort != 80) || - (scheme.equals("https") && serverPort != 443)) { - url.append(":").append(serverPort); - } - url.append(contextPath).append(servletPath); - if (pathInfo != null) { - url.append(pathInfo); - } - if (StringUtils.isNotBlank(queryString)) { - url.append("?").append(queryString); - } - return url.toString(); - } - - private String getRequestSource(HttpServletRequest request){ - Map requestSource = new HashMap(); - requestSource.put("RequestIP", NetworkUtil.getClientIpByRequest(request)); - LoginUserInfo user = JwtTokenUtil.parseUserFromRequest(request); - if(user!=null){ - requestSource.put("employeeId",user.getEmployeeId()); - requestSource.put("userName",user.getUserName()); - } - return JSONObject.toJSONString(requestSource); - } - - private String getResponseContent(HttpServletResponse response) { - String responseBody=""; - if (response instanceof ContentCachingResponseWrapper wrappedResponse) { - byte[] content = wrappedResponse.getContentAsByteArray(); - if (content.length > 0) { - try { - responseBody = new String(content, wrappedResponse.getCharacterEncoding()); - }catch(Exception ignored){ - responseBody = new String(content); - } - } - } - return responseBody; - } - - private String getHeadersAsString(HttpServletRequest request) { - StringBuilder headers = new StringBuilder(); - request.getHeaderNames().asIterator().forEachRemaining(headerName -> { - headers.append(headerName).append(": ").append(request.getHeader(headerName)).append("; "); - }); - return headers.toString(); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/logging/mappers/ApiLogMapper.java b/src/main/java/com/sjmresorts/opsapp/logging/mappers/ApiLogMapper.java deleted file mode 100644 index 5b59076..0000000 --- a/src/main/java/com/sjmresorts/opsapp/logging/mappers/ApiLogMapper.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.sjmresorts.opsapp.logging.mappers; - -import com.sjmresorts.opsapp.logging.dtos.ApiLogDto; -import com.sjmresorts.opsapp.logging.models.ApiLog; -import org.mapstruct.Mapper; - -import java.util.List; - -@Mapper(componentModel = "spring") -public interface ApiLogMapper { - - ApiLogDto toApiLogDto(ApiLog apiLog); - - List toApiLogDtoList(List apiLogList); -} diff --git a/src/main/java/com/sjmresorts/opsapp/logging/mappers/AppLogMapper.java b/src/main/java/com/sjmresorts/opsapp/logging/mappers/AppLogMapper.java deleted file mode 100644 index 20d91a1..0000000 --- a/src/main/java/com/sjmresorts/opsapp/logging/mappers/AppLogMapper.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.sjmresorts.opsapp.logging.mappers; - -import com.sjmresorts.opsapp.logging.dtos.CreateAppLogRequest; -import com.sjmresorts.opsapp.logging.models.AppLog; -import org.mapstruct.Mapper; - -@Mapper(componentModel = "spring") -public interface AppLogMapper { - - AppLog requestToEntity(CreateAppLogRequest dto); -} diff --git a/src/main/java/com/sjmresorts/opsapp/logging/mappers/AuditLogMapper.java b/src/main/java/com/sjmresorts/opsapp/logging/mappers/AuditLogMapper.java deleted file mode 100644 index 124c496..0000000 --- a/src/main/java/com/sjmresorts/opsapp/logging/mappers/AuditLogMapper.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.sjmresorts.opsapp.logging.mappers; - -import com.sjmresorts.opsapp.logging.dtos.AuditLogDto; -import com.sjmresorts.opsapp.logging.models.AuditLog; -import org.mapstruct.Mapper; - -import java.util.List; - -@Mapper(componentModel = "spring") -public interface AuditLogMapper { - - AuditLogDto toAuditLogDto(AuditLog auditLog); - - List toApiLogDtoList(List content); - -} diff --git a/src/main/java/com/sjmresorts/opsapp/logging/mappers/NotificationLogMapper.java b/src/main/java/com/sjmresorts/opsapp/logging/mappers/NotificationLogMapper.java deleted file mode 100644 index 9fd1e81..0000000 --- a/src/main/java/com/sjmresorts/opsapp/logging/mappers/NotificationLogMapper.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.sjmresorts.opsapp.logging.mappers; - -import com.sjmresorts.opsapp.logging.dtos.NotificationLogDto; -import com.sjmresorts.opsapp.logging.models.NotificationLog; -import org.mapstruct.Mapper; - -import java.util.List; - -@Mapper(componentModel = "spring") -public interface NotificationLogMapper { - - NotificationLogDto toNotificationLogDto(NotificationLog notificationLog); - - List toNotificationLogDtoList(List notificationLogList); -} diff --git a/src/main/java/com/sjmresorts/opsapp/logging/models/ApiLog.java b/src/main/java/com/sjmresorts/opsapp/logging/models/ApiLog.java deleted file mode 100644 index aa95dcd..0000000 --- a/src/main/java/com/sjmresorts/opsapp/logging/models/ApiLog.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.sjmresorts.opsapp.logging.models; - -import jakarta.persistence.*; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -import java.time.LocalDateTime; - -@Getter -@Setter -@NoArgsConstructor -@Entity -@Table(name="api_logs") -public class ApiLog { - - @Id - @Column(name = "api_log_id",nullable = false) - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long apiLogId; - @Column(name = "url") - private String url; - @Column(name = "type") - private String type; - @Column(name = "request_dtm") - private LocalDateTime requestTime; - @Column(name = "response_dtm") - private LocalDateTime responseTime; - @Column(name = "header") - private String header=""; - @Column(name = "request_body") - private String requestBody=""; - @Column(name = "request_by") - private String requestBy=""; - @Column(name = "response_body") - private String responseBody=""; - @Column(name = "debug_level") - private String debugLevel=""; - @Column(name = "status_code") - private String statusCode=""; - -} diff --git a/src/main/java/com/sjmresorts/opsapp/logging/models/AppLog.java b/src/main/java/com/sjmresorts/opsapp/logging/models/AppLog.java deleted file mode 100644 index 003d2b1..0000000 --- a/src/main/java/com/sjmresorts/opsapp/logging/models/AppLog.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.sjmresorts.opsapp.logging.models; - -import jakarta.persistence.*; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -import java.time.Instant; - -@Getter -@Setter -@NoArgsConstructor -@Entity -@Table(name="app_logs") -public class AppLog { - @Id - @Column(name = "app_logs_id",nullable = false) - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long appLogId; - @Column(name = "event_code",nullable = false) - private String eventCode; - @Column(name = "request_dtm",nullable = false) - private Instant requestTime; - @Column(name = "user_id") - private String userId; - @Column(name = "log_desc",nullable = false) - private String logDesc; - @Column(name = "tag_success") - private Integer tagSuccess; - @Column(name = "log_by") - private String logBy; -} diff --git a/src/main/java/com/sjmresorts/opsapp/logging/models/AuditLog.java b/src/main/java/com/sjmresorts/opsapp/logging/models/AuditLog.java deleted file mode 100644 index 451b47f..0000000 --- a/src/main/java/com/sjmresorts/opsapp/logging/models/AuditLog.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.sjmresorts.opsapp.logging.models; - -import jakarta.persistence.*; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -import java.time.Instant; - -@Getter -@Setter -@NoArgsConstructor -@Entity -@Table(name="audit_logs") -public class AuditLog { - - @Id - @Column(name = "audit_log_id",nullable = false) - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long auditLogId; - @Column(name = "user_id") - private String userId=""; - @Column(name = "username") - private String userName=""; - @Column(name = "action") - private String action=""; - @Column(name = "details") - private String details; - @Column(name = "old_value") - private String oldValue; - @Column(name = "new_value") - private String newValue; - @Column(name = "timestamp") - private Instant actionTime; - -} diff --git a/src/main/java/com/sjmresorts/opsapp/logging/models/NotificationLog.java b/src/main/java/com/sjmresorts/opsapp/logging/models/NotificationLog.java deleted file mode 100644 index f9749a9..0000000 --- a/src/main/java/com/sjmresorts/opsapp/logging/models/NotificationLog.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.sjmresorts.opsapp.logging.models; - -import jakarta.persistence.*; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -import java.time.Instant; - -@Getter -@Setter -@NoArgsConstructor -@Entity -@Table(name="notification_logs") -public class NotificationLog { - @Id - @Column(name = "id",nullable = false) - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - @Column(name = "notification_type",nullable = false) - private String notificationType; - @Column(name = "trigger_task") - private String triggerTask; - @Column(name = "recipients") - private String recipients; - @Column(name = "subject") - private String subject; - @Column(name = "notification_content") - private String notificationContent; - @Column(name = "requested_dtm") - private Instant requestedTime; - @Column(name = "finished_dtm") - private Instant finishedTime; - @Column(name = "result_status") - private String resultStatus; - @Column(name = "error_message") - private String errorMessage; -} diff --git a/src/main/java/com/sjmresorts/opsapp/logging/repositories/ApiLogRepository.java b/src/main/java/com/sjmresorts/opsapp/logging/repositories/ApiLogRepository.java deleted file mode 100644 index 2f9f74f..0000000 --- a/src/main/java/com/sjmresorts/opsapp/logging/repositories/ApiLogRepository.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.sjmresorts.opsapp.logging.repositories; - -import com.sjmresorts.opsapp.logging.models.ApiLog; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.domain.Specification; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -@Repository -public interface ApiLogRepository extends JpaRepository { - - Page findAll(Specification listSpec, Pageable pageable); -} diff --git a/src/main/java/com/sjmresorts/opsapp/logging/repositories/AppLogRepository.java b/src/main/java/com/sjmresorts/opsapp/logging/repositories/AppLogRepository.java deleted file mode 100644 index f39da04..0000000 --- a/src/main/java/com/sjmresorts/opsapp/logging/repositories/AppLogRepository.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.sjmresorts.opsapp.logging.repositories; - -import com.sjmresorts.opsapp.logging.models.AppLog; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -@Repository -public interface AppLogRepository extends JpaRepository { - -} diff --git a/src/main/java/com/sjmresorts/opsapp/logging/repositories/AuditLogRepository.java b/src/main/java/com/sjmresorts/opsapp/logging/repositories/AuditLogRepository.java deleted file mode 100644 index 341b688..0000000 --- a/src/main/java/com/sjmresorts/opsapp/logging/repositories/AuditLogRepository.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.sjmresorts.opsapp.logging.repositories; - -import com.sjmresorts.opsapp.logging.models.AuditLog; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.domain.Specification; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -@Repository -public interface AuditLogRepository extends JpaRepository { - - Page findAll(Specification listSpec, Pageable pageable); -} diff --git a/src/main/java/com/sjmresorts/opsapp/logging/repositories/NotificationLogRepository.java b/src/main/java/com/sjmresorts/opsapp/logging/repositories/NotificationLogRepository.java deleted file mode 100644 index dfe75e2..0000000 --- a/src/main/java/com/sjmresorts/opsapp/logging/repositories/NotificationLogRepository.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.sjmresorts.opsapp.logging.repositories; - -import com.sjmresorts.opsapp.logging.models.NotificationLog; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.domain.Specification; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -@Repository -public interface NotificationLogRepository extends JpaRepository { - - Page findAll(Specification listSpec, Pageable pageable); -} diff --git a/src/main/java/com/sjmresorts/opsapp/logging/services/ApiLogService.java b/src/main/java/com/sjmresorts/opsapp/logging/services/ApiLogService.java deleted file mode 100644 index dc91ca0..0000000 --- a/src/main/java/com/sjmresorts/opsapp/logging/services/ApiLogService.java +++ /dev/null @@ -1,107 +0,0 @@ -package com.sjmresorts.opsapp.logging.services; - -import com.sjmresorts.opsapp.common.dtos.DataTableResult; -import com.sjmresorts.opsapp.common.utils.DaoUtil; -import com.sjmresorts.opsapp.common.utils.DateTimeUtil; -import com.sjmresorts.opsapp.logging.dtos.ApiLogDto; -import com.sjmresorts.opsapp.logging.dtos.ApiLogPageRequest; -import com.sjmresorts.opsapp.logging.mappers.ApiLogMapper; -import com.sjmresorts.opsapp.logging.models.ApiLog; -import com.sjmresorts.opsapp.logging.repositories.ApiLogRepository; -import jakarta.persistence.criteria.Predicate; -import jakarta.validation.constraints.NotBlank; -import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.data.jpa.domain.Specification; -import org.springframework.scheduling.annotation.Async; -import org.springframework.stereotype.Service; - -import java.util.ArrayList; -import java.util.Date; -import java.util.List; - - -@Service -@RequiredArgsConstructor -public class ApiLogService { - private final Logger logger = LoggerFactory.getLogger(ApiLogService.class); - - private final ApiLogRepository apiLogRepository; - - private final ApiLogMapper apiLogMapper; - - @Async - public void saveApiLog(ApiLog requestLog) { - try { - if (requestLog.getRequestBody() == null) { - requestLog.setRequestBody(""); - } - apiLogRepository.save(requestLog); - } catch (Exception e) { - logger.error("Failed to save api log to database with error:{}", e.getMessage()); - } - } - - public DataTableResult getApiLogListByPage(ApiLogPageRequest pageRequest) { - - Specification listSpec = buildSpecificationByPageRequest(pageRequest); - int pageSize = pageRequest.getPageSize(); - int pageStart = pageRequest.getStartRow() / pageSize; - Sort sort = DaoUtil.buildSortByString(pageRequest.getSortOrder(), "requestTime"); - Pageable pageable = PageRequest.of(pageStart, pageSize, sort); - Page resultPage = apiLogRepository.findAll(listSpec, pageable); - List resultList = apiLogMapper.toApiLogDtoList(resultPage.getContent()); - DataTableResult dataResult = new DataTableResult<>(); - dataResult.setTotalRecords(resultPage.getTotalElements()); - dataResult.setPageSize(pageSize); - if (resultList == null) { - resultList = new ArrayList<>(); - } - dataResult.setRowData(resultList); - return dataResult; - } - - private Specification buildSpecificationByPageRequest(ApiLogPageRequest pageRequest) { - - return ((root, criteriaQuery, cb) -> { - List list = new ArrayList<>(); - if (StringUtils.isNotBlank(pageRequest.getSearchType())) { - list.add(cb.like(cb.upper(root.get("type")), - "%" + pageRequest.getSearchType().toUpperCase() + "%")); - } - if (StringUtils.isNotBlank(pageRequest.getSearchDebugLevel())) { - list.add(cb.like(cb.upper(root.get("debugLevel")), - "%" + pageRequest.getSearchDebugLevel().toUpperCase() + "%")); - } - if (pageRequest.getSearchDate() != null && !pageRequest.getSearchDate().isEmpty() - && StringUtils.isNotBlank(pageRequest.getSearchDate().getFirst())) { - Date checkOperateBeginTime = DateTimeUtil.getDateFromString(pageRequest.getSearchDate().getFirst() + " 00:00:00", "yyyy-MM-dd hh:mm:ss"); - if (checkOperateBeginTime != null) { - list.add(cb.greaterThanOrEqualTo(root.get("requestTime"), checkOperateBeginTime)); - } - } - if (pageRequest.getSearchDate() != null && pageRequest.getSearchDate().size() > 1 - && StringUtils.isNotBlank(pageRequest.getSearchDate().get(1))) { - Date checkOperateEndTime = DateTimeUtil.getDateFromString(pageRequest.getSearchDate().get(1) + " 23:59:59", "yyyy-MM-dd hh:mm:ss"); - if (checkOperateEndTime != null) { - list.add(cb.lessThanOrEqualTo(root.get("requestTime"), checkOperateEndTime)); - } - } - Predicate[] predicates = new Predicate[list.size()]; - criteriaQuery.where(list.toArray(predicates)); - return cb.and(list.toArray(predicates)); - }); - } - - public ApiLogDto queryApiLogById(@NotBlank Long apiLogId) { - ApiLog apiLog = apiLogRepository.findById(apiLogId).orElse(null); - if (apiLog == null) return null; - return apiLogMapper.toApiLogDto(apiLog); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/logging/services/AppLogService.java b/src/main/java/com/sjmresorts/opsapp/logging/services/AppLogService.java deleted file mode 100644 index f20fba0..0000000 --- a/src/main/java/com/sjmresorts/opsapp/logging/services/AppLogService.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.sjmresorts.opsapp.logging.services; - -import com.sjmresorts.opsapp.logging.models.AppLog; -import com.sjmresorts.opsapp.logging.repositories.AppLogRepository; -import lombok.RequiredArgsConstructor; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.scheduling.annotation.Async; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class AppLogService { - private final Logger logger = LoggerFactory.getLogger(AppLogService.class); - private final AppLogRepository appLogRepository; - - @Async - public void saveAppLog(AppLog appLog) { - try { - appLogRepository.save(appLog); - } catch (Exception e) { - logger.error("Failed to save app log to database with error:{}", e.getMessage()); - } - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/logging/services/AuditLogService.java b/src/main/java/com/sjmresorts/opsapp/logging/services/AuditLogService.java deleted file mode 100644 index 4314ef9..0000000 --- a/src/main/java/com/sjmresorts/opsapp/logging/services/AuditLogService.java +++ /dev/null @@ -1,147 +0,0 @@ -package com.sjmresorts.opsapp.logging.services; - -import com.alibaba.fastjson2.JSONObject; -import com.sjmresorts.opsapp.authentication.dtos.LoginUserInfo; -import com.sjmresorts.opsapp.common.dtos.DataTableResult; -import com.sjmresorts.opsapp.common.utils.DaoUtil; -import com.sjmresorts.opsapp.common.utils.DateTimeUtil; -import com.sjmresorts.opsapp.logging.dtos.AuditLogDto; -import com.sjmresorts.opsapp.logging.dtos.AuditLogPageRequest; -import com.sjmresorts.opsapp.logging.mappers.AuditLogMapper; -import com.sjmresorts.opsapp.logging.models.AuditLog; -import com.sjmresorts.opsapp.logging.repositories.AuditLogRepository; -import jakarta.persistence.criteria.Predicate; -import jakarta.validation.constraints.NotBlank; -import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.data.jpa.domain.Specification; -import org.springframework.scheduling.annotation.Async; -import org.springframework.stereotype.Service; - -import java.time.Instant; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; - - -@Service -@RequiredArgsConstructor -public class AuditLogService { - private final Logger logger = LoggerFactory.getLogger(AuditLogService.class); - private final AuditLogRepository auditLogRepository; - - private final AuditLogMapper auditLogMapper; - - @Async - public void saveAuditLog(AuditLog auditLog) { - try { - if(auditLog.getAction()==null){ - auditLog.setAction(""); - } - if(auditLog.getActionTime()==null) - auditLog.setActionTime(Instant.now()); - auditLogRepository.save(auditLog); - } catch (Exception e) { - logger.error("Failed to save audit log to database", e); - } - } - - public AuditLog createAuditLog(String action, String detail, LoginUserInfo loginUser ){ - AuditLog auditLog = new AuditLog(); - auditLog.setAction(action==null?"":action); - auditLog.setDetails(detail); - auditLog.setUserId(loginUser==null?"":loginUser.getEmployeeId()); - auditLog.setUserName(loginUser==null?"":loginUser.getUserName()); - return auditLog; - } - - public void setLogOldValue(AuditLog auditLog, Object oldValue){ - if(oldValue!=null){ - try { - auditLog.setOldValue(JSONObject.toJSONString(oldValue)); - }catch(Exception e){ - auditLog.setOldValue(e.getMessage()); - } - }else{ - auditLog.setOldValue(""); - } - } - - public void setLogNewValue(AuditLog auditLog, Object newValue){ - if(newValue!=null){ - try { - auditLog.setNewValue(JSONObject.toJSONString(newValue)); - }catch(Exception e){ - auditLog.setNewValue(e.getMessage()); - } - }else{ - auditLog.setNewValue(""); - } - auditLog.setActionTime(Instant.now()); - } - - public DataTableResult getAuditLogListByPage(AuditLogPageRequest pageRequest) { - Specification listSpec = buildSpecificationByPageRequest(pageRequest); - int pageSize = pageRequest.getPageSize(); - int pageStart = pageRequest.getStartRow() / pageSize; - Sort sort = DaoUtil.buildSortByString(pageRequest.getSortOrder(), "actionTime"); - Pageable pageable = PageRequest.of(pageStart, pageSize, sort); - Page resultPage = auditLogRepository.findAll(listSpec, pageable); - List resultList = auditLogMapper.toApiLogDtoList(resultPage.getContent()); - DataTableResult dataResult = new DataTableResult<>(); - dataResult.setTotalRecords(resultPage.getTotalElements()); - dataResult.setPageSize(pageSize); - if (resultList == null) { - resultList = new ArrayList<>(); - } - dataResult.setRowData(resultList); - return dataResult; - } - - private Specification buildSpecificationByPageRequest(AuditLogPageRequest pageRequest) { - return ((root, criteriaQuery, cb) -> { - List list = new ArrayList<>(); - if (StringUtils.isNotBlank(pageRequest.getSearchUsername())) { - list.add(cb.like(cb.upper(root.get("username")), - "%" + pageRequest.getSearchUsername().toUpperCase() + "%")); - } - if (StringUtils.isNotBlank(pageRequest.getSearchAction())) { - list.add(cb.like(cb.upper(root.get("action")), - "%" + pageRequest.getSearchAction().toUpperCase() + "%")); - } - if (StringUtils.isNotBlank(pageRequest.getSearchAdminUserCode())) { - list.add(cb.like(cb.upper(root.get("userId")), - "%" + pageRequest.getSearchAdminUserCode().toUpperCase() + "%")); - } - if (pageRequest.getSearchDate() != null && !pageRequest.getSearchDate().isEmpty() - && StringUtils.isNotBlank(pageRequest.getSearchDate().getFirst())) { - Date checkOperateBeginTime = DateTimeUtil.getDateFromString(pageRequest.getSearchDate().getFirst() + " 00:00:00", "yyyy-MM-dd hh:mm:ss"); - if (checkOperateBeginTime != null) { - list.add(cb.greaterThanOrEqualTo(root.get("actionTime"), checkOperateBeginTime)); - } - } - if (pageRequest.getSearchDate() != null && pageRequest.getSearchDate().size() > 1 - && StringUtils.isNotBlank(pageRequest.getSearchDate().get(1))) { - Date checkOperateEndTime = DateTimeUtil.getDateFromString(pageRequest.getSearchDate().get(1) + " 23:59:59", "yyyy-MM-dd hh:mm:ss"); - if (checkOperateEndTime != null) { - list.add(cb.lessThanOrEqualTo(root.get("actionTime"), checkOperateEndTime)); - } - } - Predicate[] predicates = new Predicate[list.size()]; - criteriaQuery.where(list.toArray(predicates)); - return cb.and(list.toArray(predicates)); - }); - } - - public AuditLogDto queryAuditLogById(@NotBlank Long auditLogId) { - AuditLog auditLog = auditLogRepository.findById(auditLogId).orElse(null); - if (auditLog == null) return null; - return auditLogMapper.toAuditLogDto(auditLog); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/logging/services/NotificationLogService.java b/src/main/java/com/sjmresorts/opsapp/logging/services/NotificationLogService.java deleted file mode 100644 index 33d2575..0000000 --- a/src/main/java/com/sjmresorts/opsapp/logging/services/NotificationLogService.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.sjmresorts.opsapp.logging.services; - -import com.sjmresorts.opsapp.logging.models.NotificationLog; -import com.sjmresorts.opsapp.logging.repositories.NotificationLogRepository; -import lombok.RequiredArgsConstructor; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.scheduling.annotation.Async; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class NotificationLogService { - private final Logger logger = LoggerFactory.getLogger(NotificationLogService.class); - private final NotificationLogRepository notificationLogRepository; - - @Async - public void saveNotificationLog(NotificationLog notificationLog) { - try { - notificationLogRepository.save(notificationLog); - } catch (Exception e) { - logger.error("Failed to save notification log to database", e); - } - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/notifications/config/FirebaseConfig.java b/src/main/java/com/sjmresorts/opsapp/notifications/config/FirebaseConfig.java deleted file mode 100644 index 4bd426d..0000000 --- a/src/main/java/com/sjmresorts/opsapp/notifications/config/FirebaseConfig.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.sjmresorts.opsapp.notifications.config; - -import com.google.api.client.http.HttpTransport; -import com.google.auth.http.HttpTransportFactory; -import com.google.auth.oauth2.GoogleCredentials; -import com.google.firebase.FirebaseApp; -import com.google.firebase.FirebaseOptions; -import com.sjmresorts.opsapp.common.config.GoogleApiProxyFactory; -import jakarta.annotation.PostConstruct; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.system.ApplicationHome; -import org.springframework.core.io.ClassPathResource; -import org.springframework.stereotype.Component; - -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; - -@Slf4j -@Component -@RequiredArgsConstructor -@Getter -public class FirebaseConfig { - - private final GoogleApiProxyFactory googleApiProxyFactory; - - @Value("${firebase.credential.path:}") - private String credentialPath; - - @Value("${firebase.dry-run:false}") - private boolean dryRun; - - private volatile boolean initialized; - - @PostConstruct - public void init() { - if (dryRun) { - log.warn("firebase.dry-run=true, FCM messages will not be sent to Firebase"); - initialized = true; - return; - } - if (StringUtils.isBlank(credentialPath)) { - log.warn("firebase.credential.path is empty, FCM push is disabled"); - return; - } - try (InputStream credentials = openCredentialStream(credentialPath)) { - if (credentials == null) { - log.warn("Firebase credential file not found: {}, FCM push is disabled", credentialPath); - return; - } - byte[] credentialBytes = credentials.readAllBytes(); - HttpTransport httpTransport = googleApiProxyFactory.createHttpTransport(); - HttpTransportFactory transportFactory = googleApiProxyFactory.createTransportFactory(); - GoogleCredentials googleCredentials = GoogleCredentials.fromStream( - new ByteArrayInputStream(credentialBytes), - transportFactory); - - FirebaseOptions.Builder optionsBuilder = FirebaseOptions.builder() - .setCredentials(googleCredentials) - .setHttpTransport(httpTransport); - FirebaseOptions options = optionsBuilder.build(); - if (FirebaseApp.getApps().isEmpty()) { - FirebaseApp.initializeApp(options); - } - initialized = true; - log.info("Firebase Admin SDK initialized (proxy enabled for Google HTTP transport)"); - } catch (IOException e) { - log.error("Failed to initialize Firebase Admin SDK", e); - } - } - - public boolean isReady() { - return initialized && (dryRun || !FirebaseApp.getApps().isEmpty()); - } - - private InputStream openCredentialStream(String path) throws IOException { - if (path.startsWith("classpath:")) { - String classpathLocation = path.substring("classpath:".length()); - ClassPathResource resource = new ClassPathResource(classpathLocation); - if (resource.exists()) { - return resource.getInputStream(); - } - return null; - } - ClassPathResource classpathResource = new ClassPathResource(path); - if (classpathResource.exists()) { - return classpathResource.getInputStream(); - } - File runtimeFile = new File(new ApplicationHome(FirebaseConfig.class).getDir(), path); - if (runtimeFile.isFile()) { - return new FileInputStream(runtimeFile); - } - File absoluteFile = new File(path); - if (absoluteFile.isFile()) { - return new FileInputStream(absoluteFile); - } - return null; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/notifications/controllers/NotificationController.java b/src/main/java/com/sjmresorts/opsapp/notifications/controllers/NotificationController.java deleted file mode 100644 index 3a0f75c..0000000 --- a/src/main/java/com/sjmresorts/opsapp/notifications/controllers/NotificationController.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.sjmresorts.opsapp.notifications.controllers; - -import com.sjmresorts.opsapp.authentication.dtos.LoginUserInfo; -import com.sjmresorts.opsapp.common.utils.JwtTokenUtil; -import com.sjmresorts.opsapp.notifications.dtos.SendNotificationRequest; -import com.sjmresorts.opsapp.notifications.dtos.SendNotificationResponse; -import com.sjmresorts.opsapp.notifications.services.NotificationService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.annotation.Resource; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.validation.Valid; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.CrossOrigin; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@Tag(name = "Notifications", description = "FCM push notification APIs") -@RestController -@RequestMapping("/api/v1/notifications") -@CrossOrigin(origins = "*") -public class NotificationController { - - @Resource - private NotificationService notificationService; - - @Operation( - summary = "Send FCM notification", - description = "Send push notification to employees, devices, tokens, or a topic. Auth temporarily disabled for integration testing." - ) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "All targets delivered"), - @ApiResponse(responseCode = "207", description = "Partial delivery") - }) - @PostMapping(value = "/send", produces = "application/json;charset=UTF-8") - public ResponseEntity sendNotification( - HttpServletRequest request, - @Parameter(description = "Notification send request", required = true) - @Valid @RequestBody SendNotificationRequest requestData) { - - LoginUserInfo loginUser = JwtTokenUtil.parseUserFromRequest(request); - String requesterId = "anonymous"; - if (loginUser != null) { - requesterId = loginUser.getEmployeeId() != null - ? loginUser.getEmployeeId() - : loginUser.getUserCode(); - } - SendNotificationResponse response = notificationService.send(requestData.getData(), requesterId); - - HttpStatus status = switch (response.getCode()) { - case "200" -> HttpStatus.OK; - case "207" -> HttpStatus.MULTI_STATUS; - default -> HttpStatus.BAD_REQUEST; - }; - return ResponseEntity.status(status).body(response); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/notifications/dtos/BatchFcmSendResult.java b/src/main/java/com/sjmresorts/opsapp/notifications/dtos/BatchFcmSendResult.java deleted file mode 100644 index e1117d6..0000000 --- a/src/main/java/com/sjmresorts/opsapp/notifications/dtos/BatchFcmSendResult.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.sjmresorts.opsapp.notifications.dtos; - -import lombok.Builder; -import lombok.Getter; - -import java.util.List; - -@Getter -@Builder -public class BatchFcmSendResult { - - private final int successCount; - private final int failureCount; - private final List results; -} diff --git a/src/main/java/com/sjmresorts/opsapp/notifications/dtos/FcmSendResult.java b/src/main/java/com/sjmresorts/opsapp/notifications/dtos/FcmSendResult.java deleted file mode 100644 index 82d463f..0000000 --- a/src/main/java/com/sjmresorts/opsapp/notifications/dtos/FcmSendResult.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.sjmresorts.opsapp.notifications.dtos; - -import lombok.Builder; -import lombok.Getter; - -@Getter -@Builder -public class FcmSendResult { - - private final boolean success; - private final String messageId; - private final String token; - private final String errorCode; - private final String errorMessage; - private final boolean tokenInvalid; -} diff --git a/src/main/java/com/sjmresorts/opsapp/notifications/dtos/NotificationFailureDto.java b/src/main/java/com/sjmresorts/opsapp/notifications/dtos/NotificationFailureDto.java deleted file mode 100644 index de9daf9..0000000 --- a/src/main/java/com/sjmresorts/opsapp/notifications/dtos/NotificationFailureDto.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.sjmresorts.opsapp.notifications.dtos; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@AllArgsConstructor -@Schema(description = "Per-target send failure detail") -public class NotificationFailureDto { - - @Schema(description = "Target identifier (employeeId, deviceId, or token prefix)") - private String target; - - @Schema(description = "Failure reason code", example = "NO_DEVICE_TOKEN") - private String reason; - - @Schema(description = "Firebase or internal error detail for debugging") - private String detail; - - public NotificationFailureDto(String target, String reason) { - this.target = target; - this.reason = reason; - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/notifications/dtos/NotificationPayloadDto.java b/src/main/java/com/sjmresorts/opsapp/notifications/dtos/NotificationPayloadDto.java deleted file mode 100644 index 127bdd0..0000000 --- a/src/main/java/com/sjmresorts/opsapp/notifications/dtos/NotificationPayloadDto.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.sjmresorts.opsapp.notifications.dtos; - -import lombok.Builder; -import lombok.Getter; - -import java.util.Map; - -@Getter -@Builder -public class NotificationPayloadDto { - - private final String title; - private final String body; - private final String imageUrl; - private final String clickAction; - private final Map data; -} diff --git a/src/main/java/com/sjmresorts/opsapp/notifications/dtos/SendNotificationData.java b/src/main/java/com/sjmresorts/opsapp/notifications/dtos/SendNotificationData.java deleted file mode 100644 index 4742abc..0000000 --- a/src/main/java/com/sjmresorts/opsapp/notifications/dtos/SendNotificationData.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.sjmresorts.opsapp.notifications.dtos; - -import com.sjmresorts.opsapp.notifications.enums.NotificationTargetType; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotEmpty; -import jakarta.validation.constraints.NotNull; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.List; -import java.util.Map; - -@Data -@NoArgsConstructor -@Schema(description = "FCM notification send payload") -public class SendNotificationData { - - @NotNull(message = "targetType must not be null") - @Schema(description = "Recipient resolution type: EMPLOYEE | DEVICE | TOKEN | TOPIC | ESIGN_USER", example = "EMPLOYEE") - private NotificationTargetType targetType; - - @NotEmpty(message = "targets must not be empty") - @Schema(description = "Employee IDs, device IDs, FCM tokens, or topic name") - private List targets; - - @NotBlank(message = "title must not be blank") - @Schema(description = "Notification title") - private String title; - - @NotBlank(message = "body must not be blank") - @Schema(description = "Notification body") - private String body; - - @Schema(description = "Business trigger code for audit log", example = "PIT_ALERT") - private String triggerTask; - - @Schema(description = "Custom data passed to the mobile client (string values only)") - private Map data; - - @Schema(description = "Optional large image URL") - private String imageUrl; - - @Schema(description = "Optional click action identifier") - private String clickAction; -} diff --git a/src/main/java/com/sjmresorts/opsapp/notifications/dtos/SendNotificationRequest.java b/src/main/java/com/sjmresorts/opsapp/notifications/dtos/SendNotificationRequest.java deleted file mode 100644 index fbd9356..0000000 --- a/src/main/java/com/sjmresorts/opsapp/notifications/dtos/SendNotificationRequest.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.sjmresorts.opsapp.notifications.dtos; - -import com.sjmresorts.opsapp.common.dtos.SchemaEntry; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.Valid; -import jakarta.validation.constraints.NotNull; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@Schema(description = "Request to send FCM notification") -public class SendNotificationRequest { - - @Schema(description = "Optional schema metadata from caller") - private SchemaEntry schema; - - @Valid - @NotNull(message = "data must not be null") - @Schema(description = "Notification send parameters") - private SendNotificationData data; -} diff --git a/src/main/java/com/sjmresorts/opsapp/notifications/dtos/SendNotificationResponse.java b/src/main/java/com/sjmresorts/opsapp/notifications/dtos/SendNotificationResponse.java deleted file mode 100644 index 89979fc..0000000 --- a/src/main/java/com/sjmresorts/opsapp/notifications/dtos/SendNotificationResponse.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.sjmresorts.opsapp.notifications.dtos; - -import com.sjmresorts.opsapp.common.dtos.BaseApiResponse; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@EqualsAndHashCode(callSuper = true) -@Schema(description = "Response of FCM notification send API") -public class SendNotificationResponse extends BaseApiResponse { - - @Schema(description = "Send result details") - private SendNotificationResultData data; -} diff --git a/src/main/java/com/sjmresorts/opsapp/notifications/dtos/SendNotificationResultData.java b/src/main/java/com/sjmresorts/opsapp/notifications/dtos/SendNotificationResultData.java deleted file mode 100644 index 3fc1fdb..0000000 --- a/src/main/java/com/sjmresorts/opsapp/notifications/dtos/SendNotificationResultData.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.sjmresorts.opsapp.notifications.dtos; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.ArrayList; -import java.util.List; - -@Data -@NoArgsConstructor -@Schema(description = "FCM send result summary") -public class SendNotificationResultData { - - @Schema(description = "Total targets in request") - private int totalTargets; - - @Schema(description = "Successfully delivered count") - private int successCount; - - @Schema(description = "Failed delivery count") - private int failureCount; - - @Schema(description = "Overall result status", example = "SUCCESS") - private String resultStatus; - - @Schema(description = "Per-target failures") - private List failures = new ArrayList<>(); -} diff --git a/src/main/java/com/sjmresorts/opsapp/notifications/enums/NotificationFailureReason.java b/src/main/java/com/sjmresorts/opsapp/notifications/enums/NotificationFailureReason.java deleted file mode 100644 index 6d4558e..0000000 --- a/src/main/java/com/sjmresorts/opsapp/notifications/enums/NotificationFailureReason.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.sjmresorts.opsapp.notifications.enums; - -public enum NotificationFailureReason { - NO_DEVICE_TOKEN, - USER_NOT_FOUND, - TOKEN_INVALID, - FCM_ERROR, - NETWORK_ERROR, - FCM_NOT_INITIALIZED -} diff --git a/src/main/java/com/sjmresorts/opsapp/notifications/enums/NotificationResultStatus.java b/src/main/java/com/sjmresorts/opsapp/notifications/enums/NotificationResultStatus.java deleted file mode 100644 index 894a83d..0000000 --- a/src/main/java/com/sjmresorts/opsapp/notifications/enums/NotificationResultStatus.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.sjmresorts.opsapp.notifications.enums; - -public enum NotificationResultStatus { - SUCCESS, - PARTIAL, - FAILED -} diff --git a/src/main/java/com/sjmresorts/opsapp/notifications/enums/NotificationTargetType.java b/src/main/java/com/sjmresorts/opsapp/notifications/enums/NotificationTargetType.java deleted file mode 100644 index 742a6d6..0000000 --- a/src/main/java/com/sjmresorts/opsapp/notifications/enums/NotificationTargetType.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.sjmresorts.opsapp.notifications.enums; - -public enum NotificationTargetType { - EMPLOYEE, - DEVICE, - TOKEN, - TOPIC, - /** Esign 用户 ID(ESIGN_USER.id,与登录 JWT employeeId 一致) */ - ESIGN_USER -} diff --git a/src/main/java/com/sjmresorts/opsapp/notifications/resolvers/RecipientResolver.java b/src/main/java/com/sjmresorts/opsapp/notifications/resolvers/RecipientResolver.java deleted file mode 100644 index 85446ea..0000000 --- a/src/main/java/com/sjmresorts/opsapp/notifications/resolvers/RecipientResolver.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.sjmresorts.opsapp.notifications.resolvers; - -import com.sjmresorts.opsapp.authentication.models.User; -import com.sjmresorts.opsapp.authentication.repositories.UserRepository; -import com.sjmresorts.opsapp.esign.auth.models.EsignUser; -import com.sjmresorts.opsapp.esign.auth.repositories.EsignUserRepository; -import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.StringUtils; -import org.springframework.stereotype.Component; - -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; - -@Component -@RequiredArgsConstructor -public class RecipientResolver { - - private final UserRepository userRepository; - private final EsignUserRepository esignUserRepository; - - public List resolveByEmployeeIds(List employeeIds) { - if (employeeIds == null || employeeIds.isEmpty()) { - return List.of(); - } - List normalizedIds = employeeIds.stream() - .filter(StringUtils::isNotBlank) - .map(String::trim) - .distinct() - .toList(); - if (normalizedIds.isEmpty()) { - return List.of(); - } - Map byEmployeeId = new LinkedHashMap<>(); - for (User user : userRepository.findByEmployeeIdIn(normalizedIds)) { - if (user != null && StringUtils.isNotBlank(user.getEmployeeId())) { - byEmployeeId.putIfAbsent(user.getEmployeeId(), user); - } - } - for (String employeeId : normalizedIds) { - userRepository.findByEmployeeId(employeeId).ifPresent(user -> - byEmployeeId.putIfAbsent(employeeId, user)); - } - return new ArrayList<>(byEmployeeId.values()); - } - - public Optional resolveByDeviceId(String deviceId) { - if (StringUtils.isBlank(deviceId)) { - return Optional.empty(); - } - return userRepository.findByDeviceId(deviceId.trim()); - } - - public Optional resolveByToken(String token) { - if (StringUtils.isBlank(token)) { - return Optional.empty(); - } - return userRepository.findByDeviceToken(token.trim()); - } - - public List resolveEsignByIds(List userIds) { - if (userIds == null || userIds.isEmpty()) { - return List.of(); - } - List ids = new ArrayList<>(); - for (String raw : userIds) { - if (StringUtils.isBlank(raw)) { - continue; - } - try { - ids.add(Long.parseLong(raw.trim())); - } catch (NumberFormatException ignored) { - // skip invalid id - } - } - if (ids.isEmpty()) { - return List.of(); - } - return esignUserRepository.findByIdIn(ids); - } - - public Optional resolveEsignByDeviceToken(String token) { - if (StringUtils.isBlank(token)) { - return Optional.empty(); - } - return esignUserRepository.findByDeviceToken(token.trim()); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/notifications/services/FcmPushService.java b/src/main/java/com/sjmresorts/opsapp/notifications/services/FcmPushService.java deleted file mode 100644 index 8d9040e..0000000 --- a/src/main/java/com/sjmresorts/opsapp/notifications/services/FcmPushService.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.sjmresorts.opsapp.notifications.services; - -import com.sjmresorts.opsapp.notifications.dtos.BatchFcmSendResult; -import com.sjmresorts.opsapp.notifications.dtos.FcmSendResult; -import com.sjmresorts.opsapp.notifications.dtos.NotificationPayloadDto; - -import java.util.List; - -public interface FcmPushService { - - FcmSendResult sendToToken(String token, NotificationPayloadDto payload); - - BatchFcmSendResult sendToTokens(List tokens, NotificationPayloadDto payload); - - FcmSendResult sendToTopic(String topic, NotificationPayloadDto payload); -} diff --git a/src/main/java/com/sjmresorts/opsapp/notifications/services/NotificationService.java b/src/main/java/com/sjmresorts/opsapp/notifications/services/NotificationService.java deleted file mode 100644 index eb9fa3e..0000000 --- a/src/main/java/com/sjmresorts/opsapp/notifications/services/NotificationService.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.sjmresorts.opsapp.notifications.services; - -import com.sjmresorts.opsapp.notifications.dtos.SendNotificationData; -import com.sjmresorts.opsapp.notifications.dtos.SendNotificationResponse; - -public interface NotificationService { - - SendNotificationResponse send(SendNotificationData data, String requesterId); -} diff --git a/src/main/java/com/sjmresorts/opsapp/notifications/services/impl/FcmPushServiceImpl.java b/src/main/java/com/sjmresorts/opsapp/notifications/services/impl/FcmPushServiceImpl.java deleted file mode 100644 index 01903f6..0000000 --- a/src/main/java/com/sjmresorts/opsapp/notifications/services/impl/FcmPushServiceImpl.java +++ /dev/null @@ -1,283 +0,0 @@ -package com.sjmresorts.opsapp.notifications.services.impl; - -import com.google.firebase.messaging.BatchResponse; -import com.google.firebase.messaging.FirebaseMessaging; -import com.google.firebase.messaging.FirebaseMessagingException; -import com.google.firebase.messaging.AndroidConfig; -import com.google.firebase.messaging.AndroidNotification; -import com.google.firebase.messaging.Message; -import com.google.firebase.messaging.MessagingErrorCode; -import com.google.firebase.messaging.MulticastMessage; -import com.google.firebase.messaging.Notification; -import com.google.firebase.messaging.SendResponse; -import com.sjmresorts.opsapp.notifications.config.FirebaseConfig; -import com.sjmresorts.opsapp.notifications.dtos.BatchFcmSendResult; -import com.sjmresorts.opsapp.notifications.dtos.FcmSendResult; -import com.sjmresorts.opsapp.notifications.dtos.NotificationPayloadDto; -import com.sjmresorts.opsapp.notifications.enums.NotificationFailureReason; -import com.sjmresorts.opsapp.notifications.services.FcmPushService; -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.Service; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -@Slf4j -@Service -@RequiredArgsConstructor -public class FcmPushServiceImpl implements FcmPushService { - - private static final int FCM_MULTICAST_LIMIT = 500; - /** 与 aps-fcm aar 中 @string/dcloud_feature_aps_notification_channel 默认值一致 */ - private static final String ANDROID_NOTIFICATION_CHANNEL_ID = "unipush"; - - private final FirebaseConfig firebaseConfig; - - @Value("${ops-app.notification.batch-size:500}") - private int batchSize; - - /** - * false(默认):带 notification,后台/桌面系统通知栏可收到。 - * true:仅 data 透传,利于 DCloud 前台 plus.push receive,但后台往往不再自动弹系统通知。 - */ - @Value("${ops-app.notification.android-data-only:false}") - private boolean androidDataOnly; - - @Override - public FcmSendResult sendToToken(String token, NotificationPayloadDto payload) { - if (!firebaseConfig.isReady()) { - return notInitializedResult(token); - } - if (firebaseConfig.isDryRun()) { - return dryRunResult(token); - } - try { - String messageId = FirebaseMessaging.getInstance().send(buildMessage(token, null, payload)); - return FcmSendResult.builder() - .success(true) - .messageId(messageId) - .token(token) - .build(); - } catch (FirebaseMessagingException e) { - return failedResult(token, e); - } - } - - @Override - public BatchFcmSendResult sendToTokens(List tokens, NotificationPayloadDto payload) { - List validTokens = tokens == null ? List.of() : tokens.stream() - .filter(StringUtils::isNotBlank) - .map(String::trim) - .distinct() - .toList(); - if (validTokens.isEmpty()) { - return BatchFcmSendResult.builder() - .successCount(0) - .failureCount(0) - .results(List.of()) - .build(); - } - if (!firebaseConfig.isReady()) { - List results = validTokens.stream() - .map(this::notInitializedResult) - .toList(); - return summarize(results); - } - if (firebaseConfig.isDryRun()) { - List results = validTokens.stream() - .map(this::dryRunResult) - .toList(); - return summarize(results); - } - - int chunkSize = Math.min(Math.max(batchSize, 1), FCM_MULTICAST_LIMIT); - List allResults = new ArrayList<>(); - for (int index = 0; index < validTokens.size(); index += chunkSize) { - List chunk = validTokens.subList(index, Math.min(index + chunkSize, validTokens.size())); - allResults.addAll(sendChunk(chunk, payload)); - } - return summarize(allResults); - } - - @Override - public FcmSendResult sendToTopic(String topic, NotificationPayloadDto payload) { - if (StringUtils.isBlank(topic)) { - return FcmSendResult.builder() - .success(false) - .token(topic) - .errorCode(NotificationFailureReason.FCM_ERROR.name()) - .errorMessage("Topic must not be blank") - .build(); - } - if (!firebaseConfig.isReady()) { - return notInitializedResult(topic); - } - if (firebaseConfig.isDryRun()) { - return dryRunResult(topic); - } - try { - String messageId = FirebaseMessaging.getInstance() - .send(buildMessage(null, topic.trim(), payload)); - return FcmSendResult.builder() - .success(true) - .messageId(messageId) - .token(topic) - .build(); - } catch (FirebaseMessagingException e) { - return failedResult(topic, e); - } - } - - private List sendChunk(List tokens, NotificationPayloadDto payload) { - if (tokens.size() == 1) { - return List.of(sendToToken(tokens.get(0), payload)); - } - try { - MulticastMessage.Builder builder = MulticastMessage.builder() - .addAllTokens(tokens) - .putAllData(normalizeData(payload)) - .setAndroidConfig(buildAndroidConfig(payload)); - if (!androidDataOnly) { - builder.setNotification(buildNotification(payload)); - } - BatchResponse batchResponse = FirebaseMessaging.getInstance().sendEachForMulticast(builder.build()); - List results = new ArrayList<>(); - List responses = batchResponse.getResponses(); - for (int i = 0; i < tokens.size(); i++) { - String token = tokens.get(i); - SendResponse response = i < responses.size() ? responses.get(i) : null; - if (response != null && response.isSuccessful()) { - results.add(FcmSendResult.builder() - .success(true) - .messageId(response.getMessageId()) - .token(token) - .build()); - } else { - FirebaseMessagingException exception = response == null ? null : response.getException(); - if (exception != null) { - results.add(failedResult(token, exception)); - } else { - results.add(FcmSendResult.builder() - .success(false) - .token(token) - .errorCode(NotificationFailureReason.FCM_ERROR.name()) - .errorMessage("Unknown FCM batch failure") - .build()); - } - } - } - return results; - } catch (FirebaseMessagingException e) { - log.error("FCM multicast send failed", e); - return tokens.stream() - .map(token -> failedResult(token, e)) - .toList(); - } - } - - private Message buildMessage(String token, String topic, NotificationPayloadDto payload) { - Message.Builder builder = Message.builder() - .putAllData(normalizeData(payload)) - .setAndroidConfig(buildAndroidConfig(payload)); - if (!androidDataOnly) { - builder.setNotification(buildNotification(payload)); - } - if (StringUtils.isNotBlank(token)) { - builder.setToken(token); - } - if (StringUtils.isNotBlank(topic)) { - builder.setTopic(topic); - } - return builder.build(); - } - - private AndroidConfig buildAndroidConfig(NotificationPayloadDto payload) { - AndroidConfig.Builder builder = AndroidConfig.builder() - .setPriority(AndroidConfig.Priority.HIGH); - if (!androidDataOnly && StringUtils.isNotBlank(payload.getTitle())) { - builder.setNotification(AndroidNotification.builder() - .setTitle(payload.getTitle()) - .setBody(StringUtils.defaultString(payload.getBody())) - .setChannelId(ANDROID_NOTIFICATION_CHANNEL_ID) - .build()); - } - return builder.build(); - } - - private Notification buildNotification(NotificationPayloadDto payload) { - Notification.Builder builder = Notification.builder() - .setTitle(payload.getTitle()) - .setBody(payload.getBody()); - if (StringUtils.isNotBlank(payload.getImageUrl())) { - builder.setImage(payload.getImageUrl()); - } - return builder.build(); - } - - private Map normalizeData(NotificationPayloadDto payload) { - Map normalized = new HashMap<>(); - if (payload.getData() != null) { - payload.getData().forEach((key, value) -> { - if (StringUtils.isNotBlank(key) && value != null) { - normalized.put(key, value); - } - }); - } - // 前台 onMessage 主要读 data;把 title/body 写入 data 便于 App 内展示 - if (StringUtils.isNotBlank(payload.getTitle())) { - normalized.putIfAbsent("title", payload.getTitle()); - } - if (StringUtils.isNotBlank(payload.getBody())) { - normalized.putIfAbsent("body", payload.getBody()); - normalized.putIfAbsent("content", payload.getBody()); - } - if (StringUtils.isNotBlank(payload.getClickAction())) { - normalized.put("clickAction", payload.getClickAction()); - } - return normalized; - } - - private FcmSendResult failedResult(String token, FirebaseMessagingException exception) { - MessagingErrorCode errorCode = exception.getMessagingErrorCode(); - boolean tokenInvalid = errorCode == MessagingErrorCode.UNREGISTERED - || errorCode == MessagingErrorCode.INVALID_ARGUMENT; - return FcmSendResult.builder() - .success(false) - .token(token) - .errorCode(errorCode != null ? errorCode.name() : NotificationFailureReason.FCM_ERROR.name()) - .errorMessage(exception.getMessage()) - .tokenInvalid(tokenInvalid) - .build(); - } - - private FcmSendResult notInitializedResult(String token) { - return FcmSendResult.builder() - .success(false) - .token(token) - .errorCode(NotificationFailureReason.FCM_NOT_INITIALIZED.name()) - .errorMessage("Firebase is not initialized") - .build(); - } - - private FcmSendResult dryRunResult(String token) { - return FcmSendResult.builder() - .success(true) - .messageId("dry-run") - .token(token) - .build(); - } - - private BatchFcmSendResult summarize(List results) { - int successCount = (int) results.stream().filter(FcmSendResult::isSuccess).count(); - return BatchFcmSendResult.builder() - .successCount(successCount) - .failureCount(results.size() - successCount) - .results(results) - .build(); - } -} diff --git a/src/main/java/com/sjmresorts/opsapp/notifications/services/impl/NotificationServiceImpl.java b/src/main/java/com/sjmresorts/opsapp/notifications/services/impl/NotificationServiceImpl.java deleted file mode 100644 index 52358cf..0000000 --- a/src/main/java/com/sjmresorts/opsapp/notifications/services/impl/NotificationServiceImpl.java +++ /dev/null @@ -1,412 +0,0 @@ -package com.sjmresorts.opsapp.notifications.services.impl; - -import com.alibaba.fastjson2.JSON; -import com.sjmresorts.opsapp.authentication.models.User; -import com.sjmresorts.opsapp.authentication.repositories.UserRepository; -import com.sjmresorts.opsapp.esign.auth.models.EsignUser; -import com.sjmresorts.opsapp.esign.auth.repositories.EsignUserRepository; -import com.sjmresorts.opsapp.common.constants.AppConstants; -import com.sjmresorts.opsapp.logging.models.NotificationLog; -import com.sjmresorts.opsapp.logging.services.NotificationLogService; -import com.sjmresorts.opsapp.notifications.dtos.BatchFcmSendResult; -import com.sjmresorts.opsapp.notifications.dtos.FcmSendResult; -import com.sjmresorts.opsapp.notifications.dtos.NotificationFailureDto; -import com.sjmresorts.opsapp.notifications.dtos.NotificationPayloadDto; -import com.sjmresorts.opsapp.notifications.dtos.SendNotificationData; -import com.sjmresorts.opsapp.notifications.dtos.SendNotificationResponse; -import com.sjmresorts.opsapp.notifications.dtos.SendNotificationResultData; -import com.sjmresorts.opsapp.notifications.enums.NotificationFailureReason; -import com.sjmresorts.opsapp.notifications.enums.NotificationResultStatus; -import com.sjmresorts.opsapp.notifications.enums.NotificationTargetType; -import com.sjmresorts.opsapp.notifications.resolvers.RecipientResolver; -import com.sjmresorts.opsapp.notifications.services.FcmPushService; -import com.sjmresorts.opsapp.notifications.services.NotificationService; -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.Service; - -import java.time.Instant; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; - -@Slf4j -@Service -@RequiredArgsConstructor -public class NotificationServiceImpl implements NotificationService { - - private final FcmPushService fcmPushService; - private final RecipientResolver recipientResolver; - private final UserRepository userRepository; - private final EsignUserRepository esignUserRepository; - private final NotificationLogService notificationLogService; - - @Value("${ops-app.notification.invalid-token-cleanup:true}") - private boolean invalidTokenCleanup; - - @Override - public SendNotificationResponse send(SendNotificationData data, String requesterId) { - Instant requestedAt = Instant.now(); - NotificationLog auditLog = buildAuditLog(data, requesterId, requestedAt); - - SendNotificationResultData resultData = new SendNotificationResultData(); - List failures = new ArrayList<>(); - int successCount = 0; - - List targets = normalizeTargets(data.getTargets()); - resultData.setTotalTargets(targets.size()); - - if (targets.isEmpty()) { - resultData.setSuccessCount(0); - resultData.setFailureCount(0); - return buildResponse(resultData, failures, auditLog, - NotificationResultStatus.FAILED, "targets must not be empty"); - } - - NotificationPayloadDto payload = buildPayload(data); - - if (data.getTargetType() == NotificationTargetType.TOPIC) { - return sendToTopic(data, targets, payload, resultData, failures, auditLog, requestedAt); - } - - Map tokenRecipients = new LinkedHashMap<>(); - resolveRecipients(data.getTargetType(), targets, failures, tokenRecipients); - - if (tokenRecipients.isEmpty()) { - resultData.setSuccessCount(0); - resultData.setFailureCount(failures.size()); - return buildResponse(resultData, failures, auditLog, - NotificationResultStatus.FAILED, - "No valid FCM tokens to send"); - } - - BatchFcmSendResult batchResult = fcmPushService.sendToTokens( - new ArrayList<>(tokenRecipients.keySet()), - payload); - - for (FcmSendResult sendResult : batchResult.getResults()) { - ResolvedRecipient recipient = tokenRecipients.get(sendResult.getToken()); - String targetKey = recipient == null ? maskToken(sendResult.getToken()) : recipient.targetKey(); - if (sendResult.isSuccess()) { - successCount++; - } else { - failures.add(new NotificationFailureDto( - targetKey, - mapFailureReason(sendResult), - formatFailureDetail(sendResult))); - if (invalidTokenCleanup && sendResult.isTokenInvalid()) { - cleanupInvalidToken(sendResult.getToken(), recipient); - } - } - } - - resultData.setSuccessCount(successCount); - resultData.setFailureCount(failures.size()); - resultData.setFailures(failures); - - NotificationResultStatus status = resolveStatus(successCount, failures.size(), targets.size()); - String message = status == NotificationResultStatus.SUCCESS - ? "Notification sent" - : "Notification sent with failures"; - - return buildResponse(resultData, failures, auditLog, status, message); - } - - private SendNotificationResponse sendToTopic( - SendNotificationData data, - List targets, - NotificationPayloadDto payload, - SendNotificationResultData resultData, - List failures, - NotificationLog auditLog, - Instant requestedAt) { - - String topic = targets.get(0); - if (targets.size() > 1) { - log.warn("TOPIC send uses first target only, ignored {} extra targets", targets.size() - 1); - } - - FcmSendResult sendResult = fcmPushService.sendToTopic(topic, payload); - if (sendResult.isSuccess()) { - resultData.setSuccessCount(1); - resultData.setFailureCount(0); - return buildResponse(resultData, failures, auditLog, - NotificationResultStatus.SUCCESS, "Notification sent to topic"); - } - - failures.add(new NotificationFailureDto(topic, mapFailureReason(sendResult), formatFailureDetail(sendResult))); - resultData.setSuccessCount(0); - resultData.setFailureCount(1); - return buildResponse(resultData, failures, auditLog, - NotificationResultStatus.FAILED, "Failed to send topic notification"); - } - - private void resolveRecipients( - NotificationTargetType targetType, - List targets, - List failures, - Map tokenRecipients) { - - switch (targetType) { - case EMPLOYEE -> resolveEmployees(targets, failures, tokenRecipients); - case DEVICE -> resolveDevices(targets, failures, tokenRecipients); - case TOKEN -> resolveTokens(targets, failures, tokenRecipients); - case ESIGN_USER -> resolveEsignUsers(targets, failures, tokenRecipients); - default -> targets.forEach(target -> failures.add(new NotificationFailureDto( - target, NotificationFailureReason.FCM_ERROR.name()))); - } - } - - private void resolveEmployees( - List targets, - List failures, - Map tokenRecipients) { - Map usersByEmployee = new LinkedHashMap<>(); - for (User user : recipientResolver.resolveByEmployeeIds(targets)) { - usersByEmployee.put(user.getEmployeeId(), user); - } - for (String employeeId : targets) { - User user = usersByEmployee.get(employeeId); - if (user == null) { - failures.add(new NotificationFailureDto(employeeId, NotificationFailureReason.USER_NOT_FOUND.name())); - continue; - } - addTokenRecipient(employeeId, user, failures, tokenRecipients); - } - } - - private void resolveDevices( - List targets, - List failures, - Map tokenRecipients) { - for (String deviceId : targets) { - Optional userOptional = recipientResolver.resolveByDeviceId(deviceId); - if (userOptional.isEmpty()) { - failures.add(new NotificationFailureDto(deviceId, NotificationFailureReason.USER_NOT_FOUND.name())); - continue; - } - addTokenRecipient(deviceId, userOptional.get(), failures, tokenRecipients); - } - } - - private void resolveTokens( - List targets, - List failures, - Map tokenRecipients) { - for (String token : targets) { - if (StringUtils.isBlank(token)) { - failures.add(new NotificationFailureDto("", NotificationFailureReason.TOKEN_INVALID.name())); - continue; - } - String trimmedToken = token.trim(); - User user = recipientResolver.resolveByToken(trimmedToken).orElse(null); - EsignUser esignUser = recipientResolver.resolveEsignByDeviceToken(trimmedToken).orElse(null); - tokenRecipients.putIfAbsent(trimmedToken, - new ResolvedRecipient(maskToken(trimmedToken), trimmedToken, user, esignUser)); - } - } - - private void resolveEsignUsers( - List targets, - List failures, - Map tokenRecipients) { - Map byId = new LinkedHashMap<>(); - for (EsignUser user : recipientResolver.resolveEsignByIds(targets)) { - byId.put(user.getId(), user); - } - for (String target : targets) { - if (StringUtils.isBlank(target)) { - continue; - } - Long id; - try { - id = Long.parseLong(target.trim()); - } catch (NumberFormatException e) { - failures.add(new NotificationFailureDto(target, NotificationFailureReason.USER_NOT_FOUND.name())); - continue; - } - EsignUser user = byId.get(id); - if (user == null) { - failures.add(new NotificationFailureDto(target, NotificationFailureReason.USER_NOT_FOUND.name())); - continue; - } - addEsignTokenRecipient(target, user, failures, tokenRecipients); - } - } - - private void addEsignTokenRecipient( - String targetKey, - EsignUser user, - List failures, - Map tokenRecipients) { - if (StringUtils.isBlank(user.getDeviceToken())) { - failures.add(new NotificationFailureDto(targetKey, NotificationFailureReason.NO_DEVICE_TOKEN.name())); - return; - } - tokenRecipients.putIfAbsent(user.getDeviceToken(), - new ResolvedRecipient(targetKey, user.getDeviceToken(), null, user)); - } - - private void addTokenRecipient( - String targetKey, - User user, - List failures, - Map tokenRecipients) { - if (StringUtils.isBlank(user.getDeviceToken())) { - failures.add(new NotificationFailureDto(targetKey, NotificationFailureReason.NO_DEVICE_TOKEN.name())); - return; - } - tokenRecipients.putIfAbsent(user.getDeviceToken(), - new ResolvedRecipient(targetKey, user.getDeviceToken(), user, null)); - } - - private void cleanupInvalidToken(String token, ResolvedRecipient recipient) { - if (StringUtils.isBlank(token)) { - return; - } - try { - int cleared = userRepository.clearDeviceTokenByToken(token); - if (cleared == 0 && recipient != null && recipient.user() != null - && StringUtils.isNotBlank(recipient.user().getEmployeeId())) { - userRepository.clearDeviceTokenByEmployeeId(recipient.user().getEmployeeId()); - } - if (recipient != null && recipient.esignUser() != null) { - EsignUser esignUser = recipient.esignUser(); - esignUser.setDeviceToken(null); - esignUserRepository.save(esignUser); - } - } catch (Exception e) { - log.warn("Failed to clear invalid device token", e); - } - } - - private NotificationPayloadDto buildPayload(SendNotificationData data) { - Map payloadData = new HashMap<>(); - if (data.getData() != null) { - payloadData.putAll(data.getData()); - } - if (StringUtils.isNotBlank(data.getTriggerTask())) { - payloadData.put("triggerTask", data.getTriggerTask()); - } - return NotificationPayloadDto.builder() - .title(data.getTitle()) - .body(data.getBody()) - .imageUrl(data.getImageUrl()) - .clickAction(data.getClickAction()) - .data(payloadData) - .build(); - } - - private NotificationLog buildAuditLog(SendNotificationData data, String requesterId, Instant requestedAt) { - NotificationLog auditLog = new NotificationLog(); - auditLog.setNotificationType(AppConstants.FIREBASE_NOTIFICATION); - auditLog.setTriggerTask(StringUtils.defaultIfBlank(data.getTriggerTask(), requesterId)); - auditLog.setRecipients(String.join(",", normalizeTargets(data.getTargets()))); - auditLog.setSubject(data.getTitle()); - auditLog.setNotificationContent(JSON.toJSONString(data)); - auditLog.setRequestedTime(requestedAt); - return auditLog; - } - - private SendNotificationResponse buildResponse( - SendNotificationResultData resultData, - List failures, - NotificationLog auditLog, - NotificationResultStatus status, - String message) { - - resultData.setFailures(failures); - resultData.setResultStatus(status.name()); - if (resultData.getFailureCount() == 0 && !failures.isEmpty()) { - resultData.setFailureCount(failures.size()); - } - - auditLog.setFinishedTime(Instant.now()); - auditLog.setResultStatus(status.name()); - if (status == NotificationResultStatus.FAILED && !failures.isEmpty()) { - auditLog.setErrorMessage(failures.get(0).getReason()); - } else if (status == NotificationResultStatus.PARTIAL) { - auditLog.setErrorMessage("partial failures: " + failures.size()); - } - notificationLogService.saveNotificationLog(auditLog); - - SendNotificationResponse response = new SendNotificationResponse(); - response.setSuccess(status != NotificationResultStatus.FAILED); - response.setCode(switch (status) { - case SUCCESS -> "200"; - case PARTIAL -> "207"; - case FAILED -> "400"; - }); - response.setMessage(message); - response.setData(resultData); - return response; - } - - private NotificationResultStatus resolveStatus(int successCount, int failureCount, int totalTargets) { - if (successCount <= 0) { - return NotificationResultStatus.FAILED; - } - if (failureCount > 0 || successCount < totalTargets) { - return NotificationResultStatus.PARTIAL; - } - return NotificationResultStatus.SUCCESS; - } - - private String mapFailureReason(FcmSendResult sendResult) { - if (sendResult.isTokenInvalid()) { - return NotificationFailureReason.TOKEN_INVALID.name(); - } - if (NotificationFailureReason.FCM_NOT_INITIALIZED.name().equals(sendResult.getErrorCode())) { - return NotificationFailureReason.FCM_NOT_INITIALIZED.name(); - } - if ("INVALID_ARGUMENT".equals(sendResult.getErrorCode()) - || "UNREGISTERED".equals(sendResult.getErrorCode())) { - return NotificationFailureReason.TOKEN_INVALID.name(); - } - String detail = StringUtils.defaultString(sendResult.getErrorMessage()) - + " " + StringUtils.defaultString(sendResult.getErrorCode()); - if (detail.contains("Connect timed out") - || detail.contains("Timed out") - || detail.contains("getting access token")) { - return NotificationFailureReason.NETWORK_ERROR.name(); - } - return NotificationFailureReason.FCM_ERROR.name(); - } - - private String formatFailureDetail(FcmSendResult sendResult) { - if (sendResult == null) { - return null; - } - String code = sendResult.getErrorCode(); - String message = sendResult.getErrorMessage(); - if (StringUtils.isNotBlank(code) && StringUtils.isNotBlank(message)) { - return code + ": " + message; - } - return StringUtils.defaultIfBlank(message, code); - } - - private List normalizeTargets(List targets) { - if (targets == null) { - return List.of(); - } - return targets.stream() - .filter(StringUtils::isNotBlank) - .map(String::trim) - .toList(); - } - - private String maskToken(String token) { - if (StringUtils.isBlank(token) || token.length() <= 12) { - return "token"; - } - return token.substring(0, 6) + "..." + token.substring(token.length() - 4); - } - - private record ResolvedRecipient(String targetKey, String token, User user, EsignUser esignUser) { - } -} diff --git a/src/test/java/com/sjmresorts/opsapp/logging/support/RegistrationCardMetaExtractorTest.java b/src/test/java/com/sjmresorts/opsapp/logging/support/RegistrationCardMetaExtractorTest.java deleted file mode 100644 index eed2deb..0000000 --- a/src/test/java/com/sjmresorts/opsapp/logging/support/RegistrationCardMetaExtractorTest.java +++ /dev/null @@ -1,120 +0,0 @@ -package com.sjmresorts.opsapp.logging.support; - -import com.sjmresorts.opsapp.esign.printer.dtos.RegistrationCardMeta; -import com.sjmresorts.opsapp.esign.printer.dtos.RegistrationCardMetaExtractor; -import org.junit.Assert; -import org.junit.Test; - -public class RegistrationCardMetaExtractorTest { - - @Test - public void extractOperaRoomNumberFromNextLine() { - String text = - "ORACLE HOSPITALITY REGISTRATION CARD\n" - + "Guest Name / 姓名\n" - + "Mr Tsz Wai CHAN\n" - + "Conf. No. / 确认号\n" - + "3033472\n" - + "STAY INFORMATION / 住宿资讯\n" - + "Arrival / 入住 02-05-26\n" - + "Departure / 退房 31-05-26\n" - + "Room Type / 房型\n" - + "DLXK\n" - + "Room Number / 房号\n" - + "0202\n" - + "Number of Adults / 人数\n" - + "2\n" - + "Rate / 房价\n" - + "1,578.00\n"; - - RegistrationCardMeta meta = RegistrationCardMetaExtractor.extractFromText(text); - Assert.assertEquals("0202", meta.getRoomNo()); - Assert.assertEquals("3033472", meta.getConfirmationNo()); - } - - @Test - public void extractRoyalParkConfirmationNumberFromSameLineLabel() { - String text = - "ROYAL PARK HOTEL Guest Registration\n" - + "Confirmation No. 訂房編號 317462715\n" - + "Room Type 房間類別 Superior Double\n" - + "Check-In Date 入住日期 16-08-2025\n"; - - RegistrationCardMeta meta = RegistrationCardMetaExtractor.extractFromText(text); - Assert.assertEquals("317462715", meta.getConfirmationNo()); - } - - @Test - public void extractOperaRoomNumber0232WithDatesAndRate() { - String text = - "ORACLE HOSPITALITY REGISTRATION CARD\n" - + "Conf. No. / 确认号\n" - + "3033472\n" - + "Arrival / 入住 02-05-26\n" - + "Departure / 退房 03-05-26\n" - + "Room Type / 房型\n" - + "DLXK\n" - + "Room Number / 房号\n" - + "0232\n" - + "Phone / 电话\n" - + "+853 2254 1234\n" - + "Rate / 房价\n" - + "2,254.00\n"; - - RegistrationCardMeta meta = RegistrationCardMetaExtractor.extractFromText(text); - Assert.assertEquals("0232", meta.getRoomNo()); - } - - @Test - public void roomNumberLabelBeforeDatesDoesNotGrabDateDigits() { - String text = - "Room Number / 房号\n" - + "Arrival / 入住 02-05-26\n" - + "Departure / 退房 03-05-26\n" - + "0232\n"; - - RegistrationCardMeta meta = RegistrationCardMetaExtractor.extractFromText(text); - Assert.assertEquals("0232", meta.getRoomNo()); - } - - @Test - public void ignoreRateAmountWhenNoRoomLabel() { - String text = - "STAY INFORMATION\n" - + "Room Type / 房型\n" - + "DLXK\n" - + "Rate / 房价\n" - + "78.00 MOP\n" - + "Room Number / 房号\n" - + "0202\n"; - - RegistrationCardMeta meta = RegistrationCardMetaExtractor.extractFromText(text); - Assert.assertEquals("0202", meta.getRoomNo()); - } - - @Test - public void mergedWithPrefersLongerRoomOverTwoDigitNoise() { - RegistrationCardMeta textMeta = - RegistrationCardMeta.builder().roomNo("78").confirmationNo("3033472").build(); - RegistrationCardMeta ocrMeta = - RegistrationCardMeta.builder().roomNo("0202").confirmationNo("3033472").build(); - - RegistrationCardMeta merged = textMeta.mergedWith(ocrMeta); - Assert.assertEquals("0202", merged.getRoomNo()); - } - - @Test - public void mergedWithPrefersLeadingZeroRoomOverPhoneFragment() { - RegistrationCardMeta textMeta = - RegistrationCardMeta.builder() - .roomNo("2254") - .confirmationNo("3033472") - .phone("+853 2254 1234") - .build(); - RegistrationCardMeta ocrMeta = - RegistrationCardMeta.builder().roomNo("0232").confirmationNo("3033472").build(); - - RegistrationCardMeta merged = textMeta.mergedWith(ocrMeta); - Assert.assertEquals("0232", merged.getRoomNo()); - } -}