21
This commit is contained in:
parent
f0bbcef48b
commit
190302b318
|
|
@ -11,9 +11,9 @@
|
|||
|
||||
// 测试环境配置 - .NET 10 后端
|
||||
const testing = {
|
||||
// baseUrl: 'https://app.zpc-xy.com/honey/api',
|
||||
baseUrl: 'https://app.zpc-xy.com/honey/api',
|
||||
// baseUrl: 'http://192.168.1.24:5238',
|
||||
baseUrl: 'http://192.168.195.15:2822',
|
||||
// baseUrl: 'http://192.168.195.15:2822',
|
||||
imageUrl: 'https://youdas-1308826010.cos.ap-shanghai.myqcloud.com',
|
||||
loginPage: '',
|
||||
wxAppId: ''
|
||||
|
|
|
|||
43
server/HoneyBox/scripts/seed_box_profit_menu.sql
Normal file
43
server/HoneyBox/scripts/seed_box_profit_menu.sql
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
-- 盒子利润统计菜单种子数据
|
||||
-- 执行前请确保统计报表目录菜单已存在
|
||||
|
||||
USE honey_box_admin;
|
||||
GO
|
||||
|
||||
-- 查找统计报表目录的ID
|
||||
DECLARE @statisticsMenuId BIGINT;
|
||||
SELECT @statisticsMenuId = Id FROM menus WHERE Path = '/business/statistics' AND MenuType = 1;
|
||||
|
||||
-- 如果统计报表目录不存在,先创建它
|
||||
IF @statisticsMenuId IS NULL
|
||||
BEGIN
|
||||
DECLARE @businessMenuId BIGINT;
|
||||
SELECT @businessMenuId = Id FROM menus WHERE Path = '/business' AND MenuType = 1;
|
||||
|
||||
INSERT INTO menus (Name, Path, Component, Icon, SortOrder, MenuType, Status, ParentId, Permission, CreatedAt, UpdatedAt, IsExternal, IsCache)
|
||||
VALUES (N'统计报表', '/business/statistics', NULL, 'DataAnalysis', 80, 1, 1, @businessMenuId, NULL, GETDATE(), GETDATE(), 0, 0);
|
||||
|
||||
SET @statisticsMenuId = SCOPE_IDENTITY();
|
||||
PRINT N'创建统计报表目录菜单,ID: ' + CAST(@statisticsMenuId AS NVARCHAR(10));
|
||||
END
|
||||
|
||||
-- 检查盒子利润统计菜单是否已存在
|
||||
IF NOT EXISTS (SELECT 1 FROM menus WHERE Path = '/business/statistics/box-profit')
|
||||
BEGIN
|
||||
INSERT INTO menus (Name, Path, Component, Icon, SortOrder, MenuType, Status, ParentId, Permission, CreatedAt, UpdatedAt, IsExternal, IsCache)
|
||||
VALUES (N'盒子利润统计', '/business/statistics/box-profit', 'business/statistics/box-profit', 'TrendCharts', 2, 2, 1,
|
||||
@statisticsMenuId, 'statistics:box-profit', GETDATE(), GETDATE(), 0, 1);
|
||||
|
||||
PRINT N'创建盒子利润统计菜单成功';
|
||||
END
|
||||
ELSE
|
||||
BEGIN
|
||||
PRINT N'盒子利润统计菜单已存在,跳过创建';
|
||||
END
|
||||
|
||||
-- 查看结果
|
||||
SELECT Id, Name, Path, Component, Permission, ParentId, SortOrder, MenuType, Status
|
||||
FROM menus
|
||||
WHERE Path LIKE '/business/statistics%'
|
||||
ORDER BY ParentId, SortOrder;
|
||||
GO
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
using HoneyBox.Admin.Business.Attributes;
|
||||
using HoneyBox.Admin.Business.Models.Statistics;
|
||||
using HoneyBox.Admin.Business.Services.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
|
|
@ -64,4 +65,56 @@ public class StatisticsController : BusinessControllerBase
|
|||
var result = await _statisticsService.GetUserStatsAsync();
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取单个盒子的统计数据
|
||||
/// </summary>
|
||||
/// <param name="goodsId">盒子ID</param>
|
||||
/// <param name="startTime">开始时间</param>
|
||||
/// <param name="endTime">结束时间</param>
|
||||
/// <returns>盒子统计数据</returns>
|
||||
[HttpGet("box-statistics")]
|
||||
[BusinessPermission("statistics:view")]
|
||||
public async Task<IActionResult> GetBoxStatistics(
|
||||
[FromQuery] int goodsId,
|
||||
[FromQuery] DateTime? startTime,
|
||||
[FromQuery] DateTime? endTime)
|
||||
{
|
||||
var request = new BoxStatisticsRequest
|
||||
{
|
||||
GoodsId = goodsId,
|
||||
StartTime = startTime,
|
||||
EndTime = endTime
|
||||
};
|
||||
|
||||
var result = await _statisticsService.GetBoxStatisticsAsync(request);
|
||||
return Ok(new { code = 0, msg = "获取成功", data = result });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取盒子利润统计列表
|
||||
/// </summary>
|
||||
/// <param name="request">请求参数</param>
|
||||
/// <returns>盒子利润统计列表</returns>
|
||||
[HttpGet("box-profit-list")]
|
||||
[BusinessPermission("statistics:view")]
|
||||
public async Task<IActionResult> GetBoxProfitList([FromQuery] BoxProfitListRequest request)
|
||||
{
|
||||
var result = await _statisticsService.GetBoxProfitListAsync(request);
|
||||
return Ok(new
|
||||
{
|
||||
code = 0,
|
||||
msg = "获取数据成功",
|
||||
count = result.Total,
|
||||
data = result.List,
|
||||
summary = new BoxProfitSummary
|
||||
{
|
||||
TotalIncome = 0,
|
||||
TotalCost = 0,
|
||||
TotalProfit = 0,
|
||||
TotalReMoney = 0,
|
||||
TotalFhMoney = 0
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,236 @@
|
|||
namespace HoneyBox.Admin.Business.Models.Statistics;
|
||||
|
||||
/// <summary>
|
||||
/// 盒子统计请求
|
||||
/// </summary>
|
||||
public class BoxStatisticsRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 盒子ID
|
||||
/// </summary>
|
||||
public int GoodsId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 开始时间
|
||||
/// </summary>
|
||||
public DateTime? StartTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 结束时间
|
||||
/// </summary>
|
||||
public DateTime? EndTime { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 盒子统计响应
|
||||
/// </summary>
|
||||
public class BoxStatisticsResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// 盒子ID
|
||||
/// </summary>
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 消费金额(充值金额 + 余额消费)
|
||||
/// </summary>
|
||||
public decimal UseMoney { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 出货成本(奖品价值)
|
||||
/// </summary>
|
||||
public decimal ScMoney { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 兑换成本(已回收的奖品价值)
|
||||
/// </summary>
|
||||
public decimal ReMoney { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 发货成本(已发货的奖品价值)
|
||||
/// </summary>
|
||||
public decimal FhMoney { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 抽奖次数
|
||||
/// </summary>
|
||||
public int CjCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 利润 = UseMoney - (ScMoney - ReMoney)
|
||||
/// </summary>
|
||||
public decimal Profit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 利润率
|
||||
/// </summary>
|
||||
public decimal ProfitRate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否亏损
|
||||
/// </summary>
|
||||
public bool IsNegative { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 盒子利润统计列表请求
|
||||
/// </summary>
|
||||
public class BoxProfitListRequest : PagedRequest
|
||||
{
|
||||
/// <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; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 盒子利润统计列表项
|
||||
/// </summary>
|
||||
public class BoxProfitItem
|
||||
{
|
||||
/// <summary>
|
||||
/// 盒子ID
|
||||
/// </summary>
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 盒子名称
|
||||
/// </summary>
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 盒子图片
|
||||
/// </summary>
|
||||
public string? ImgUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 单价
|
||||
/// </summary>
|
||||
public decimal Price { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 库存
|
||||
/// </summary>
|
||||
public int Stock { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态:0-下架,1-上架
|
||||
/// </summary>
|
||||
public int Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态名称
|
||||
/// </summary>
|
||||
public string StatusName => Status == 1 ? "上架" : "下架";
|
||||
|
||||
/// <summary>
|
||||
/// 盒子类型
|
||||
/// </summary>
|
||||
public int Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 类型名称
|
||||
/// </summary>
|
||||
public string TypeName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 消费金额
|
||||
/// </summary>
|
||||
public decimal UseMoney { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 出货成本
|
||||
/// </summary>
|
||||
public decimal ScMoney { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 兑换成本
|
||||
/// </summary>
|
||||
public decimal ReMoney { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 发货成本
|
||||
/// </summary>
|
||||
public decimal FhMoney { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 抽奖次数
|
||||
/// </summary>
|
||||
public int CjCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 利润
|
||||
/// </summary>
|
||||
public decimal Profit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 利润率
|
||||
/// </summary>
|
||||
public decimal ProfitRate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否亏损
|
||||
/// </summary>
|
||||
public bool IsNegative { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否已加载统计数据
|
||||
/// </summary>
|
||||
public bool Loaded { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 盒子利润汇总
|
||||
/// </summary>
|
||||
public class BoxProfitSummary
|
||||
{
|
||||
/// <summary>
|
||||
/// 总收入
|
||||
/// </summary>
|
||||
public decimal TotalIncome { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 总成本
|
||||
/// </summary>
|
||||
public decimal TotalCost { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 总利润
|
||||
/// </summary>
|
||||
public decimal TotalProfit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 总兑换金额
|
||||
/// </summary>
|
||||
public decimal TotalReMoney { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 总发货金额
|
||||
/// </summary>
|
||||
public decimal TotalFhMoney { get; set; }
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
using HoneyBox.Admin.Business.Models;
|
||||
using HoneyBox.Admin.Business.Models.Statistics;
|
||||
|
||||
namespace HoneyBox.Admin.Business.Services.Interfaces;
|
||||
|
|
@ -30,4 +31,18 @@ public interface IStatisticsService
|
|||
/// </summary>
|
||||
/// <returns>用户统计</returns>
|
||||
Task<UserStatsResponse> GetUserStatsAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 获取单个盒子的统计数据
|
||||
/// </summary>
|
||||
/// <param name="request">请求参数</param>
|
||||
/// <returns>盒子统计数据</returns>
|
||||
Task<BoxStatisticsResponse> GetBoxStatisticsAsync(BoxStatisticsRequest request);
|
||||
|
||||
/// <summary>
|
||||
/// 获取盒子利润统计列表
|
||||
/// </summary>
|
||||
/// <param name="request">请求参数</param>
|
||||
/// <returns>盒子利润统计列表</returns>
|
||||
Task<PagedResult<BoxProfitItem>> GetBoxProfitListAsync(BoxProfitListRequest request);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using HoneyBox.Admin.Business.Models;
|
||||
using HoneyBox.Admin.Business.Models.Statistics;
|
||||
using HoneyBox.Admin.Business.Services.Interfaces;
|
||||
using HoneyBox.Model.Data;
|
||||
|
|
@ -14,6 +15,20 @@ public class StatisticsService : IStatisticsService
|
|||
private readonly HoneyBoxDbContext _dbContext;
|
||||
private readonly ILogger<StatisticsService> _logger;
|
||||
|
||||
// 盒子类型名称映射
|
||||
private static readonly Dictionary<int, string> BoxTypeNames = new()
|
||||
{
|
||||
{ 1, "一番赏" },
|
||||
{ 2, "无限赏" },
|
||||
{ 3, "擂台赏" },
|
||||
{ 4, "抽卡机" },
|
||||
{ 5, "福袋" },
|
||||
{ 6, "幸运赏" },
|
||||
{ 8, "盲盒" },
|
||||
{ 9, "扭蛋" },
|
||||
{ 15, "福利屋" }
|
||||
};
|
||||
|
||||
public StatisticsService(HoneyBoxDbContext dbContext, ILogger<StatisticsService> logger)
|
||||
{
|
||||
_dbContext = dbContext;
|
||||
|
|
@ -386,4 +401,186 @@ public class StatisticsService : IStatisticsService
|
|||
ShippedAmount = shippedAmount
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取单个盒子的统计数据
|
||||
/// </summary>
|
||||
public async Task<BoxStatisticsResponse> GetBoxStatisticsAsync(BoxStatisticsRequest request)
|
||||
{
|
||||
if (request.GoodsId <= 0)
|
||||
{
|
||||
throw new ArgumentException("盒子ID无效");
|
||||
}
|
||||
|
||||
// 验证盒子存在
|
||||
var goodsExists = await _dbContext.Goods.AnyAsync(g => g.Id == request.GoodsId && g.DeletedAt == null);
|
||||
if (!goodsExists)
|
||||
{
|
||||
throw new ArgumentException("盒子不存在");
|
||||
}
|
||||
|
||||
// 获取测试用户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 => o.GoodsId == request.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 useMoney = priceSum + useMoneySum;
|
||||
|
||||
// 查询2:获取出货成本(奖品价值)
|
||||
var scMoneyQuery = _dbContext.OrderItems
|
||||
.Where(oi => oi.GoodsId == request.GoodsId)
|
||||
.Where(oi => !testUserIds.Contains(oi.UserId));
|
||||
|
||||
if (hasTimeRange)
|
||||
{
|
||||
scMoneyQuery = scMoneyQuery.Where(oi => oi.CreatedAt > request.StartTime && oi.CreatedAt < request.EndTime);
|
||||
}
|
||||
|
||||
var scMoney = await scMoneyQuery.SumAsync(oi => (decimal?)oi.GoodslistMoney) ?? 0;
|
||||
|
||||
// 查询3:获取兑换成本(已回收的奖品价值,status=1)
|
||||
var reMoneyQuery = _dbContext.OrderItems
|
||||
.Where(oi => oi.GoodsId == request.GoodsId)
|
||||
.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 reMoney = await reMoneyQuery.SumAsync(oi => (decimal?)oi.GoodslistMoney) ?? 0;
|
||||
|
||||
// 查询4:获取发货成本(已发货的奖品价值,status=2)
|
||||
var fhMoneyQuery = _dbContext.OrderItems
|
||||
.Where(oi => oi.GoodsId == request.GoodsId)
|
||||
.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 fhMoney = await fhMoneyQuery.SumAsync(oi => (decimal?)oi.GoodslistMoney) ?? 0;
|
||||
|
||||
// 查询5:获取抽奖次数(parent_goods_list_id = 0 表示主抽奖记录)
|
||||
var cjCountQuery = _dbContext.OrderItems
|
||||
.Where(oi => oi.GoodsId == request.GoodsId)
|
||||
.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 cjCount = await cjCountQuery.CountAsync();
|
||||
|
||||
// 计算利润和利润率
|
||||
var profit = useMoney - (scMoney - reMoney);
|
||||
var profitRate = useMoney > 0 ? Math.Round((profit / useMoney) * 100, 2) : 0;
|
||||
|
||||
return new BoxStatisticsResponse
|
||||
{
|
||||
Id = request.GoodsId,
|
||||
UseMoney = useMoney,
|
||||
ScMoney = scMoney,
|
||||
ReMoney = reMoney,
|
||||
FhMoney = fhMoney,
|
||||
CjCount = cjCount,
|
||||
Profit = profit,
|
||||
ProfitRate = profitRate,
|
||||
IsNegative = profit < 0
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取盒子利润统计列表
|
||||
/// </summary>
|
||||
public async Task<PagedResult<BoxProfitItem>> GetBoxProfitListAsync(BoxProfitListRequest request)
|
||||
{
|
||||
// 构建查询条件
|
||||
var query = _dbContext.Goods
|
||||
.AsNoTracking()
|
||||
.Where(g => g.DeletedAt == null);
|
||||
|
||||
if (request.GoodsId.HasValue && request.GoodsId > 0)
|
||||
{
|
||||
query = query.Where(g => g.Id == request.GoodsId);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.Title))
|
||||
{
|
||||
query = query.Where(g => g.Title.Contains(request.Title));
|
||||
}
|
||||
|
||||
if (request.Status.HasValue)
|
||||
{
|
||||
query = query.Where(g => g.Status == request.Status);
|
||||
}
|
||||
|
||||
if (request.Type.HasValue && request.Type > 0)
|
||||
{
|
||||
query = query.Where(g => g.Type == request.Type);
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
var total = await query.CountAsync();
|
||||
|
||||
// 分页获取盒子列表
|
||||
var goods = await query
|
||||
.OrderByDescending(g => g.Id)
|
||||
.Skip(request.Skip)
|
||||
.Take(request.PageSize)
|
||||
.Select(g => new BoxProfitItem
|
||||
{
|
||||
Id = g.Id,
|
||||
Title = g.Title,
|
||||
ImgUrl = g.ImgUrl,
|
||||
Price = g.Price,
|
||||
Stock = g.Stock,
|
||||
Status = g.Status,
|
||||
Type = g.Type,
|
||||
TypeName = BoxTypeNames.ContainsKey(g.Type) ? BoxTypeNames[g.Type] : "未知类型",
|
||||
// 统计数据初始化为0,后续异步加载
|
||||
UseMoney = 0,
|
||||
ScMoney = 0,
|
||||
ReMoney = 0,
|
||||
FhMoney = 0,
|
||||
CjCount = 0,
|
||||
Profit = 0,
|
||||
ProfitRate = 0,
|
||||
IsNegative = false,
|
||||
Loaded = false
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
return PagedResult<BoxProfitItem>.Create(goods, total, request.Page, request.PageSize);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -163,3 +163,160 @@ export function getUserStats(): Promise<ApiResponse<UserStats>> {
|
|||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== 盒子统计类型定义 ====================
|
||||
|
||||
/**
|
||||
* 盒子统计请求参数
|
||||
*/
|
||||
export interface BoxStatisticsParams {
|
||||
/** 盒子ID */
|
||||
goodsId: number
|
||||
/** 开始时间 */
|
||||
startTime?: string
|
||||
/** 结束时间 */
|
||||
endTime?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 盒子统计响应
|
||||
*/
|
||||
export interface BoxStatistics {
|
||||
/** 盒子ID */
|
||||
id: number
|
||||
/** 消费金额(充值金额 + 余额消费) */
|
||||
useMoney: number
|
||||
/** 出货成本(奖品价值) */
|
||||
scMoney: number
|
||||
/** 兑换成本(已回收的奖品价值) */
|
||||
reMoney: number
|
||||
/** 发货成本(已发货的奖品价值) */
|
||||
fhMoney: number
|
||||
/** 抽奖次数 */
|
||||
cjCount: number
|
||||
/** 利润 */
|
||||
profit: number
|
||||
/** 利润率 */
|
||||
profitRate: number
|
||||
/** 是否亏损 */
|
||||
isNegative: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* 盒子利润列表请求参数
|
||||
*/
|
||||
export interface BoxProfitListParams {
|
||||
/** 页码 */
|
||||
page?: number
|
||||
/** 每页数量 */
|
||||
pageSize?: number
|
||||
/** 盒子ID */
|
||||
goodsId?: number
|
||||
/** 盒子名称 */
|
||||
title?: string
|
||||
/** 状态 */
|
||||
status?: number
|
||||
/** 盒子类型 */
|
||||
type?: number
|
||||
/** 开始时间 */
|
||||
startTime?: string
|
||||
/** 结束时间 */
|
||||
endTime?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 盒子利润列表项
|
||||
*/
|
||||
export interface BoxProfitItem {
|
||||
/** 盒子ID */
|
||||
id: number
|
||||
/** 盒子名称 */
|
||||
title: string
|
||||
/** 盒子图片 */
|
||||
imgUrl: string
|
||||
/** 单价 */
|
||||
price: number
|
||||
/** 库存 */
|
||||
stock: number
|
||||
/** 状态 */
|
||||
status: number
|
||||
/** 状态名称 */
|
||||
statusName: string
|
||||
/** 盒子类型 */
|
||||
type: number
|
||||
/** 类型名称 */
|
||||
typeName: string
|
||||
/** 消费金额 */
|
||||
useMoney: number
|
||||
/** 出货成本 */
|
||||
scMoney: number
|
||||
/** 兑换成本 */
|
||||
reMoney: number
|
||||
/** 发货成本 */
|
||||
fhMoney: number
|
||||
/** 抽奖次数 */
|
||||
cjCount: number
|
||||
/** 利润 */
|
||||
profit: number
|
||||
/** 利润率 */
|
||||
profitRate: number
|
||||
/** 是否亏损 */
|
||||
isNegative: boolean
|
||||
/** 是否已加载统计数据 */
|
||||
loaded: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* 盒子利润汇总
|
||||
*/
|
||||
export interface BoxProfitSummary {
|
||||
/** 总收入 */
|
||||
totalIncome: number
|
||||
/** 总成本 */
|
||||
totalCost: number
|
||||
/** 总利润 */
|
||||
totalProfit: number
|
||||
/** 总兑换金额 */
|
||||
totalReMoney: number
|
||||
/** 总发货金额 */
|
||||
totalFhMoney: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 盒子利润列表响应
|
||||
*/
|
||||
export interface BoxProfitListResponse {
|
||||
code: number
|
||||
msg: string
|
||||
count: number
|
||||
data: BoxProfitItem[]
|
||||
summary: BoxProfitSummary
|
||||
}
|
||||
|
||||
// ==================== 盒子统计 API ====================
|
||||
|
||||
/**
|
||||
* 获取单个盒子的统计数据
|
||||
* @param params 请求参数
|
||||
* @returns 盒子统计数据
|
||||
*/
|
||||
export function getBoxStatistics(params: BoxStatisticsParams): Promise<ApiResponse<{ code: number; msg: string; data: BoxStatistics }>> {
|
||||
return request({
|
||||
url: `${STATISTICS_BASE_URL}/box-statistics`,
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取盒子利润统计列表
|
||||
* @param params 请求参数
|
||||
* @returns 盒子利润统计列表
|
||||
*/
|
||||
export function getBoxProfitList(params: BoxProfitListParams): Promise<ApiResponse<BoxProfitListResponse>> {
|
||||
return request({
|
||||
url: `${STATISTICS_BASE_URL}/box-profit-list`,
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -589,6 +589,16 @@ export const businessRoutes: RouteRecordRaw[] = [
|
|||
permission: 'statistics:data-stand',
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'box-profit',
|
||||
name: 'BoxProfit',
|
||||
component: () => import('@/views/business/statistics/box-profit.vue'),
|
||||
meta: {
|
||||
title: '盒子利润统计',
|
||||
permission: 'statistics:box-profit',
|
||||
keepAlive: true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1199,7 +1209,9 @@ export const welfareTaskPermissions = {
|
|||
*/
|
||||
export const statisticsPermissions = {
|
||||
// 数据看板
|
||||
dataStand: 'statistics:data-stand'
|
||||
dataStand: 'statistics:data-stand',
|
||||
// 盒子利润统计
|
||||
boxProfit: 'statistics:box-profit'
|
||||
}
|
||||
|
||||
export default businessRoutes
|
||||
|
|
|
|||
|
|
@ -0,0 +1,304 @@
|
|||
<template>
|
||||
<div class="box-profit-container">
|
||||
<!-- 搜索表单 -->
|
||||
<el-card class="search-card" shadow="never">
|
||||
<el-form :model="searchForm" inline>
|
||||
<el-form-item label="盒子ID">
|
||||
<el-input v-model.number="searchForm.goodsId" placeholder="请输入盒子ID" clearable style="width: 120px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="盒子名称">
|
||||
<el-input v-model="searchForm.title" placeholder="请输入盒子名称" clearable style="width: 160px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="searchForm.status" placeholder="全部" clearable style="width: 100px">
|
||||
<el-option label="上架" :value="1" />
|
||||
<el-option label="下架" :value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="盒子类型">
|
||||
<el-select v-model="searchForm.type" placeholder="全部" clearable style="width: 120px">
|
||||
<el-option v-for="item in boxTypes" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="时间范围">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :icon="Search" @click="handleSearch">搜索</el-button>
|
||||
<el-button :icon="Refresh" @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<el-card class="table-card" shadow="never">
|
||||
<el-table :data="tableData" v-loading="loading" stripe border style="width: 100%">
|
||||
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||
<el-table-column prop="title" label="盒子名称" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<div class="goods-info">
|
||||
<el-image v-if="row.imgUrl" :src="row.imgUrl" fit="cover" class="goods-img" />
|
||||
<span class="goods-title">{{ row.title }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="typeName" label="类型" width="100" align="center" />
|
||||
<el-table-column prop="price" label="单价" width="80" align="right">
|
||||
<template #default="{ row }">¥{{ row.price.toFixed(2) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="statusName" label="状态" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">{{ row.statusName }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="cjCount" label="抽奖次数" width="100" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.loaded">{{ row.cjCount }}</span>
|
||||
<el-button v-else type="primary" link size="small" @click="loadBoxStats(row)">加载</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="useMoney" label="消费金额" width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.loaded">¥{{ row.useMoney.toFixed(2) }}</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="scMoney" label="出货成本" width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.loaded">¥{{ row.scMoney.toFixed(2) }}</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="reMoney" label="兑换成本" width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.loaded">¥{{ row.reMoney.toFixed(2) }}</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="fhMoney" label="发货成本" width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.loaded">¥{{ row.fhMoney.toFixed(2) }}</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="profit" label="利润" width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.loaded" :class="{ 'text-danger': row.isNegative, 'text-success': !row.isNegative }">
|
||||
¥{{ row.profit.toFixed(2) }}
|
||||
</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="profitRate" label="利润率" width="100" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.loaded" :class="{ 'text-danger': row.isNegative, 'text-success': !row.isNegative }">
|
||||
{{ row.profitRate.toFixed(2) }}%
|
||||
</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-wrapper">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="pagination.total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
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'
|
||||
|
||||
// 盒子类型选项
|
||||
const boxTypes = [
|
||||
{ value: 1, label: '一番赏' },
|
||||
{ value: 2, label: '无限赏' },
|
||||
{ value: 3, label: '擂台赏' },
|
||||
{ value: 4, label: '抽卡机' },
|
||||
{ value: 5, label: '福袋' },
|
||||
{ value: 6, label: '幸运赏' },
|
||||
{ value: 8, label: '盲盒' },
|
||||
{ value: 9, label: '扭蛋' },
|
||||
{ value: 15, label: '福利屋' }
|
||||
]
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
goodsId: undefined as number | undefined,
|
||||
title: '',
|
||||
status: undefined as number | undefined,
|
||||
type: undefined as number | undefined
|
||||
})
|
||||
|
||||
const dateRange = ref<[string, string] | null>(null)
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<BoxProfitItem[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 加载列表数据
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params: any = {
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize
|
||||
}
|
||||
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 getBoxProfitList(params) as any
|
||||
if (res.code === 0) {
|
||||
tableData.value = res.data
|
||||
pagination.total = res.count
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error)
|
||||
ElMessage.error('加载数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载单个盒子的统计数据
|
||||
async function loadBoxStats(row: BoxProfitItem) {
|
||||
try {
|
||||
const params: any = { goodsId: row.id }
|
||||
if (dateRange.value) {
|
||||
params.startTime = dateRange.value[0]
|
||||
params.endTime = dateRange.value[1]
|
||||
}
|
||||
|
||||
const res = await getBoxStatistics(params) as any
|
||||
if (res.code === 0 && res.data) {
|
||||
const stats = res.data
|
||||
row.useMoney = stats.useMoney
|
||||
row.scMoney = stats.scMoney
|
||||
row.reMoney = stats.reMoney
|
||||
row.fhMoney = stats.fhMoney
|
||||
row.cjCount = stats.cjCount
|
||||
row.profit = stats.profit
|
||||
row.profitRate = stats.profitRate
|
||||
row.isNegative = stats.isNegative
|
||||
row.loaded = true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载盒子统计失败:', error)
|
||||
ElMessage.error('加载盒子统计失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
function handleSearch() {
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 重置
|
||||
function handleReset() {
|
||||
searchForm.goodsId = undefined
|
||||
searchForm.title = ''
|
||||
searchForm.status = undefined
|
||||
searchForm.type = undefined
|
||||
dateRange.value = null
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 分页大小变化
|
||||
function handleSizeChange(size: number) {
|
||||
pagination.pageSize = size
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 页码变化
|
||||
function handleCurrentChange(page: number) {
|
||||
pagination.page = page
|
||||
loadData()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.box-profit-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.search-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.goods-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.goods-img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.goods-title {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pagination-wrapper {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: #67c23a;
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in New Issue
Block a user