1. 系统需求
- 可以维护商品信息库,采购进货单可以从中选择商品,销售出货从库存中选择商品;
- 采购进货和销售出货支持月结和现金结算,月结的单据需要与客户和供应商对账单;
- 采购和销售需支持退货;
- 提供库存查询、进出退货明细、利润表等报表功能;
- 支持单机桌面版和云Web版。
2. 功能模块
- 基础数据:包含数据字典、组织结构、商品信息、供应商、客户管理。
- 进货管理:包含采购进货单、采购退货单。
- 销货管理:包含销售出货单、销售退货单。
- 库存管理:包含商品库存查询。
- 财务管理:包含客户对账单、供应商对账单。
- 统计报表:包含进货明细表、进退货明细表、销货明细表、销退货明细表、商品利润表。
- 系统管理:包含角色管理、用户管理、系统附件、系统日志。
3. 项目结构
├─JxcLite -> 包含配置、常量、枚举、模型、服务接口、路由、页面。 |
├─JxcLite.Core -> 后端类库,包含实体、业务逻辑、数据访问。 |
├─JxcLite.Wasm -> 项目WebAssembly,Auto模式前端程序。 |
├─JxcLite.Web -> 项目Web App,云Web程序。 |
├─JxcLite.WinForm -> 项目WinForm App,单机桌面程序。 |
├─JxcLite.sln -> 项目解决方案文件。 |
4. 框架搭建
打开VS2022
创建一个空的解决方案JxcLite.sln
,然后再添加各个项目。
4.1. JxcLite项目
- 添加
JxcLite
类库,引用Known 3.*
,工程文件如下:
<Project Sdk="Microsoft.NET.Sdk.Razor"> |
<ItemGroup> |
<PackageReference Include="Known" Version="3.*" /> |
</ItemGroup> |
</Project> |
├─wwwroot -> 静态文件夹,包含css、img、js,桌面和Web共用资产。 |
├─Apps -> 移动端页面文件夹。 |
├─Models -> 前后端数据交互模型文件夹。 |
├─Pages -> PC端页面文件夹。 |
├─Services -> 前后端数据交互服务接口和Http客户端文件夹。 |
├─Shared -> 模块共享组件文件夹。 |
├─_Imports.razor -> 全局命名空间引用文件。 |
├─AppConfig.cs -> 系统配置类。 |
├─AppConstant.cs -> 系统所有常量类文件。 |
├─AppEnums.cs -> 系统所有枚举文件。 |
├─AppModule.cs -> 系统一级模块配置类。 |
├─Routes.razor -> 系统路由组件。 |
4.2. JxcLite.Core项目
- 添加
JxcLite.Core
类库,引用JxcLite
项目和Known.Core 3.*
等库,工程文件如下:
<Project Sdk="Microsoft.NET.Sdk"> |
<ItemGroup> |
<PackageReference Include="Known.Cells" Version="1.*" /> |
<PackageReference Include="Known.Core" Version="3.*" /> |
<ProjectReference Include="..\JxcLite\JxcLite.csproj" /> |
</ItemGroup> |
</Project> |
├─Entities -> 实体类文件夹。 |
├─Extensions -> 后端业务扩展类文件夹。 |
├─Imports -> 数据导入类文件夹。 |
├─Repositories -> 数据访问类文件夹,SQL语句都写在此处。 |
├─Services -> 业务逻辑服务实现类文件夹。 |
├─_Imports.cs -> 全局命名空间引用文件。 |
├─AppCore.cs -> 后端配置类。 |
4.3. JxcLite.Wasm项目
- 添加
JxcLite.Wasm
类库,引用JxcLite
项目和WebAssembly
等库,工程文件如下:
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly"> |
<ItemGroup> |
<ProjectReference Include="..\JxcLite\JxcLite.csproj" /> |
</ItemGroup> |
</Project> |
- 该项目只有一个Wasm程序入口文件
Program.cs
├─Program.cs -> Wasm程序入口。 |
4.4. JxcLite.Web项目
- 添加
JxcLite.Web
类库,引用JxcLite.Core
和JxcLite.Wasm
项目,工程文件如下:
<Project Sdk="Microsoft.NET.Sdk.Web"> |
<ItemGroup> |
<ProjectReference Include="..\JxcLite.Core\JxcLite.Core.csproj" /> |
<ProjectReference Include="..\JxcLite.Wasm\JxcLite.Wasm.csproj" /> |
</ItemGroup> |
</Project> |
├─wwwroot -> 静态文件夹。 |
├─_Imports.razor -> 全局命名空间引用文件。 |
├─App.razor -> 主程序。 |
├─appsettings.json -> 配置文件。 |
├─Program.cs -> Web程序入口。 |
- 添加
JxcLite.WinForm
类库,引用JxcLite.Core
项目,工程文件如下:
<Project Sdk="Microsoft.NET.Sdk.Razor"> |
<ItemGroup> |
<ProjectReference Include="..\JxcLite.Core\JxcLite.Core.csproj" /> |
</ItemGroup> |
</Project> |
├─wwwroot -> 静态文件夹,包含css、img、index.html。 |
├─_Imports.razor -> 全局命名空间引用文件。 |
├─App.razor -> 主程序路由。 |
├─AppSetting.cs -> 程序设置类。 |
├─Dialog.cs -> WinForm对话框类。 |
├─favicon.ico -> 图标。 |
├─MainForm.cs -> 主窗体。 |
├─Program.cs -> 桌面程序入口。 |
5. 项目配置
5.1. 前端配置
- 前端配置写在
JxcLite
项目的AppConfig.cs
文件中,示例如下:
public static class AppConfig { |
public static string AppId => "JxcLite"; |
public static string AppName => "进销存管理系统"; |
|
// 添加应用程序配置,云Web、Wasm和桌面需要调用 |
public static void AddApplication(this IServiceCollection services, AppType type) { |
var assembly = typeof(AppConfig).Assembly; |
Config.AddModule(assembly); |
|
services.AddKnown(option => { }); // 添加Known |
services.AddModules(); // 添加一级模块 |
services.ConfigUI(); // 配置界面 |
} |
|
// 添加Wasm模式的Http客户端,Wasm需要调用 |
public static void AddApplicationClient(this IServiceCollection services, Action<ClientOption> action) { |
var assembly = typeof(AppConfig).Assembly; |
services.AddKnownClient(action); // 添加Known客户端 |
services.AddClients(assembly); // 自动注入Auto模式客户端实现 |
} |
} |
- 系统一级模块配置写在
JxcLite
项目的AppModule.cs
文件中,示例如下:
static class AppModule { |
// 添加模块菜单 |
internal static void AddModules(this IServiceCollection services) { |
Config.Modules.AddItem("0", AppConstant.Import, "进货管理", "import", 2); |
Config.Modules.AddItem("0", AppConstant.Export, "销货管理", "export", 3); |
Config.Modules.AddItem("0", AppConstant.Inventory, "库存管理", "block", 4); |
Config.Modules.AddItem("0", AppConstant.Finance, "财务管理", "pay-circle", 5); |
Config.Modules.AddItem("0", AppConstant.Report, "统计报表", "bar-chart", 6); |
} |
} |
5.2. 后端配置
- 后端配置写在
JxcLite.Core
项目的AppCore.cs
文件中,示例如下:
public static class AppCore { |
// 添加PC云Web端,云Web端需要调用 |
public static void AddApplicationWeb(this IServiceCollection services, Action<CoreOption> action) { |
services.AddApplicationCore(); |
services.AddKnownWeb(option => SetOption(option, action)); |
} |
|
// 添加单机桌面端,桌面端需要调用 |
public static void AddApplicationWin(this IServiceCollection services, Action<CoreOption> action) { |
services.AddApplicationCore(); |
services.AddKnownWin(option => SetOption(option, action)); |
} |
|
// Web端使用程序静态文件,云Web端需要调用 |
public static void UseApplication(this WebApplication app) { |
app.UseKnown(); |
} |
|
private static void AddApplicationCore(this IServiceCollection services) { |
var assembly = typeof(AppCore).Assembly; |
services.AddServices(assembly); // 自动注入服务接口后端实现 |
services.AddKnownCells(); // 添加Excel操作插件 |
} |
|
private static void SetOption(CoreOption option, Action<CoreOption> action) { |
action?.Invoke(option); |
option.Database = db => { |
var connString = "Data Source=JxcLite.db;"; // 配置数据库连接 |
db.AddSQLite<Microsoft.Data.Sqlite.SqliteFactory>(connString); |
}; |
} |
} |
6. 模块示例
项目模块较多,大部分单表业务模块CRUD
可以同通过框架开发中心的代码生成模块进行生成。本文只举商品信息模块为例,其他模块可查看项目源码进行学习。
6.1. 数据模型
名称 | 代码 | 类型 | 长度 | 必填 |
---|
商品信息 | JxGoods | | | |
商品编码 | Code | Text | 50 | Y |
商品名称 | Name | Text | 200 | Y |
商品类别 | Category | Text | 50 | Y |
规格型号 | Model | Text | 500 | |
产地 | Producer | Text | 50 | |
计量单位 | Unit | Text | 50 | Y |
采购单价 | BuyPrice | Number | 18,2 | |
销售单价 | SalePrice | Number | 18,2 | |
安全库存 | SafeQty | Number | | |
备注 | Note | TextArea | | |
附件 | Files | Text | 500 | |
6.2. 信息类
信息类一是作为前后端数据交互的模型,即数据传输对象DTO
,二是通过[Column]
和[Form]
特性配置列表和表单界面。商品信息类示例如下:
[DisplayName("商品信息")] |
public class GoodsInfo { |
/// 取得或设置商品编码。 |
[Required] |
[MaxLength(50)] |
[Column(Width = 120, IsViewLink = true)] // IsViewLink为列表查看连接字段 |
[Form(Row = 1, Column = 1)] // Form配置表单字段 |
[DisplayName("商品编码")] // DisplayName配置显示名称 |
public string Code { get; set; } |
|
/// 取得或设置商品名称。 |
[Column(Width = 120, IsQuery = true)] // 配置查询条件 |
public string Name { get; set; } |
|
/// 取得或设置商品类别。 |
[Form(Row = 1, Column = 3, Type = nameof(FieldType.Select))] // 配置下拉框 |
[Category(AppConstant.GoodsType)] // 下拉框数据字典 |
public string Category { get; set; } |
} |
6.3. 实体类
实体类是数据库表的映射,框架默认内置Database
简易ORM
,商品实体类示例如下:
public class JxGoods : EntityBase { // 使用内置ORM需要继承EntityBase |
[DisplayName("商品编码")] |
[Required] |
[MaxLength(50)] |
public string Code { get; set; } |
|
[DisplayName("商品名称")] |
[Required] |
[MaxLength(200)] |
public string Name { get; set; } |
} |
6.4. 页面组件
页面组件用户配置模块菜单、通过[Action]
特性定义模块操作按钮,商品列表页面组件示例如下:
[Route("/bds/goods")] // 页面路由 |
[Menu(Constants.BaseData, "商品信息", "ordered-list", 4)] // 配置模块菜单 |
public class GoodsList : BaseTablePage<GoodsInfo> |
{ |
private IBaseDataService Service; |
|
protected override async Task OnInitPageAsync() { |
await base.OnInitPageAsync(); |
Service = await CreateServiceAsync<IBaseDataService>(); |
Table.Form = new FormInfo { Width = 800 }; |
Table.OnQuery = Service.QueryGoodsesAsync; |
} |
|
// Action配置按钮,带参数的方法为表格操作列,不带参数的为工具条按钮 |
[Action] public void New() => Table.NewForm(Service.SaveGoodsAsync, new GoodsInfo()); |
[Action] public void DeleteM() => Table.DeleteM(Service.DeleteGoodsesAsync); |
[Action] public void Edit(GoodsInfo row) => Table.EditForm(Service.SaveGoodsAsync, row); |
[Action] public void Delete(GoodsInfo row) => Table.Delete(Service.DeleteGoodsesAsync, row); |
[Action] public Task Import() => Table.ShowImportAsync(); |
[Action] public Task Export() => Table.ExportDataAsync(); |
} |
6.5. 服务接口
服务接口定义前后端数据交互的操作方法,如增删改查导。商品服务示例如下:
public interface IBaseDataService : IService |
{ |
// 分页查询和导出 |
Task<PagingResult<GoodsInfo>> QueryGoodsesAsync(PagingCriteria criteria); |
Task<List<GoodsInfo>> GetGoodsesAsync(); // 查询 |
Task<Result> DeleteGoodsesAsync(List<GoodsInfo> infos); // 删除 |
Task<Result> SaveGoodsAsync(UploadInfo<GoodsInfo> info); // 保存 |
} |
|
[Client] // 配置Client,自动注入接口的客户端实现 |
class BaseDataClient(HttpClient http) : ClientBase(http), IBaseDataService |
{ |
public Task<PagingResult<GoodsInfo>> QueryGoodsesAsync(PagingCriteria criteria) { |
return Http.QueryAsync<GoodsInfo>("/BaseData/QueryGoodses", criteria); |
} |
|
public Task<List<GoodsInfo>> GetGoodsesAsync() { |
return Http.GetAsync<List<GoodsInfo>>("/BaseData/GetGoodses"); |
} |
|
public Task<Result> DeleteGoodsesAsync(List<GoodsInfo> infos) { |
return Http.PostAsync("/BaseData/DeleteGoodses", infos); |
} |
|
public Task<Result> SaveGoodsAsync(UploadInfo<GoodsInfo> info) { |
return Http.PostAsync("/BaseData/SaveGoods", info); |
} |
} |
6.6. 服务实现
服务实现提供前后端数据交互接口的具体业务逻辑实现。商品服务实现示例如下:
[WebApi, Service] // 配置WebApi和自动注入接口的服务端实现 |
class BaseDataService(Context context) : ServiceBase(context), IBaseDataService { |
public Task<PagingResult<GoodsInfo>> QueryGoodsesAsync(PagingCriteria criteria) { |
// 分页查询排序和导出共用,查询条件自动拼接 |
return Database.Query<JxGoods>(criteria).ToPageAsync<GoodsInfo>(); |
} |
|
public Task<List<GoodsInfo>> GetGoodsesAsync() { |
return Database.Query<JxGoods>().Where(d => d.CompNo == CurrentUser.CompNo).ToListAsync<GoodsInfo>(); |
} |
|
public async Task<Result> DeleteGoodsesAsync(List<GoodsInfo> infos) { |
if (infos == null || infos.Count == 0) |
return Result.Error(Language.SelectOneAtLeast); |
|
var database = Database; |
var oldFiles = new List<string>(); |
var result = await database.TransactionAsync(Language.Delete, async db => { |
foreach (var item in infos) { |
await db.DeleteFilesAsync(item.Id, oldFiles); |
await db.DeleteAsync<JxGoods>(item.Id); |
} |
}); |
if (result.IsValid) AttachFile.DeleteFiles(oldFiles); |
return result; |
} |
|
public async Task<Result> SaveGoodsAsync(UploadInfo<GoodsInfo> info) { |
var database = Database; |
var model = await database.QueryByIdAsync<JxGoods>(info.Model.Id); |
model ??= new JxGoods(); |
model.FillModel(info.Model); |
|
var vr = model.Validate(Context); |
if (vr.IsValid) { |
if (await database.ExistsAsync<JxGoods>(d => d.Id != model.Id && d.Code == model.Code)) |
vr.AddError($"商品[{model.Code}]已存在!"); |
} |
if (!vr.IsValid) return vr; |
|
var fileFiles = info.Files?.GetAttachFiles(nameof(JxGoods.Files), "GoodsFiles"); |
return await database.TransactionAsync(Language.Save, async db => { |
await db.AddFilesAsync(fileFiles, model.Id, key => model.Files = key); |
await db.SaveAsync(model); |
info.Model.Id = model.Id; |
}, info.Model); |
} |
} |