刘帅
2026-09-22 474724c0e016be419fe84967fdcb085722e6b444
bug:营收统计数据修改
5个文件已修改
330 ■■■■■ 已修改文件
cloud-server-activity/src/main/java/com/dsh/activity/controller/PointMercharsController.java 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cloud-server-competition/src/main/java/com/dsh/competition/controller/CompetitionController.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cloud-server-management/src/main/java/com/dsh/course/feignClient/activity/PointMercharsClient.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cloud-server-management/src/main/java/com/dsh/guns/modular/system/controller/code/DataStatisticsController.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cloud-server-management/src/main/webapp/WEB-INF/view/system/dataStatistics/operatorIncome.html 240 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cloud-server-activity/src/main/java/com/dsh/activity/controller/PointMercharsController.java
@@ -2173,30 +2173,63 @@
    @PostMapping("/base/pointMerchars/queryFee")
    @ResponseBody
    public HashMap<String, Object> queryFee(@RequestBody QueryDataFee queryDataFee) {
        return queryTicketFee(queryDataFee, false);
    }
    /**
     * 按购买记录的 storeId 统计门票收入。购买表按 userId 分片,未带 userId 条件时
     * ShardingSphere 会读取各分表,确保 t_user_points_merchandise1-5 的记录都参与统计。
     */
    @PostMapping("/base/pointMerchars/queryTicketFee")
    @ResponseBody
    public HashMap<String, Object> queryTicketFee(@RequestBody QueryDataFee queryDataFee) {
        return queryTicketFee(queryDataFee, true);
    }
    private HashMap<String, Object> queryTicketFee(QueryDataFee queryDataFee, boolean scopeByStore) {
        HashMap<String, Object> map = new HashMap<>();
        String data = queryDataFee.getData();
        List<Integer> ids = queryDataFee.getIds() == null
                ? new ArrayList<>() : new ArrayList<>(queryDataFee.getIds());
        if (ids.size() == 0) {
            ids.add(-1);
        }
        // 只查询门票商品(type=3),并按购买记录关联的商品 ID 统计。
        // 旧逻辑会对每一笔订单遍历全部门票商品,导致日/月/季/年卡被重复累加。
        // cardType 是门票卡种的唯一分类依据:1 日卡、2 月卡、3 季卡、4 年卡。
        // 不能额外依赖历史商品的 type/redemptionMethod,避免有效门票记录被遗漏。
        List<PointsMerchandise> tickets = pmdsService.list(new LambdaQueryWrapper<PointsMerchandise>()
                .eq(PointsMerchandise::getRedemptionMethod, 3)
                .eq(PointsMerchandise::getType, 3));
                .in(PointsMerchandise::getCardType, 1, 2, 3, 4));
        LambdaQueryWrapper<UserPointsMerchandise> vipPaymentLambdaQueryWrapper = new LambdaQueryWrapper<>();
        if (ToolUtil.isNotEmpty(data)) {
            String stime = data.split(" - ")[0] + " 00:00:00";
            String etime = data.split(" - ")[1] + " 23:59:59";
            vipPaymentLambdaQueryWrapper.between(UserPointsMerchandise::getInsertTime, stime, etime);
            // 实收以支付时间归属;没有支付时间的历史数据才回退到创建时间。
            vipPaymentLambdaQueryWrapper.and(wrapper -> wrapper
                    .between(UserPointsMerchandise::getPaymentTime, stime, etime)
                    .or(item -> item.isNull(UserPointsMerchandise::getPaymentTime)
                            .between(UserPointsMerchandise::getInsertTime, stime, etime)));
        }
        vipPaymentLambdaQueryWrapper.in(UserPointsMerchandise::getUserId, ids);
        if (!ids.isEmpty()) {
            if (scopeByStore) {
                // 门票记录通常写入 storeId;全国/城市通用等历史记录可能未写该字段,
                // 此时按商品配置的门店关系归属到运营商/门店统计范围。
                List<Integer> scopedTicketIds = pmdstoService.list(new LambdaQueryWrapper<PointsMerchandiseStore>()
                                .in(PointsMerchandiseStore::getStoreId, ids))
                        .stream().map(PointsMerchandiseStore::getPointsMerchandiseId)
                        .filter(Objects::nonNull).distinct().collect(Collectors.toList());
                vipPaymentLambdaQueryWrapper.and(wrapper -> {
                    wrapper.in(UserPointsMerchandise::getStoreId, ids);
                    if (!scopedTicketIds.isEmpty()) {
                        wrapper.or().in(UserPointsMerchandise::getPointsMerchandiseId, scopedTicketIds);
                    }
                });
            } else {
                vipPaymentLambdaQueryWrapper.in(UserPointsMerchandise::getUserId, ids);
            }
        }
        List<Integer> collect = tickets.stream().map(PointsMerchandise::getId).collect(Collectors.toList());
        if (collect.size() == 0) {
            collect.add(-1);
        }
        vipPaymentLambdaQueryWrapper.in(UserPointsMerchandise::getPointsMerchandiseId, collect);
        // payStatus=2 才表示支付成功;payType 是兑换/支付方式,不能固定为 2,
        // 否则会把其它已支付门票记录错误排除。
        vipPaymentLambdaQueryWrapper.eq(UserPointsMerchandise::getPayStatus, 2);
        vipPaymentLambdaQueryWrapper.and(wrapper -> wrapper.isNull(UserPointsMerchandise::getState)
                .or().eq(UserPointsMerchandise::getState, 1));
