| | |
| | | import com.ruoyi.order.vo.EndOfChargePageInfo; |
| | | import com.ruoyi.order.vo.ChargingSimulationVO; |
| | | import com.ruoyi.order.vo.IcbcAdminSubOrderVO; |
| | | import com.ruoyi.order.vo.OrderCountByDate; |
| | | import com.ruoyi.order.vo.StatisticsOfBatteryVO; |
| | | import com.ruoyi.order.vo.LargeChargingPowerVo; |
| | | import com.ruoyi.order.vo.ReservationRecommendationVO; |
| | |
| | | import org.springframework.transaction.support.TransactionSynchronization; |
| | | import org.springframework.transaction.support.TransactionSynchronizationManager; |
| | | import org.springframework.transaction.support.TransactionTemplate; |
| | | import org.springframework.util.DigestUtils; |
| | | import org.springframework.util.StringUtils; |
| | | |
| | | import javax.annotation.Resource; |
| | |
| | | import java.math.BigDecimal; |
| | | import java.math.MathContext; |
| | | import java.math.RoundingMode; |
| | | import java.nio.charset.StandardCharsets; |
| | | import java.text.ParseException; |
| | | import java.text.SimpleDateFormat; |
| | | import java.time.*; |
| | |
| | | private static final int ENTERPRISE_INVOICE_BATCH_SIZE = 500; |
| | | private static final int ENTERPRISE_ORDER_ID_BATCH_SIZE = 500; |
| | | private static final int CHARGING_PERIOD_ACCOUNTING_BATCH_SIZE = 500; |
| | | private static final String LARGE_SCREEN_CACHE_PREFIX = "large:screen:"; |
| | | private static final long LARGE_SCREEN_CACHE_SECONDS = 60L; |
| | | private static final LocalDate LARGE_SCREEN_STAT_DATA_START = LocalDate.of(2020, 1, 1); |
| | | private static final DateTimeFormatter LARGE_SCREEN_MONTH_FMT = DateTimeFormatter.ofPattern("yyyy-MM"); |
| | | private static final DateTimeFormatter LARGE_SCREEN_DAY_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd"); |
| | | private static final DateTimeFormatter LARGE_SCREEN_SQL_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); |
| | | |
| | | @Override |
| | | public ReservationRecommendationVO getReservationRecommendation(Integer siteId) { |
| | |
| | | String startTime2 = timeRange[1]; |
| | | String endTime1 = timeRange[2]; |
| | | String endTime2 = timeRange[3]; |
| | | PageInfo<ChargingOrderVO> pageInfo = new PageInfo<>(dto.getPageCurr(), dto.getPageSize()); |
| | | int pageCurr = dto.getPageCurr() == null ? 1 : dto.getPageCurr(); |
| | | int pageSize = dto.getPageSize() == null ? 50 : dto.getPageSize(); |
| | | PageInfo<ChargingOrderVO> pageInfo = new PageInfo<>(pageCurr, pageSize); |
| | | Long userId = tokenService.getLoginUser().getUserid(); |
| | | SysUser sysUser = sysUserClient.getSysUser(userId).getData(); |
| | | Integer roleType = sysUser.getRoleType(); |
| | | setChargingOrderAuthSiteIds(dto, sysUser, userId, roleType); |
| | | List<ChargingOrderVO> list = this.baseMapper.chargingOrder(pageInfo, dto, startTime1, startTime2, endTime1, endTime2); |
| | | if (StringUtils.hasLength(dto.getPhone()) && (dto.getUserIds() == null || dto.getUserIds().isEmpty())) { |
| | | List<Long> data = appUserClient.getUserIdsByPhone(dto.getPhone()).getData(); |
| | | if (data == null || data.isEmpty()) { |
| | | pageInfo.setTotal(0L); |
| | | pageInfo.setRecords(Collections.emptyList()); |
| | | return pageInfo; |
| | | } |
| | | dto.setUserIds(data); |
| | | } |
| | | // 手动分页:不传 IPage(避免分页拦截器生成无法下推的全量 COUNT),COUNT 与 ORDER BY + LIMIT 均由 SQL 下推到分片 |
| | | long offset = (long) (pageCurr - 1) * pageSize; |
| | | long total = this.baseMapper.chargingOrderCount(dto, startTime1, startTime2, endTime1, endTime2); |
| | | pageInfo.setTotal(total); |
| | | List<ChargingOrderVO> list = total == 0L ? Collections.emptyList() |
| | | : this.baseMapper.chargingOrder(dto, startTime1, startTime2, endTime1, endTime2, offset, pageSize); |
| | | fillChargingOrderBalanceIncome(list); |
| | | fillChargingOrderShareIncome(list); |
| | | fillChargingOrderPageExtra(list, dto.getSiteIds(), sysUser, roleType); |
| | |
| | | public TCharingOrderVO chargingOrderSummary(ChargingOrderQuery dto) { |
| | | TCharingOrderVO tCharingOrderVO = new TCharingOrderVO(); |
| | | fillEmptyChargingOrderSummary(tCharingOrderVO); |
| | | if (isChargingOrderFullSweep(dto)) { |
| | | // 无任何筛选即全量扫描:汇总聚合跨分片 JOIN(accounting_strategy,分片键 charging_order_id 与 t_charging_order 不一致) |
| | | // 且需把全量订单 id 反查 share/balance,MyCat 只能全量拉回代理层,必然超时。汇总没有 LIMIT 可下推,直接返回空汇总。 |
| | | return tCharingOrderVO; |
| | | } |
| | | String[] timeRange = parseChargingOrderTimeRange(dto); |
| | | String startTime1 = timeRange[0]; |
| | | String startTime2 = timeRange[1]; |
| | |
| | | return new String[]{startTime1, startTime2, endTime1, endTime2}; |
| | | } |
| | | |
| | | /** 是否全量扫描:用户未提供任何可收窄的筛选条件(时间/订单号/手机号/站点/桩/枪/状态等),无界聚合无法下推。 */ |
| | | private boolean isChargingOrderFullSweep(ChargingOrderQuery dto) { |
| | | return !StringUtils.hasLength(dto.getStartTime()) |
| | | && !StringUtils.hasLength(dto.getEndTime()) |
| | | && !StringUtils.hasLength(dto.getCode()) |
| | | && !StringUtils.hasLength(dto.getPhone()) |
| | | && dto.getSiteId() == null |
| | | && dto.getPileId() == null |
| | | && dto.getGunId() == null |
| | | && dto.getOrderType() == null |
| | | && dto.getStatus() == null |
| | | && dto.getIsWhitelist() == null |
| | | && dto.getPaymentType() == null |
| | | && dto.getOrderSource() == null |
| | | && (dto.getUserIds() == null || dto.getUserIds().isEmpty()); |
| | | } |
| | | |
| | | private void fillChargingOrderPageExtra(List<ChargingOrderVO> list, List<Integer> siteIds, SysUser sysUser, Integer roleType) { |
| | | if (list == null || list.isEmpty()) { |
| | | return; |
| | |
| | | Map<Integer, Site> siteMap = pageSites.stream() |
| | | .filter(item -> item.getId() != null) |
| | | .collect(Collectors.toMap(Site::getId, item -> item, (a, b) -> a)); |
| | | // 列表 SQL 不再跨库 JOIN app_user/partner(避免分片表无筛选全量广播),手机号与合作商名在此回填 |
| | | Map<Long, String> phoneMap = new HashMap<>(); |
| | | List<Long> pageUserIds = list.stream().map(ChargingOrderVO::getAppUserId).filter(Objects::nonNull).distinct().collect(Collectors.toList()); |
| | | if (!pageUserIds.isEmpty()) { |
| | | List<TAppUser> pageUsers = Optional.ofNullable(appUserClient.getUserByIds(pageUserIds).getData()).orElse(new ArrayList<>()); |
| | | for (TAppUser user : pageUsers) { |
| | | if (user != null && user.getId() != null) { |
| | | phoneMap.put(user.getId(), user.getPhone()); |
| | | } |
| | | } |
| | | } |
| | | Map<Integer, String> partnerNameMap = new HashMap<>(); |
| | | List<Integer> pagePartnerIds = siteMap.values().stream().map(Site::getPartnerId).filter(Objects::nonNull).distinct().collect(Collectors.toList()); |
| | | for (Integer partnerId : pagePartnerIds) { |
| | | Partner partner = Optional.ofNullable(partnerClient.getPartnerById(partnerId).getData()).orElse(null); |
| | | if (partner != null && partner.getId() != null) { |
| | | partnerNameMap.put(partner.getId(), partner.getName()); |
| | | } |
| | | } |
| | | Map<Long, List<TChargingOrderAccountingStrategy>> strategyMap = orderIds.isEmpty() ? new HashMap<>() : |
| | | chargingOrderAccountingStrategyService.lambdaQuery() |
| | | .in(TChargingOrderAccountingStrategy::getChargingOrderId, orderIds) |
| | |
| | | if (roleType == 2 && chargingOrderVO.getSiteId() != null) { |
| | | chargingOrderVO.setAuthRecord(Boolean.TRUE.equals(authRecordMap.get(chargingOrderVO.getSiteId()))); |
| | | } |
| | | chargingOrderVO.setPhone(phoneMap.get(chargingOrderVO.getAppUserId())); |
| | | chargingOrderVO.setCommissionAmount(chargingOrderVO.getRechargeAmount() != null ? chargingOrderVO.getRechargeAmount().multiply(new BigDecimal("0.006")) : new BigDecimal("0")); |
| | | chargingOrderVO.setPlatFormMoney(chargingOrderVO.getRechargeAmount() != null ? chargingOrderVO.getRechargeAmount().multiply(new BigDecimal("0.006")).setScale(2, BigDecimal.ROUND_DOWN) : new BigDecimal("0")); |
| | | chargingOrderVO.setUid(chargingOrderVO.getId() + ""); |
| | | Site site = siteMap.get(chargingOrderVO.getSiteId()); |
| | | if (site != null) { |
| | | chargingOrderVO.setSiteName(site.getName()); |
| | | chargingOrderVO.setPartnerName(partnerNameMap.get(site.getPartnerId())); |
| | | } |
| | | TChargingGun gun = gunMap.get(chargingOrderVO.getChargingGunId()); |
| | | TChargingPile pile = pileMap.get(chargingOrderVO.getChargingPileId()); |
| | |
| | | */ |
| | | @Override |
| | | public List<ChargingOrderBalanceIncomeDetailVO> balanceIncomeDetail(ChargingOrderQuery dto) { |
| | | if (isChargingOrderFullSweep(dto)) { |
| | | // 与汇总同根因:全量扫描会把全量订单 id 反查 exchange_code_use_record,MyCat 超时,直接返回空明细 |
| | | return new ArrayList<>(); |
| | | } |
| | | String startTime1 = null; |
| | | String startTime2 = null; |
| | | String endTime1 = null; |
| | |
| | | |
| | | @Override |
| | | public R<PageInfo<EnterpriseChargingAnalysisVO>> enterpriseChargingAnalysis(EnterpriseChargingAnalysisQuery query) { |
| | | PageInfo<EnterpriseChargingAnalysisVO> pageInfo = new PageInfo<>(query.getPageCurr(), query.getPageSize()); |
| | | List<EnterpriseChargingAnalysisVO> list = this.baseMapper.enterpriseChargingAnalysis(pageInfo, query); |
| | | if (list == null || list.isEmpty()) { |
| | | int pageCurr = query.getPageCurr() == null ? 1 : query.getPageCurr(); |
| | | int pageSize = query.getPageSize() == null ? 50 : query.getPageSize(); |
| | | PageInfo<EnterpriseChargingAnalysisVO> pageInfo = new PageInfo<>(pageCurr, pageSize); |
| | | // 逆反查聚合(不再跨分片 JOIN,构建块见 TChargingOrderMapper.xml enterpriseInvoiceRows 等): |
| | | // 1) t_order_invoice 单表查企业发票(10 片按 app_user_id 分片,抬头级条件可下推) |
| | | // 2) t_order_invoice_detail 按 order_invoice_id IN 路由查充电明细 |
| | | // 3) 命中订单级条件(站点/时间等)→ 命中订单 id |
| | | // 4) t_charging_order 按 id IN 走主键取 start_time/site_id |
| | | // 5) t_site 单表取站点名 |
| | | // 6) 按公司聚合 + 手工分页(不再传 IPage,避免分页拦截器生成无法下推的全量 COUNT) |
| | | List<EnterpriseChargingAggRow.Invoice> invoices = this.baseMapper.enterpriseInvoiceRows(query); |
| | | if (invoices == null || invoices.isEmpty()) { |
| | | pageInfo.setRecords(Collections.emptyList()); |
| | | return R.ok(pageInfo); |
| | | } |
| | | List<String> companyNames = list.stream() |
| | | .map(EnterpriseChargingAnalysisVO::getCompanyName) |
| | | Map<Long, List<EnterpriseChargingAggRow.Detail>> detailsByInvoice = new HashMap<>(); |
| | | Map<Long, EnterpriseChargingAggRow.Detail> detailByOrder = new LinkedHashMap<>(); |
| | | List<Long> invoiceIds = invoices.stream() |
| | | .map(EnterpriseChargingAggRow.Invoice::getId) |
| | | .filter(Objects::nonNull) |
| | | .distinct() |
| | | .collect(Collectors.toList()); |
| | | Map<String, List<Map<String, Object>>> topSitesByCompany = groupByCompany( |
| | | this.baseMapper.enterpriseTopSitesByCompanies(query, companyNames)); |
| | | Map<String, List<Map<String, Object>>> chargingTimesByCompany = groupByCompany( |
| | | this.baseMapper.enterpriseChargingTimesByCompanies(query, companyNames)); |
| | | for (EnterpriseChargingAnalysisVO row : list) { |
| | | String companyKey = companyKey(row.getCompanyName(), row.getSocialCreditCode()); |
| | | List<Map<String, Object>> topSites = topSitesByCompany.getOrDefault(companyKey, Collections.emptyList()); |
| | | String topSiteNames = topSites.stream() |
| | | .sorted(Comparator.comparing((Map<String, Object> item) -> ((Number) item.getOrDefault("counts", 0)).longValue()).reversed()) |
| | | .limit(3) |
| | | .map(item -> Objects.toString(item.get("siteName"), "")) |
| | | .filter(org.springframework.util.StringUtils::hasText) |
| | | .collect(Collectors.joining("、")); |
| | | row.setTopSiteNames(topSiteNames); |
| | | List<LocalDateTime> chargingTimes = chargingTimesByCompany.getOrDefault(companyKey, Collections.emptyList()).stream() |
| | | .map(item -> toLocalDateTime(item.get("startTime"))) |
| | | .filter(Objects::nonNull) |
| | | .sorted() |
| | | .collect(Collectors.toList()); |
| | | row.setAverageChargingInterval(calculateAverageChargingInterval(chargingTimes)); |
| | | for (int start = 0; start < invoiceIds.size(); start += ENTERPRISE_INVOICE_BATCH_SIZE) { |
| | | int end = Math.min(start + ENTERPRISE_INVOICE_BATCH_SIZE, invoiceIds.size()); |
| | | List<EnterpriseChargingAggRow.Detail> details = |
| | | this.baseMapper.enterpriseInvoiceDetailRows(invoiceIds.subList(start, end)); |
| | | if (details == null) { |
| | | continue; |
| | | } |
| | | for (EnterpriseChargingAggRow.Detail detail : details) { |
| | | if (detail.getOrderId() == null) { |
| | | continue; |
| | | } |
| | | detailsByInvoice.computeIfAbsent(detail.getInvoiceId(), k -> new ArrayList<>()).add(detail); |
| | | detailByOrder.putIfAbsent(detail.getOrderId(), detail); |
| | | } |
| | | } |
| | | pageInfo.setRecords(list); |
| | | if (detailByOrder.isEmpty()) { |
| | | pageInfo.setRecords(Collections.emptyList()); |
| | | return R.ok(pageInfo); |
| | | } |
| | | List<Long> candidateOrderIds = new ArrayList<>(detailByOrder.keySet()); |
| | | boolean orderFilter = hasEnterpriseOrderFilter(query); |
| | | Set<Long> matchedOrderIds = null; |
| | | if (orderFilter) { |
| | | matchedOrderIds = new HashSet<>(); |
| | | for (int start = 0; start < candidateOrderIds.size(); start += ENTERPRISE_ORDER_ID_BATCH_SIZE) { |
| | | int end = Math.min(start + ENTERPRISE_ORDER_ID_BATCH_SIZE, candidateOrderIds.size()); |
| | | List<Long> matched = this.baseMapper.enterpriseAnalysisMatchingOrderIds( |
| | | candidateOrderIds.subList(start, end), query); |
| | | if (matched != null) { |
| | | matchedOrderIds.addAll(matched); |
| | | } |
| | | } |
| | | if (matchedOrderIds.isEmpty()) { |
| | | pageInfo.setRecords(Collections.emptyList()); |
| | | return R.ok(pageInfo); |
| | | } |
| | | } |
| | | List<Long> effectiveOrderIds = orderFilter ? new ArrayList<>(matchedOrderIds) : candidateOrderIds; |
| | | Map<Long, EnterpriseChargingAggRow.Order> orderMap = new HashMap<>(); |
| | | for (int start = 0; start < effectiveOrderIds.size(); start += ENTERPRISE_ORDER_ID_BATCH_SIZE) { |
| | | int end = Math.min(start + ENTERPRISE_ORDER_ID_BATCH_SIZE, effectiveOrderIds.size()); |
| | | List<EnterpriseChargingAggRow.Order> orders = |
| | | this.baseMapper.enterpriseAnalysisChargingOrders(effectiveOrderIds.subList(start, end)); |
| | | if (orders == null) { |
| | | continue; |
| | | } |
| | | for (EnterpriseChargingAggRow.Order order : orders) { |
| | | orderMap.put(order.getOrderId(), order); |
| | | } |
| | | } |
| | | Set<Long> siteIds = orderMap.values().stream() |
| | | .map(EnterpriseChargingAggRow.Order::getSiteId) |
| | | .filter(Objects::nonNull) |
| | | .collect(Collectors.toSet()); |
| | | Map<Long, String> siteNameMap = new HashMap<>(); |
| | | if (!siteIds.isEmpty()) { |
| | | List<EnterpriseChargingAggRow.Site> sites = this.baseMapper.enterpriseAnalysisSites(new ArrayList<>(siteIds)); |
| | | if (sites != null) { |
| | | for (EnterpriseChargingAggRow.Site site : sites) { |
| | | siteNameMap.put(site.getSiteId(), site.getSiteName()); |
| | | } |
| | | } |
| | | } |
| | | Map<String, EnterpriseCompanyAgg> companyAggs = new LinkedHashMap<>(); |
| | | for (EnterpriseChargingAggRow.Invoice invoice : invoices) { |
| | | List<EnterpriseChargingAggRow.Detail> details = |
| | | detailsByInvoice.getOrDefault(invoice.getId(), Collections.emptyList()); |
| | | if (details.isEmpty()) { |
| | | // 无充电明细(order_type=1)的发票不计,等同原 INNER JOIN tod.order_type=1 语义 |
| | | continue; |
| | | } |
| | | List<EnterpriseChargingAggRow.Order> invoiceOrders = new ArrayList<>(); |
| | | BigDecimal detailAmount = BigDecimal.ZERO; |
| | | for (EnterpriseChargingAggRow.Detail detail : details) { |
| | | if (orderFilter && !matchedOrderIds.contains(detail.getOrderId())) { |
| | | continue; |
| | | } |
| | | detailAmount = detailAmount.add(nullToZero(detail.getInvoiceAmount())); |
| | | EnterpriseChargingAggRow.Order order = orderMap.get(detail.getOrderId()); |
| | | if (order != null) { |
| | | invoiceOrders.add(order); |
| | | } |
| | | } |
| | | if (orderFilter && invoiceOrders.isEmpty()) { |
| | | // 订单级筛选后该发票无命中订单 → 剔除,等同原 LEFT JOIN + 订单级 WHERE 语义 |
| | | continue; |
| | | } |
| | | BigDecimal invoiceAmount = detailAmount.signum() > 0 ? detailAmount : nullToZero(invoice.getTotalAmount()); |
| | | String key = companyKey(invoice.getCompanyName(), invoice.getSocialCreditCode()); |
| | | EnterpriseCompanyAgg agg = companyAggs.get(key); |
| | | if (agg == null) { |
| | | agg = new EnterpriseCompanyAgg(invoice.getCompanyName(), invoice.getSocialCreditCode(), |
| | | invoice.getCompanyAddress(), invoice.getCompanyPhone()); |
| | | companyAggs.put(key, agg); |
| | | } |
| | | agg.invoiceCount++; |
| | | agg.invoiceAmount = agg.invoiceAmount.add(invoiceAmount); |
| | | for (EnterpriseChargingAggRow.Order order : invoiceOrders) { |
| | | if (order.getStartTime() != null) { |
| | | agg.startTimes.add(order.getStartTime()); |
| | | if (agg.latestChargingTime == null || order.getStartTime().isAfter(agg.latestChargingTime)) { |
| | | agg.latestChargingTime = order.getStartTime(); |
| | | } |
| | | } |
| | | if (order.getSiteId() != null) { |
| | | agg.siteOrderIds.computeIfAbsent(order.getSiteId(), k -> new HashSet<>()).add(order.getOrderId()); |
| | | } |
| | | } |
| | | } |
| | | if (companyAggs.isEmpty()) { |
| | | pageInfo.setRecords(Collections.emptyList()); |
| | | return R.ok(pageInfo); |
| | | } |
| | | List<EnterpriseChargingAnalysisVO> all = companyAggs.values().stream() |
| | | .sorted(Comparator.comparingLong((EnterpriseCompanyAgg agg) -> agg.invoiceCount).reversed()) |
| | | .map(agg -> buildEnterpriseAnalysisVO(agg, siteNameMap)) |
| | | .collect(Collectors.toList()); |
| | | pageInfo.setTotal(all.size()); |
| | | long offset = (long) (pageCurr - 1L) * pageSize; |
| | | if (offset >= all.size()) { |
| | | pageInfo.setRecords(Collections.emptyList()); |
| | | } else { |
| | | int toIndex = (int) Math.min(offset + pageSize, all.size()); |
| | | pageInfo.setRecords(all.subList((int) offset, toIndex)); |
| | | } |
| | | return R.ok(pageInfo); |
| | | } |
| | | |
| | | private Map<String, List<Map<String, Object>>> groupByCompany(List<Map<String, Object>> rows) { |
| | | if (rows == null || rows.isEmpty()) { |
| | | return Collections.emptyMap(); |
| | | private boolean hasEnterpriseOrderFilter(EnterpriseChargingAnalysisQuery query) { |
| | | return query.getSiteType() != null |
| | | || query.getPartnerId() != null |
| | | || query.getSiteId() != null |
| | | || query.getCreateTime1() != null |
| | | || query.getCreateTime2() != null |
| | | || (query.getCode() != null && !query.getCode().isEmpty()) |
| | | || query.getOrderType() != null |
| | | || (query.getPhone() != null && !query.getPhone().isEmpty()) |
| | | || query.getStatus() != null |
| | | || (query.getPartnerName() != null && !query.getPartnerName().isEmpty()) |
| | | || query.getOrderSource() != null |
| | | || query.getStartTime1() != null |
| | | || query.getStartTime2() != null |
| | | || query.getEndTime1() != null |
| | | || query.getEndTime2() != null; |
| | | } |
| | | |
| | | private EnterpriseChargingAnalysisVO buildEnterpriseAnalysisVO(EnterpriseCompanyAgg agg, |
| | | Map<Long, String> siteNameMap) { |
| | | EnterpriseChargingAnalysisVO vo = new EnterpriseChargingAnalysisVO(); |
| | | vo.setCompanyName(agg.companyName); |
| | | vo.setSocialCreditCode(agg.socialCreditCode); |
| | | vo.setCompanyAddress(agg.companyAddress); |
| | | vo.setCompanyPhone(agg.companyPhone); |
| | | vo.setInvoiceCount(agg.invoiceCount); |
| | | vo.setInvoiceAmount(agg.invoiceAmount); |
| | | vo.setLatestChargingTime(agg.latestChargingTime); |
| | | String topSiteNames = agg.siteOrderIds.entrySet().stream() |
| | | .sorted(Comparator.comparingInt((Map.Entry<Long, Set<Long>> e) -> e.getValue().size()).reversed()) |
| | | .limit(3) |
| | | .map(e -> siteNameMap.getOrDefault(e.getKey(), "")) |
| | | .filter(org.springframework.util.StringUtils::hasText) |
| | | .collect(Collectors.joining("、")); |
| | | vo.setTopSiteNames(topSiteNames); |
| | | vo.setAverageChargingInterval(calculateAverageChargingInterval(new ArrayList<>(agg.startTimes))); |
| | | return vo; |
| | | } |
| | | |
| | | private static class EnterpriseCompanyAgg { |
| | | private final String companyName; |
| | | private final String socialCreditCode; |
| | | private final String companyAddress; |
| | | private final String companyPhone; |
| | | private long invoiceCount; |
| | | private BigDecimal invoiceAmount = BigDecimal.ZERO; |
| | | private LocalDateTime latestChargingTime; |
| | | private final Map<Long, Set<Long>> siteOrderIds = new HashMap<>(); |
| | | private final TreeSet<LocalDateTime> startTimes = new TreeSet<>(); |
| | | |
| | | private EnterpriseCompanyAgg(String companyName, String socialCreditCode, |
| | | String companyAddress, String companyPhone) { |
| | | this.companyName = companyName; |
| | | this.socialCreditCode = socialCreditCode; |
| | | this.companyAddress = companyAddress; |
| | | this.companyPhone = companyPhone; |
| | | } |
| | | return rows.stream().collect(Collectors.groupingBy(row -> companyKey( |
| | | Objects.toString(row.get("companyName"), null), |
| | | Objects.toString(row.get("socialCreditCode"), "")))); |
| | | } |
| | | |
| | | private String companyKey(String companyName, String socialCreditCode) { |
| | | return Objects.toString(companyName, "") + "\u0000" + Objects.toString(socialCreditCode, ""); |
| | | } |
| | | |
| | | private LocalDateTime toLocalDateTime(Object value) { |
| | | if (value instanceof LocalDateTime) { |
| | | return (LocalDateTime) value; |
| | | } |
| | | if (value instanceof java.sql.Timestamp) { |
| | | return ((java.sql.Timestamp) value).toLocalDateTime(); |
| | | } |
| | | if (value instanceof Date) { |
| | | return LocalDateTime.ofInstant(((Date) value).toInstant(), ZoneId.systemDefault()); |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | @Override |
| | |
| | | .divide(BigDecimal.valueOf(3600), 2, RoundingMode.HALF_UP); |
| | | } |
| | | |
| | | private static final int SERIAL_ORDER_ID_SEARCH_LIMIT = 5000; |
| | | private static final int SITE_ORDER_ID_SEARCH_LIMIT = 50000; |
| | | |
| | | @Override |
| | | public R<PageInfo<TChargingOrderRefund>> getRefundList(ChargingRefundDto chargingRefundDto) { |
| | | PageInfo<TChargingOrderRefund> pageInfo = new PageInfo<>(chargingRefundDto.getPageCurr(), chargingRefundDto.getPageSize()); |
| | | boolean withoutOrderJoin = !StringUtils.hasLength(chargingRefundDto.getSerialNumber()) |
| | | && (chargingRefundDto.getSiteIds() == null || chargingRefundDto.getSiteIds().isEmpty()); |
| | | List<TChargingOrderRefund> list = withoutOrderJoin |
| | | ? this.baseMapper.getRefundListWithoutOrderJoin(pageInfo, chargingRefundDto) |
| | | : this.baseMapper.getRefundList(pageInfo, chargingRefundDto); |
| | | if (withoutOrderJoin) { |
| | | enrichRefundOrderInfo(list); |
| | | int pageCurr = chargingRefundDto.getPageCurr() == null ? 1 : chargingRefundDto.getPageCurr(); |
| | | int pageSize = chargingRefundDto.getPageSize() == null ? 50 : chargingRefundDto.getPageSize(); |
| | | PageInfo<TChargingOrderRefund> pageInfo = new PageInfo<>(pageCurr, pageSize); |
| | | long offset = (long) (pageCurr - 1) * pageSize; |
| | | // 每个 UNION 分支须取 offset+pageSize 行,外层再按 offset,pageSize 切页,否则翻页时池子不足导致丢数据 |
| | | int branchLimit = (int) Math.min(offset + pageSize, Integer.MAX_VALUE); |
| | | boolean filterBySerial = StringUtils.hasLength(chargingRefundDto.getSerialNumber()); |
| | | boolean filterBySite = chargingRefundDto.getSiteIds() != null && !chargingRefundDto.getSiteIds().isEmpty(); |
| | | |
| | | List<TChargingOrderRefund> list; |
| | | if (!filterBySerial && !filterBySite) { |
| | | // 无筛选:手工分页。分页拦截器会对 UNION 查询生成全量 COUNT 且无法把 ORDER BY/LIMIT 下推到分片, |
| | | // 这里改为分支内下推(getRefundListPage)加分支 COUNT 求和(countRefundList) |
| | | pageInfo.setTotal(this.baseMapper.countRefundList(chargingRefundDto)); |
| | | list = this.baseMapper.getRefundListPage(chargingRefundDto, offset, pageSize, branchLimit); |
| | | } else { |
| | | // 筛选:逆反查。先按条件查订单 id(t_charging_order 等可下推/走索引), |
| | | // 再按退款表分片键 IN 过滤(可路由),替代跨分片 LEFT JOIN |
| | | RefundOrderIds ids = resolveRefundOrderIds(chargingRefundDto); |
| | | if (ids.isEmpty()) { |
| | | pageInfo.setRecords(Collections.emptyList()); |
| | | return R.ok(pageInfo); |
| | | } |
| | | pageInfo.setTotal(this.baseMapper.countRefundListByOrderIds(chargingRefundDto, |
| | | ids.type1OrderIds, ids.type2OrderIds, ids.type3OrderIds)); |
| | | list = this.baseMapper.getRefundListByOrderIds(chargingRefundDto, |
| | | ids.type1OrderIds, ids.type2OrderIds, ids.type3OrderIds, offset, pageSize, branchLimit); |
| | | } |
| | | enrichRefundOrderInfo(list); |
| | | for (TChargingOrderRefund refund : list) { |
| | | refund.setUid(refund.getType() + "-" + refund.getId()); |
| | | } |
| | | pageInfo.setRecords(list); |
| | | |
| | | return R.ok(pageInfo); |
| | | } |
| | | |
| | | /** |
| | | * 逆反查订单 id:serialNumber 匹配充电/商城/会员订单 id(各自 LIMIT 防爆), |
| | | * siteIds 匹配充电订单 id;两者同时存在时取交集。 |
| | | * 返回约定:null = 该类型不过滤;空列表 = 该类型无匹配(SQL 侧 AND 1=0)。 |
| | | */ |
| | | private RefundOrderIds resolveRefundOrderIds(ChargingRefundDto chargingRefundDto) { |
| | | List<Long> type1OrderIds = null; |
| | | List<Long> type2OrderIds = null; |
| | | List<Long> type3OrderIds = null; |
| | | if (StringUtils.hasLength(chargingRefundDto.getSerialNumber())) { |
| | | // 模糊匹配结果可能很大(短流水号),截断上限防 IN 列表爆炸;匹配不到则列表为空 |
| | | type1OrderIds = safeIds(this.baseMapper.listChargingOrderIdsBySerialNumber( |
| | | chargingRefundDto.getSerialNumber(), SERIAL_ORDER_ID_SEARCH_LIMIT)); |
| | | type2OrderIds = safeIds(this.baseMapper.listShoppingOrderIdsBySerialNumber( |
| | | chargingRefundDto.getSerialNumber(), SERIAL_ORDER_ID_SEARCH_LIMIT)); |
| | | type3OrderIds = safeIds(this.baseMapper.listVipOrderIdsBySerialNumber( |
| | | chargingRefundDto.getSerialNumber(), SERIAL_ORDER_ID_SEARCH_LIMIT)); |
| | | warnIfTruncated(chargingRefundDto.getSerialNumber(), type1OrderIds, |
| | | SERIAL_ORDER_ID_SEARCH_LIMIT, "充电订单"); |
| | | warnIfTruncated(chargingRefundDto.getSerialNumber(), type2OrderIds, |
| | | SERIAL_ORDER_ID_SEARCH_LIMIT, "商城订单"); |
| | | warnIfTruncated(chargingRefundDto.getSerialNumber(), type3OrderIds, |
| | | SERIAL_ORDER_ID_SEARCH_LIMIT, "会员订单"); |
| | | } |
| | | if (chargingRefundDto.getSiteIds() != null && !chargingRefundDto.getSiteIds().isEmpty()) { |
| | | List<Long> siteMatched = safeIds(this.baseMapper.listChargingOrderIdsBySiteIds( |
| | | chargingRefundDto.getSiteIds(), SITE_ORDER_ID_SEARCH_LIMIT)); |
| | | warnIfTruncated(String.valueOf(chargingRefundDto.getSiteIds()), siteMatched, |
| | | SITE_ORDER_ID_SEARCH_LIMIT, "站点订单"); |
| | | if (type1OrderIds == null) { |
| | | type1OrderIds = siteMatched; |
| | | } else { |
| | | type1OrderIds.retainAll(new HashSet<>(siteMatched)); |
| | | } |
| | | } |
| | | return new RefundOrderIds(type1OrderIds, type2OrderIds, type3OrderIds); |
| | | } |
| | | |
| | | /** 反查结果达到上限说明可能被截断,告警便于排查(截断会导致部分退款不在列表内) */ |
| | | private void warnIfTruncated(String condition, List<Long> ids, int limit, String orderType) { |
| | | if (ids.size() >= limit) { |
| | | log.warn("退款列表逆反查:条件[{}]匹配{}数达到上限{},结果可能被截断", condition, orderType, limit); |
| | | } |
| | | } |
| | | |
| | | private static List<Long> safeIds(List<Long> ids) { |
| | | return ids == null ? new ArrayList<>() : new ArrayList<>(ids); |
| | | } |
| | | |
| | | /** 逆反查结果:null = 不过滤;空列表 = 无匹配 */ |
| | | private static class RefundOrderIds { |
| | | private final List<Long> type1OrderIds; |
| | | private final List<Long> type2OrderIds; |
| | | private final List<Long> type3OrderIds; |
| | | |
| | | private RefundOrderIds(List<Long> type1OrderIds, List<Long> type2OrderIds, List<Long> type3OrderIds) { |
| | | this.type1OrderIds = type1OrderIds; |
| | | this.type2OrderIds = type2OrderIds; |
| | | this.type3OrderIds = type3OrderIds; |
| | | } |
| | | |
| | | /** 三个类型全部明确无匹配(非 null 且为空)时,整页为空,可短路 */ |
| | | private boolean isEmpty() { |
| | | return type1OrderIds != null && type1OrderIds.isEmpty() |
| | | && type2OrderIds != null && type2OrderIds.isEmpty() |
| | | && type3OrderIds != null && type3OrderIds.isEmpty(); |
| | | } |
| | | } |
| | | |
| | | private void enrichRefundOrderInfo(List<TChargingOrderRefund> refunds) { |
| | |
| | | } |
| | | } else { |
| | | List<SixChargingDto> settlementIncome = this.baseMapper.chargeSettlement(sixBeforeValue, siteIds); |
| | | if (settlementIncome != null) { |
| | | result.addAll(settlementIncome); |
| | | } |
| | | List<SixChargingDto> orderHistoryIncome = this.baseMapper.chargeOrderHistory(sixBeforeValue, siteIds); |
| | | result.addAll(mergeMissingMonthlyIncome(settlementIncome, orderHistoryIncome)); |
| | | String siteIdsCsv = siteIds == null ? null : siteIds.stream() |
| | | .filter(Objects::nonNull) |
| | | .map(String::valueOf) |
| | |
| | | return result; |
| | | } |
| | | |
| | | static List<SixChargingDto> mergeMissingMonthlyIncome(List<SixChargingDto> settlementIncome, |
| | | List<SixChargingDto> orderHistoryIncome) { |
| | | List<SixChargingDto> merged = new ArrayList<>(); |
| | | Set<Integer> settledMonths = new HashSet<>(); |
| | | if (settlementIncome != null) { |
| | | for (SixChargingDto item : settlementIncome) { |
| | | if (item == null) { |
| | | continue; |
| | | } |
| | | merged.add(item); |
| | | Integer month = incomeMonth(item); |
| | | if (month != null) { |
| | | settledMonths.add(month); |
| | | } |
| | | } |
| | | } |
| | | if (orderHistoryIncome != null) { |
| | | for (SixChargingDto item : orderHistoryIncome) { |
| | | Integer month = incomeMonth(item); |
| | | if (item != null && month != null && settledMonths.add(month)) { |
| | | merged.add(item); |
| | | } |
| | | } |
| | | } |
| | | return merged; |
| | | } |
| | | |
| | | private static Integer incomeMonth(SixChargingDto item) { |
| | | if (item == null || item.getMonth() == null) { |
| | | return null; |
| | | } |
| | | try { |
| | | int month = Integer.parseInt(item.getMonth()); |
| | | return month >= 1 && month <= 12 ? month : null; |
| | | } catch (NumberFormatException ignored) { |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public List<SixCircleDto> circle(List<Integer> siteIds, LocalDate sixBefore, Boolean partnerView) { |
| | | List<SixCircleDto> result = this.baseMapper.circle(siteIds, sixBefore, partnerView); |
| | |
| | | } |
| | | |
| | | @Override |
| | | public Map<String, Object> countAll(List<Integer> siteIds, Boolean partnerView) { |
| | | Map<String, Object> result = this.baseMapper.countAll(siteIds, partnerView); |
| | | public Map<String, Object> countAll(List<Integer> siteIds, Boolean partnerView, LocalDate sixBefore) { |
| | | Map<String, Object> result = this.baseMapper.countAll(siteIds, partnerView, sixBefore); |
| | | if (!Boolean.TRUE.equals(partnerView) && result != null) { |
| | | result.put("commission_amount", this.baseMapper.sumSuccessfulShareAmount(siteIds)); |
| | | result.put("commission_amount", this.baseMapper.sumSuccessfulShareAmount(siteIds, sixBefore)); |
| | | } |
| | | return result; |
| | | } |
| | |
| | | |
| | | @Override |
| | | public Map<String, Object> getLargeScreenSummary(List<Integer> siteIds) { |
| | | return this.baseMapper.getLargeScreenSummary(siteIds); |
| | | String cacheKey = largeScreenCacheKey("summary", siteIds); |
| | | Map<String, Object> cached = redisService.getCacheObject(cacheKey); |
| | | if (cached != null) { |
| | | return cached; |
| | | } |
| | | Map<String, Object> result = new HashMap<>(); |
| | | try { |
| | | Map<String, Object> summary = this.baseMapper.summaryFromStat(siteIds); |
| | | if (summary != null) { |
| | | result.putAll(summary); |
| | | } |
| | | // 充电用户数精确去重(用户-站点对表, 跨站不重复); 对表未回填时回退到按站点求和 |
| | | Long pairCount = this.baseMapper.userSiteTableCount(); |
| | | if (pairCount != null && pairCount > 0) { |
| | | Long exactUserCount = this.baseMapper.userCountFromStat(siteIds); |
| | | if (exactUserCount != null) { |
| | | result.put("chargingUserCount", exactUserCount); |
| | | } |
| | | } |
| | | Long registered = this.baseMapper.registeredFromStat(); |
| | | result.put("registeredUserCount", registered == null ? 0L : registered); |
| | | } catch (Exception e) { |
| | | log.warn("大屏累计指标读取预聚合表失败, siteIds={}", siteIds, e); |
| | | } |
| | | Arrays.asList("validOrderCount", "chargingUserCount", "registeredUserCount", |
| | | "totalChargingCapacity", "electricityFee", "serviceFee", "totalIncome") |
| | | .forEach(key -> result.putIfAbsent(key, 0)); |
| | | redisService.setCacheObject(cacheKey, result, LARGE_SCREEN_CACHE_SECONDS, TimeUnit.SECONDS); |
| | | return result; |
| | | } |
| | | |
| | | @Override |
| | | public List<SixChargingDto> getLargeScreenIncomeTrend(List<Integer> siteIds, LocalDate sixBefore) { |
| | | String siteIdsCsv = toSiteIdsCsv(siteIds); |
| | | List<SixChargingDto> result = new ArrayList<>(); |
| | | LocalDate currentMonth = LocalDate.now().withDayOfMonth(1); |
| | | LocalDate month = sixBefore.withDayOfMonth(1); |
| | | while (!month.isAfter(currentMonth)) { |
| | | LocalDate nextMonth = month.plusMonths(1); |
| | | SixChargingDto income = month.equals(currentMonth) |
| | | ? this.baseMapper.getLargeScreenCurrentMonthIncome( |
| | | siteIdsCsv, month.toString(), nextMonth.toString()) |
| | | : this.baseMapper.getLargeScreenSettlementIncome( |
| | | siteIdsCsv, month.toString(), nextMonth.toString()); |
| | | if (income == null) { |
| | | income = new SixChargingDto(); |
| | | } |
| | | income.setMonth(String.format("%02d", month.getMonthValue())); |
| | | if (income.getElectrovalence() == null) { |
| | | income.setElectrovalence(BigDecimal.ZERO); |
| | | } |
| | | if (income.getServiceCharge() == null) { |
| | | income.setServiceCharge(BigDecimal.ZERO); |
| | | } |
| | | result.add(income); |
| | | month = nextMonth; |
| | | String cacheKey = largeScreenCacheKey("incomeTrend", siteIds); |
| | | List<SixChargingDto> cached = redisService.getCacheObject(cacheKey); |
| | | if (cached != null) { |
| | | return cached; |
| | | } |
| | | LocalDate currentMonth = LocalDate.now().withDayOfMonth(1); |
| | | List<SixChargingDto> result = new ArrayList<>(); |
| | | try { |
| | | String sixBeforeMonth = sixBefore.withDayOfMonth(1).format(LARGE_SCREEN_MONTH_FMT); |
| | | List<SixChargingDto> incomeList = this.baseMapper.incomeTrendFromStat(siteIds, sixBeforeMonth); |
| | | Map<String, SixChargingDto> incomeByMonth = new HashMap<>(); |
| | | if (incomeList != null) { |
| | | for (SixChargingDto income : incomeList) { |
| | | if (income != null && income.getMonth() != null) { |
| | | incomeByMonth.put(income.getMonth(), income); |
| | | } |
| | | } |
| | | } |
| | | LocalDate month = sixBefore.withDayOfMonth(1); |
| | | while (!month.isAfter(currentMonth)) { |
| | | SixChargingDto income = incomeByMonth.get(month.format(LARGE_SCREEN_MONTH_FMT)); |
| | | if (income == null) { |
| | | income = new SixChargingDto(); |
| | | } |
| | | income.setMonth(String.format("%02d", month.getMonthValue())); |
| | | if (income.getElectrovalence() == null) { |
| | | income.setElectrovalence(BigDecimal.ZERO); |
| | | } |
| | | if (income.getServiceCharge() == null) { |
| | | income.setServiceCharge(BigDecimal.ZERO); |
| | | } |
| | | result.add(income); |
| | | month = month.plusMonths(1); |
| | | } |
| | | } catch (Exception e) { |
| | | log.warn("大屏收入趋势读取预聚合表失败, siteIds={}, sixBefore={}", siteIds, sixBefore, e); |
| | | LocalDate month = sixBefore.withDayOfMonth(1); |
| | | while (!month.isAfter(currentMonth)) { |
| | | SixChargingDto income = new SixChargingDto(); |
| | | income.setMonth(String.format("%02d", month.getMonthValue())); |
| | | income.setElectrovalence(BigDecimal.ZERO); |
| | | income.setServiceCharge(BigDecimal.ZERO); |
| | | result.add(income); |
| | | month = month.plusMonths(1); |
| | | } |
| | | } |
| | | redisService.setCacheObject(cacheKey, result, LARGE_SCREEN_CACHE_SECONDS, TimeUnit.SECONDS); |
| | | return result; |
| | | } |
| | | |
| | | @Override |
| | | public List<LargeChargingPowerVo> getLargeScreenChargingPower(List<Integer> siteIds, LocalDateTime startTime, LocalDateTime endTime) { |
| | | DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); |
| | | String siteIdsCsv = toSiteIdsCsv(siteIds); |
| | | List<LargeChargingPowerVo> result = new ArrayList<>(); |
| | | LocalDateTime dayStart = startTime; |
| | | while (dayStart.isBefore(endTime)) { |
| | | LocalDateTime dayEnd = dayStart.plusDays(1); |
| | | Map<String, Object> aggregate = this.baseMapper.getLargeScreenChargingPower( |
| | | siteIdsCsv, dayStart.format(formatter), dayEnd.format(formatter)); |
| | | BigDecimal totalPower = decimalValue(aggregate, "total_power"); |
| | | BigDecimal orderCount = decimalValue(aggregate, "order_count"); |
| | | BigDecimal power = orderCount.signum() == 0 ? BigDecimal.ZERO |
| | | : totalPower.divide(orderCount, 0, RoundingMode.HALF_UP); |
| | | LargeChargingPowerVo item = new LargeChargingPowerVo(); |
| | | item.setTime(dayStart.toLocalDate().toString()); |
| | | item.setPower(power == null ? BigDecimal.ZERO : power); |
| | | result.add(item); |
| | | dayStart = dayEnd; |
| | | String cacheKey = largeScreenCacheKey("power", siteIds); |
| | | List<LargeChargingPowerVo> cached = redisService.getCacheObject(cacheKey); |
| | | if (cached != null) { |
| | | return cached; |
| | | } |
| | | List<LargeChargingPowerVo> result = new ArrayList<>(); |
| | | try { |
| | | String dayStart = startTime.toLocalDate().format(LARGE_SCREEN_DAY_FMT); |
| | | String dayEnd = endTime.toLocalDate().format(LARGE_SCREEN_DAY_FMT); |
| | | List<Map<String, Object>> dayAggs = this.baseMapper.powerFromStat(siteIds, dayStart, dayEnd); |
| | | if (dayAggs != null) { |
| | | for (Map<String, Object> agg : dayAggs) { |
| | | BigDecimal totalPower = decimalValue(agg, "total_power"); |
| | | BigDecimal orderCount = decimalValue(agg, "order_count"); |
| | | BigDecimal power = orderCount.signum() == 0 ? BigDecimal.ZERO |
| | | : totalPower.divide(orderCount, 0, RoundingMode.HALF_UP); |
| | | LargeChargingPowerVo item = new LargeChargingPowerVo(); |
| | | Object day = agg.get("day"); |
| | | item.setTime(day == null ? null : String.valueOf(day)); |
| | | item.setPower(power); |
| | | result.add(item); |
| | | } |
| | | } |
| | | } catch (Exception e) { |
| | | log.warn("大屏功率曲线读取预聚合表失败, siteIds={}", siteIds, e); |
| | | } |
| | | redisService.setCacheObject(cacheKey, result, LARGE_SCREEN_CACHE_SECONDS, TimeUnit.SECONDS); |
| | | return result; |
| | | } |
| | | |
| | | @Override |
| | | public BigDecimal getLargeScreenTotalCapacity(List<Integer> siteIds) { |
| | | String cacheKey = largeScreenCacheKey("capacity", siteIds); |
| | | BigDecimal cached = redisService.getCacheObject(cacheKey); |
| | | if (cached != null) { |
| | | return cached; |
| | | } |
| | | BigDecimal capacity = BigDecimal.ZERO; |
| | | try { |
| | | BigDecimal fromStat = this.baseMapper.capacityFromStat(siteIds); |
| | | if (fromStat != null) { |
| | | capacity = fromStat; |
| | | } |
| | | } catch (Exception e) { |
| | | log.warn("大屏累计充电量读取预聚合表失败, siteIds={}", siteIds, e); |
| | | } |
| | | redisService.setCacheObject(cacheKey, capacity, LARGE_SCREEN_CACHE_SECONDS, TimeUnit.SECONDS); |
| | | return capacity; |
| | | } |
| | | |
| | | @Override |
| | | public void rebuildLargeScreenStat() { |
| | | log.info("大屏预聚合表全量重建开始"); |
| | | long start = System.currentTimeMillis(); |
| | | // 1. 月度切片全量(数据起始月 ~ 当前月), 每次切片一次查询, 避免单条大查询超时 |
| | | LocalDate month = LARGE_SCREEN_STAT_DATA_START.withDayOfMonth(1); |
| | | LocalDate currentMonth = LocalDate.now().withDayOfMonth(1); |
| | | while (!month.isAfter(currentMonth)) { |
| | | writeMonthSlice(month, month.plusMonths(1)); |
| | | writeUserSitePairs(month, month.plusMonths(1)); |
| | | month = month.plusMonths(1); |
| | | } |
| | | // 2. 近90天日切片 |
| | | LocalDate day = LocalDate.now().minusDays(89); |
| | | LocalDate today = LocalDate.now(); |
| | | while (!day.isAfter(today)) { |
| | | writeDaySlice(day); |
| | | day = day.plusDays(1); |
| | | } |
| | | // 3. 全局注册用户数 |
| | | writeGlobal(); |
| | | clearLargeScreenCache(); |
| | | log.info("大屏预聚合表全量重建完成, 耗时 {}ms", System.currentTimeMillis() - start); |
| | | } |
| | | |
| | | @Override |
| | | public void refreshLargeScreenStat() { |
| | | long start = System.currentTimeMillis(); |
| | | LocalDate currentMonth = LocalDate.now().withDayOfMonth(1); |
| | | writeMonthSlice(currentMonth, currentMonth.plusMonths(1)); |
| | | writeDaySlice(LocalDate.now()); |
| | | writeUserSitePairs(LocalDate.now(), LocalDate.now().plusDays(1)); |
| | | writeGlobal(); |
| | | clearLargeScreenCache(); |
| | | log.info("大屏预聚合表刷新完成, 耗时 {}ms", System.currentTimeMillis() - start); |
| | | } |
| | | |
| | | /** |
| | | * 写入时间切片内的 (site_id, app_user_id) 对(充电用户精确去重用, INSERT IGNORE 幂等) |
| | | */ |
| | | private void writeUserSitePairs(LocalDate sliceStart, LocalDate sliceEnd) { |
| | | List<Map<String, Object>> pairs = this.baseMapper.userSitePairsAgg( |
| | | sliceStart.atStartOfDay().format(LARGE_SCREEN_SQL_FMT), |
| | | sliceEnd.atStartOfDay().format(LARGE_SCREEN_SQL_FMT)); |
| | | if (pairs == null || pairs.isEmpty()) { |
| | | return; |
| | | } |
| | | List<Map<String, Object>> rows = new ArrayList<>(); |
| | | for (Map<String, Object> pair : pairs) { |
| | | if (pair == null || pair.get("site_id") == null || pair.get("app_user_id") == null) { |
| | | continue; |
| | | } |
| | | Map<String, Object> row = new HashMap<>(); |
| | | row.put("siteId", pair.get("site_id")); |
| | | row.put("appUserId", pair.get("app_user_id")); |
| | | rows.add(row); |
| | | } |
| | | if (!rows.isEmpty()) { |
| | | this.baseMapper.insertUserSitePairs(rows); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 写入一个月度切片聚合(M 行) |
| | | * 订单数取未删除、状态1-5口径;金额取已结束且已支付口径。 |
| | | * 充电用户数取 monthSliceUserAgg(工作台口径: 该站点所有订单的去重用户, 不区分状态/删除标记) |
| | | */ |
| | | private void writeMonthSlice(LocalDate monthStart, LocalDate monthEnd) { |
| | | String startTime = monthStart.atStartOfDay().format(LARGE_SCREEN_SQL_FMT); |
| | | String endTime = monthEnd.atStartOfDay().format(LARGE_SCREEN_SQL_FMT); |
| | | List<Map<String, Object>> rows = this.baseMapper.monthSliceAgg(startTime, endTime); |
| | | List<Map<String, Object>> userRows = this.baseMapper.monthSliceUserAgg(startTime, endTime); |
| | | if ((rows == null || rows.isEmpty()) && (userRows == null || userRows.isEmpty())) { |
| | | return; |
| | | } |
| | | Map<Object, BigDecimal> userCountBySite = new HashMap<>(); |
| | | if (userRows != null) { |
| | | for (Map<String, Object> userRow : userRows) { |
| | | if (userRow != null && userRow.get("site_id") != null) { |
| | | userCountBySite.put(userRow.get("site_id"), decimalValue(userRow, "user_count")); |
| | | } |
| | | } |
| | | } |
| | | String period = monthStart.format(LARGE_SCREEN_MONTH_FMT); |
| | | List<Map<String, Object>> statRows = new ArrayList<>(); |
| | | Map<Object, Boolean> written = new HashMap<>(); |
| | | if (rows != null) { |
| | | for (Map<String, Object> row : rows) { |
| | | if (row == null || row.get("site_id") == null) { |
| | | continue; |
| | | } |
| | | BigDecimal electricFee = decimalValue(row, "electric_fee"); |
| | | BigDecimal serviceFee = decimalValue(row, "service_fee"); |
| | | Map<String, Object> stat = new HashMap<>(); |
| | | stat.put("statType", "M"); |
| | | stat.put("period", period); |
| | | stat.put("siteId", row.get("site_id")); |
| | | stat.put("orderCount", decimalValue(row, "order_count")); |
| | | stat.put("userCount", userCountBySite.getOrDefault(row.get("site_id"), BigDecimal.ZERO)); |
| | | stat.put("capacity", decimalValue(row, "capacity")); |
| | | stat.put("electricFee", electricFee); |
| | | stat.put("serviceFee", serviceFee); |
| | | stat.put("income", electricFee.add(serviceFee)); |
| | | stat.put("totalPower", BigDecimal.ZERO); |
| | | stat.put("registeredUserCount", 0L); |
| | | statRows.add(stat); |
| | | written.put(row.get("site_id"), Boolean.TRUE); |
| | | } |
| | | } |
| | | // 当月仅有充电用户、暂无已结束订单的站点, 补一行保用户数(金额为0) |
| | | for (Map.Entry<Object, BigDecimal> entry : userCountBySite.entrySet()) { |
| | | if (!written.containsKey(entry.getKey())) { |
| | | Map<String, Object> stat = new HashMap<>(); |
| | | stat.put("statType", "M"); |
| | | stat.put("period", period); |
| | | stat.put("siteId", entry.getKey()); |
| | | stat.put("orderCount", BigDecimal.ZERO); |
| | | stat.put("userCount", entry.getValue()); |
| | | stat.put("capacity", BigDecimal.ZERO); |
| | | stat.put("electricFee", BigDecimal.ZERO); |
| | | stat.put("serviceFee", BigDecimal.ZERO); |
| | | stat.put("income", BigDecimal.ZERO); |
| | | stat.put("totalPower", BigDecimal.ZERO); |
| | | stat.put("registeredUserCount", 0L); |
| | | statRows.add(stat); |
| | | } |
| | | } |
| | | if (!statRows.isEmpty()) { |
| | | this.baseMapper.replaceSiteStatBatch(statRows); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 写入一个日切片聚合(D 行) |
| | | */ |
| | | private void writeDaySlice(LocalDate day) { |
| | | List<Map<String, Object>> rows = this.baseMapper.daySliceAgg( |
| | | day.atStartOfDay().format(LARGE_SCREEN_SQL_FMT), |
| | | day.plusDays(1).atStartOfDay().format(LARGE_SCREEN_SQL_FMT)); |
| | | if (rows == null || rows.isEmpty()) { |
| | | return; |
| | | } |
| | | List<Map<String, Object>> statRows = new ArrayList<>(); |
| | | for (Map<String, Object> row : rows) { |
| | | if (row == null || row.get("site_id") == null) { |
| | | continue; |
| | | } |
| | | Map<String, Object> stat = new HashMap<>(); |
| | | stat.put("statType", "D"); |
| | | stat.put("period", day.format(LARGE_SCREEN_DAY_FMT)); |
| | | stat.put("siteId", row.get("site_id")); |
| | | stat.put("orderCount", decimalValue(row, "order_count")); |
| | | stat.put("userCount", BigDecimal.ZERO); |
| | | stat.put("capacity", BigDecimal.ZERO); |
| | | stat.put("electricFee", BigDecimal.ZERO); |
| | | stat.put("serviceFee", BigDecimal.ZERO); |
| | | stat.put("income", BigDecimal.ZERO); |
| | | stat.put("totalPower", decimalValue(row, "total_power")); |
| | | stat.put("registeredUserCount", 0L); |
| | | statRows.add(stat); |
| | | } |
| | | if (!statRows.isEmpty()) { |
| | | this.baseMapper.replaceSiteStatBatch(statRows); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 写入全局注册用户数(G 行) |
| | | */ |
| | | private void writeGlobal() { |
| | | Long registered = this.baseMapper.globalRegisteredUsers(); |
| | | Map<String, Object> stat = new HashMap<>(); |
| | | stat.put("statType", "G"); |
| | | stat.put("period", "ALL"); |
| | | stat.put("siteId", 0); |
| | | stat.put("orderCount", 0L); |
| | | stat.put("userCount", 0L); |
| | | stat.put("capacity", BigDecimal.ZERO); |
| | | stat.put("electricFee", BigDecimal.ZERO); |
| | | stat.put("serviceFee", BigDecimal.ZERO); |
| | | stat.put("income", BigDecimal.ZERO); |
| | | stat.put("totalPower", BigDecimal.ZERO); |
| | | stat.put("registeredUserCount", registered == null ? 0L : registered); |
| | | this.baseMapper.replaceSiteStatBatch(Collections.singletonList(stat)); |
| | | } |
| | | |
| | | private String largeScreenCacheKey(String endpoint, List<Integer> siteIds) { |
| | | String idPart = siteIds == null ? "all" : DigestUtils.md5DigestAsHex( |
| | | siteIds.stream().sorted().map(String::valueOf).collect(Collectors.joining(",")) |
| | | .getBytes(StandardCharsets.UTF_8)); |
| | | return LARGE_SCREEN_CACHE_PREFIX + endpoint + ":" + idPart; |
| | | } |
| | | |
| | | private void clearLargeScreenCache() { |
| | | Collection<String> keys = redisService.keys(LARGE_SCREEN_CACHE_PREFIX + "*"); |
| | | if (keys != null && !keys.isEmpty()) { |
| | | redisService.deleteObject(keys); |
| | | } |
| | | } |
| | | |
| | | private BigDecimal decimalValue(Map<String, Object> values, String key) { |
| | |
| | | */ |
| | | @Override |
| | | public R<StatisticsOfBatteryVO> getStatisticsOfBattery(List<Integer> siteIds) { |
| | | String cacheKey = largeScreenCacheKey("battery", siteIds); |
| | | StatisticsOfBatteryVO cached = redisService.getCacheObject(cacheKey); |
| | | if (cached != null) { |
| | | return R.ok(cached); |
| | | } |
| | | StatisticsOfBatteryVO vo = new StatisticsOfBatteryVO(); |
| | | List<Integer> effectiveSiteIds = siteIds == null ? Arrays.asList(25, 26) : siteIds; |
| | | if (effectiveSiteIds.isEmpty()) { |
| | |
| | | return R.ok(vo); |
| | | } |
| | | List<Integer> gunIds = r.getData(); |
| | | //统计 |
| | | // 统计: gunIds 分批查询, 避免超大 IN 列表撑爆 MyCat 规划器 |
| | | Long total = 0L; |
| | | if (gunIds != null && !gunIds.isEmpty()) { |
| | | total = this.baseMapper.selectCount(new LambdaQueryWrapper<TChargingOrder>() |
| | | .eq(TChargingOrder::getDelFlag, 0)//未删除 |
| | | .in(TChargingOrder::getStatus, Arrays.asList(3, 5))//充电中、已结束 |
| | | .in(TChargingOrder::getChargingGunId, gunIds));//符合条件的充电枪 |
| | | for (List<Integer> chunk : chunkList(gunIds, 500)) { |
| | | Long chunkCount = this.baseMapper.selectCount(new LambdaQueryWrapper<TChargingOrder>() |
| | | .eq(TChargingOrder::getDelFlag, 0)//未删除 |
| | | .in(TChargingOrder::getStatus, Arrays.asList(3, 5))//充电中、已结束 |
| | | .in(TChargingOrder::getChargingGunId, chunk));//符合条件的充电枪 |
| | | total += chunkCount == null ? 0L : chunkCount; |
| | | } |
| | | } |
| | | vo.setTotal(total.intValue()); |
| | | //2.折线图 |
| | | /* // 计算日期范围:前七天(不含今日)直接查找数据量太大,内存报错 |
| | | LocalDate today = LocalDate.now(); |
| | | LocalDateTime startDate = today.minusDays(7).atStartOfDay(); |
| | | LocalDateTime endDate = today.minusDays(1).atTime(LocalTime.MAX); |
| | | // 查询数据库 |
| | | List<OrderCountByDate> orderCounts = this.baseMapper.countOrdersByDate(startDate, endDate, gunIds);*/ |
| | | |
| | | |
| | | List<LocalDate> dates = new ArrayList<>(); |
| | | List<Integer> counts = new ArrayList<>(); |
| | | LocalDate today = LocalDate.now(); |
| | | // 直接查找数据量太大,内存报错,分每天查找 |
| | | for (int i = 7; i >= 1; i--) {//升序 |
| | | LocalDate day = today.minusDays(i); |
| | | LocalDateTime startOfDay = day.atStartOfDay(); |
| | | LocalDateTime endOfDay = day.atTime(LocalTime.MAX); |
| | | //统计 |
| | | Long count = 0L; |
| | | LocalDateTime dayStart = day.atStartOfDay(); |
| | | LocalDateTime dayEnd = day.plusDays(1).atStartOfDay(); |
| | | long dayCount = 0L; |
| | | if (gunIds != null && !gunIds.isEmpty()) { |
| | | count = this.baseMapper.selectCount(new LambdaQueryWrapper<TChargingOrder>() |
| | | .eq(TChargingOrder::getDelFlag, 0)//未删除 |
| | | .in(TChargingOrder::getStatus, Arrays.asList(3, 5))//充电中、已结束 |
| | | .in(TChargingOrder::getChargingGunId, gunIds)//符合条件的充电枪 站点 超充快充 |
| | | .between(TChargingOrder::getCreateTime, startOfDay, endOfDay));//每天 |
| | | for (List<Integer> chunk : chunkList(gunIds, 500)) { |
| | | Long chunkCount = this.baseMapper.countOrdersInRangeByGunIds(dayStart, dayEnd, chunk); |
| | | dayCount += chunkCount == null ? 0L : chunkCount; |
| | | } |
| | | } |
| | | dates.add(day); |
| | | counts.add(count.intValue()); |
| | | counts.add((int) dayCount); |
| | | } |
| | | vo.setDates(dates); |
| | | vo.setCounts(counts); |
| | | redisService.setCacheObject(cacheKey, vo, LARGE_SCREEN_CACHE_SECONDS, TimeUnit.SECONDS); |
| | | return R.ok(vo); |
| | | } |
| | | |
| | | /** |
| | | * 列表分批, 控制单条 SQL 的 IN 列表规模 |
| | | */ |
| | | private <T> List<List<T>> chunkList(List<T> list, int size) { |
| | | List<List<T>> chunks = new ArrayList<>(); |
| | | for (int i = 0; i < list.size(); i += size) { |
| | | chunks.add(list.subList(i, Math.min(i + size, list.size()))); |
| | | } |
| | | return chunks; |
| | | } |
| | | |
| | | @Override |
| | |
| | | if (chargingOrder == null || !StringUtils.hasLength(chargingOrder.getCode())) { |
| | | return; |
| | | } |
| | | LambdaQueryWrapper<TChargingOrderShareRecord> wrapper = new LambdaQueryWrapper<>(); |
| | | wrapper.eq(TChargingOrderShareRecord::getChargingOrderCode, chargingOrder.getCode()) |
| | | .notIn(TChargingOrderShareRecord::getOnlineShareStatus, "SUCCESS", "PROCESSING"); |
| | | int count = chargingOrderShareRecordMapper.delete(wrapper); |
| | | // 使用自定义 Mapper 方法执行原生 DELETE,绕过 MyBatis-Plus @TableLogic 逻辑删除拦截 |
| | | int count = chargingOrderShareRecordMapper.physicalDeleteStaleByOrderCode(chargingOrder.getCode()); |
| | | if (count > 0) { |
| | | log.info("物理删除充电订单旧分账记录,订单号:{},删除数量:{}", chargingOrder.getCode(), count); |
| | | } |
| | |
| | | |
| | | private boolean isNeedChargingOrderShare(TChargingOrder chargingOrder) { |
| | | return chargingOrder != null |
| | | && Integer.valueOf(5).equals(chargingOrder.getStatus()) |
| | | && Integer.valueOf(2).equals(chargingOrder.getRechargePaymentStatus()) |
| | | && Integer.valueOf(2).equals(chargingOrder.getRefundStatus()) |
| | | && (Integer.valueOf(2).equals(chargingOrder.getRefundStatus()) || Integer.valueOf(5).equals(chargingOrder.getStatus())) |
| | | && getChargingOrderShareBaseAmount(chargingOrder).compareTo(BigDecimal.ZERO) > 0; |
| | | } |
| | | |