diff --git a/pom.xml b/pom.xml index 0c18960..31d97a9 100644 --- a/pom.xml +++ b/pom.xml @@ -243,8 +243,21 @@ shedlock-provider-redis-spring 4.44.0 + + + com.douyin.openapi + sdk + 1.0.6 + + + + douyin-openapi-repo + https://artifacts-cn-beijing.volces.com/repository/douyin-openapi/ + + + sczx_order diff --git a/src/main/java/com/sczx/order/config/DouyinTokenManager.java b/src/main/java/com/sczx/order/config/DouyinTokenManager.java new file mode 100644 index 0000000..d314771 --- /dev/null +++ b/src/main/java/com/sczx/order/config/DouyinTokenManager.java @@ -0,0 +1,107 @@ +package com.sczx.order.config; + +import com.aliyun.tea.TeaException; +import com.douyin.openapi.client.Client; +import com.douyin.openapi.client.models.OauthClientTokenRequest; +import com.douyin.openapi.client.models.OauthClientTokenResponse; +import com.douyin.openapi.credential.models.Config; + +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +/** + * 抖音 client_token 管理器 + * 负责定时获取和缓存 client_token + */ +public class DouyinTokenManager { + + // 应用凭证信息 + private static final String CLIENT_KEY = "awomt6nnjlfc491m"; + private static final String CLIENT_SECRET = "c678c411c7a68c6f97969f2dbd8ef8fc"; + + // Token 缓存 + private static final AtomicReference tokenCache = + new AtomicReference<>(); + + // 定时任务执行器 + private static final ScheduledThreadPoolExecutor scheduler = + new ScheduledThreadPoolExecutor(1, r -> { + Thread t = new Thread(r, "DouyinTokenRefreshThread"); + t.setDaemon(false); + return t; + }); + + static { + // 初始化时立即获取一次 token + refreshClientToken(); + + // 每小时更新一次 token (3600秒) + scheduler.scheduleAtFixedRate( + DouyinTokenManager::refreshClientToken, + 3600, + 3600, + TimeUnit.SECONDS + ); + } + + /** + * 获取当前有效的 client_token + * + * @return 当前有效的 access_token + */ + public static String getCurrentToken() { + OauthClientTokenResponse response = tokenCache.get(); + if (response != null && response.getData() != null) { + return response.getData().getAccessToken(); + } + return null; + } + + /** + * 刷新 client_token + */ + private static void refreshClientToken() { + try { + Config config = new Config() + .setClientKey(CLIENT_KEY) + .setClientSecret(CLIENT_SECRET); + + Client client = new Client(config); + + OauthClientTokenRequest sdkRequest = new OauthClientTokenRequest(); + sdkRequest.setClientKey(CLIENT_KEY); + sdkRequest.setClientSecret(CLIENT_SECRET); + sdkRequest.setGrantType("client_credential"); + + OauthClientTokenResponse sdkResponse = client.OauthClientToken(sdkRequest); + + // 更新缓存 + tokenCache.set(sdkResponse); + + if (sdkResponse.getData() != null) { + System.out.println("抖音 client_token 更新成功,有效期至: " + + (System.currentTimeMillis() + sdkResponse.getData().getExpiresIn() * 1000)); + } + } catch (TeaException e) { + System.err.println("获取抖音 client_token 失败 (TeaException): " + e.getMessage()); + } catch (Exception e) { + System.err.println("获取抖音 client_token 失败 (Exception): " + e.getMessage()); + } + } + + /** + * 关闭定时任务 + */ + public static void shutdown() { + scheduler.shutdown(); + + // 获取当前有效的 client_token + String token = DouyinTokenManager.getCurrentToken(); + + // 使用 token 调用抖音 API + if (token != null) { + // 调用订单查询等接口 + } + } +} diff --git a/src/main/java/com/sczx/order/controller/ClientOrderController.java b/src/main/java/com/sczx/order/controller/ClientOrderController.java index bf13fd6..818b9db 100644 --- a/src/main/java/com/sczx/order/controller/ClientOrderController.java +++ b/src/main/java/com/sczx/order/controller/ClientOrderController.java @@ -65,6 +65,12 @@ public class ClientOrderController { return Result.ok(orderService.depositFreePayRentCarOrder(rentCarOrderReq)); } + @ApiOperation(value = "第三方订单") + @PostMapping("/thirdPlatformRentCarOrder") + public Result thirdPlatformRentCarOrder(@Valid @RequestBody RentCarThirdPlatformOrderReq rentCarOrderReq){ + return Result.ok(orderService.thirdPlatformRentCarOrder(rentCarOrderReq)); + } + @ApiOperation(value = "续租车") @PostMapping("/reRentalCarOrder") public Result reRentalCarOrder(@Valid @RequestBody ReRentCarReq rentCarOrderReq){ diff --git a/src/main/java/com/sczx/order/controller/MeiTuanController.java b/src/main/java/com/sczx/order/controller/MeiTuanController.java new file mode 100644 index 0000000..b3c2d92 --- /dev/null +++ b/src/main/java/com/sczx/order/controller/MeiTuanController.java @@ -0,0 +1,23 @@ +package com.sczx.order.controller; + +import com.sczx.order.common.Result; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; + + +@Slf4j +public class MeiTuanController { + + @ApiOperation(value = "接收需同步数据接口") + @GetMapping("/authorization") + public Result authorization(@RequestParam("code") String code, @RequestParam("sign") String sign, @RequestParam("developerId") Long developerId, @RequestParam("businessId") int businessId, + @RequestParam("state") String state){ + + log.info("接收美团授权数据 - code: {}, sign: {}, developerId: {}, businessId: {}, state: {}", + code, sign, developerId, businessId, state); + + return null; + } +} diff --git a/src/main/java/com/sczx/order/controller/StoreOrderController.java b/src/main/java/com/sczx/order/controller/StoreOrderController.java index 70d0741..59d996f 100644 --- a/src/main/java/com/sczx/order/controller/StoreOrderController.java +++ b/src/main/java/com/sczx/order/controller/StoreOrderController.java @@ -60,4 +60,10 @@ public class StoreOrderController { public Result confirmReturnCar(@RequestBody ReturnCarReq returnCarReq){ return Result.ok(orderService.confirmReturnCar(returnCarReq)); } + + @ApiOperation(value = "抖音团购核销") + @PostMapping("/verifyCar") + public Result verifyCar(@RequestBody ReturnCarReq returnCarReq){ + return Result.ok(orderService.confirmReturnCar(returnCarReq)); + } } diff --git a/src/main/java/com/sczx/order/convert/OrderConvert.java b/src/main/java/com/sczx/order/convert/OrderConvert.java index 3a710e5..2dbd1a2 100644 --- a/src/main/java/com/sczx/order/convert/OrderConvert.java +++ b/src/main/java/com/sczx/order/convert/OrderConvert.java @@ -1,9 +1,6 @@ package com.sczx.order.convert; -import com.sczx.order.dto.OrderDTO; -import com.sczx.order.dto.OrderDetailDTO; -import com.sczx.order.dto.RentCarOrderReq; -import com.sczx.order.dto.SimpleUserInfoDTO; +import com.sczx.order.dto.*; import com.sczx.order.po.OrderMainPO; import com.sczx.order.po.OrderSubPO; import com.sczx.order.thirdpart.dto.*; @@ -46,6 +43,31 @@ public interface OrderConvert { }) OrderMainPO subOrderToPo(RentCarOrderReq rentCarOrderReq, SimpleUserInfoDTO userInfoDTO, RentCarRuleDTO rentCarRuleDTO); + @Mappings({ + @Mapping(target = "orderId", ignore = true), + @Mapping(target = "createTime", ignore = true), + @Mapping(target = "updateTime", ignore = true), + @Mapping(target = "delFlag", ignore = true), + @Mapping(source = "rentCarOrderReq.storeId", target = "storeId"), + @Mapping(source = "rentCarOrderReq.operatorId", target = "operatorId"), + @Mapping(source = "rentCarOrderReq.carModelId", target = "carModelId"), + @Mapping(source = "rentCarOrderReq.rentCarRuleId", target = "rentCarRuleId"), + @Mapping(source = "rentCarOrderReq.rentBatteyRuleId", target = "rentBatteyRuleId"), + @Mapping(source = "rentCarOrderReq.isDepositFree", target = "isDepositFree"), + @Mapping(source = "rentCarOrderReq.batteryType", target = "batteryType"), + @Mapping(source = "userInfoDTO.userId", target = "customerId"), + @Mapping(source = "userInfoDTO.userName", target = "customerName"), + @Mapping(source = "userInfoDTO.phoneNumber", target = "customerPhone"), + @Mapping(source = "rentCarRuleDTO.rentalType", target = "rentalType"), + @Mapping(source = "rentCarRuleDTO.rentalDays", target = "rentalDays"), + @Mapping(source = "rentCarRuleDTO.rentalPrice", target = "rentalPrice"), + @Mapping(source = "rentCarRuleDTO.depositPrice", target = "depositPrice"), + @Mapping(source = "rentCarRuleDTO.overdueFee", target = "overdueFee"), + @Mapping(source = "rentCarRuleDTO.overdueType", target = "overdueType") + + }) + OrderMainPO subOrderToPo(RentCarThirdPlatformOrderReq rentCarOrderReq, SimpleUserInfoDTO userInfoDTO, RentCarRuleDTO rentCarRuleDTO); + @Mappings({ @Mapping(source = "orderMainPO.orderId", target = "orderId"), diff --git a/src/main/java/com/sczx/order/dto/RentCarThirdPlatformOrderReq.java b/src/main/java/com/sczx/order/dto/RentCarThirdPlatformOrderReq.java new file mode 100644 index 0000000..469214a --- /dev/null +++ b/src/main/java/com/sczx/order/dto/RentCarThirdPlatformOrderReq.java @@ -0,0 +1,62 @@ +package com.sczx.order.dto; + + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +/** + * @Author: 张黎 + * @Date: 2025/07/25/16:58 + * @Description: + */ +@Data +@ApiModel(value = "租车订单请求参数") +public class RentCarThirdPlatformOrderReq { + + @ApiModelProperty(value = "订单编号,租车不需要传,续租和逾期处理需要传") + private String orderNo; + + @ApiModelProperty(value = "运营商id") + private Long operatorId; + + @ApiModelProperty(value = "门店id") + @NotNull(message = "门店id不能为空") + private Long storeId; + + @ApiModelProperty(value = "客户id") + private Long customerId; + + @ApiModelProperty(value = "客户姓名") + private String customerName; + + @ApiModelProperty(value = "客户电话") + private String customerPhone; + + @ApiModelProperty(value = "第三方订单号") + private String thirdOrderNo; + + @ApiModelProperty(value = "订单来源") + private String orderSource; + + @ApiModelProperty("车型ID") + @NotNull(message = "车型ID不能为空") + private Long carModelId; + + @ApiModelProperty(value = "租车套餐id") + @NotNull(message = "租车套餐id不能为空") + private Long rentCarRuleId; + + + @ApiModelProperty(value = "租电套餐id") + private Long rentBatteyRuleId; + + @ApiModelProperty("选择的电池类型") + private String batteryType; + + + @ApiModelProperty("是否开通免押") + private Boolean isDepositFree = false; +} diff --git a/src/main/java/com/sczx/order/service/DouyinService.java b/src/main/java/com/sczx/order/service/DouyinService.java new file mode 100644 index 0000000..a502dda --- /dev/null +++ b/src/main/java/com/sczx/order/service/DouyinService.java @@ -0,0 +1,8 @@ +package com.sczx.order.service; + +public interface DouyinService { + + String resolveShortUrlToGetObjectId(String shortUrl) throws Exception; + + +} diff --git a/src/main/java/com/sczx/order/service/OrderService.java b/src/main/java/com/sczx/order/service/OrderService.java index d3c4c30..a53dd36 100644 --- a/src/main/java/com/sczx/order/service/OrderService.java +++ b/src/main/java/com/sczx/order/service/OrderService.java @@ -36,6 +36,14 @@ public interface OrderService { */ RentCarOrderResultDTO depositFreePayRentCarOrder(RentCarOrderReq rentCarOrderReq); + + /** + * 第三方订单 + * @param rentCarOrderReq + * @return + */ + RentCarOrderResultDTO thirdPlatformRentCarOrder(RentCarThirdPlatformOrderReq rentCarOrderReq); + /** * 续租车 * @param rentCarOrderReq diff --git a/src/main/java/com/sczx/order/service/impl/DouyinServiceImpl.java b/src/main/java/com/sczx/order/service/impl/DouyinServiceImpl.java new file mode 100644 index 0000000..4d5f55c --- /dev/null +++ b/src/main/java/com/sczx/order/service/impl/DouyinServiceImpl.java @@ -0,0 +1,51 @@ +package com.sczx.order.service.impl; + + +import com.sczx.order.exception.InnerException; +import com.sczx.order.service.DouyinService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.net.HttpURLConnection; +import java.net.URL; + +@Slf4j +@Service +public class DouyinServiceImpl implements DouyinService { + + @Transactional(rollbackFor = Exception.class) + @Override + public String resolveShortUrlToGetObjectId(String shortUrl) { + try{ + URL url = new URL(shortUrl); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setInstanceFollowRedirects(false); // 禁止自动重定向 + connection.connect(); + + String longUrl = connection.getHeaderField("Location"); + connection.disconnect(); + + if (longUrl == null || longUrl.isEmpty()) { + return null; + } + + // 使用正则表达式匹配object_id参数 + java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("object_id=([^&]*)"); + java.util.regex.Matcher matcher = pattern.matcher(longUrl); + + if (matcher.find()) { + return matcher.group(1); + } + + return null; + }catch (Exception e){ + log.error("二维码不正确"); + throw new InnerException("扫码核销失败,二维码不正确"); + } + + + + } + +} diff --git a/src/main/java/com/sczx/order/service/impl/OrderServiceImpl.java b/src/main/java/com/sczx/order/service/impl/OrderServiceImpl.java index fa4aee2..65f522c 100644 --- a/src/main/java/com/sczx/order/service/impl/OrderServiceImpl.java +++ b/src/main/java/com/sczx/order/service/impl/OrderServiceImpl.java @@ -462,6 +462,161 @@ public class OrderServiceImpl implements OrderService { } + @Transactional(rollbackFor = Exception.class) + @Override + public RentCarOrderResultDTO thirdPlatformRentCarOrder(RentCarThirdPlatformOrderReq rentCarOrderReq) { + + LambdaQueryWrapper currentOrderWrapper = new LambdaQueryWrapper<>(); + currentOrderWrapper.eq(OrderMainPO::getCustomerId, rentCarOrderReq.getCustomerId()) + .notIn(OrderMainPO::getOrderStatus, Arrays.asList(OrderStatusEnum.AUTO_END.getCode(), OrderStatusEnum.MANUAL_END.getCode())) + .eq(OrderMainPO::getDelFlag, "0"); + List currentOrderList = orderMainRepo.list(currentOrderWrapper); + OrderMainPO waitPayOrder = currentOrderList.stream().filter(order -> order.getOrderStatus().equals(OrderStatusEnum.WAIT_PAY.getCode())).findFirst().orElse(null); + if(!currentOrderList.isEmpty() && waitPayOrder==null){ + throw new BizException("您有未完成的订单,请先完成订单"); + } + if(waitPayOrder!=null){ + log.info("存在待支付的订单,取消订单"); + PayOrderReq payOrderReq = new PayOrderReq(); + payOrderReq.setOrderNo(waitPayOrder.getOrderNo()); + cancelOrder(payOrderReq,null); + } + + //判断是否存有空闲车辆可用 + LambdaQueryWrapper carWrapper = new LambdaQueryWrapper<>(); + carWrapper.eq(CarPO::getModelId, rentCarOrderReq.getCarModelId()) + .eq(CarPO::getDelFlag, "0") + .eq(CarPO::getBrsStatus, "0") + .eq(CarPO::getStoreId, rentCarOrderReq.getStoreId()); + List carPOList = carRepo.list(carWrapper); + if(CollectionUtils.isEmpty(carPOList)){ + throw new BizException("门店没有该车型的车辆可租"); + } + + //获取门店信息 +// CompanyStoreDTO companyStoreDTO = storeInteg.getStoreById(Integer.valueOf(rentCarOrderReq.getStoreId().toString())); +// if(Objects.isNull(companyStoreDTO)){ +// throw new BizException("运营商或门店已下架"); +// } + + + CarModelSimpleDTO carModelSimpleDTO = carInteg.getCarModelByModelId(rentCarOrderReq.getCarModelId()); + + RentCarRuleDTO rentCarRuleDTO = carInteg.getRentCarRuleByCarRuleId(rentCarOrderReq.getRentCarRuleId()); + + RentBatteyRuleDTO rentBatteyRuleDTO = null; + if(rentCarOrderReq.getRentBatteyRuleId()!=null){ + rentBatteyRuleDTO = carInteg.getRentBatteyRuleByBatteyRuleId(rentCarOrderReq.getRentBatteyRuleId()); + } + + String redisLockKey = RedisKeyConstants.ORDER_SUB_KEY + rentCarOrderReq.getCustomerId(); + + + SimpleUserInfoDTO userInfoDTO = new SimpleUserInfoDTO(); + userInfoDTO.setUserId(Math.toIntExact(rentCarOrderReq.getCustomerId())); + userInfoDTO.setUserName(rentCarOrderReq.getCustomerName()); + userInfoDTO.setPhoneNumber(rentCarOrderReq.getCustomerPhone()); + + if(redisUtil.getRedisLock(redisLockKey, "租车下单")) { + try{ + + //TODO 这里保存订单要做事物处理 + //生成订单主表 + OrderMainPO orderMainPO = OrderConvert.INSTANCE.subOrderToPo(rentCarOrderReq, userInfoDTO, rentCarRuleDTO); + //orderMainPO.setOperatorId(Long.valueOf(companyStoreDTO.getOperatingCompanyId())); + orderMainPO.setOrderNo(OrderUtil.generateOrderNo()); + orderMainPO.setOrderStatus(OrderStatusEnum.WAIT_PICK.getCode()); + orderMainPO.setFirstOrderTime(LocalDateTime.now()); + + //设置预计还车时间 + LocalDateTime endRentTime = OrderUtil.getEndRentTime(orderMainPO.getFirstOrderTime(),1,rentCarRuleDTO.getRentalDays(), rentCarRuleDTO.getRentalType()); + orderMainPO.setEndRentTime(endRentTime); + + + //生成子表订单 + String paymentType = rentCarOrderReq.getOrderSource(); + + List orderSubPOList = new ArrayList<>(); + + //如果未开通免押则要生成押金订单 + if(!rentCarOrderReq.getIsDepositFree()){ + OrderSubPO depositOrder = new OrderSubPO(); + depositOrder.setSuborderNo(OrderUtil.generateSubOrderNo(OrderUtil.YJ_PREFIX)); + depositOrder.setSuborderType(SubOrderTypeEnum.DEPOSIT.getCode()); + depositOrder.setAmount(orderMainPO.getDepositPrice()); + depositOrder.setCreatedAt(LocalDateTime.now()); + depositOrder.setPaymentMethod(paymentType); + depositOrder.setTransactionId(rentCarOrderReq.getThirdOrderNo()); + orderSubPOList.add(depositOrder); + } + //如果选择了租电套餐,则还需要生成租电子订单 + if(rentBatteyRuleDTO!=null){ + rentBatteyRuleDTO = carInteg.getRentBatteyRuleByBatteyRuleId(rentCarOrderReq.getRentBatteyRuleId()); + OrderSubPO depositOrder = new OrderSubPO(); + depositOrder.setSuborderNo(OrderUtil.generateSubOrderNo(OrderUtil.ZD_PREFIX)); + depositOrder.setSuborderType(SubOrderTypeEnum.RENTBATTEY.getCode()); + depositOrder.setAmount(rentBatteyRuleDTO.getRentPrice()); + depositOrder.setCreatedAt(LocalDateTime.now()); + depositOrder.setPaymentMethod(paymentType); + depositOrder.setReturnTime(endRentTime); + depositOrder.setTransactionId(rentCarOrderReq.getThirdOrderNo()); + orderSubPOList.add(depositOrder); + } + //生成租车订单 + BigDecimal rentCarOrderAmount = OrderUtil.getRentCarAmount(rentCarRuleDTO.getRentalType(), rentCarRuleDTO.getRentalPrice(), rentCarRuleDTO.getRentalDays()); + OrderSubPO rentOrder = new OrderSubPO(); + rentOrder.setSuborderNo(OrderUtil.generateSubOrderNo(OrderUtil.ZC_PREFIX)); + rentOrder.setSuborderType(SubOrderTypeEnum.RENTCAR.getCode()); + rentOrder.setAmount(rentCarOrderAmount); + rentOrder.setCreatedAt(LocalDateTime.now()); + rentOrder.setPaymentMethod(paymentType); + rentOrder.setReturnTime(endRentTime); + rentOrder.setTransactionId(rentCarOrderReq.getThirdOrderNo()); + orderSubPOList.add(rentOrder); + + BigDecimal orderAmount = orderSubPOList.stream().map(OrderSubPO::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add); + orderMainPO.setOrderAmount(orderAmount); + + //发起支付返回预支付信息 + String paymentId = OrderUtil.generateSubOrderNo(OrderUtil.ZF_PREFIX); + + + for(OrderSubPO orderSubPO : orderSubPOList){ + orderSubPO.setPaymentId(paymentId); + orderSubPO.setPayStatus(PayStatusEnum.USERPAYING.getCode()); + } + + orderMainRepo.save(orderMainPO); + + orderSubPOList.forEach(orderSubPO -> { + orderSubPO.setOrderId(orderMainPO.getOrderId()); + }); + orderSubRepo.saveBatch(orderSubPOList); + + + //返回订单信息 + OrderDTO orderDTO = OrderConvert.INSTANCE.poToDto(orderMainPO); + //orderDTO.setCompanyStoreDTO(companyStoreDTO); + orderDTO.setCarModelSimpleDTO(carModelSimpleDTO); + + RentCarOrderResultDTO rentCarOrderResultDTO = new RentCarOrderResultDTO(); + rentCarOrderResultDTO.setOrderMainInfo(orderDTO); + //rentCarOrderResultDTO.setUnifiedPaymentInfo(unifiedPaymentInfoDTO); + + return rentCarOrderResultDTO; + + }catch (Exception e){ + log.warn("下单失败", e); + throw e; + } finally { + redisUtil.deleteRedisLock(redisLockKey); + } + } else { + log.warn("下单失败,锁已被占用"); + throw new InnerException("服务器正在处理,请稍后再试"); + } + } + @Transactional(rollbackFor = Exception.class) @Override