@@ -2214,16 +2247,18 @@
            if (ticket == null || userPointsMerchandise.getCash() == null) {
                continue;
            }
            BigDecimal paidCash = userPointsMerchandise.getCash();
            all = all.add(paidCash);
            // 以 t_user_points_merchandise1-5 中实际支付的 cash 为准,
            // 不使用商品当前价格,避免商品改价影响历史营收。
            BigDecimal ticketAmount = userPointsMerchandise.getCash();
            all = all.add(ticketAmount);
            if (Integer.valueOf(1).equals(ticket.getCardType())) {
                day = day.add(paidCash);
                day = day.add(ticketAmount);
            } else if (Integer.valueOf(2).equals(ticket.getCardType())) {
                month = month.add(paidCash);
                month = month.add(ticketAmount);
            } else if (Integer.valueOf(3).equals(ticket.getCardType())) {
                quarter = quarter.add(paidCash);
                quarter = quarter.add(ticketAmount);
            } else if (Integer.valueOf(4).equals(ticket.getCardType())) {
                year = year.add(paidCash);
                year = year.add(ticketAmount);
            }
        }
cloud-server-competition/src/main/java/com/dsh/competition/controller/CompetitionController.java
@@ -1213,10 +1213,8 @@
    private LambdaQueryWrapper<Competition> buildCompetitionCountWrapper(CompetionVO vo, String dateText) {
        LambdaQueryWrapper<Competition> wrapper = new LambdaQueryWrapper<Competition>()
                .like(Competition::getInsertTime, dateText);
        // 平台统计由平台门店范围决定,历史平台赛事的 operatorId 可能为空或为 0。
        if (vo.getOperatorId() != null && !Integer.valueOf(0).equals(vo.getOperatorId())) {
            wrapper.eq(Competition::getOperatorId, vo.getOperatorId());
        }
        // 运营商活动统计以其名下门店为准。历史活动的 operatorId 可能为空或未同步,
        // 不能再以该字段过滤,否则活动管理列表能看到的门店活动会被统计遗漏。
        appendStoreIdFilter(wrapper, vo.getStoreIds());
        return wrapper;
    }
cloud-server-management/src/main/java/com/dsh/course/feignClient/activity/PointMercharsClient.java
@@ -106,6 +106,10 @@
    @PostMapping("/base/pointMerchars/queryFee")
    HashMap<String, Object> queryFee(QueryDataFee queryDataFee);
    /** 门票收入按购买记录所属门店统计,供平台、运营商和门店营收饼图使用。 */
    @PostMapping("/base/pointMerchars/queryTicketFee")
    HashMap<String, Object> queryTicketFee(QueryDataFee queryDataFee);
    /**
     * 根据id查询
     */
