Merge branch 'fix/franchisee-income-align' into 2.2
| | |
| | | import com.ruoyi.chargingPile.api.model.TChargingGun; |
| | | import com.ruoyi.chargingPile.api.model.TChargingPile; |
| | | import com.ruoyi.chargingPile.api.model.TParkingRecord; |
| | | import com.ruoyi.chargingPile.api.model.TPartnerAppUser; |
| | | import com.ruoyi.chargingPile.api.vo.GetChargingGunByChargingPileIdsVo; |
| | | import com.ruoyi.common.core.domain.R; |
| | | import com.ruoyi.common.core.dto.ChargingOrderGroup; |
| | |
| | | } |
| | | chargingUsersByDay.computeIfAbsent(agg.getStatDay(), key -> new HashSet<>()).add(agg.getAppUserId()); |
| | | } |
| | | List<TPartnerAppUser> relations = Collections.emptyList(); |
| | | if (partner != null && partner.getId() != null) { |
| | | R<List<TPartnerAppUser>> relationResult = partnerClient.getBoundAppUserRelations(partner.getId()); |
| | | if (relationResult != null && relationResult.getData() != null) { |
| | | relations = relationResult.getData(); |
| | | Map<LocalDate, Long> newUsersByDay = new LinkedHashMap<>(); |
| | | if (range != null && range[0] != null && range[1] != null) { |
| | | List<FranchiseeNewUserStat> newUserStats = chargingOrderMapper.franchiseeNewUserStats(range[0], range[1]); |
| | | if (newUserStats != null) { |
| | | for (FranchiseeNewUserStat stat : newUserStats) { |
| | | if (stat != null && stat.getStatDay() != null && stat.getUserCount() != null) { |
| | | newUsersByDay.put(stat.getStatDay(), stat.getUserCount()); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | FranchiseeUserStatistics.Result userStatistics = FranchiseeUserStatistics.calculateByUserDays( |
| | | chargingUsersByDay, relations, range[0], range[1]); |
| | | chargingUsersByDay, newUsersByDay, range[0], range[1]); |
| | | |
| | | Set<Long> users = aggs.stream().map(FranchiseeUserAgg::getAppUserId).filter(Objects::nonNull).collect(Collectors.toSet()); |
| | | List<TAppUser> appUsers = Collections.emptyList(); |
| New file |
| | |
| | | package com.ruoyi.order.dto; |
| | | |
| | | import lombok.Data; |
| | | |
| | | import java.time.LocalDate; |
| | | |
| | | /** |
| | | * 平台新增用户按注册日聚合行(t_app_user.create_time,全平台口径)。 |
| | | * 供加盟商看板"新增用户"统计使用,与"充电用户"(加盟商站点口径)分离。 |
| | | */ |
| | | @Data |
| | | public class FranchiseeNewUserStat { |
| | | |
| | | /** 注册日 */ |
| | | private LocalDate statDay; |
| | | |
| | | /** 该日平台新增注册用户数 */ |
| | | private Long userCount; |
| | | } |
| | |
| | | List<TChargingOrder> getNoInvoicedOrder(@Param("appUserId") Long appUserId, @Param("month") String month, |
| | | @Param("pageCurr") Integer pageCurr, @Param("pageSize") Integer pageSize); |
| | | |
| | | List<ChargingOrderVO> chargingOrder(@Param("pageInfo") PageInfo<ChargingOrderVO> pageInfo, @Param("req") ChargingOrderQuery dto, @Param("startTime1") String startTime1, @Param("startTime2") String startTime2, @Param("endTime1") String endTime1, @Param("endTime2") String endTime11); |
| | | List<ChargingOrderVO> chargingOrder(@Param("req") ChargingOrderQuery dto, @Param("startTime1") String startTime1, @Param("startTime2") String startTime2, @Param("endTime1") String endTime1, @Param("endTime2") String endTime11, @Param("offset") long offset, @Param("pageSize") long pageSize); |
| | | |
| | | long chargingOrderCount(@Param("req") ChargingOrderQuery dto, @Param("startTime1") String startTime1, @Param("startTime2") String startTime2, @Param("endTime1") String endTime1, @Param("endTime2") String endTime11); |
| | | |
| | | Map<String, Object> chargingOrderBaseSummary(@Param("req") ChargingOrderQuery dto, @Param("startTime1") String startTime1, @Param("startTime2") String startTime2, @Param("endTime1") String endTime1, @Param("endTime2") String endTime11); |
| | | |
| | |
| | | @Param("end") LocalDateTime end); |
| | | |
| | | /** |
| | | * 平台新增用户按注册日聚合(全平台口径,t_app_user.create_time,跨库)。 |
| | | */ |
| | | List<FranchiseeNewUserStat> franchiseeNewUserStats(@Param("start") LocalDateTime start, |
| | | @Param("end") LocalDateTime end); |
| | | |
| | | /** |
| | | * 流量来源统计(按 tripartite_platform_name 分组,仅 status=5 已结束订单)。 |
| | | */ |
| | | List<FranchiseeSourceStat> franchiseeTrafficStats(@Param("siteIds") List<Integer> siteIds, |
| | |
| | | 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; |
| | |
| | | package com.ruoyi.order.support; |
| | | |
| | | import com.ruoyi.chargingPile.api.model.TPartnerAppUser; |
| | | import com.ruoyi.order.api.model.TChargingOrder; |
| | | |
| | | import java.time.LocalDate; |
| | |
| | | |
| | | /** |
| | | * 旧入口:由订单列表推导"每日充电用户集合"后计算。 |
| | | * 新增用户为全平台口径(platformNewUsersByDay 为 t_app_user 注册日聚合),与加盟商绑定无关。 |
| | | */ |
| | | public static Result calculate(List<TChargingOrder> orders, |
| | | List<TPartnerAppUser> relations, |
| | | Map<LocalDate, Long> platformNewUsersByDay, |
| | | LocalDateTime start, |
| | | LocalDateTime end) { |
| | | List<TChargingOrder> safeOrders = orders == null ? Collections.emptyList() : orders; |
| | |
| | | chargingUsersByDay.computeIfAbsent(order.getEndTime().toLocalDate(), key -> new HashSet<>()) |
| | | .add(order.getAppUserId()); |
| | | } |
| | | return calculateByUserDays(chargingUsersByDay, relations, start, end); |
| | | return calculateByUserDays(chargingUsersByDay, platformNewUsersByDay, start, end); |
| | | } |
| | | |
| | | /** |
| | | * 加盟商看板入口:每日去重充电用户集合已由 SQL 聚合得到 |
| | | * (t_charging_order GROUP BY app_user_id, DATE(end_time),避免全量订单拉回 JVM)。 |
| | | * 加盟商看板入口: |
| | | * 充电用户由每日去重充电用户集合得到(t_charging_order GROUP BY app_user_id, DATE(end_time), |
| | | * 避免全量订单拉回 JVM,仅加盟商站点范围); |
| | | * 新增用户为全平台口径(t_app_user.create_time 按日聚合),与加盟商站点/绑定无关。 |
| | | */ |
| | | public static Result calculateByUserDays(Map<LocalDate, Set<Long>> chargingUsersByDay, |
| | | List<TPartnerAppUser> relations, |
| | | Map<LocalDate, Long> platformNewUsersByDay, |
| | | LocalDateTime start, |
| | | LocalDateTime end) { |
| | | Map<LocalDate, Set<Long>> safeByDay = chargingUsersByDay == null ? Collections.emptyMap() : chargingUsersByDay; |
| | | List<TPartnerAppUser> safeRelations = relations == null ? Collections.emptyList() : relations; |
| | | Map<LocalDate, Long> safeNewByDay = platformNewUsersByDay == null ? Collections.emptyMap() : platformNewUsersByDay; |
| | | |
| | | Set<Long> chargingUsers = new HashSet<>(); |
| | | safeByDay.values().forEach(chargingUsers::addAll); |
| | | |
| | | Map<Long, LocalDateTime> firstBindingByUser = new HashMap<>(); |
| | | for (TPartnerAppUser relation : safeRelations) { |
| | | if (relation == null || relation.getAppUserId() == null || relation.getCreateTime() == null) { |
| | | continue; |
| | | long newUserCount = 0; |
| | | for (Map.Entry<LocalDate, Long> entry : safeNewByDay.entrySet()) { |
| | | if (entry.getValue() != null && inDayRange(entry.getKey(), start, end)) { |
| | | newUserCount += entry.getValue(); |
| | | } |
| | | firstBindingByUser.merge(relation.getAppUserId(), relation.getCreateTime(), |
| | | (left, right) -> left.isBefore(right) ? left : right); |
| | | } |
| | | Map<LocalDate, Set<Long>> newUsersByDay = new HashMap<>(); |
| | | Set<Long> newUsers = new HashSet<>(); |
| | | firstBindingByUser.forEach((appUserId, createTime) -> { |
| | | if (inRange(createTime, start, end)) { |
| | | newUsers.add(appUserId); |
| | | newUsersByDay.computeIfAbsent(createTime.toLocalDate(), key -> new HashSet<>()).add(appUserId); |
| | | } |
| | | }); |
| | | |
| | | Map<String, Object> summary = new LinkedHashMap<>(); |
| | | summary.put("chargeUserCount", (long) chargingUsers.size()); |
| | | summary.put("newUserCount", (long) newUsers.size()); |
| | | summary.put("newUserCount", newUserCount); |
| | | |
| | | LocalDate firstDay = start == null ? LocalDate.now() : start.toLocalDate(); |
| | | LocalDate exclusiveEndDay = end == null ? firstDay.plusDays(1) : end.toLocalDate(); |
| | |
| | | for (LocalDate day = firstDay; day.isBefore(exclusiveEndDay); day = day.plusDays(1)) { |
| | | categories.add(String.format("%02d-%02d", day.getMonthValue(), day.getDayOfMonth())); |
| | | chargingSeries.add((long) safeByDay.getOrDefault(day, Collections.emptySet()).size()); |
| | | newSeries.add((long) newUsersByDay.getOrDefault(day, Collections.emptySet()).size()); |
| | | newSeries.add(safeNewByDay.getOrDefault(day, 0L)); |
| | | } |
| | | |
| | | Map<String, Object> trend = new LinkedHashMap<>(); |
| | |
| | | return new Result(summary, trend); |
| | | } |
| | | |
| | | private static boolean inDayRange(LocalDate day, LocalDateTime start, LocalDateTime end) { |
| | | return day != null |
| | | && (start == null || !day.isBefore(start.toLocalDate())) |
| | | && (end == null || day.isBefore(end.toLocalDate())); |
| | | } |
| | | |
| | | private static boolean inRange(LocalDateTime time, LocalDateTime start, LocalDateTime end) { |
| | | return time != null |
| | | && (start == null || !time.isBefore(start)) |
| | |
| | | </if> |
| | | </select> |
| | | |
| | | <select id="chargingOrder" resultType="com.ruoyi.order.api.vo.ChargingOrderVO"> |
| | | select t1.*, |
| | | app_user.phone, |
| | | partner.name as partnerName, |
| | | IFNULL(t1.whitelist_electric_refund_amount, 0) + IFNULL(t1.whitelist_service_refund_amount, 0) as whitelistRefundAmount |
| | | from t_charging_order t1 |
| | | left join `charging_pile_account`.`t_app_user` app_user on app_user.id = t1.app_user_id |
| | | left join `charging_pile_service`.`t_site` site on site.id = t1.site_id |
| | | left join `charging_pile_service`.`t_partner` partner on partner.id = site.partner_id |
| | | <!-- |
| | | 充电桩订单分页列表。手动分页(不传 IPage,避免分页拦截器生成无法下推的全量 COUNT); |
| | | ORDER BY + LIMIT 内联,MyCat 可下推到各分片。不跨库 JOIN app_user/site/partner(无筛选时会导致全量广播拉取), |
| | | 手机号在 Service 层预解析为 userIds(app_user_id 即分片键,可直接路由),phone/partnerName 由 Service 层回填。 |
| | | --> |
| | | <sql id="ChargingOrderListWhere"> |
| | | where t1.del_flag = 0 |
| | | <if test="null != req.code and req.code!=''"> |
| | | and t1.code LIKE CONCAT('%',#{req.code},'%') |
| | |
| | | <foreach collection="req.userIds" item="item" index="index" separator="," open="(" close=")"> |
| | | #{item} |
| | | </foreach> |
| | | </if> |
| | | <if test="req.phone != null and req.phone != ''"> |
| | | and app_user.phone LIKE CONCAT('%',#{req.phone},'%') |
| | | </if> |
| | | <if test="null != req.siteIds and req.siteIds.size()>0"> |
| | | and t1.site_id in |
| | |
| | | <if test="endTime1 != null and endTime1!=''"> |
| | | and (t1.end_time between #{endTime1} and #{endTime2}) |
| | | </if> |
| | | and t1.del_flag = 0 |
| | | </sql> |
| | | <select id="chargingOrder" resultType="com.ruoyi.order.api.vo.ChargingOrderVO"> |
| | | select t1.*, |
| | | IFNULL(t1.whitelist_electric_refund_amount, 0) + IFNULL(t1.whitelist_service_refund_amount, 0) as whitelistRefundAmount |
| | | from t_charging_order t1 |
| | | <include refid="ChargingOrderListWhere"/> |
| | | order by t1.create_time desc |
| | | limit #{offset}, #{pageSize} |
| | | </select> |
| | | <!-- 充电桩订单总数(列表口径,手动分页用)。单表广播,COUNT 可下推到各分片后由代理层求和。 --> |
| | | <select id="chargingOrderCount" resultType="long"> |
| | | select count(*) |
| | | from t_charging_order t1 |
| | | <include refid="ChargingOrderListWhere"/> |
| | | </select> |
| | | <select id="payOrderQuery" resultType="com.ruoyi.order.dto.PayOrderDto"> |
| | | SELECT o.*,i.phone FROM ( |
| | |
| | | GROUP BY 1, 2, 3 |
| | | </select> |
| | | |
| | | <!-- 平台新增用户按注册日聚合(全平台口径,跨库 t_app_user;GROUP BY 序号防 Calcite 崩溃) --> |
| | | <select id="franchiseeNewUserStats" resultType="com.ruoyi.order.dto.FranchiseeNewUserStat"> |
| | | SELECT DATE(create_time) AS statDay, |
| | | COUNT(*) AS userCount |
| | | FROM charging_pile_account.t_app_user |
| | | WHERE del_flag = 0 |
| | | AND create_time >= #{start} |
| | | AND create_time < #{end} |
| | | GROUP BY 1 |
| | | ORDER BY 1 |
| | | </select> |
| | | |
| | | <!-- 流量来源统计(仅 status=5 已结束订单,空值归"明星充电"由服务端处理) --> |
| | | <select id="franchiseeTrafficStats" resultType="com.ruoyi.order.dto.FranchiseeSourceStat"> |
| | | SELECT IFNULL(tripartite_platform_name, '') AS source, |
| | |
| | | package com.ruoyi.order.support; |
| | | |
| | | import com.ruoyi.chargingPile.api.model.TPartnerAppUser; |
| | | import com.ruoyi.order.api.model.TChargingOrder; |
| | | import org.junit.jupiter.api.Test; |
| | | |
| | | import java.time.LocalDate; |
| | | import java.time.LocalDateTime; |
| | | import java.util.Arrays; |
| | | import java.util.Collections; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | |
| | | private static final LocalDateTime END = LocalDateTime.of(2026, 7, 4, 0, 0); |
| | | |
| | | @Test |
| | | void countsDistinctChargingUsersAndBucketsBindingsOnTheirExactDate() { |
| | | void countsDistinctChargingUsersAndSumsPlatformNewUsersByRegistrationDay() { |
| | | List<TChargingOrder> orders = Arrays.asList( |
| | | order(1L, LocalDateTime.of(2026, 7, 1, 8, 0)), |
| | | order(1L, LocalDateTime.of(2026, 7, 1, 18, 0)), |
| | | order(1L, LocalDateTime.of(2026, 7, 2, 8, 0)), |
| | | order(2L, LocalDateTime.of(2026, 7, 2, 9, 0)) |
| | | ); |
| | | List<TPartnerAppUser> relations = Arrays.asList( |
| | | relation(1L, LocalDateTime.of(2026, 6, 30, 23, 59)), |
| | | relation(2L, LocalDateTime.of(2026, 7, 2, 10, 0)), |
| | | relation(3L, LocalDateTime.of(2026, 7, 2, 11, 0)), |
| | | relation(3L, LocalDateTime.of(2026, 7, 2, 12, 0)) |
| | | ); |
| | | Map<LocalDate, Long> newUsersByDay = new LinkedHashMap<>(); |
| | | newUsersByDay.put(LocalDate.of(2026, 7, 1), 3L); |
| | | newUsersByDay.put(LocalDate.of(2026, 7, 2), 2L); |
| | | |
| | | FranchiseeUserStatistics.Result result = FranchiseeUserStatistics.calculate(orders, relations, START, END); |
| | | FranchiseeUserStatistics.Result result = FranchiseeUserStatistics.calculate(orders, newUsersByDay, START, END); |
| | | |
| | | assertEquals(2L, result.getSummary().get("chargeUserCount")); |
| | | assertEquals(2L, result.getSummary().get("newUserCount")); |
| | | assertEquals(5L, result.getSummary().get("newUserCount")); |
| | | assertEquals(Arrays.asList("07-01", "07-02", "07-03"), result.getTrend().get("categories")); |
| | | assertEquals(Arrays.asList(1L, 2L, 0L), seriesData(result.getTrend(), 0)); |
| | | assertEquals(Arrays.asList(0L, 2L, 0L), seriesData(result.getTrend(), 1)); |
| | | assertEquals(Arrays.asList(3L, 2L, 0L), seriesData(result.getTrend(), 1)); |
| | | } |
| | | |
| | | @Test |
| | | void ignoresPlatformRegistrationsOutsideTheRange() { |
| | | Map<LocalDate, Long> newUsersByDay = new LinkedHashMap<>(); |
| | | newUsersByDay.put(LocalDate.of(2026, 6, 30), 99L); // 区间外,不计入 |
| | | newUsersByDay.put(LocalDate.of(2026, 7, 1), 1L); |
| | | |
| | | FranchiseeUserStatistics.Result result = FranchiseeUserStatistics.calculateByUserDays( |
| | | Collections.emptyMap(), newUsersByDay, START, END); |
| | | |
| | | assertEquals(1L, result.getSummary().get("newUserCount")); |
| | | assertEquals(Arrays.asList(1L, 0L, 0L), seriesData(result.getTrend(), 1)); |
| | | } |
| | | |
| | | @Test |
| | | void returnsZeroFilledSeriesWhenThereIsNoActivity() { |
| | | FranchiseeUserStatistics.Result result = FranchiseeUserStatistics.calculate( |
| | | Collections.emptyList(), Collections.emptyList(), START, END); |
| | | Collections.emptyList(), Collections.emptyMap(), START, END); |
| | | |
| | | assertEquals(0L, result.getSummary().get("chargeUserCount")); |
| | | assertEquals(0L, result.getSummary().get("newUserCount")); |
| | |
| | | order.setAppUserId(appUserId); |
| | | order.setEndTime(endTime); |
| | | return order; |
| | | } |
| | | |
| | | private TPartnerAppUser relation(Long appUserId, LocalDateTime createTime) { |
| | | TPartnerAppUser relation = new TPartnerAppUser(); |
| | | relation.setAppUserId(appUserId); |
| | | relation.setCreateTime(createTime); |
| | | return relation; |
| | | } |
| | | } |