插件开发流程详细指南
本文档旨在为新手开发者提供一份详细的Admin.NET系统插件开发流程指南,帮助你快速上手插件开发。
一、环境准备
1. 开发环境要求
- .NET 8.0 SDK 或更高版本
- Visual Studio 2022 或更高版本
- SQL Server 或 MySQL 数据库
- Node.js 16.0 或更高版本(前端开发)
2. 系统结构了解
在开始开发插件之前,建议先了解系统的基本结构:
Admin.NET.Core:核心库,包含基础功能和工具Admin.NET.Application:应用层,包含业务逻辑Admin.NET.Web.Entry:Web入口,包含API控制器Plugins:插件目录,存放所有插件
二、创建插件项目
1. 创建插件目录
在 Admin.NET/Plugins/ 目录下创建新的插件文件夹,命名为 Admin.NET.Plugin.{PluginName},例如 Admin.NET.Plugin.Test。
2. 创建项目文件
在插件目录中创建 Admin.NET.Plugin.Test.csproj 文件,内容如下:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Admin.NET.Core\Admin.NET.Core.csproj" />
<ProjectReference Include="..\..\Admin.NET.Application\Admin.NET.Application.csproj" />
</ItemGroup>
</Project>3. 创建全局使用文件
创建 GlobalUsings.cs 文件,添加常用的命名空间:
global using Admin.NET.Core;
global using Admin.NET.Core.Entity;
global using Admin.NET.Core.Service;
global using SqlSugar;
global using System.ComponentModel.DataAnnotations;4. 创建启动类
创建 Startup.cs 文件,实现插件启动逻辑:
using Microsoft.Extensions.DependencyInjection;
namespace Admin.NET.Plugin.Test;
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// 注册插件服务
services.AddScoped<TestService>();
}
}三、定义实体类
1. 创建实体目录
在插件目录中创建 Entity/ 文件夹,用于存放实体类。
2. 创建实体类
创建一个简单的实体类,例如 TestEntity.cs:
using Admin.NET.Core.Entity;
using SqlSugar;
namespace Admin.NET.Plugin.Test.Entity;
[SugarTable("TestEntity")]
public class TestEntity : EntityBase
{
/// <summary>
/// 名称
/// </summary>
[SugarColumn(ColumnDescription = "名称")]
[Required]
public string Name { get; set; }
/// <summary>
/// 描述
/// </summary>
[SugarColumn(ColumnDescription = "描述")]
public string Description { get; set; }
/// <summary>
/// 状态
/// </summary>
[SugarColumn(ColumnDescription = "状态")]
public int Status { get; set; }
}四、实现服务层
1. 创建服务目录
在插件目录中创建 Service/ 文件夹,用于存放服务类。
2. 创建DTO目录
在 Service/ 目录中创建 Dto/ 文件夹,用于存放数据传输对象。
3. 创建输入输出DTO
创建 TestInput.cs 和 TestOutput.cs 文件:
// TestInput.cs
using Admin.NET.Core.Utils;
namespace Admin.NET.Plugin.Test.Service.Dto;
public class TestInput : BasePageInput
{
/// <summary>
/// 名称
/// </summary>
public string? Name { get; set; }
/// <summary>
/// 状态
/// </summary>
public int? Status { get; set; }
}
// TestOutput.cs
namespace Admin.NET.Plugin.Test.Service.Dto;
public class TestOutput
{
/// <summary>
/// 主键
/// </summary>
public long Id { get; set; }
/// <summary>
/// 名称
/// </summary>
public string Name { get; set; }
/// <summary>
/// 描述
/// </summary>
public string Description { get; set; }
/// <summary>
/// 状态
/// </summary>
public int Status { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreateTime { get; set; }
}4. 创建服务类
创建 TestService.cs 文件,实现业务逻辑:
using Admin.NET.Core.Service;
using Admin.NET.Plugin.Test.Entity;
using Admin.NET.Plugin.Test.Service.Dto;
using SqlSugar;
namespace Admin.NET.Plugin.Test.Service;
public class TestService : BaseService<TestEntity>
{
/// <summary>
/// 获取分页列表
/// </summary>
public async Task<SqlSugarPagedList<TestOutput>> GetPage(TestInput input)
{
return await Queryable()
.WhereIF(!string.IsNullOrWhiteSpace(input.Name), x => x.Name.Contains(input.Name))
.WhereIF(input.Status.HasValue, x => x.Status == input.Status)
.Select<TestOutput>()
.ToPagedListAsync(input.Page, input.PageSize);
}
/// <summary>
/// 添加
/// </summary>
public async Task Add(TestEntity entity)
{
await InsertAsync(entity);
}
/// <summary>
/// 修改
/// </summary>
public async Task Update(TestEntity entity)
{
await UpdateAsync(entity);
}
/// <summary>
/// 删除
/// </summary>
public async Task Delete(long id)
{
await DeleteAsync(id);
}
/// <summary>
/// 获取详情
/// </summary>
public async Task<TestEntity> GetDetail(long id)
{
return await GetByIdAsync(id);
}
}五、配置种子数据
1. 创建种子数据目录
在插件目录中创建 SeedData/ 文件夹,用于存放种子数据。
2. 创建菜单种子数据
创建 SysMenuSeedData.cs 文件,用于初始化插件菜单:
using Admin.NET.Core.Entity;
using Admin.NET.Core.SeedData;
namespace Admin.NET.Plugin.Test.SeedData;
[SeedData]
public class SysMenuSeedData : ISqlSugarEntitySeedData<SysMenu>
{
public IEnumerable<SysMenu> SeedData()
{
return new List<SysMenu>
{
new SysMenu
{
Id = 100000,
ParentId = 0,
Name = "测试管理",
Path = "/test",
Component = "Layout",
Sort = 999,
IsHidden = false,
Icon = "el-icon-s-operation",
IsSys = false,
Status = 1
},
new SysMenu
{
Id = 100001,
ParentId = 100000,
Name = "测试列表",
Path = "testList",
Component = "/test/testList/index",
Sort = 1,
IsHidden = false,
Icon = "el-icon-s-grid",
IsSys = false,
Status = 1
}
};
}
}六、注册插件服务
1. 更新Startup.cs
更新 Startup.cs 文件,注册插件服务和种子数据:
using Microsoft.Extensions.DependencyInjection;
namespace Admin.NET.Plugin.Test;
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// 注册插件服务
services.AddScoped<TestService>();
}
}2. 集成到主系统
在主项目的 Admin.NET.Web.Entry.csproj 文件中添加对插件项目的引用:
<ProjectReference Include="..\Plugins\Admin.NET.Plugin.Test\Admin.NET.Plugin.Test.csproj" />七、前端开发
1. 创建前端页面
在 Web/src/views/ 目录下创建 test/testList/ 文件夹,用于存放前端页面。
2. 创建索引页面
创建 index.vue 文件,实现前端界面:
<template>
<div class="test-list">
<el-card>
<template #header>
<div class="card-header">
<span>测试列表</span>
<el-button type="primary" @click="handleAdd">添加</el-button>
</div>
</template>
<el-form :model="searchForm" :inline="true" class="search-form">
<el-form-item label="名称">
<el-input v-model="searchForm.name" placeholder="请输入名称"></el-input>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="searchForm.status" placeholder="请选择状态">
<el-option label="启用" value="1"></el-option>
<el-option label="禁用" value="0"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="resetForm">重置</el-button>
</el-form-item>
</el-form>
<el-table :data="tableData" style="width: 100%">
<el-table-column prop="id" label="ID" width="80"></el-table-column>
<el-table-column prop="name" label="名称"></el-table-column>
<el-table-column prop="description" label="描述"></el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template #default="scope">
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">
{{ scope.row.status === 1 ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="180"></el-table-column>
<el-table-column label="操作" width="150">
<template #default="scope">
<el-button size="small" @click="handleEdit(scope.row)">编辑</el-button>
<el-button size="small" type="danger" @click="handleDelete(scope.row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="pageInfo.page"
v-model:page-size="pageInfo.pageSize"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="pageInfo.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
></el-pagination>
</el-card>
<!-- 编辑对话框 -->
<el-dialog v-model="dialogVisible" :title="dialogTitle">
<el-form :model="form" :rules="rules" ref="formRef">
<el-form-item label="名称" prop="name">
<el-input v-model="form.name" placeholder="请输入名称"></el-input>
</el-form-item>
<el-form-item label="描述" prop="description">
<el-input v-model="form.description" type="textarea" placeholder="请输入描述"></el-input>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="form.status" placeholder="请选择状态">
<el-option label="启用" value="1"></el-option>
<el-option label="禁用" value="0"></el-option>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { testApi } from '/@/api/test';
const tableData = ref([]);
const pageInfo = reactive({
page: 1,
pageSize: 10,
total: 0
});
const searchForm = reactive({
name: '',
status: ''
});
const dialogVisible = ref(false);
const dialogTitle = ref('添加测试');
const form = reactive({
id: 0,
name: '',
description: '',
status: 1
});
const rules = reactive({
name: [{ required: true, message: '请输入名称', trigger: 'blur' }]
});
const formRef = ref();
// 加载数据
const loadData = async () => {
const res = await testApi.getPage({
page: pageInfo.page,
pageSize: pageInfo.pageSize,
name: searchForm.name,
status: searchForm.status
});
if (res.data?.code === 200) {
tableData.value = res.data.result.items;
pageInfo.total = res.data.result.total;
}
};
// 搜索
const handleSearch = () => {
pageInfo.page = 1;
loadData();
};
// 重置
const resetForm = () => {
searchForm.name = '';
searchForm.status = '';
pageInfo.page = 1;
loadData();
};
// 分页
const handleSizeChange = (size) => {
pageInfo.pageSize = size;
loadData();
};
const handleCurrentChange = (current) => {
pageInfo.page = current;
loadData();
};
// 添加
const handleAdd = () => {
dialogTitle.value = '添加测试';
form.id = 0;
form.name = '';
form.description = '';
form.status = 1;
dialogVisible.value = true;
};
// 编辑
const handleEdit = (row) => {
dialogTitle.value = '编辑测试';
form.id = row.id;
form.name = row.name;
form.description = row.description;
form.status = row.status;
dialogVisible.value = true;
};
// 删除
const handleDelete = (id) => {
ElMessageBox.confirm('确定要删除这条数据吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
const res = await testApi.delete(id);
if (res.data?.code === 200) {
ElMessage.success('删除成功');
loadData();
}
});
};
// 提交
const handleSubmit = async () => {
if (!formRef.value) return;
await formRef.value.validate(async (valid) => {
if (valid) {
let res;
if (form.id === 0) {
res = await testApi.add(form);
} else {
res = await testApi.update(form);
}
if (res.data?.code === 200) {
ElMessage.success(form.id === 0 ? '添加成功' : '修改成功');
dialogVisible.value = false;
loadData();
}
}
});
};
// 初始化
onMounted(() => {
loadData();
});
</script>
<style scoped>
.test-list {
padding: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.search-form {
margin-bottom: 20px;
}
.dialog-footer {
text-align: right;
}
</style>3. 创建API文件
在 Web/src/api/ 目录下创建 test.ts 文件,用于定义API调用:
import request from '/@/utils/request';
// 测试API
export const testApi = {
// 获取分页列表
getPage: (params) => request({ url: '/api/test/getPage', method: 'get', params }),
// 添加
add: (data) => request({ url: '/api/test/add', method: 'post', data }),
// 修改
update: (data) => request({ url: '/api/test/update', method: 'post', data }),
// 删除
delete: (id) => request({ url: `/api/test/delete/${id}`, method: 'post' }),
// 获取详情
getDetail: (id) => request({ url: `/api/test/getDetail/${id}`, method: 'get' })
};八、API控制器
1. 创建控制器目录
在插件目录中创建 Controllers/ 文件夹,用于存放API控制器。
2. 创建控制器类
创建 TestController.cs 文件,实现API接口:
using Admin.NET.Core;
using Admin.NET.Plugin.Test.Entity;
using Admin.NET.Plugin.Test.Service;
using Admin.NET.Plugin.Test.Service.Dto;
using Microsoft.AspNetCore.Mvc;
namespace Admin.NET.Plugin.Test.Controllers;
[ApiController]
[Route("api/test")]
public class TestController : ControllerBase
{
private readonly TestService _testService;
public TestController(TestService testService)
{
_testService = testService;
}
/// <summary>
/// 获取分页列表
/// </summary>
[HttpGet("getPage")]
public async Task<dynamic> GetPage([FromQuery] TestInput input)
{
var result = await _testService.GetPage(input);
return new { code = 200, result };
}
/// <summary>
/// 添加
/// </summary>
[HttpPost("add")]
public async Task<dynamic> Add([FromBody] TestEntity entity)
{
await _testService.Add(entity);
return new { code = 200, message = "添加成功" };
}
/// <summary>
/// 修改
/// </summary>
[HttpPost("update")]
public async Task<dynamic> Update([FromBody] TestEntity entity)
{
await _testService.Update(entity);
return new { code = 200, message = "修改成功" };
}
/// <summary>
/// 删除
/// </summary>
[HttpPost("delete/{id}")]
public async Task<dynamic> Delete(long id)
{
await _testService.Delete(id);
return new { code = 200, message = "删除成功" };
}
/// <summary>
/// 获取详情
/// </summary>
[HttpGet("getDetail/{id}")]
public async Task<dynamic> GetDetail(long id)
{
var result = await _testService.GetDetail(id);
return new { code = 200, result };
}
}九、插件部署与测试
1. 构建插件
在插件目录中运行以下命令构建插件:
dotnet build2. 运行主系统
在 Admin.NET.Web.Entry 目录中运行以下命令启动主系统:
dotnet run --framework net8.03. 测试功能
- 打开浏览器,访问
http://localhost:5001 - 登录系统
- 在左侧菜单中找到 "测试管理" -> "测试列表"
- 测试添加、编辑、删除、查询等功能
4. 调试
如果需要调试插件,可以:
- 在 Visual Studio 中打开主解决方案
- 设置
Admin.NET.Web.Entry为启动项目 - 在插件代码中设置断点
- 按 F5 启动调试
十、最佳实践与注意事项
1. 命名规范
- 插件名称:
Admin.NET.Plugin.{PluginName} - 实体类:
{EntityName}.cs - 服务类:
{EntityName}Service.cs - 控制器类:
{EntityName}Controller.cs - DTO类:
{EntityName}Input.cs、{EntityName}Output.cs
2. 代码规范
- 使用 async/await 进行异步操作
- 使用依赖注入管理服务实例
- 实现统一的错误处理
- 添加适当的日志记录
- 使用属性注解描述字段含义
3. 性能优化
- 使用分页查询处理大量数据
- 合理使用缓存
- 优化数据库查询
- 避免重复计算
4. 安全性
- 实现适当的权限控制
- 验证用户输入
- 防止 SQL 注入
- 保护敏感数据
5. 扩展性
- 设计模块化的代码结构
- 提供配置选项
- 支持插件间的依赖关系
- 考虑未来的功能扩展
十一、常见问题与解决方案
1. 插件不被加载
- 检查插件项目是否正确引用
- 检查
Startup.cs是否正确实现 - 检查插件项目是否成功构建
2. 数据库表不创建
- 检查实体类是否正确定义
- 检查实体类是否继承自
EntityBase - 检查数据库连接是否正确
3. 菜单不显示
- 检查
SysMenuSeedData是否正确实现 - 检查菜单的父级ID是否正确
- 检查菜单的路径是否与前端路由匹配
4. API 404 错误
- 检查控制器的路由配置是否正确
- 检查控制器的方法是否正确标注 HTTP 方法
- 检查前端 API 调用的 URL 是否正确
5. 权限问题
- 检查用户是否有访问插件菜单的权限
- 检查角色是否分配了插件的权限
- 检查权限注解是否正确实现
十二、示例插件分析
1. 仪器管理插件 (Admin.NET.Plugin.Instrument)
- 功能:管理仪器信息、校准记录、维护计划等
- 实体:
BaseInstrumentInfo、BaseInstrumentCalibrationRecord等 - 服务:
BaseInstrumentInfoService、BaseInstrumentCalibrationRecordService等 - 种子数据:仪器管理菜单、仪器类型等
2. LIMS配置插件 (Admin.NET.Plugin.LIMSConfig)
- 功能:管理公式、变量、舍入规则、符号、单位等
- 实体:
LimsFormula、LimsUnit、LimsSymbol等 - 服务:
LimsFormulaService、LimsUnitService等 - 种子数据:系统字典、菜单等
3. 物料管理插件 (Admin.NET.Plugin.Material)
- 功能:管理物料、申购单、入库单、出库单等
- 实体:
LimsMaterial、LimsMaterialApply、LimsMaterialIn等 - 服务:
LimsMaterialService、LimsMaterialApplyService等 - 种子数据:物料分类、供应商、货位等
十三、总结
插件开发是 Admin.NET 系统的重要扩展方式,通过本文档的指导,你应该能够:
- 了解插件的目录结构和开发流程
- 掌握实体类、服务层、API控制器的开发方法
- 学会配置种子数据和前端页面
- 理解插件的部署与测试方法
- 遵循最佳实践和注意事项
希望本文档能够帮助你快速上手插件开发,为 Admin.NET 系统添加更多实用功能。如果你在开发过程中遇到问题,可以参考系统中的现有插件,或者查阅相关文档和资料。
祝你开发顺利!