cloud-server-management/src/main/java/com/dsh/guns/modular/system/controller/code/DataStatisticsController.java
@@ -600,19 +600,16 @@
    }
    private Map<String, Object> getTicketIncome(String time, Integer operatorId, Integer storeId) {
        List<Integer> userIds;
        List<Integer> storeIds;
        if (storeId != null) {
            userIds = listStatisticsAppUserIdsForStore(storeId);
            storeIds = Collections.singletonList(storeId);
        } else if (operatorId != null) {
            userIds = listStatisticsAppUserIds(operatorId);
            storeIds = listStatisticsStoreIds(operatorId, false);
        } else {
            userIds = listStatisticsAppUserIds(null);
            // 平台统计不限制 storeId,包含全国/城市通用门票等购买记录 storeId 为空的数据。
            storeIds = Collections.emptyList();
        }
        if (userIds == null) {
            userIds = new ArrayList<>();
        }
        // 活动服务对空集合会补充 -1,传入可变副本以兼容该历史实现。
        return pointMercharsClient.queryFee(new QueryDataFee(time, new ArrayList<>(userIds)));
        return pointMercharsClient.queryTicketFee(new QueryDataFee(time, new ArrayList<>(storeIds)));
    }
    /** 惠民卡采用支付、退款两条事实流水,赠送卡不计营收。 */
