Skip to content

插件开发流程详细指南

本文档旨在为新手开发者提供一份详细的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 文件,内容如下:

xml
<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 文件,添加常用的命名空间:

csharp
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 文件,实现插件启动逻辑:

csharp
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

csharp
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.csTestOutput.cs 文件:

csharp
// 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 文件,实现业务逻辑:

csharp
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 文件,用于初始化插件菜单:

csharp
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 文件,注册插件服务和种子数据:

csharp
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 文件中添加对插件项目的引用:

xml
<ProjectReference Include="..\Plugins\Admin.NET.Plugin.Test\Admin.NET.Plugin.Test.csproj" />

七、前端开发

1. 创建前端页面

Web/src/views/ 目录下创建 test/testList/ 文件夹,用于存放前端页面。

2. 创建索引页面

创建 index.vue 文件,实现前端界面:

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调用:

typescript
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接口:

csharp
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. 构建插件

在插件目录中运行以下命令构建插件:

bash
dotnet build

2. 运行主系统

Admin.NET.Web.Entry 目录中运行以下命令启动主系统:

bash
dotnet run --framework net8.0

3. 测试功能

  1. 打开浏览器,访问 http://localhost:5001
  2. 登录系统
  3. 在左侧菜单中找到 "测试管理" -> "测试列表"
  4. 测试添加、编辑、删除、查询等功能

4. 调试

如果需要调试插件,可以:

  1. 在 Visual Studio 中打开主解决方案
  2. 设置 Admin.NET.Web.Entry 为启动项目
  3. 在插件代码中设置断点
  4. 按 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)

  • 功能:管理仪器信息、校准记录、维护计划等
  • 实体BaseInstrumentInfoBaseInstrumentCalibrationRecord
  • 服务BaseInstrumentInfoServiceBaseInstrumentCalibrationRecordService
  • 种子数据:仪器管理菜单、仪器类型等

2. LIMS配置插件 (Admin.NET.Plugin.LIMSConfig)

  • 功能:管理公式、变量、舍入规则、符号、单位等
  • 实体LimsFormulaLimsUnitLimsSymbol
  • 服务LimsFormulaServiceLimsUnitService
  • 种子数据:系统字典、菜单等

3. 物料管理插件 (Admin.NET.Plugin.Material)

  • 功能:管理物料、申购单、入库单、出库单等
  • 实体LimsMaterialLimsMaterialApplyLimsMaterialIn
  • 服务LimsMaterialServiceLimsMaterialApplyService
  • 种子数据:物料分类、供应商、货位等

十三、总结

插件开发是 Admin.NET 系统的重要扩展方式,通过本文档的指导,你应该能够:

  1. 了解插件的目录结构和开发流程
  2. 掌握实体类、服务层、API控制器的开发方法
  3. 学会配置种子数据和前端页面
  4. 理解插件的部署与测试方法
  5. 遵循最佳实践和注意事项

希望本文档能够帮助你快速上手插件开发,为 Admin.NET 系统添加更多实用功能。如果你在开发过程中遇到问题,可以参考系统中的现有插件,或者查阅相关文档和资料。

祝你开发顺利!