package com.gxwebsoft.hjc.service.impl; import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.fasterxml.jackson.databind.ObjectMapper; import com.gxwebsoft.hjc.dto.CreatePurchaseDetails; import com.gxwebsoft.hjc.dto.HjcOneStopProjectPush; import com.gxwebsoft.hjc.entity.HjcBidProject; import com.gxwebsoft.hjc.entity.HjcEnterprise; import com.gxwebsoft.hjc.entity.HjcOrder; import com.gxwebsoft.hjc.entity.HjcOrderPushLog; import com.gxwebsoft.hjc.service.HjcBidProjectService; import com.gxwebsoft.hjc.service.HjcBizService; import com.gxwebsoft.hjc.service.HjcEnterpriseService; import com.gxwebsoft.hjc.service.HjcOrderService; import com.gxwebsoft.hjc.service.HjcOrderPushLogService; import com.gxwebsoft.hjc.util.HjcOneStopAuthUtil; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.client.RestTemplate; import javax.annotation.Resource; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.List; /** * 汇吉采一站式双向对接业务 */ @Service public class HjcBizServiceImpl implements HjcBizService { private static final DateTimeFormatter DT_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); private static final String STATUS_PAID = "PAID"; private static final String STATUS_REFUNDED = "REFUNDED"; /** 退款原因为空时的兜底值:一站式要求 REFUNDED 必带退款原因,不能推空串 */ private static final String DEFAULT_REFUND_REASON = "管理员操作退款"; /** 退款原因入库/推送长度上限(hjc_order.refund_reason) */ private static final int REFUND_REASON_MAX = 255; @Value("${hjc.one-stop.base-url:}") private String oneStopBaseUrl; @Value("${hjc.one-stop.create-purchase-details-path:/api/biz/createPurchaseDetails}") private String createPurchaseDetailsPath; @Resource private HjcBidProjectService hjcBidProjectService; @Resource private HjcEnterpriseService hjcEnterpriseService; @Resource private HjcOrderService hjcOrderService; @Resource private HjcOrderPushLogService hjcOrderPushLogService; @Resource private RestTemplate restTemplate; @Resource private ObjectMapper objectMapper; @Override @Transactional(rollbackFor = Exception.class) public HjcBidProject upsertFromPush(HjcOneStopProjectPush push, Integer tenantId) { HjcBidProject project = hjcBidProjectService.getByProjectNo(push.getProjectNo()); boolean isNew = project == null; if (isNew) { project = new HjcBidProject(); project.setProjectNo(push.getProjectNo()); project.setTenantId(tenantId); project.setSaleCount(0); project.setDataSource("PUSH"); project.setStatus(1); } project.setProjectName(push.getProjectName()); project.setTenderer(push.getCustomerName()); project.setWinnerSupplier(push.getSupplierName()); project.setBidAmount(push.getBidAmount()); project.setBulletinTitle(push.getBulletinName()); project.setBulletinContent(push.getContent()); project.setBulletinFileList(push.getFileList()); project.setTenderPrice(push.getTenderPrice()); project.setTenderFile(push.getFiles()); project.setOnsaleTime(parseDateTime(push.getTenderOnsaleTime())); project.setOffsaleTime(parseDateTime(push.getTenderOffsaleTime())); // 发布时间=开售时间、投标截止=截止时间(同一语义,直接映射) project.setPublishTime(parseDateTime(push.getTenderOnsaleTime())); project.setDeadlineTime(parseDateTime(push.getTenderOffsaleTime())); project.setSellingMethod(push.getSellingMethod()); project.setNeedSell(push.getNeedSellTender()); project.setDataSource("PUSH"); if (isNew) { hjcBidProjectService.save(project); } else { hjcBidProjectService.updateById(project); } return project; } @Override public void pushOrderToOneStop(HjcOrder order) { if (order == null || StrUtil.isBlank(order.getOrderNo())) { return; } HjcOrderPushLog logEntity = new HjcOrderPushLog(); logEntity.setOrderId(order.getId()); logEntity.setOrderNo(order.getOrderNo()); logEntity.setTenantId(order.getTenantId()); logEntity.setPushStatus(0); logEntity.setAttemptCount(0); hjcOrderPushLogService.save(logEntity); doPush(order, logEntity); } @Override @Transactional(rollbackFor = Exception.class) public void retryPendingPush() { List pending = hjcOrderPushLogService.list(new LambdaQueryWrapper() .in(HjcOrderPushLog::getPushStatus, 0, 2) .and(w -> w.isNull(HjcOrderPushLog::getNextRetryTime) .or().le(HjcOrderPushLog::getNextRetryTime, LocalDateTime.now())) .last("limit 50")); for (HjcOrderPushLog logEntity : pending) { HjcOrder order = hjcOrderService.getByOrderNo(logEntity.getOrderNo()); if (order != null) { doPush(order, logEntity); } } } @Override @Transactional(rollbackFor = Exception.class) public HjcOrder markPaid(String orderNo) { HjcOrder order = hjcOrderService.getByOrderNo(orderNo); if (order == null) { throw new RuntimeException("订单不存在"); } if (order.getPayStatus() != null && order.getPayStatus() == 1) { return order; } order.setPayStatus(1); order.setOrderStatus(1); order.setPayTime(LocalDateTime.now()); hjcOrderService.updateById(order); // 支付成功后触发一站式推送 pushOrderToOneStop(order); return order; } @Override @Transactional(rollbackFor = Exception.class) public HjcOrder refund(String orderNo, String refundReason) { HjcOrder order = hjcOrderService.getByOrderNo(orderNo); if (order == null) { throw new RuntimeException("订单不存在"); } if (order.getPayStatus() != null && order.getPayStatus() == 3) { return order; } order.setPayStatus(3); // 退款时间/原因必须落库:推送失败重试时按订单重建报文,否则重试会丢掉这两个字段 order.setRefundTime(LocalDateTime.now()); order.setRefundReason(StrUtil.isBlank(refundReason) ? DEFAULT_REFUND_REASON : limitStr(refundReason.trim(), REFUND_REASON_MAX)); hjcOrderService.updateById(order); // 退款后触发一站式推送(status=REFUNDED + 退款时间/原因) pushOrderToOneStop(order); return order; } @Override @Transactional(rollbackFor = Exception.class) public boolean cancelUnpaidOrder(String orderNo) { // 条件更新自带幂等与安全:只有把 order_status 从 0 翻到 2 的那一次会返回 1 行; // 已支付(pay_status=1/3)与已取消(order_status=2)的单都不会被改到。 // 这里**不推送一站式**,理由见 HjcBizService#cancelUnpaidOrder 的接口注释。 return hjcOrderService.cancelPending(orderNo) > 0; } private void doPush(HjcOrder order, HjcOrderPushLog logEntity) { try { CreatePurchaseDetails body = buildCreatePurchaseDetails(order); String payload = objectMapper.writeValueAsString(body); logEntity.setPayload(payload); if (StrUtil.isBlank(oneStopBaseUrl)) { logEntity.setAttemptCount(logEntity.getAttemptCount() + 1); logEntity.setPushStatus(2); logEntity.setErrorMsg("一站式base-url未配置"); logEntity.setNextRetryTime(LocalDateTime.now().plusMinutes(30)); hjcOrderPushLogService.updateById(logEntity); return; } String ts = HjcOneStopAuthUtil.timestamp(); String sign = HjcOneStopAuthUtil.sign(ts); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.set("appKey", HjcOneStopAuthUtil.APP_KEY); headers.set("timestamp", ts); headers.set("sign", sign); String url = oneStopBaseUrl + createPurchaseDetailsPath; ResponseEntity resp = restTemplate.postForEntity(url, new HttpEntity<>(payload, headers), String.class); logEntity.setPayload(payload); logEntity.setAttemptCount(logEntity.getAttemptCount() + 1); logEntity.setHttpCode(resp.getStatusCode().value()); logEntity.setResponseBody(limitStr(resp.getBody(), 3000)); if (resp.getStatusCode().is2xxSuccessful()) { logEntity.setPushStatus(1); logEntity.setErrorMsg(null); logEntity.setNextRetryTime(null); } else { logEntity.setPushStatus(2); logEntity.setErrorMsg("HTTP " + resp.getStatusCode().value()); logEntity.setNextRetryTime(LocalDateTime.now().plusMinutes(30)); } } catch (Exception e) { logEntity.setAttemptCount(logEntity.getAttemptCount() + 1); logEntity.setPushStatus(2); logEntity.setErrorMsg(limitStr(e.getMessage(), 1000)); logEntity.setNextRetryTime(LocalDateTime.now().plusMinutes(30)); } hjcOrderPushLogService.updateById(logEntity); } private CreatePurchaseDetails buildCreatePurchaseDetails(HjcOrder order) { CreatePurchaseDetails body = new CreatePurchaseDetails(); body.setIdempotencyKey("HJC_ORD_" + order.getOrderNo()); body.setOrderNo(order.getOrderNo()); body.setProjectNo(order.getProjectNo()); body.setProjectName(order.getProjectName()); body.setTenderPrice(order.getUnitPrice()); body.setQuantity(order.getQuantity()); body.setTotalAmount(order.getTotalAmount()); body.setPaidAt(order.getPayTime() == null ? null : order.getPayTime().format(DT_FMT)); body.setPayMethod(order.getPayMethod()); boolean refunded = order.getPayStatus() != null && order.getPayStatus() == 3; body.setStatus(refunded ? STATUS_REFUNDED : STATUS_PAID); // 一站式要求退款推送必带退款时间与退款原因(非退款单不推这两个字段) if (refunded) { LocalDateTime refundTime = order.getRefundTime() != null ? order.getRefundTime() : order.getUpdateTime(); body.setRefundTime(refundTime == null ? null : refundTime.format(DT_FMT)); body.setRefundReason(StrUtil.isBlank(order.getRefundReason()) ? DEFAULT_REFUND_REASON : order.getRefundReason()); } body.setInvoiceStatus(invoiceStatus(order.getInvoiceStatus())); CreatePurchaseDetails.Buyer buyer = new CreatePurchaseDetails.Buyer(); buyer.setEnterpriseName(order.getEnterpriseName()); buyer.setContactName(order.getContactName()); buyer.setContactPhone(order.getContactPhone()); buyer.setContactEmail(order.getContactEmail()); if (order.getEnterpriseId() != null) { HjcEnterprise enterprise = hjcEnterpriseService.getById(order.getEnterpriseId()); if (enterprise != null) { buyer.setCreditCode(enterprise.getCreditCode()); } } body.setBuyer(buyer); return body; } private String invoiceStatus(Integer invoiceStatus) { if (invoiceStatus == null) { return "NONE"; } switch (invoiceStatus) { case 1: return "APPLIED"; case 2: return "ISSUED"; default: return "NONE"; } } private LocalDateTime parseDateTime(String text) { if (StrUtil.isBlank(text)) { return null; } String t = text.trim(); try { if (t.length() == 14) { return LocalDateTime.parse(t, DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); } if (t.contains("T")) { return LocalDateTime.parse(t); } return LocalDateTime.parse(t, DT_FMT); } catch (Exception e) { return null; } } private String limitStr(String s, int max) { if (s == null) { return null; } return s.length() > max ? s.substring(0, max) : s; } }