cloud-server-management/src/main/webapp/WEB-INF/view/system/dataStatistics/operatorIncome.html
@@ -43,7 +43,7 @@
        gap: 16px;
        width: 100%;
        max-width: 1280px;
        margin: 18px 0;
        margin: 18px 0 14px;
        text-align: left;
    }
    .income-card {
@@ -67,8 +67,56 @@
    .income-card.net-card { border-top: 3px solid #409eff; }
    .income-card.coupon-card { border-top: 3px solid #e6a23c; }
    .income-card.refund-card { border-top: 3px solid #f56c6c; }
    .income-period-grid {
        display: grid;
        grid-template-columns: repeat(2, minmax(360px, 1fr));
        gap: 10px 16px;
        width: 100%;
        max-width: 1280px;
        margin-bottom: 18px;
        text-align: left;
    }
    .income-period-item {
        min-height: 110px;
        padding: 9px 12px;
        border-left: 3px solid #1ab394;
        background: #f8fafc;
        color: #606266;
        line-height: 20px;
    }
    .income-period-title {
        margin-bottom: 6px;
        color: #303133;
        font-weight: 600;
    }
    .income-period-records {
        max-height: 185px;
        overflow-y: auto;
    }
    .income-period-record {
        padding: 4px 0;
        border-top: 1px solid #edf0f5;
        font-size: 13px;
    }
    .income-trend-grid {
        display: grid;
        grid-template-columns: repeat(2, minmax(480px, 1fr));
        gap: 18px;
        width: 100%;
        max-width: 1280px;
        margin-bottom: 18px;
    }
    .income-trend-chart {
        height: 300px;
        padding: 8px;
        border: 1px solid #e4e7ed;
        border-radius: 6px;
        background: #fff;
    }
    \@media (max-width: 992px) {
        .income-overview { grid-template-columns: repeat(2, minmax(180px, 1fr)); }
        .income-period-grid { grid-template-columns: 1fr; }
        .income-trend-grid { grid-template-columns: 1fr; }
    }
</style>
<div class="ibox float-e-margins">
@@ -119,12 +167,23 @@
                                            <div class="income-card coupon-card">优惠券抵扣金额<strong id="couponDeductionAmount">0</strong></div>
                                            <div class="income-card refund-card">退款金额<strong id="refundAmount">0</strong></div>
                                        </div>
                                        <div class="income-period-grid">
                                            <div id="yearIncomeStatistics" class="income-period-item"></div>
                                            <div id="monthIncomeStatistics" class="income-period-item"></div>
                                            <div id="weekIncomeStatistics" class="income-period-item"></div>
                                            <div id="dayIncomeStatistics" class="income-period-item"></div>
                                        </div>
                                        <span id="yearIncome" style="display:none"></span>
                                        <span id="monthIncome" style="display:none"></span>
                                        <span id="weekIncome" style="display:none"></span>
                                        <span id="todayIncome" style="display:none"></span>
                                    </div>
                                    <div id="income" style="width: 1280px;height:500px;"></div>
                                    <div class="income-trend-grid">
                                        <div id="incomeYearChart" class="income-trend-chart"></div>
                                        <div id="incomeMonthChart" class="income-trend-chart"></div>
                                        <div id="incomeWeekChart" class="income-trend-chart"></div>
                                        <div id="incomeDayChart" class="income-trend-chart"></div>
                                    </div>
                                </div>
                                <div class="col-sm-10">
                                    <h2>收入类型统计</h2>
@@ -367,30 +426,18 @@
        }
    }
    function income(){
        var id = currentOperatorId()
        var beginTime = $("#beginTime").val()
        console.log(id)
        if(id==""){
        var id = currentOperatorId();
        var beginTime = $("#beginTime").val();
        if (id === "") {
            Feng.info("请先选择运营商");
            return;
        }
        var ajax = new $ax(Feng.ctxPath + "/data/getIncomeData", function(resp){
            yearX=resp.year
            var list = resp.yearMount;
            console.log(list);
            var t1 = document.getElementById("totalIncome");
            var t2 = document.getElementById("yearIncome");
            var t3 = document.getElementById("monthIncome");
            var t4 = document.getElementById("weekIncome");
            var t5 = document.getElementById("todayIncome");
            var netIncomeAmount = document.getElementById("netIncomeAmount");
            var couponDeductionAmount = document.getElementById("couponDeductionAmount");
            var refundAmount = document.getElementById("refundAmount");
            t1.innerText = formatStatisticNumber(list[0]);
            t2.innerText = "年度应收:" + formatStatisticNumber(list[1]) + "         ";
            netIncomeAmount.innerText = formatStatisticNumber(resp.netIncomeAmount);
            couponDeductionAmount.innerText = formatStatisticNumber(resp.couponDeductionAmount);
            refundAmount.innerText = formatStatisticNumber(resp.refundAmount);
            var statistics = resp.incomeStatistics || {};
            $("#totalIncome").text(formatStatisticNumber(statistics.totalIncomeAmount));
            $("#netIncomeAmount").text(formatStatisticNumber(statistics.netIncomeAmount));
            $("#couponDeductionAmount").text(formatStatisticNumber(statistics.couponDeductionAmount));
            $("#refundAmount").text(formatStatisticNumber(statistics.refundAmount));
            var incomeTypeNet = resp.incomeTypeNet || {};
            $("#fee1").text(formatStatisticNumber(incomeTypeNet[1]));
            $("#fee2").text(formatStatisticNumber(incomeTypeNet[2]));
@@ -398,108 +445,58 @@
            $("#fee4").text(formatStatisticNumber(incomeTypeNet[4]));
            $("#fee5").text(formatStatisticNumber(incomeTypeNet[5]));
            $("#fee7").text(formatStatisticNumber(incomeTypeNet[6]));
            t3.innerText = "月度营收:" + formatStatisticNumber(list[2]) + "         ";
            t4.innerText = "周度营收:" + formatStatisticNumber(list[3]) + "         ";
            t5.innerText = "日度营收:" + formatStatisticNumber(list[4]) + "         ";
            var year =  resp.yearIncome;
            var month =  resp.monthIncome;
            var week =  resp.weekIncome;
            console.log("周度营收")
            console.log(year)
            console.log(week)
            var days = week[0].length;
            var day =  resp.dayIncome;
            option  = {
                tooltip: {
                    trigger: 'axis'
                },
                legend: {
                    data: ['年度营收', '月度营收','周度营收','日度营收'],
                    icon: 'stack',
                    selectedMode: 'single', // 单选
                    selected: {
                        年度营收: true,
                        月度营收: false,
                        周度营收: false,
                        日度营收: false
                    }
                },
                grid: {
                    left: '3%',
                    right: '4%',
                    bottom: '3%',
                    containLabel: true
                },
                toolbox: {
                    feature: {
                        saveAsImage: {}
                    }
                },
                xAxis: {
                    type: 'category',
                    boundaryGap: false,
                    data: yearX
                },
                yAxis: {
                    type: 'value'
                },
                series: [
                    {
                        name: '年度营收',
                        type: 'line',
                        stack: 'Total',
                        data: [year[0],year[1],year[2],year[3],year[4],year[5],year[6],year[7],year[8],year[9],year[10]]
                    },
                    {
                        name: '月度营收',
                        type: 'line',
                        stack: 'Total',
                        data: [month[0][0],month[0][1], month[0][2], month[0][3], month[0][4],
                            month[0][5], month[0][6], month[0][7], month[0][8], month[0][9], month[0][10], month[0][11]]
                    },
                    {
                        name: '周度营收',
                        type: 'line',
                        stack: 'Total',
                        data: [
                            week[0][0], week[0][1], week[0][2], week[0][3], week[0][4]
                        ]
                    },
                    {
                        name: '日度营收',
                        type: 'line',
                        stack: 'Total',
                        data: [
                            day[0][0], day[0][1],day[0][2], day[0][3], day[0][4], day[0][5], day[0][6]
                        ]
                    }
                ]
            };
            renderIncomeStatisticsList("yearIncomeStatistics", "历年营收明细", resp.yearLabels || [], resp.yearIncomeStatistics || []);
            renderIncomeStatisticsList("monthIncomeStatistics", "本年各月营收明细", resp.monthLabels || [], resp.monthIncomeStatistics || []);
            renderIncomeStatisticsList("weekIncomeStatistics", "近四周营收明细", resp.weekLabels || [], resp.weekIncomeStatistics || []);
            renderIncomeStatisticsList("dayIncomeStatistics", "近七日营收明细", resp.dayLabels || [], resp.dayIncomeStatistics || []);
            renderIncomeTrendChart("incomeYearChart", "多年营收趋势", resp.yearLabels || [], resp.yearIncomeStatistics || []);
            renderIncomeTrendChart("incomeMonthChart", "本年各月营收趋势", resp.monthLabels || [], resp.monthIncomeStatistics || []);
            renderIncomeTrendChart("incomeWeekChart", "近四周营收趋势", resp.weekLabels || [], resp.weekIncomeStatistics || []);
            renderIncomeTrendChart("incomeDayChart", "近七日营收趋势", resp.dayLabels || [], resp.dayIncomeStatistics || []);
        });
        ajax.set("type",1);
        ajax.set("operatorId",id);
        ajax.set("time",beginTime);
        ajax.start();
        // 营收
        var income  = echarts.init(document.getElementById('income'));
        income.on('legendselectchanged', obj => {
            var options = income.getOption()
            //这里是选择切换什么样的x轴,那么他会进行对Y值的切换
            if (obj.name == '年度营收') {
                options.xAxis[0].data = yearX
            } else if (obj.name == '月度营收') {
                options.xAxis[0].data = monthX
            } else if (obj.name == '周度营收') {
                options.xAxis[0].data = weekX
            } else if (obj.name == '日度营收') {
                options.xAxis[0].data = dataX
            }
            income.setOption(options, true)
        })
        // 使用刚指定的配置项和数据显示图表。
        income.setOption(option);
    }
    function renderIncomeStatisticsList(elementId, title, labels, statistics) {
        var records = labels.map(function (label, index) {
            var item = statistics[index] || {};
            return '<div class="income-period-record"><strong>' + label + '</strong>:总营收金额 '
                + formatStatisticNumber(item.totalIncomeAmount) + ',净收入金额 ' + formatStatisticNumber(item.netIncomeAmount)
                + ',优惠券抵扣金额 ' + formatStatisticNumber(item.couponDeductionAmount)
                + ',退款金额 ' + formatStatisticNumber(item.refundAmount) + '</div>';
        }).join('');
        document.getElementById(elementId).innerHTML = '<div class="income-period-title">' + title
            + '</div><div class="income-period-records">' + records + '</div>';
    }
    function renderIncomeTrendChart(elementId, title, labels, statistics) {
        var chartDom = document.getElementById(elementId);
        var chart = echarts.getInstanceByDom(chartDom) || echarts.init(chartDom);
        chart.setOption({
            title: {text: title, left: 'center', textStyle: {fontSize: 15}},
            tooltip: {
                trigger: 'axis',
                formatter: function (params) {
                    var index = params[0].dataIndex;
                    var item = statistics[index] || {};
                    return labels[index] + '<br/>总营收:' + formatStatisticNumber(item.totalIncomeAmount)
                        + '<br/>净收入:' + formatStatisticNumber(item.netIncomeAmount)
                        + '<br/>优惠券抵扣:' + formatStatisticNumber(item.couponDeductionAmount)
                        + '<br/>退款:' + formatStatisticNumber(item.refundAmount);
                }
            },
            grid: {left: '5%', right: '4%', bottom: '18%', containLabel: true},
            xAxis: {type: 'category', boundaryGap: false, data: labels, axisLabel: {rotate: labels.length > 7 ? 35 : 0, interval: 0}},
            yAxis: {type: 'value', name: '金额(元)'},
            series: [{
                name: '总营收', type: 'line', smooth: true,
                data: statistics.map(function (item) { return item.totalIncomeAmount || 0; }),
                areaStyle: {opacity: 0.12}
            }]
        }, true);
    }
@@ -521,11 +518,6 @@
        getContent(1);
        getContent1(1);
        getContent2(1);
        // 基于准备好的dom,初始化echarts实例
        var myChart  = echarts.init(document.getElementById('income'));
        var myChart2 = echarts.init(document.getElementById('user'));
        var myChart3 = echarts.init(document.getElementById('vip'));