package com.xiaopeng.automaticPilot.service.impl; import com.xiaopeng.api.feignClient.account.XpOperationUserClient; import com.xiaopeng.api.feignClient.account.XpOperationsRoleClient; import com.xiaopeng.api.feignClient.operation.SmsClient; import com.xiaopeng.api.model.account.XpOperationsRole; import com.xiaopeng.api.model.account.XpOperationsUser; import com.xiaopeng.api.vo.operation.SmsVo; import com.xiaopeng.automaticPilot.dto.XpCommonResponse; import com.xiaopeng.automaticPilot.dto.RobotaxiSmsCodeRequest; import com.xiaopeng.automaticPilot.dto.RobotaxiSmsValidateRequest; import com.xiaopeng.automaticPilot.service.VehicleRtInfoService; import com.xiaopeng.api.dto.vehicleConvoy.UpdateVehicleStateDto; import com.xiaopeng.api.feignClient.vehicleConvoy.XpVehicleClient; import com.xiaopeng.api.model.vehicleConvoy.XpVehicle; import com.xiaopeng.common.core.domain.R; import com.xiaopeng.common.core.utils.MsgUtil; import com.xiaopeng.common.core.utils.StringUtils; import com.xiaopeng.common.redis.service.RedisService; import com.xiaopeng.automaticPilot.enums.RobotaxiErrorEnum; import com.xiaopeng.automaticPilot.enums.SmsPushType; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; import javax.annotation.Resource; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; @Slf4j @Service public class VehicleRtInfoServiceImpl implements VehicleRtInfoService { private static final long CODE_TTL_MINUTES = 5L; public static final String ROBOTAXI_VERIFY_CODE_SMS_TPL = "RobotaxiVerifyCodeSmsTpl"; public static final String ROBOTAXI_VERIFY_CODE_SMS = "RobotaxiVerifyCodeSms"; @Resource private RedisService redisService; @Resource private XpVehicleClient xpVehicleClient; @Resource private SmsClient smsClient; @Resource private XpOperationUserClient xpOperationUserClient; @Resource private XpOperationsRoleClient xpOperationsRoleClient; @Value("${robotaxi.sms.app-id:1476153389173952512}") private String smsAppId; @Value("#{'${robotaxi.sms.whitelist.mobiles:}'.split(',')}") private List whitelistMobiles; /** * 安全员角色ID,未配置或为空则不做安全员角色校验 */ @Value("${robotaxi.sms.safety-officer-role-id:#{null}}") private Long safetyOfficerRoleId; @Override public XpCommonResponse getRobotaxiSmsCode(RobotaxiSmsCodeRequest request) { XpCommonResponse paramCheckError = validateSmsCodeRequest(request); if (paramCheckError != null) { return paramCheckError; } // 校验用户是否为安全员角色 XpCommonResponse roleCheckError = validateSafetyOfficerRole(request.getMobile()); if (roleCheckError != null) { return roleCheckError; } XpCommonResponse vehicleCheckError = validateVehicleReadyIfNeeded(request.getSmsType(), request.getVin()); if (vehicleCheckError != null) { return vehicleCheckError; } String code = MsgUtil.createCode(); log.info("Robotaxi SMS code generated for mobile {}: {}", request.getMobile(), code); // TODO: Call SMS interface to send code when available // sendSms(request.getMobile(), code); Map param = Collections.singletonMap("code", code); SmsVo smsVo = new SmsVo(); smsVo.setAppId(smsAppId); smsVo.setParams(param); smsVo.setTemplateCode(ROBOTAXI_VERIFY_CODE_SMS_TPL); smsVo.setAppPushConfigCode(ROBOTAXI_VERIFY_CODE_SMS); smsVo.setReceivers(request.getMobile()); smsVo.setPushType(SmsPushType.SMS.getCode()); smsClient.pushSms(smsVo); String key = buildSmsCodeKey(request.getSmsType(), request.getVin(), request.getMobile()); redisService.setCacheObject(key, code, CODE_TTL_MINUTES, TimeUnit.MINUTES); return XpCommonResponse.ok(Collections.emptyMap()); } @Override public XpCommonResponse validateRobotaxiSmsCode(RobotaxiSmsValidateRequest request) { XpCommonResponse paramCheckError = validateSmsValidateRequest(request); if (paramCheckError != null) { return paramCheckError; } XpCommonResponse vehicleCheckError = validateVehicleReadyIfNeeded(request.getSmsType(), request.getVin()); if (vehicleCheckError != null) { return vehicleCheckError; } boolean isWhitelisted = whitelistMobiles != null && whitelistMobiles.contains(request.getMobile()); String key = buildSmsCodeKey(request.getSmsType(), request.getVin(), request.getMobile()); if (!isWhitelisted) { String cachedCode = redisService.getCacheObject(key); if (StringUtils.isEmpty(cachedCode)) { return XpCommonResponse.fail(RobotaxiErrorEnum.VERIFY_CODE_ERROR); } if (!cachedCode.equals(request.getCode())) { return XpCommonResponse.fail(RobotaxiErrorEnum.VERIFY_CODE_ERROR); } redisService.deleteObject(key); } else { redisService.deleteObject(key); } return XpCommonResponse.ok(Collections.emptyMap()); } private XpCommonResponse validateSmsCodeRequest(RobotaxiSmsCodeRequest request) { if (StringUtils.isAnyEmpty(request.getSmsType(), request.getVin(), request.getMobile())) { return XpCommonResponse.fail(RobotaxiErrorEnum.GENERAL_ERROR); } if (!isSupportedSmsType(request.getSmsType())) { return XpCommonResponse.fail(RobotaxiErrorEnum.GENERAL_ERROR); } return null; } private XpCommonResponse validateSmsValidateRequest(RobotaxiSmsValidateRequest request) { if (StringUtils.isAnyEmpty(request.getSmsType(), request.getVin(), request.getMobile(), request.getCode())) { return XpCommonResponse.fail(RobotaxiErrorEnum.GENERAL_ERROR); } if (!isSupportedSmsType(request.getSmsType())) { return XpCommonResponse.fail(RobotaxiErrorEnum.GENERAL_ERROR); } return null; } private boolean isSupportedSmsType(String smsType) { return SmsType.ACCEPT_RIDES.name().equals(smsType) || SmsType.STOP_ACCEPT_RIDES.name().equals(smsType); } private XpCommonResponse validateVehicleReadyIfNeeded(String smsType, String vin) { if (!SmsType.ACCEPT_RIDES.name().equals(smsType)) { return null; } XpVehicle vehicle = xpVehicleClient.findByVehicleVin(vin).getData(); if (vehicle == null) { return XpCommonResponse.fail(RobotaxiErrorEnum.MOBILE_VIN_MISMATCH); } Integer status = vehicle.getStatus(); Integer operatingStatus = vehicle.getOperatingStatus(); if (smsType.equals(SmsType.ACCEPT_RIDES.name()) && operatingStatus != 1) { return XpCommonResponse.fail(RobotaxiErrorEnum.VEHICLE_NOT_READY); } if (smsType.equals(SmsType.STOP_ACCEPT_RIDES.name()) && operatingStatus != 3) { return XpCommonResponse.fail(RobotaxiErrorEnum.VEHICLE_NOT_READY); } return null; } /** * 校验手机号对应的运维人员是否拥有安全员角色。 * 若未配置 safetyOfficerRoleId 则跳过校验。 */ private XpCommonResponse validateSafetyOfficerRole(String mobile) { if (safetyOfficerRoleId == null) { return null; } try { R userResult = xpOperationUserClient.getByExactPhone(mobile); if (userResult == null || userResult.getData() == null) { log.warn("安全员角色校验失败:手机号 {} 未找到对应运维人员", mobile); return XpCommonResponse.fail(RobotaxiErrorEnum.NOT_SAFETY_OFFICER); } R> roleRelResult = xpOperationsRoleClient.listByOperationUserId(userResult.getData().getId()); if (roleRelResult != null && !CollectionUtils.isEmpty(roleRelResult.getData())) { for (XpOperationsRole operationsRole : roleRelResult.getData()) { if (safetyOfficerRoleId.equals(operationsRole.getRoleId())) { return null; // 校验通过 } } } log.warn("安全员角色校验失败:运维人员 {} 不具备安全员角色(roleId={})", userResult.getData().getId(), safetyOfficerRoleId); return XpCommonResponse.fail(RobotaxiErrorEnum.NOT_SAFETY_OFFICER); } catch (Exception e) { log.error("安全员角色校验异常,mobile={}", mobile, e); return XpCommonResponse.fail(RobotaxiErrorEnum.GENERAL_ERROR); } } private String buildSmsCodeKey(String smsType, String vin, String mobile) { return "ROBOTAXI_SMS_CODE:" + smsType + ":" + vin + ":" + mobile; } }