package cn.dev.exam.push.service; import cn.dev.e...
Creato il: 14 aprile 2026
Risposto usando GPT-5.4 Thinking da Chat01
Creato il: 14 aprile 2026
Risposto usando GPT-5.4 Thinking da Chat01
package cn.dev.exam.push.service;
import cn.dev.exam.push.config.HealthRoomPushProperties;
import cn.dev.exam.push.dto.GroupItem;
import cn.dev.exam.push.dto.HealthRoomPushRequest;
import cn.dev.exam.push.dto.Item;
import cn.dev.exam.push.dto.RemotePushResponse;
import cn.dev.exam.push.exception.PushValidationException;
import cn.dev.exam.push.util.SignatureUtils;
import cn.dev.exam.push.util.Sm4Utils;
import java.math.BigDecimal;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;
@Service
public class HealthRoomPushService {
textprivate static final Logger log = LoggerFactory.getLogger(HealthRoomPushService.class); private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); private final RestTemplate restTemplate; private final HealthRoomPushProperties properties; private final Sm4Utils sm4Utils; public HealthRoomPushService(RestTemplate restTemplate, HealthRoomPushProperties properties, Sm4Utils sm4Utils) { this.restTemplate = restTemplate; this.properties = properties; this.sm4Utils = sm4Utils; } public RemotePushResponse pushCheckinInfo(HealthRoomPushRequest request) { if (request == null) { throw new PushValidationException("请求体不能为空"); } request.normalize(); validateRequest(request); HealthRoomPushProperties.EndpointProperties endpoint = properties.getActiveEndpoint(); String targetUrl = buildUrl(endpoint); String timestamp = String.valueOf(Instant.now().getEpochSecond()); String signature = SignatureUtils.md5(endpoint.getAppid() + endpoint.getSecret() + timestamp); HealthRoomPushRequest outboundRequest = buildOutboundRequest(request); System.out.println(outboundRequest.getUserIdcard()); log.info("开始推送健康小屋数据 env={}, targetUrl={}, reportId={}, reportNo={}, deviceSn={}, userName={}, userIdcard={}, timestamp={}", properties.getActiveEnv(), targetUrl, request.getReportId(), request.getReportNo(), request.getDeviceSn(), maskName(request.getUserName()), maskIdCard(request.getUserIdcard()), timestamp); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.set("timestamp", timestamp); headers.set("signature", signature); HttpEntity<HealthRoomPushRequest> entity = new HttpEntity<>(outboundRequest, headers); try { ResponseEntity<RemotePushResponse> response = restTemplate.exchange( targetUrl, HttpMethod.POST, entity, RemotePushResponse.class); RemotePushResponse responseBody = response.getBody() == null ? new RemotePushResponse(500, "远端接口未返回响应体", null) : response.getBody(); log.info("远端接口响应完成 reportId={}, httpStatus={}, resultCode={}, resultMessage={}", request.getReportId(), response.getStatusCodeValue(), responseBody.getCode(), responseBody.getMessage()); return responseBody; } catch (RestClientException exception) { log.error("调用远端健康小屋接口失败 reportId={}, targetUrl={}, error={}", request.getReportId(), targetUrl, exception.getMessage(), exception); throw new IllegalStateException("调用健康小屋推送接口失败: " + exception.getMessage(), exception); } } private HealthRoomPushRequest buildOutboundRequest(HealthRoomPushRequest request) { HealthRoomPushRequest outbound = new HealthRoomPushRequest(); outbound.setReportId(request.getReportId()); outbound.setReportNo(request.getReportNo()); outbound.setReportTime(request.getReportTime()); outbound.setUploadTime(request.getUploadTime()); outbound.setUserName(sm4Utils.encrypt(request.getUserName())); outbound.setUserGender(request.getUserGender()); outbound.setUserMobile(request.getUserMobile()); outbound.setUserIdcard(sm4Utils.encrypt(request.getUserIdcard())); outbound.setUserAge(request.getUserAge()); outbound.setDeviceSn(request.getDeviceSn()); outbound.setGruopItem(copyGroupItems(request.getGruopItem())); return outbound; } private List<GroupItem> copyGroupItems(List<GroupItem> source) { List<GroupItem> target = new ArrayList<GroupItem>(); for (GroupItem groupItem : source) { GroupItem copy = new GroupItem(); copy.setGroupCode(groupItem.getGroupCode()); copy.setGroupName(groupItem.getGroupName()); List<Item> items = new ArrayList<Item>(); for (Item item : groupItem.getItemList()) { Item itemCopy = new Item(); itemCopy.setItemCode(item.getItemCode()); itemCopy.setItemName(item.getItemName()); itemCopy.setItemValType(item.getItemValType()); itemCopy.setItemValue(item.getItemValue()); itemCopy.setItemArrow(item.getItemArrow()); itemCopy.setItemUnit(item.getItemUnit()); itemCopy.setItemRange(item.getItemRange()); itemCopy.setItemSort(item.getItemSort()); itemCopy.setSuccessFlag(item.getSuccessFlag()); items.add(itemCopy); } copy.setItemList(items); target.add(copy); } return target; } private String buildUrl(HealthRoomPushProperties.EndpointProperties endpoint) { String baseUrl = endpoint.getBaseUrl() == null ? "" : endpoint.getBaseUrl().trim(); String path = endpoint.getPath() == null ? "" : endpoint.getPath().trim(); if (baseUrl.endsWith("/") && path.startsWith("/")) { return baseUrl.substring(0, baseUrl.length() - 1) + path; } if (!baseUrl.endsWith("/") && !path.startsWith("/")) { return baseUrl + "/" + path; } return baseUrl + path; } private void validateRequest(HealthRoomPushRequest request) { requireText(request.getReportId(), "reportId不能为空"); requireText(request.getReportTime(), "reportTime不能为空"); requireText(request.getUserName(), "userName不能为空"); requireText(request.getUserIdcard(), "userIdcard不能为空"); requireText(request.getDeviceSn(), "deviceSn不能为空"); validateDateTime(request.getReportTime(), "reportTime格式错误,应为yyyy-MM-dd HH:mm:ss"); if (request.getUploadTime() != null) { validateDateTime(request.getUploadTime(), "uploadTime格式错误,应为yyyy-MM-dd HH:mm:ss"); } List<GroupItem> groupItems = request.getGruopItem(); if (CollectionUtils.isEmpty(groupItems)) { throw new PushValidationException("gruopItem不能为空"); } for (int groupIndex = 0; groupIndex < groupItems.size(); groupIndex++) { GroupItem groupItem = groupItems.get(groupIndex); if (groupItem == null) { throw new PushValidationException("gruopItem[" + groupIndex + "]不能为空"); } requireText(groupItem.getGroupCode(), "gruopItem[" + groupIndex + "].groupCode不能为空"); requireText(groupItem.getGroupName(), "gruopItem[" + groupIndex + "].groupName不能为空"); if (CollectionUtils.isEmpty(groupItem.getItemList())) { throw new PushValidationException("gruopItem[" + groupIndex + "].itemList不能为空"); } for (int itemIndex = 0; itemIndex < groupItem.getItemList().size(); itemIndex++) { validateItem(groupItem.getItemList().get(itemIndex), groupIndex, itemIndex); } } } private void validateItem(Item item, int groupIndex, int itemIndex) { String itemPath = "gruopItem[" + groupIndex + "].itemList[" + itemIndex + "]"; if (item == null) { throw new PushValidationException(itemPath + "不能为空"); } requireText(item.getItemCode(), itemPath + ".itemCode不能为空"); requireText(item.getItemValType(), itemPath + ".itemValType不能为空"); requireText(item.getItemValue(), itemPath + ".itemValue不能为空"); if (item.getItemSort() == null) { throw new PushValidationException(itemPath + ".itemSort不能为空"); } String itemValType = item.getItemValType(); if (!"INT".equals(itemValType) && !"FLOAT".equals(itemValType) && !"NUMBER".equals(itemValType) && !"STRING".equals(itemValType) && !"IMAGE".equals(itemValType)) { throw new PushValidationException(itemPath + ".itemValType仅支持INT/FLOAT/NUMBER/STRING/IMAGE"); } if ("INT".equals(itemValType)) { validateInteger(item.getItemValue(), itemPath); return; } if ("FLOAT".equals(itemValType) || "NUMBER".equals(itemValType)) { validateNumber(item.getItemValue(), itemPath); return; } if ("IMAGE".equals(itemValType)) { validateImageUrl(item.getItemValue(), itemPath); } } private void validateInteger(String value, String itemPath) { if (!value.matches("[-+]?\\d+")) { throw new PushValidationException(itemPath + ".itemValue必须是整数"); } } private void validateNumber(String value, String itemPath) { try { new BigDecimal(value); } catch (NumberFormatException exception) { throw new PushValidationException(itemPath + ".itemValue必须是数字"); } } private void validateImageUrl(String value, String itemPath) { if (value.length() > 4000) { throw new PushValidationException(itemPath + ".itemValue长度不能超过4000"); } if (!value.startsWith("http://") && !value.startsWith("https://")) { throw new PushValidationException(itemPath + ".itemValue必须是图片URL地址"); } } private void validateDateTime(String value, String message) { try { LocalDateTime.parse(value, DATE_TIME_FORMATTER); } catch (DateTimeParseException exception) { throw new PushValidationException(message); } } private void requireText(String value, String message) { if (value == null || value.isEmpty()) { throw new PushValidationException(message); } } private String maskIdCard(String idCard) { if (idCard == null || idCard.length() < 8) { return idCard; } return idCard.substring(0, 3) + "***********" + idCard.substring(idCard.length() - 4); } private String maskName(String userName) { if (userName == null || userName.isEmpty()) { return userName; } if (userName.length() == 1) { return "*"; } return userName.substring(0, 1) + "**"; }
}可以优化吗
可以,而且这段代码有几处是既能优化可读性,也能降低风险的。
我先说最值得改的几项。
javaSystem.out.println(outboundRequest.getUserIdcard());
这是最明显的问题。userIdcard 属于敏感信息,即使这里是加密后的值,也不应该直接往标准输出打。
统一走日志,并且日志里只保留脱敏字段。
requireText 校验太弱,空格会漏掉你现在是:
javaprivate void requireText(String value, String message) { if (value == null || value.isEmpty()) { throw new PushValidationException(message); } }
像 " " 这种纯空格会被当成合法值。建议改成:
javaprivate void requireText(String value, String message) { if (value == null || value.trim().isEmpty()) { throw new PushValidationException(message); } }
如果项目里可以用 Spring 工具类,直接:
javaimport org.springframework.util.StringUtils; private void requireText(String value, String message) { if (!StringUtils.hasText(value)) { throw new PushValidationException(message); } }
validateItem 分支可以更清晰现在一堆 if,逻辑没错,但维护性一般。可以改成 switch,更直观:
javaprivate void validateItem(Item item, int groupIndex, int itemIndex) { String itemPath = "gruopItem[" + groupIndex + "].itemList[" + itemIndex + "]"; if (item == null) { throw new PushValidationException(itemPath + "不能为空"); } requireText(item.getItemCode(), itemPath + ".itemCode不能为空"); requireText(item.getItemValType(), itemPath + ".itemValType不能为空"); requireText(item.getItemValue(), itemPath + ".itemValue不能为空"); if (item.getItemSort() == null) { throw new PushValidationException(itemPath + ".itemSort不能为空"); } switch (item.getItemValType()) { case "INT": validateInteger(item.getItemValue(), itemPath); break; case "FLOAT": case "NUMBER": validateNumber(item.getItemValue(), itemPath); break; case "STRING": break; case "IMAGE": validateImageUrl(item.getItemValue(), itemPath); break; default: throw new PushValidationException(itemPath + ".itemValType仅支持INT/FLOAT/NUMBER/STRING/IMAGE"); } }
buildOutboundRequest 和 copyGroupItems 有重复样板代码这块不是性能问题,主要是代码太长,后续字段一多很容易漏改。
这个比较稳,适合现有项目风格。
javaprivate HealthRoomPushRequest buildOutboundRequest(HealthRoomPushRequest request) { HealthRoomPushRequest outbound = new HealthRoomPushRequest(); outbound.setReportId(request.getReportId()); outbound.setReportNo(request.getReportNo()); outbound.setReportTime(request.getReportTime()); outbound.setUploadTime(request.getUploadTime()); outbound.setUserName(encryptIfPresent(request.getUserName())); outbound.setUserGender(request.getUserGender()); outbound.setUserMobile(request.getUserMobile()); outbound.setUserIdcard(encryptIfPresent(request.getUserIdcard())); outbound.setUserAge(request.getUserAge()); outbound.setDeviceSn(request.getDeviceSn()); outbound.setGruopItem(copyGroupItems(request.getGruopItem())); return outbound; } private String encryptIfPresent(String value) { return value == null ? null : sm4Utils.encrypt(value); }
DTO 映射会更整洁,但这属于工程层面的优化,不一定要立刻上。
当前实现基本可用,但如果 baseUrl 为空,结果可能不太符合预期。可以更明确一些:
javaprivate String buildUrl(HealthRoomPushProperties.EndpointProperties endpoint) { String baseUrl = endpoint.getBaseUrl() == null ? "" : endpoint.getBaseUrl().trim(); String path = endpoint.getPath() == null ? "" : endpoint.getPath().trim(); if (baseUrl.isEmpty()) { return path; } if (path.isEmpty()) { return baseUrl; } if (baseUrl.endsWith("/") && path.startsWith("/")) { return baseUrl.substring(0, baseUrl.length() - 1) + path; } if (!baseUrl.endsWith("/") && !path.startsWith("/")) { return baseUrl + "/" + path; } return baseUrl + path; }
RestTemplate 错误处理可以更细现在你只 catch 了 RestClientException:
java} catch (RestClientException exception) { ... throw new IllegalStateException("调用健康小屋推送接口失败: " + exception.getMessage(), exception); }
实际项目里更建议区分:
例如:
javaimport org.springframework.web.client.HttpStatusCodeException; try { ResponseEntity<RemotePushResponse> response = restTemplate.exchange( targetUrl, HttpMethod.POST, entity, RemotePushResponse.class); RemotePushResponse body = response.getBody(); return body == null ? new RemotePushResponse(500, "远端接口未返回响应体", null) : body; } catch (HttpStatusCodeException e) { log.error("远端接口返回异常状态 reportId={}, status={}, body={}", request.getReportId(), e.getStatusCode().value(), e.getResponseBodyAsString(), e); throw new IllegalStateException("远端接口返回异常状态: " + e.getStatusCode().value(), e); } catch (RestClientException e) { log.error("调用远端健康小屋接口失败 reportId={}, targetUrl={}, error={}", request.getReportId(), targetUrl, e.getMessage(), e); throw new IllegalStateException("调用健康小屋推送接口失败: " + e.getMessage(), e); }
这样排查问题会容易很多。
gruopItem 拼写错误你代码里一直是 gruopItem,应该是 groupItem。
如果这是对接外部接口字段名,DTO 可以保留 JSON 字段不变,但 Java 属性最好改正,比如:
java@JsonProperty("gruopItem") private List<GroupItem> groupItem;
这样 Java 代码层面就不会一直带错别字。
你现在:
javaprivate String maskName(String userName) { if (userName == null || userName.isEmpty()) { return userName; } if (userName.length() == 1) { return "*"; } return userName.substring(0, 1) + "**"; }
两字、三字、四字姓名都变成固定 张**,问题不大,但可读性一般。可以改成保留首字,其余全 *:
javaprivate String maskName(String userName) { if (userName == null || userName.trim().isEmpty()) { return userName; } if (userName.length() == 1) { return "*"; } StringBuilder sb = new StringBuilder(); sb.append(userName.charAt(0)); for (int i = 1; i < userName.length(); i++) { sb.append('*'); } return sb.toString(); }
现在这个 service 既做:
职责有点重。后面维护时建议拆成:
HealthRoomPushValidatorHealthRoomPushMapperHealthRoomRemoteClient这样 service 只负责流程编排。
如果你不想大改架构,我建议至少做到下面这版:
requireText 改成 hasTextvalidateItem 改 switchencryptIfPresentgruopItem 逐步纠正为 groupItem这样改完,代码会明显更干净。
不是“性能”,而是这两个:
第一,敏感信息处理风险
System.out.println(outboundRequest.getUserIdcard())第二,可维护性一般
gruopItem 拼写错误会长期污染代码javapublic RemotePushResponse pushCheckinInfo(HealthRoomPushRequest request) { if (request == null) { throw new PushValidationException("请求体不能为空"); } request.normalize(); validateRequest(request); HealthRoomPushProperties.EndpointProperties endpoint = properties.getActiveEndpoint(); String targetUrl = buildUrl(endpoint); String timestamp = String.valueOf(Instant.now().getEpochSecond()); String signature = SignatureUtils.md5(endpoint.getAppid() + endpoint.getSecret() + timestamp); HealthRoomPushRequest outboundRequest = buildOutboundRequest(request); log.info("开始推送健康小屋数据 env={}, targetUrl={}, reportId={}, reportNo={}, deviceSn={}, userName={}, userIdcard={}, timestamp={}", properties.getActiveEnv(), targetUrl, request.getReportId(), request.getReportNo(), request.getDeviceSn(), maskName(request.getUserName()), maskIdCard(request.getUserIdcard()), timestamp); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.set("timestamp", timestamp); headers.set("signature", signature); HttpEntity<HealthRoomPushRequest> entity = new HttpEntity<>(outboundRequest, headers); try { ResponseEntity<RemotePushResponse> response = restTemplate.exchange(targetUrl, HttpMethod.POST, entity, RemotePushResponse.class); RemotePushResponse body = response.getBody(); if (body == null) { body = new RemotePushResponse(500, "远端接口未返回响应体", null); } log.info("远端接口响应完成 reportId={}, httpStatus={}, resultCode={}, resultMessage={}", request.getReportId(), response.getStatusCodeValue(), body.getCode(), body.getMessage()); return body; } catch (HttpStatusCodeException e) { log.error("远端接口返回异常状态 reportId={}, status={}, body={}", request.getReportId(), e.getStatusCode().value(), e.getResponseBodyAsString(), e); throw new IllegalStateException("远端接口返回异常状态: " + e.getStatusCode().value(), e); } catch (RestClientException e) { log.error("调用远端健康小屋接口失败 reportId={}, targetUrl={}, error={}", request.getReportId(), targetUrl, e.getMessage(), e); throw new IllegalStateException("调用健康小屋推送接口失败: " + e.getMessage(), e); } } private String encryptIfPresent(String value) { return value == null ? null : sm4Utils.encrypt(value); }
你这段代码整体结构其实已经不差,主要是还没做“生产级收口”。
真正值得优先改的是:敏感信息输出、空白字符串校验、异常处理、拼写问题。
你要是愿意,我可以直接帮你给这整个 HealthRoomPushService 出一版完整优化后的可替换代码。