21
This commit is contained in:
parent
fb795c082e
commit
289821fc7f
|
|
@ -117,4 +117,38 @@ public class StatisticsController : BusinessControllerBase
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有盒子的汇总统计数据
|
||||
/// </summary>
|
||||
/// <param name="goodsId">盒子ID</param>
|
||||
/// <param name="title">盒子名称</param>
|
||||
/// <param name="status">状态</param>
|
||||
/// <param name="type">盒子类型</param>
|
||||
/// <param name="startTime">开始时间</param>
|
||||
/// <param name="endTime">结束时间</param>
|
||||
/// <returns>汇总统计数据</returns>
|
||||
[HttpGet("summary-statistics")]
|
||||
[BusinessPermission("statistics:view")]
|
||||
public async Task<IActionResult> GetSummaryStatistics(
|
||||
[FromQuery] int? goodsId,
|
||||
[FromQuery] string? title,
|
||||
[FromQuery] int? status,
|
||||
[FromQuery] int? type,
|
||||
[FromQuery] DateTime? startTime,
|
||||
[FromQuery] DateTime? endTime)
|
||||
{
|
||||
var request = new BoxSummaryStatisticsRequest
|
||||
{
|
||||
GoodsId = goodsId,
|
||||
Title = title,
|
||||
Status = status,
|
||||
Type = type,
|
||||
StartTime = startTime,
|
||||
EndTime = endTime
|
||||
};
|
||||
|
||||
var result = await _statisticsService.GetSummaryStatisticsAsync(request);
|
||||
return Ok(new { code = 0, msg = "获取成功", data = result });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -233,4 +233,55 @@ public class BoxProfitSummary
|
|||
/// 总发货金额
|
||||
/// </summary>
|
||||
public decimal TotalFhMoney { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 总抽奖次数
|
||||
/// </summary>
|
||||
public int TotalCjCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 利润率
|
||||
/// </summary>
|
||||
public decimal ProfitRate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否亏损
|
||||
/// </summary>
|
||||
public bool IsNegative { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 盒子汇总统计请求
|
||||
/// </summary>
|
||||
public class BoxSummaryStatisticsRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 盒子ID
|
||||
/// </summary>
|
||||
public int? GoodsId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 盒子名称
|
||||
/// </summary>
|
||||
public string? Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态:0-下架,1-上架
|
||||
/// </summary>
|
||||
public int? Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 盒子类型
|
||||
/// </summary>
|
||||
public int? Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 开始时间
|
||||
/// </summary>
|
||||
public DateTime? StartTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 结束时间
|
||||
/// </summary>
|
||||
public DateTime? EndTime { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,4 +45,11 @@ public interface IStatisticsService
|
|||
/// <param name="request">请求参数</param>
|
||||
/// <returns>盒子利润统计列表</returns>
|
||||
Task<PagedResult<BoxProfitItem>> GetBoxProfitListAsync(BoxProfitListRequest request);
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有盒子的汇总统计数据
|
||||
/// </summary>
|
||||
/// <param name="request">请求参数</param>
|
||||
/// <returns>汇总统计数据</returns>
|
||||
Task<BoxProfitSummary> GetSummaryStatisticsAsync(BoxSummaryStatisticsRequest request);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -583,4 +583,152 @@ public class StatisticsService : IStatisticsService
|
|||
|
||||
return PagedResult<BoxProfitItem>.Create(goods, total, request.Page, request.PageSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有盒子的汇总统计数据
|
||||
/// </summary>
|
||||
public async Task<BoxProfitSummary> GetSummaryStatisticsAsync(BoxSummaryStatisticsRequest request)
|
||||
{
|
||||
// 构建盒子查询条件
|
||||
var goodsQuery = _dbContext.Goods
|
||||
.AsNoTracking()
|
||||
.Where(g => g.DeletedAt == null);
|
||||
|
||||
if (request.GoodsId.HasValue && request.GoodsId > 0)
|
||||
{
|
||||
goodsQuery = goodsQuery.Where(g => g.Id == request.GoodsId);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.Title))
|
||||
{
|
||||
goodsQuery = goodsQuery.Where(g => g.Title.Contains(request.Title));
|
||||
}
|
||||
|
||||
if (request.Status.HasValue)
|
||||
{
|
||||
goodsQuery = goodsQuery.Where(g => g.Status == request.Status);
|
||||
}
|
||||
|
||||
if (request.Type.HasValue && request.Type > 0)
|
||||
{
|
||||
goodsQuery = goodsQuery.Where(g => g.Type == request.Type);
|
||||
}
|
||||
|
||||
// 获取符合条件的盒子ID列表
|
||||
var goodsIds = await goodsQuery.Select(g => g.Id).ToListAsync();
|
||||
|
||||
if (!goodsIds.Any())
|
||||
{
|
||||
return new BoxProfitSummary
|
||||
{
|
||||
TotalIncome = 0,
|
||||
TotalCost = 0,
|
||||
TotalProfit = 0,
|
||||
TotalReMoney = 0,
|
||||
TotalFhMoney = 0,
|
||||
TotalCjCount = 0,
|
||||
ProfitRate = 0,
|
||||
IsNegative = false
|
||||
};
|
||||
}
|
||||
|
||||
// 获取测试用户ID列表
|
||||
var testUserIds = await _dbContext.Users
|
||||
.Where(u => u.IsTest > 0 || u.Status == 2)
|
||||
.Select(u => u.Id)
|
||||
.ToListAsync();
|
||||
|
||||
if (!testUserIds.Any())
|
||||
{
|
||||
testUserIds = new List<int> { 0 };
|
||||
}
|
||||
|
||||
// 构建时间范围条件(转换为Unix时间戳)
|
||||
var hasTimeRange = request.StartTime.HasValue && request.EndTime.HasValue;
|
||||
var startTimestamp = hasTimeRange ? (int)((DateTimeOffset)request.StartTime!.Value).ToUnixTimeSeconds() : 0;
|
||||
var endTimestamp = hasTimeRange ? (int)((DateTimeOffset)request.EndTime!.Value).ToUnixTimeSeconds() : 0;
|
||||
|
||||
// 查询1:获取消费金额(充值金额 + 余额消费)
|
||||
var orderQuery = _dbContext.Orders
|
||||
.Where(o => o.Status == 1)
|
||||
.Where(o => o.Price > 0 || o.UseMoney > 0)
|
||||
.Where(o => goodsIds.Contains(o.GoodsId))
|
||||
.Where(o => !testUserIds.Contains(o.UserId));
|
||||
|
||||
if (hasTimeRange)
|
||||
{
|
||||
orderQuery = orderQuery.Where(o => o.PayTime > startTimestamp && o.PayTime < endTimestamp);
|
||||
}
|
||||
|
||||
var priceSum = await orderQuery.SumAsync(o => (decimal?)o.Price) ?? 0;
|
||||
var useMoneySum = await orderQuery.SumAsync(o => (decimal?)o.UseMoney) ?? 0;
|
||||
var totalIncome = priceSum + useMoneySum;
|
||||
|
||||
// 查询2:获取出货成本(奖品价值)
|
||||
var scMoneyQuery = _dbContext.OrderItems
|
||||
.Where(oi => oi.GoodsId.HasValue && goodsIds.Contains(oi.GoodsId.Value))
|
||||
.Where(oi => !testUserIds.Contains(oi.UserId));
|
||||
|
||||
if (hasTimeRange)
|
||||
{
|
||||
scMoneyQuery = scMoneyQuery.Where(oi => oi.CreatedAt > request.StartTime && oi.CreatedAt < request.EndTime);
|
||||
}
|
||||
|
||||
var totalCost = await scMoneyQuery.SumAsync(oi => (decimal?)oi.GoodslistMoney) ?? 0;
|
||||
|
||||
// 查询3:获取兑换成本(已回收的奖品价值,status=1)
|
||||
var reMoneyQuery = _dbContext.OrderItems
|
||||
.Where(oi => oi.GoodsId.HasValue && goodsIds.Contains(oi.GoodsId.Value))
|
||||
.Where(oi => oi.Status == 1)
|
||||
.Where(oi => !testUserIds.Contains(oi.UserId));
|
||||
|
||||
if (hasTimeRange)
|
||||
{
|
||||
reMoneyQuery = reMoneyQuery.Where(oi => oi.CreatedAt > request.StartTime && oi.CreatedAt < request.EndTime);
|
||||
}
|
||||
|
||||
var totalReMoney = await reMoneyQuery.SumAsync(oi => (decimal?)oi.GoodslistMoney) ?? 0;
|
||||
|
||||
// 查询4:获取发货成本(已发货的奖品价值,status=2)
|
||||
var fhMoneyQuery = _dbContext.OrderItems
|
||||
.Where(oi => oi.GoodsId.HasValue && goodsIds.Contains(oi.GoodsId.Value))
|
||||
.Where(oi => oi.Status == 2)
|
||||
.Where(oi => !testUserIds.Contains(oi.UserId));
|
||||
|
||||
if (hasTimeRange)
|
||||
{
|
||||
fhMoneyQuery = fhMoneyQuery.Where(oi => oi.CreatedAt > request.StartTime && oi.CreatedAt < request.EndTime);
|
||||
}
|
||||
|
||||
var totalFhMoney = await fhMoneyQuery.SumAsync(oi => (decimal?)oi.GoodslistMoney) ?? 0;
|
||||
|
||||
// 查询5:获取抽奖次数
|
||||
var cjCountQuery = _dbContext.OrderItems
|
||||
.Where(oi => oi.GoodsId.HasValue && goodsIds.Contains(oi.GoodsId.Value))
|
||||
.Where(oi => !testUserIds.Contains(oi.UserId))
|
||||
.Where(oi => oi.ParentGoodsListId == 0);
|
||||
|
||||
if (hasTimeRange)
|
||||
{
|
||||
cjCountQuery = cjCountQuery.Where(oi => oi.CreatedAt > request.StartTime && oi.CreatedAt < request.EndTime);
|
||||
}
|
||||
|
||||
var totalCjCount = await cjCountQuery.CountAsync();
|
||||
|
||||
// 计算总利润和利润率
|
||||
var totalProfit = totalIncome - (totalCost - totalReMoney);
|
||||
var profitRate = totalIncome > 0 ? Math.Round((totalProfit / totalIncome) * 100, 2) : 0;
|
||||
|
||||
return new BoxProfitSummary
|
||||
{
|
||||
TotalIncome = totalIncome,
|
||||
TotalCost = totalCost,
|
||||
TotalProfit = totalProfit,
|
||||
TotalReMoney = totalReMoney,
|
||||
TotalFhMoney = totalFhMoney,
|
||||
TotalCjCount = totalCjCount,
|
||||
ProfitRate = profitRate,
|
||||
IsNegative = totalProfit < 0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
using HoneyBox.Admin.Business.Models;
|
||||
using HoneyBox.Admin.Business.Models;
|
||||
using HoneyBox.Admin.Business.Models.User;
|
||||
using HoneyBox.Admin.Business.Services.Interfaces;
|
||||
using HoneyBox.Model.Data;
|
||||
using HoneyBox.Model.Entities;
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
|
|
@ -1083,14 +1084,14 @@ public class UserBusinessService : IUserBusinessService
|
|||
// 尝试解析为用户ID
|
||||
if (int.TryParse(keyword, out var keywordUserId))
|
||||
{
|
||||
query = query.Where(u =>
|
||||
u.Id == keywordUserId ||
|
||||
query = query.Where(u =>
|
||||
u.Id == keywordUserId ||
|
||||
(u.Mobile != null && u.Mobile.Contains(keyword)) ||
|
||||
u.Nickname.Contains(keyword));
|
||||
}
|
||||
else
|
||||
{
|
||||
query = query.Where(u =>
|
||||
query = query.Where(u =>
|
||||
(u.Mobile != null && u.Mobile.Contains(keyword)) ||
|
||||
u.Nickname.Contains(keyword));
|
||||
}
|
||||
|
|
@ -1099,6 +1100,7 @@ public class UserBusinessService : IUserBusinessService
|
|||
if (request.UserId.HasValue)
|
||||
{
|
||||
query = query.Where(u => u.Id == request.UserId.Value);
|
||||
query = query.Where(u => u.Uid == request.UserId.ToString());
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.Mobile))
|
||||
|
|
|
|||
|
|
@ -280,6 +280,12 @@ export interface BoxProfitSummary {
|
|||
totalReMoney: number
|
||||
/** 总发货金额 */
|
||||
totalFhMoney: number
|
||||
/** 总抽奖次数 */
|
||||
totalCjCount: number
|
||||
/** 利润率 */
|
||||
profitRate: number
|
||||
/** 是否亏损 */
|
||||
isNegative: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -320,3 +326,34 @@ export function getBoxProfitList(params: BoxProfitListParams): Promise<ApiRespon
|
|||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 盒子汇总统计请求参数
|
||||
*/
|
||||
export interface BoxSummaryStatisticsParams {
|
||||
/** 盒子ID */
|
||||
goodsId?: number
|
||||
/** 盒子名称 */
|
||||
title?: string
|
||||
/** 状态 */
|
||||
status?: number
|
||||
/** 盒子类型 */
|
||||
type?: number
|
||||
/** 开始时间 */
|
||||
startTime?: string
|
||||
/** 结束时间 */
|
||||
endTime?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有盒子的汇总统计数据
|
||||
* @param params 请求参数
|
||||
* @returns 汇总统计数据
|
||||
*/
|
||||
export function getSummaryStatistics(params: BoxSummaryStatisticsParams): Promise<ApiResponse<{ code: number; msg: string; data: BoxProfitSummary }>> {
|
||||
return request({
|
||||
url: `${STATISTICS_BASE_URL}/summary-statistics`,
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,57 @@
|
|||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- 汇总统计卡片 -->
|
||||
<el-card class="summary-card" shadow="never" v-loading="summaryLoading">
|
||||
<template #header>
|
||||
<div class="summary-header">
|
||||
<span>汇总统计</span>
|
||||
<el-button type="primary" link @click="loadSummary">刷新汇总</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="4">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">总抽奖次数</div>
|
||||
<div class="stat-value">{{ summary.totalCjCount.toLocaleString() }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">总收入</div>
|
||||
<div class="stat-value">¥{{ summary.totalIncome.toFixed(2) }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">总出货成本</div>
|
||||
<div class="stat-value">¥{{ summary.totalCost.toFixed(2) }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">总兑换金额</div>
|
||||
<div class="stat-value">¥{{ summary.totalReMoney.toFixed(2) }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">总发货金额</div>
|
||||
<div class="stat-value">¥{{ summary.totalFhMoney.toFixed(2) }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">总利润</div>
|
||||
<div class="stat-value" :class="{ 'text-danger': summary.isNegative, 'text-success': !summary.isNegative }">
|
||||
¥{{ summary.totalProfit.toFixed(2) }}
|
||||
<span class="profit-rate">({{ summary.profitRate.toFixed(2) }}%)</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<el-card class="table-card" shadow="never">
|
||||
<el-table :data="tableData" v-loading="loading" stripe border style="width: 100%">
|
||||
|
|
@ -127,7 +178,7 @@
|
|||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { Search, Refresh } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getBoxProfitList, getBoxStatistics, type BoxProfitItem } from '@/api/business/statistics'
|
||||
import { getBoxProfitList, getBoxStatistics, getSummaryStatistics, type BoxProfitItem, type BoxProfitSummary } from '@/api/business/statistics'
|
||||
|
||||
// 盒子类型选项
|
||||
const boxTypes = [
|
||||
|
|
@ -156,6 +207,19 @@ const dateRange = ref<[string, string] | null>(null)
|
|||
const tableData = ref<BoxProfitItem[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
// 汇总统计
|
||||
const summaryLoading = ref(false)
|
||||
const summary = reactive<BoxProfitSummary>({
|
||||
totalIncome: 0,
|
||||
totalCost: 0,
|
||||
totalProfit: 0,
|
||||
totalReMoney: 0,
|
||||
totalFhMoney: 0,
|
||||
totalCjCount: 0,
|
||||
profitRate: 0,
|
||||
isNegative: false
|
||||
})
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
|
|
@ -163,6 +227,32 @@ const pagination = reactive({
|
|||
total: 0
|
||||
})
|
||||
|
||||
// 加载汇总统计
|
||||
async function loadSummary() {
|
||||
summaryLoading.value = true
|
||||
try {
|
||||
const params: any = {}
|
||||
if (searchForm.goodsId) params.goodsId = searchForm.goodsId
|
||||
if (searchForm.title) params.title = searchForm.title
|
||||
if (searchForm.status !== undefined) params.status = searchForm.status
|
||||
if (searchForm.type) params.type = searchForm.type
|
||||
if (dateRange.value) {
|
||||
params.startTime = dateRange.value[0]
|
||||
params.endTime = dateRange.value[1]
|
||||
}
|
||||
|
||||
const res = await getSummaryStatistics(params) as any
|
||||
if (res.code === 0 && res.data) {
|
||||
Object.assign(summary, res.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载汇总统计失败:', error)
|
||||
ElMessage.error('加载汇总统计失败')
|
||||
} finally {
|
||||
summaryLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载列表数据
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
|
|
@ -225,6 +315,7 @@ async function loadBoxStats(row: BoxProfitItem) {
|
|||
function handleSearch() {
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
loadSummary()
|
||||
}
|
||||
|
||||
// 重置
|
||||
|
|
@ -236,6 +327,7 @@ function handleReset() {
|
|||
dateRange.value = null
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
loadSummary()
|
||||
}
|
||||
|
||||
// 分页大小变化
|
||||
|
|
@ -253,6 +345,7 @@ function handleCurrentChange(page: number) {
|
|||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
loadSummary()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
@ -265,6 +358,38 @@ onMounted(() => {
|
|||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.summary-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.profit-rate {
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user