feat(export): Replace CSV export with Excel format for distribution data
Some checks failed
continuous-integration/drone/push Build is failing

- Replace CSV export with ClosedXML Excel workbook generation for invite codes
- Replace CSV export with ClosedXML Excel workbook generation for commissions
- Replace CSV export with ClosedXML Excel workbook generation for withdrawals
- Add formatted headers with bold font and blue background styling
- Auto-adjust column widths to fit content
- Update file extensions from .csv to .xlsx
- Remove legacy CSV generation helper methods
- Improve data presentation and readability in exported files
This commit is contained in:
zpc 2026-04-10 19:32:48 +08:00
parent 52f9efc098
commit 5d06f17868
6 changed files with 252 additions and 91 deletions

View File

@ -3,6 +3,7 @@ using MiAssessment.Admin.Business.Models;
using MiAssessment.Admin.Business.Models.Common;
using MiAssessment.Admin.Business.Models.Distribution;
using MiAssessment.Admin.Business.Services.Interfaces;
using ClosedXML.Excel;
using Microsoft.AspNetCore.Mvc;
namespace MiAssessment.Admin.Business.Controllers;
@ -91,11 +92,42 @@ public class DistributionController : BusinessControllerBase
{
var items = await _distributionService.ExportInviteCodesAsync(request);
// 生成CSV内容
var csvContent = GenerateInviteCodeCsv(items);
var fileName = $"invite_codes_{DateTime.Now:yyyyMMddHHmmss}.csv";
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("邀请码列表");
return File(System.Text.Encoding.UTF8.GetBytes(csvContent), "text/csv", fileName);
var headers = new[] { "ID", "邀请码", "批次号", "分配用户", "分配时间", "使用用户", "使用订单ID", "使用时间", "状态", "创建时间" };
for (var i = 0; i < headers.Length; i++)
worksheet.Cell(1, i + 1).Value = headers[i];
var headerRow = worksheet.Range(1, 1, 1, headers.Length);
headerRow.Style.Font.Bold = true;
headerRow.Style.Fill.BackgroundColor = XLColor.FromHtml("#4A90E2");
headerRow.Style.Font.FontColor = XLColor.White;
for (var i = 0; i < items.Count; i++)
{
var row = i + 2;
var item = items[i];
worksheet.Cell(row, 1).Value = item.Id;
worksheet.Cell(row, 2).Value = item.Code;
worksheet.Cell(row, 3).Value = item.BatchNo ?? "";
worksheet.Cell(row, 4).Value = item.AssignUserNickname ?? "";
worksheet.Cell(row, 5).Value = item.AssignTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? "";
worksheet.Cell(row, 6).Value = item.UseUserNickname ?? "";
worksheet.Cell(row, 7).Value = item.UseOrderId?.ToString() ?? "";
worksheet.Cell(row, 8).Value = item.UseTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? "";
worksheet.Cell(row, 9).Value = item.StatusName;
worksheet.Cell(row, 10).Value = item.CreateTime.ToString("yyyy-MM-dd HH:mm:ss");
}
worksheet.Columns().AdjustToContents();
using var stream = new MemoryStream();
workbook.SaveAs(stream);
return File(stream.ToArray(),
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
$"邀请码列表_{DateTime.Now:yyyyMMddHHmmss}.xlsx");
}
#endregion
@ -162,11 +194,43 @@ public class DistributionController : BusinessControllerBase
{
var items = await _distributionService.ExportCommissionsAsync(request);
// 生成CSV内容
var csvContent = GenerateCommissionCsv(items);
var fileName = $"commissions_{DateTime.Now:yyyyMMddHHmmss}.csv";
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("佣金记录");
return File(System.Text.Encoding.UTF8.GetBytes(csvContent), "text/csv", fileName);
var headers = new[] { "ID", "获佣用户", "来源用户", "订单编号", "订单金额", "佣金比例", "佣金金额", "层级", "状态", "结算时间", "创建时间" };
for (var i = 0; i < headers.Length; i++)
worksheet.Cell(1, i + 1).Value = headers[i];
var headerRow = worksheet.Range(1, 1, 1, headers.Length);
headerRow.Style.Font.Bold = true;
headerRow.Style.Fill.BackgroundColor = XLColor.FromHtml("#4A90E2");
headerRow.Style.Font.FontColor = XLColor.White;
for (var i = 0; i < items.Count; i++)
{
var row = i + 2;
var item = items[i];
worksheet.Cell(row, 1).Value = item.Id;
worksheet.Cell(row, 2).Value = item.UserNickname ?? "";
worksheet.Cell(row, 3).Value = item.FromUserNickname ?? "";
worksheet.Cell(row, 4).Value = item.OrderNo ?? "";
worksheet.Cell(row, 5).Value = item.OrderAmount;
worksheet.Cell(row, 6).Value = item.CommissionRate;
worksheet.Cell(row, 7).Value = item.CommissionAmount;
worksheet.Cell(row, 8).Value = item.LevelName;
worksheet.Cell(row, 9).Value = item.StatusName;
worksheet.Cell(row, 10).Value = item.SettleTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? "";
worksheet.Cell(row, 11).Value = item.CreateTime.ToString("yyyy-MM-dd HH:mm:ss");
}
worksheet.Columns().AdjustToContents();
using var stream = new MemoryStream();
workbook.SaveAs(stream);
return File(stream.ToArray(),
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
$"佣金记录_{DateTime.Now:yyyyMMddHHmmss}.xlsx");
}
#endregion
@ -292,79 +356,47 @@ public class DistributionController : BusinessControllerBase
{
var items = await _distributionService.ExportWithdrawalsAsync(request);
// 生成CSV内容
var csvContent = GenerateWithdrawalCsv(items);
var fileName = $"withdrawals_{DateTime.Now:yyyyMMddHHmmss}.csv";
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("提现记录");
return File(System.Text.Encoding.UTF8.GetBytes(csvContent), "text/csv", fileName);
var headers = new[] { "提现单号", "用户昵称", "手机号", "提现金额", "提现前余额", "提现后余额", "状态", "审核时间", "审核备注", "打款时间", "打款交易号", "创建时间" };
for (var i = 0; i < headers.Length; i++)
worksheet.Cell(1, i + 1).Value = headers[i];
var headerRow = worksheet.Range(1, 1, 1, headers.Length);
headerRow.Style.Font.Bold = true;
headerRow.Style.Fill.BackgroundColor = XLColor.FromHtml("#4A90E2");
headerRow.Style.Font.FontColor = XLColor.White;
for (var i = 0; i < items.Count; i++)
{
var row = i + 2;
var item = items[i];
worksheet.Cell(row, 1).Value = item.WithdrawalNo;
worksheet.Cell(row, 2).Value = item.UserNickname ?? "";
worksheet.Cell(row, 3).Value = item.UserPhone ?? "";
worksheet.Cell(row, 4).Value = item.Amount;
worksheet.Cell(row, 5).Value = item.BeforeBalance;
worksheet.Cell(row, 6).Value = item.AfterBalance;
worksheet.Cell(row, 7).Value = item.StatusName;
worksheet.Cell(row, 8).Value = item.AuditTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? "";
worksheet.Cell(row, 9).Value = item.AuditRemark ?? "";
worksheet.Cell(row, 10).Value = item.PayTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? "";
worksheet.Cell(row, 11).Value = item.PayTransactionId ?? "";
worksheet.Cell(row, 12).Value = item.CreateTime.ToString("yyyy-MM-dd HH:mm:ss");
}
worksheet.Columns().AdjustToContents();
using var stream = new MemoryStream();
workbook.SaveAs(stream);
return File(stream.ToArray(),
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
$"提现记录_{DateTime.Now:yyyyMMddHHmmss}.xlsx");
}
#endregion
#region
/// <summary>
/// 生成邀请码CSV内容
/// </summary>
/// <param name="items">邀请码列表</param>
/// <returns>CSV内容</returns>
private static string GenerateInviteCodeCsv(List<InviteCodeDto> items)
{
var sb = new System.Text.StringBuilder();
// CSV 头部
sb.AppendLine("ID,邀请码,批次号,分配用户,分配时间,使用用户,使用订单ID,使用时间,状态,创建时间");
// CSV 数据行
foreach (var item in items)
{
sb.AppendLine($"{item.Id},{item.Code},{item.BatchNo ?? ""},{item.AssignUserNickname ?? ""},{item.AssignTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? ""},{item.UseUserNickname ?? ""},{item.UseOrderId?.ToString() ?? ""},{item.UseTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? ""},{item.StatusName},{item.CreateTime:yyyy-MM-dd HH:mm:ss}");
}
return sb.ToString();
}
/// <summary>
/// 生成佣金记录CSV内容
/// </summary>
/// <param name="items">佣金记录列表</param>
/// <returns>CSV内容</returns>
private static string GenerateCommissionCsv(List<CommissionDto> items)
{
var sb = new System.Text.StringBuilder();
// CSV 头部
sb.AppendLine("ID,获得佣金用户ID,获得佣金用户,来源用户ID,来源用户,订单ID,订单编号,订单金额,佣金比例,佣金金额,层级,状态,结算时间,创建时间");
// CSV 数据行
foreach (var item in items)
{
sb.AppendLine($"{item.Id},{item.UserId},{item.UserNickname ?? ""},{item.FromUserId},{item.FromUserNickname ?? ""},{item.OrderId},{item.OrderNo ?? ""},{item.OrderAmount},{item.CommissionRate},{item.CommissionAmount},{item.LevelName},{item.StatusName},{item.SettleTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? ""},{item.CreateTime:yyyy-MM-dd HH:mm:ss}");
}
return sb.ToString();
}
/// <summary>
/// 生成提现记录CSV内容
/// </summary>
/// <param name="items">提现记录列表</param>
/// <returns>CSV内容</returns>
private static string GenerateWithdrawalCsv(List<WithdrawalDto> items)
{
var sb = new System.Text.StringBuilder();
// CSV 头部
sb.AppendLine("ID,提现单号,用户ID,用户昵称,用户手机号,提现金额,提现前余额,提现后余额,状态,审核人ID,审核时间,审核备注,打款时间,打款交易号,创建时间");
// CSV 数据行
foreach (var item in items)
{
sb.AppendLine($"{item.Id},{item.WithdrawalNo},{item.UserId},{item.UserNickname ?? ""},{item.UserPhone ?? ""},{item.Amount},{item.BeforeBalance},{item.AfterBalance},{item.StatusName},{item.AuditUserId?.ToString() ?? ""},{item.AuditTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? ""},{item.AuditRemark ?? ""},{item.PayTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? ""},{item.PayTransactionId ?? ""},{item.CreateTime:yyyy-MM-dd HH:mm:ss}");
}
return sb.ToString();
}
#endregion
}

View File

@ -3,6 +3,7 @@ using MiAssessment.Admin.Business.Models;
using MiAssessment.Admin.Business.Models.Common;
using MiAssessment.Admin.Business.Models.Order;
using MiAssessment.Admin.Business.Services.Interfaces;
using ClosedXML.Excel;
using Microsoft.AspNetCore.Mvc;
namespace MiAssessment.Admin.Business.Controllers;
@ -105,7 +106,7 @@ public class OrderController : BusinessControllerBase
/// 导出订单列表
/// </summary>
/// <param name="request">查询请求</param>
/// <returns>导出数据</returns>
/// <returns>Excel文件</returns>
[HttpGet("export")]
[BusinessPermission("order:view")]
public async Task<IActionResult> Export([FromQuery] OrderQueryRequest request)
@ -113,7 +114,45 @@ public class OrderController : BusinessControllerBase
try
{
var result = await _orderService.ExportOrdersAsync(request);
return Ok(result);
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("订单列表");
var headers = new[] { "订单编号", "用户UID", "用户昵称", "手机号", "订单类型", "商品名称", "订单金额", "实付金额", "支付方式", "状态", "支付时间", "创建时间" };
for (var i = 0; i < headers.Length; i++)
worksheet.Cell(1, i + 1).Value = headers[i];
var headerRow = worksheet.Range(1, 1, 1, headers.Length);
headerRow.Style.Font.Bold = true;
headerRow.Style.Fill.BackgroundColor = XLColor.FromHtml("#4A90E2");
headerRow.Style.Font.FontColor = XLColor.White;
for (var i = 0; i < result.Count; i++)
{
var row = i + 2;
var o = result[i];
worksheet.Cell(row, 1).Value = o.OrderNo;
worksheet.Cell(row, 2).Value = o.UserUid;
worksheet.Cell(row, 3).Value = o.UserNickname;
worksheet.Cell(row, 4).Value = o.UserPhone;
worksheet.Cell(row, 5).Value = o.OrderTypeName;
worksheet.Cell(row, 6).Value = o.ProductName;
worksheet.Cell(row, 7).Value = o.Amount;
worksheet.Cell(row, 8).Value = o.PayAmount;
worksheet.Cell(row, 9).Value = o.PayTypeName;
worksheet.Cell(row, 10).Value = o.StatusName;
worksheet.Cell(row, 11).Value = o.PayTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? "";
worksheet.Cell(row, 12).Value = o.CreateTime.ToString("yyyy-MM-dd HH:mm:ss");
}
worksheet.Columns().AdjustToContents();
using var stream = new MemoryStream();
workbook.SaveAs(stream);
return File(stream.ToArray(),
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
$"订单列表_{DateTime.Now:yyyyMMddHHmmss}.xlsx");
}
catch (BusinessException ex)
{

View File

@ -3,6 +3,7 @@ using MiAssessment.Admin.Business.Models;
using MiAssessment.Admin.Business.Models.Common;
using MiAssessment.Admin.Business.Models.Planner;
using MiAssessment.Admin.Business.Services.Interfaces;
using ClosedXML.Excel;
using Microsoft.AspNetCore.Mvc;
namespace MiAssessment.Admin.Business.Controllers;
@ -259,7 +260,7 @@ public class PlannerController : BusinessControllerBase
/// 导出预约记录列表
/// </summary>
/// <param name="request">查询请求</param>
/// <returns>导出数据</returns>
/// <returns>Excel文件</returns>
[HttpGet("booking/export")]
[BusinessPermission("planner:view")]
public async Task<IActionResult> ExportBookings([FromQuery] BookingQueryRequest request)
@ -267,7 +268,46 @@ public class PlannerController : BusinessControllerBase
try
{
var result = await _plannerService.ExportBookingsAsync(request);
return Ok(result);
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("预约记录");
var headers = new[] { "用户UID", "用户昵称", "手机号", "订单编号", "规划师", "预约日期", "预约时间", "学生姓名", "联系电话", "性别", "年级", "状态", "创建时间" };
for (var i = 0; i < headers.Length; i++)
worksheet.Cell(1, i + 1).Value = headers[i];
var headerRow = worksheet.Range(1, 1, 1, headers.Length);
headerRow.Style.Font.Bold = true;
headerRow.Style.Fill.BackgroundColor = XLColor.FromHtml("#4A90E2");
headerRow.Style.Font.FontColor = XLColor.White;
for (var i = 0; i < result.Count; i++)
{
var row = i + 2;
var item = result[i];
worksheet.Cell(row, 1).Value = item.UserUid ?? "";
worksheet.Cell(row, 2).Value = item.UserNickname ?? "";
worksheet.Cell(row, 3).Value = item.UserPhone ?? "";
worksheet.Cell(row, 4).Value = item.OrderNo ?? "";
worksheet.Cell(row, 5).Value = item.PlannerName ?? "";
worksheet.Cell(row, 6).Value = item.BookingDate.ToString("yyyy-MM-dd");
worksheet.Cell(row, 7).Value = item.BookingTime;
worksheet.Cell(row, 8).Value = item.Name;
worksheet.Cell(row, 9).Value = item.Phone;
worksheet.Cell(row, 10).Value = item.GenderName;
worksheet.Cell(row, 11).Value = item.GradeName;
worksheet.Cell(row, 12).Value = item.StatusName;
worksheet.Cell(row, 13).Value = item.CreateTime.ToString("yyyy-MM-dd HH:mm:ss");
}
worksheet.Columns().AdjustToContents();
using var stream = new MemoryStream();
workbook.SaveAs(stream);
return File(stream.ToArray(),
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
$"预约记录_{DateTime.Now:yyyyMMddHHmmss}.xlsx");
}
catch (BusinessException ex)
{

View File

@ -3,6 +3,7 @@ using MiAssessment.Admin.Business.Models;
using MiAssessment.Admin.Business.Models.Common;
using MiAssessment.Admin.Business.Models.User;
using MiAssessment.Admin.Business.Services.Interfaces;
using ClosedXML.Excel;
using Microsoft.AspNetCore.Mvc;
namespace MiAssessment.Admin.Business.Controllers;
@ -191,7 +192,7 @@ public class UserController : BusinessControllerBase
/// 导出用户列表
/// </summary>
/// <param name="request">查询请求</param>
/// <returns>导出数据</returns>
/// <returns>Excel文件</returns>
[HttpGet("export")]
[BusinessPermission("user:view")]
public async Task<IActionResult> Export([FromQuery] UserQueryRequest request)
@ -199,7 +200,51 @@ public class UserController : BusinessControllerBase
try
{
var result = await _userService.ExportUsersAsync(request);
return Ok(result);
// 生成 Excel 文件
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("用户列表");
// 表头
var headers = new[] { "UID", "手机号", "昵称", "用户等级", "余额", "累计收入", "状态", "创建时间", "最后登录" };
for (var i = 0; i < headers.Length; i++)
{
worksheet.Cell(1, i + 1).Value = headers[i];
}
// 表头样式
var headerRow = worksheet.Range(1, 1, 1, headers.Length);
headerRow.Style.Font.Bold = true;
headerRow.Style.Fill.BackgroundColor = XLColor.FromHtml("#4A90E2");
headerRow.Style.Font.FontColor = XLColor.White;
// 数据行
for (var i = 0; i < result.Count; i++)
{
var row = i + 2;
var user = result[i];
worksheet.Cell(row, 1).Value = user.Uid;
worksheet.Cell(row, 2).Value = user.Phone;
worksheet.Cell(row, 3).Value = user.Nickname;
worksheet.Cell(row, 4).Value = user.UserLevelName;
worksheet.Cell(row, 5).Value = user.Balance;
worksheet.Cell(row, 6).Value = user.TotalIncome;
worksheet.Cell(row, 7).Value = user.StatusName;
worksheet.Cell(row, 8).Value = user.CreateTime.ToString("yyyy-MM-dd HH:mm:ss");
worksheet.Cell(row, 9).Value = user.LastLoginTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? "";
}
// 自动列宽
worksheet.Columns().AdjustToContents();
using var stream = new MemoryStream();
workbook.SaveAs(stream);
stream.Position = 0;
var fileName = $"用户列表_{DateTime.Now:yyyyMMddHHmmss}.xlsx";
return File(stream.ToArray(),
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
fileName);
}
catch (BusinessException ex)
{

View File

@ -191,6 +191,11 @@ service.interceptors.request.use(
// 响应拦截器
service.interceptors.response.use(
(response: AxiosResponse<ApiResponse>) => {
// blob 类型直接返回,不走业务 code 判断
if (response.config.responseType === 'blob') {
return { code: 0, message: 'success', data: response.data } as any
}
const res = response.data
// code 为 0 表示成功

View File

@ -185,7 +185,7 @@
<el-skeleton :loading="state.pendingLoading" animated :rows="3">
<template #default>
<div class="pending-items">
<div class="pending-item" @click="$router.push('/business/distribution/withdrawal')">
<div class="pending-item" @click="$router.push('/distribution/withdrawal')">
<div class="pending-icon pending-icon-warning">
<el-icon size="24"><Wallet /></el-icon>
</div>
@ -195,7 +195,7 @@
</div>
<el-icon class="pending-arrow"><ArrowRight /></el-icon>
</div>
<div class="pending-item" @click="$router.push('/business/planner/booking')">
<div class="pending-item" @click="$router.push('/planner/booking')">
<div class="pending-icon pending-icon-primary">
<el-icon size="24"><Calendar /></el-icon>
</div>
@ -234,22 +234,22 @@
<span>快捷操作</span>
</template>
<div class="quick-actions">
<el-button type="primary" @click="$router.push('/business/user')">
<el-button type="primary" @click="$router.push('/user/list')">
<el-icon><User /></el-icon>
</el-button>
<el-button type="success" @click="$router.push('/business/order')">
<el-button type="success" @click="$router.push('/order/list')">
<el-icon><ShoppingCart /></el-icon>
</el-button>
<el-button type="warning" @click="$router.push('/business/planner/list')">
<el-button type="warning" @click="$router.push('/planner/list')">
<el-icon><UserFilled /></el-icon>
</el-button>
<el-button type="info" @click="$router.push('/business/distribution/withdrawal')">
<el-button type="info" @click="$router.push('/distribution/withdrawal')">
<el-icon><Wallet /></el-icon>
</el-button>
<el-button @click="$router.push('/business/content/banner')">
<el-button @click="$router.push('/content/banner')">
<el-icon><Picture /></el-icon>
</el-button>
<el-button @click="$router.push('/business/assessment/type')">
<el-button @click="$router.push('/assessment/type')">
<el-icon><Document /></el-icon>
</el-button>
</div>