znlgis 博客

GIS开发与技术分享

第七章:二次开发实战-创建自定义模块

目录

  1. 二次开发概述
  2. 创建业务实体
  3. 创建服务层
  4. 创建API接口
  5. 前端页面开发
  6. 权限配置
  7. 代码生成器使用
  8. 完整示例:订单管理模块

1. 二次开发概述

1.1 开发模式选择

Admin.NET推荐的二次开发模式是创建独立的应用层项目,这样可以:

推荐项目结构

Admin.NET/
├── Admin.NET.Core/           # 核心层(不修改)
├── Admin.NET.Application/    # 示例应用层(参考用)
├── Admin.NET.Web.Core/       # Web核心层(不修改)
├── Admin.NET.Web.Entry/      # Web入口层
├── MyCompany.Application/    # 自定义应用层(新建)
│   ├── Entity/               # 业务实体
│   ├── Service/              # 业务服务
│   ├── Dto/                  # 数据传输对象
│   ├── EventBus/             # 事件处理
│   └── Const/                # 常量定义
└── Plugins/                  # 插件项目

1.2 创建应用层项目

步骤1:创建类库项目

dotnet new classlib -n MyCompany.Application -f net8.0

步骤2:添加项目引用

MyCompany.Application.csproj中添加:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <!-- 引用Core层 -->
    <ProjectReference Include="..\Admin.NET.Core\Admin.NET.Core.csproj" />
  </ItemGroup>
</Project>

步骤3:在Web.Entry中引用

<ItemGroup>
  <ProjectReference Include="..\MyCompany.Application\MyCompany.Application.csproj" />
</ItemGroup>

步骤4:创建GlobalUsings.cs

// MyCompany.Application/GlobalUsings.cs
global using System;
global using System.Collections.Generic;
global using System.ComponentModel;
global using System.ComponentModel.DataAnnotations;
global using System.Linq;
global using System.Threading.Tasks;
global using Admin.NET.Core;
global using Furion.DynamicApiController;
global using Mapster;
global using Microsoft.AspNetCore.Mvc;
global using SqlSugar;

2. 创建业务实体

2.1 实体设计原则

2.2 创建实体类

// MyCompany.Application/Entity/Product.cs
namespace MyCompany.Application;

/// <summary>
/// 产品表
/// </summary>
[SugarTable(null, "产品表")]
public class Product : EntityTenant
{
    /// <summary>
    /// 产品编码
    /// </summary>
    [SugarColumn(ColumnDescription = "产品编码", Length = 64)]
    [Required, MaxLength(64)]
    public string Code { get; set; }

    /// <summary>
    /// 产品名称
    /// </summary>
    [SugarColumn(ColumnDescription = "产品名称", Length = 128)]
    [Required, MaxLength(128)]
    public string Name { get; set; }

    /// <summary>
    /// 产品分类Id
    /// </summary>
    [SugarColumn(ColumnDescription = "产品分类Id")]
    public long CategoryId { get; set; }

    /// <summary>
    /// 产品分类
    /// </summary>
    [Navigate(NavigateType.OneToOne, nameof(CategoryId))]
    public ProductCategory Category { get; set; }

    /// <summary>
    /// 规格型号
    /// </summary>
    [SugarColumn(ColumnDescription = "规格型号", Length = 256)]
    public string? Specification { get; set; }

    /// <summary>
    /// 单位
    /// </summary>
    [SugarColumn(ColumnDescription = "单位", Length = 16)]
    public string? Unit { get; set; }

    /// <summary>
    /// 单价
    /// </summary>
    [SugarColumn(ColumnDescription = "单价", DecimalDigits = 2)]
    public decimal Price { get; set; }

    /// <summary>
    /// 库存数量
    /// </summary>
    [SugarColumn(ColumnDescription = "库存数量")]
    public int StockQty { get; set; }

    /// <summary>
    /// 产品图片
    /// </summary>
    [SugarColumn(ColumnDescription = "产品图片", Length = 512)]
    public string? ImageUrl { get; set; }

    /// <summary>
    /// 产品描述
    /// </summary>
    [SugarColumn(ColumnDescription = "产品描述", ColumnDataType = StaticConfig.CodeFirst_BigString)]
    public string? Description { get; set; }

    /// <summary>
    /// 状态(1正常 0停用)
    /// </summary>
    [SugarColumn(ColumnDescription = "状态")]
    public StatusEnum Status { get; set; } = StatusEnum.Enable;

    /// <summary>
    /// 排序
    /// </summary>
    [SugarColumn(ColumnDescription = "排序")]
    public int OrderNo { get; set; } = 100;

    /// <summary>
    /// 备注
    /// </summary>
    [SugarColumn(ColumnDescription = "备注", Length = 512)]
    public string? Remark { get; set; }
}

/// <summary>
/// 产品分类表
/// </summary>
[SugarTable(null, "产品分类表")]
public class ProductCategory : EntityTenant
{
    /// <summary>
    /// 父级Id
    /// </summary>
    [SugarColumn(ColumnDescription = "父级Id")]
    public long Pid { get; set; }

    /// <summary>
    /// 分类名称
    /// </summary>
    [SugarColumn(ColumnDescription = "分类名称", Length = 64)]
    [Required, MaxLength(64)]
    public string Name { get; set; }

    /// <summary>
    /// 分类编码
    /// </summary>
    [SugarColumn(ColumnDescription = "分类编码", Length = 64)]
    public string? Code { get; set; }

    /// <summary>
    /// 排序
    /// </summary>
    [SugarColumn(ColumnDescription = "排序")]
    public int OrderNo { get; set; } = 100;

    /// <summary>
    /// 状态
    /// </summary>
    [SugarColumn(ColumnDescription = "状态")]
    public StatusEnum Status { get; set; } = StatusEnum.Enable;

    /// <summary>
    /// 备注
    /// </summary>
    [SugarColumn(ColumnDescription = "备注", Length = 256)]
    public string? Remark { get; set; }

    /// <summary>
    /// 子分类
    /// </summary>
    [SugarColumn(IsIgnore = true)]
    public List<ProductCategory> Children { get; set; }
}

2.3 创建DTO

// MyCompany.Application/Dto/ProductDto.cs
namespace MyCompany.Application;

/// <summary>
/// 产品分页查询输入
/// </summary>
public class PageProductInput : BasePageInput
{
    /// <summary>
    /// 产品编码
    /// </summary>
    public string? Code { get; set; }

    /// <summary>
    /// 产品名称
    /// </summary>
    public string? Name { get; set; }

    /// <summary>
    /// 分类Id
    /// </summary>
    public long? CategoryId { get; set; }

    /// <summary>
    /// 状态
    /// </summary>
    public StatusEnum? Status { get; set; }
}

/// <summary>
/// 添加产品输入
/// </summary>
public class AddProductInput
{
    /// <summary>
    /// 产品编码
    /// </summary>
    [Required(ErrorMessage = "产品编码不能为空")]
    [MaxLength(64, ErrorMessage = "产品编码最大长度为64")]
    public string Code { get; set; }

    /// <summary>
    /// 产品名称
    /// </summary>
    [Required(ErrorMessage = "产品名称不能为空")]
    [MaxLength(128, ErrorMessage = "产品名称最大长度为128")]
    public string Name { get; set; }

    /// <summary>
    /// 分类Id
    /// </summary>
    [Required(ErrorMessage = "请选择产品分类")]
    public long CategoryId { get; set; }

    /// <summary>
    /// 规格型号
    /// </summary>
    public string? Specification { get; set; }

    /// <summary>
    /// 单位
    /// </summary>
    public string? Unit { get; set; }

    /// <summary>
    /// 单价
    /// </summary>
    [Range(0, double.MaxValue, ErrorMessage = "单价不能小于0")]
    public decimal Price { get; set; }

    /// <summary>
    /// 库存数量
    /// </summary>
    [Range(0, int.MaxValue, ErrorMessage = "库存数量不能小于0")]
    public int StockQty { get; set; }

    /// <summary>
    /// 产品图片
    /// </summary>
    public string? ImageUrl { get; set; }

    /// <summary>
    /// 产品描述
    /// </summary>
    public string? Description { get; set; }

    /// <summary>
    /// 状态
    /// </summary>
    public StatusEnum Status { get; set; } = StatusEnum.Enable;

    /// <summary>
    /// 排序
    /// </summary>
    public int OrderNo { get; set; } = 100;

    /// <summary>
    /// 备注
    /// </summary>
    public string? Remark { get; set; }
}

/// <summary>
/// 更新产品输入
/// </summary>
public class UpdateProductInput : AddProductInput
{
    /// <summary>
    /// 主键Id
    /// </summary>
    [Required(ErrorMessage = "Id不能为空")]
    public long Id { get; set; }
}

/// <summary>
/// 产品输出
/// </summary>
public class ProductOutput
{
    /// <summary>
    /// 主键Id
    /// </summary>
    public long Id { get; set; }

    /// <summary>
    /// 产品编码
    /// </summary>
    public string Code { get; set; }

    /// <summary>
    /// 产品名称
    /// </summary>
    public string Name { get; set; }

    /// <summary>
    /// 分类Id
    /// </summary>
    public long CategoryId { get; set; }

    /// <summary>
    /// 分类名称
    /// </summary>
    public string? CategoryName { get; set; }

    /// <summary>
    /// 规格型号
    /// </summary>
    public string? Specification { get; set; }

    /// <summary>
    /// 单位
    /// </summary>
    public string? Unit { get; set; }

    /// <summary>
    /// 单价
    /// </summary>
    public decimal Price { get; set; }

    /// <summary>
    /// 库存数量
    /// </summary>
    public int StockQty { get; set; }

    /// <summary>
    /// 产品图片
    /// </summary>
    public string? ImageUrl { get; set; }

    /// <summary>
    /// 状态
    /// </summary>
    public StatusEnum Status { get; set; }

    /// <summary>
    /// 创建时间
    /// </summary>
    public DateTime? CreateTime { get; set; }
}

3. 创建服务层

3.1 产品服务

// MyCompany.Application/Service/ProductService.cs
namespace MyCompany.Application;

/// <summary>
/// 产品服务
/// </summary>
[ApiDescriptionSettings(Order = 200)]
public class ProductService : IDynamicApiController, ITransient
{
    private readonly SqlSugarRepository<Product> _productRep;
    private readonly SqlSugarRepository<ProductCategory> _categoryRep;
    private readonly SysCacheService _cacheService;

    public ProductService(
        SqlSugarRepository<Product> productRep,
        SqlSugarRepository<ProductCategory> categoryRep,
        SysCacheService cacheService)
    {
        _productRep = productRep;
        _categoryRep = categoryRep;
        _cacheService = cacheService;
    }

    /// <summary>
    /// 获取产品分页列表
    /// </summary>
    [DisplayName("获取产品分页列表")]
    public async Task<SqlSugarPagedList<ProductOutput>> Page(PageProductInput input)
    {
        return await _productRep.AsQueryable()
            .LeftJoin<ProductCategory>((p, c) => p.CategoryId == c.Id)
            .WhereIF(!string.IsNullOrWhiteSpace(input.Code), (p, c) => p.Code.Contains(input.Code))
            .WhereIF(!string.IsNullOrWhiteSpace(input.Name), (p, c) => p.Name.Contains(input.Name))
            .WhereIF(input.CategoryId.HasValue, (p, c) => p.CategoryId == input.CategoryId)
            .WhereIF(input.Status.HasValue, (p, c) => p.Status == input.Status)
            .OrderBy((p, c) => p.OrderNo)
            .OrderByDescending((p, c) => p.CreateTime)
            .Select((p, c) => new ProductOutput
            {
                Id = p.Id,
                Code = p.Code,
                Name = p.Name,
                CategoryId = p.CategoryId,
                CategoryName = c.Name,
                Specification = p.Specification,
                Unit = p.Unit,
                Price = p.Price,
                StockQty = p.StockQty,
                ImageUrl = p.ImageUrl,
                Status = p.Status,
                CreateTime = p.CreateTime
            })
            .ToPagedListAsync(input.Page, input.PageSize);
    }

    /// <summary>
    /// 获取产品详情
    /// </summary>
    [DisplayName("获取产品详情")]
    public async Task<Product> GetDetail([FromQuery] long id)
    {
        var product = await _productRep.AsQueryable()
            .Includes(p => p.Category)
            .FirstAsync(p => p.Id == id);
        
        return product ?? throw Oops.Oh("产品不存在");
    }

    /// <summary>
    /// 添加产品
    /// </summary>
    [ApiDescriptionSettings(Name = "Add"), HttpPost]
    [DisplayName("添加产品")]
    public async Task<long> Add(AddProductInput input)
    {
        // 验证编码唯一
        var exist = await _productRep.IsAnyAsync(p => p.Code == input.Code);
        if (exist)
            throw Oops.Oh("产品编码已存在");

        // 验证分类存在
        var category = await _categoryRep.GetByIdAsync(input.CategoryId);
        if (category == null)
            throw Oops.Oh("产品分类不存在");

        var product = input.Adapt<Product>();
        await _productRep.InsertAsync(product);
        
        return product.Id;
    }

    /// <summary>
    /// 更新产品
    /// </summary>
    [ApiDescriptionSettings(Name = "Update"), HttpPost]
    [DisplayName("更新产品")]
    public async Task Update(UpdateProductInput input)
    {
        var product = await _productRep.GetByIdAsync(input.Id);
        if (product == null)
            throw Oops.Oh("产品不存在");

        // 验证编码唯一(排除自身)
        var exist = await _productRep.IsAnyAsync(p => p.Code == input.Code && p.Id != input.Id);
        if (exist)
            throw Oops.Oh("产品编码已存在");

        input.Adapt(product);
        await _productRep.UpdateAsync(product);
    }

    /// <summary>
    /// 删除产品
    /// </summary>
    [ApiDescriptionSettings(Name = "Delete"), HttpPost]
    [DisplayName("删除产品")]
    public async Task Delete([FromBody] DeleteInput input)
    {
        var product = await _productRep.GetByIdAsync(input.Id);
        if (product == null)
            throw Oops.Oh("产品不存在");

        await _productRep.FakeDeleteAsync(product);
    }

    /// <summary>
    /// 批量删除产品
    /// </summary>
    [ApiDescriptionSettings(Name = "BatchDelete"), HttpPost]
    [DisplayName("批量删除产品")]
    public async Task BatchDelete([FromBody] List<long> ids)
    {
        await _productRep.Context.Updateable<Product>()
            .SetColumns(p => p.IsDelete == true)
            .Where(p => ids.Contains(p.Id))
            .ExecuteCommandAsync();
    }

    /// <summary>
    /// 修改产品状态
    /// </summary>
    [ApiDescriptionSettings(Name = "SetStatus"), HttpPost]
    [DisplayName("修改产品状态")]
    public async Task SetStatus([FromBody] SetStatusInput input)
    {
        var product = await _productRep.GetByIdAsync(input.Id);
        if (product == null)
            throw Oops.Oh("产品不存在");

        product.Status = input.Status;
        await _productRep.UpdateAsync(product);
    }

    /// <summary>
    /// 导出产品
    /// </summary>
    [ApiDescriptionSettings(Name = "Export"), HttpPost]
    [DisplayName("导出产品")]
    public async Task<IActionResult> Export(PageProductInput input)
    {
        var list = await _productRep.AsQueryable()
            .LeftJoin<ProductCategory>((p, c) => p.CategoryId == c.Id)
            .WhereIF(!string.IsNullOrWhiteSpace(input.Code), (p, c) => p.Code.Contains(input.Code))
            .WhereIF(!string.IsNullOrWhiteSpace(input.Name), (p, c) => p.Name.Contains(input.Name))
            .Select((p, c) => new ProductOutput
            {
                Id = p.Id,
                Code = p.Code,
                Name = p.Name,
                CategoryName = c.Name,
                Specification = p.Specification,
                Unit = p.Unit,
                Price = p.Price,
                StockQty = p.StockQty,
                Status = p.Status,
                CreateTime = p.CreateTime
            })
            .ToListAsync();

        var result = await list.ExportExcel();
        return result;
    }
}

/// <summary>
/// 删除输入
/// </summary>
public class DeleteInput
{
    /// <summary>
    /// Id
    /// </summary>
    [Required]
    public long Id { get; set; }
}

/// <summary>
/// 设置状态输入
/// </summary>
public class SetStatusInput
{
    /// <summary>
    /// Id
    /// </summary>
    [Required]
    public long Id { get; set; }

    /// <summary>
    /// 状态
    /// </summary>
    public StatusEnum Status { get; set; }
}

3.2 产品分类服务

// MyCompany.Application/Service/ProductCategoryService.cs
namespace MyCompany.Application;

/// <summary>
/// 产品分类服务
/// </summary>
[ApiDescriptionSettings(Order = 199)]
public class ProductCategoryService : IDynamicApiController, ITransient
{
    private readonly SqlSugarRepository<ProductCategory> _categoryRep;
    private readonly SqlSugarRepository<Product> _productRep;

    public ProductCategoryService(
        SqlSugarRepository<ProductCategory> categoryRep,
        SqlSugarRepository<Product> productRep)
    {
        _categoryRep = categoryRep;
        _productRep = productRep;
    }

    /// <summary>
    /// 获取分类树形列表
    /// </summary>
    [DisplayName("获取分类树形列表")]
    public async Task<List<ProductCategory>> GetTree()
    {
        return await _categoryRep.AsQueryable()
            .Where(c => c.Status == StatusEnum.Enable)
            .OrderBy(c => c.OrderNo)
            .ToTreeAsync(c => c.Children, c => c.Pid, 0);
    }

    /// <summary>
    /// 获取分类列表
    /// </summary>
    [DisplayName("获取分类列表")]
    public async Task<List<ProductCategory>> GetList()
    {
        return await _categoryRep.AsQueryable()
            .OrderBy(c => c.OrderNo)
            .ToListAsync();
    }

    /// <summary>
    /// 添加分类
    /// </summary>
    [ApiDescriptionSettings(Name = "Add"), HttpPost]
    [DisplayName("添加分类")]
    public async Task<long> Add(AddCategoryInput input)
    {
        // 验证名称唯一
        var exist = await _categoryRep.IsAnyAsync(c => 
            c.Name == input.Name && c.Pid == input.Pid);
        if (exist)
            throw Oops.Oh("分类名称已存在");

        var category = input.Adapt<ProductCategory>();
        await _categoryRep.InsertAsync(category);
        
        return category.Id;
    }

    /// <summary>
    /// 更新分类
    /// </summary>
    [ApiDescriptionSettings(Name = "Update"), HttpPost]
    [DisplayName("更新分类")]
    public async Task Update(UpdateCategoryInput input)
    {
        var category = await _categoryRep.GetByIdAsync(input.Id);
        if (category == null)
            throw Oops.Oh("分类不存在");

        // 不能将自己设为父级
        if (input.Pid == input.Id)
            throw Oops.Oh("不能将自己设为父级分类");

        input.Adapt(category);
        await _categoryRep.UpdateAsync(category);
    }

    /// <summary>
    /// 删除分类
    /// </summary>
    [ApiDescriptionSettings(Name = "Delete"), HttpPost]
    [DisplayName("删除分类")]
    public async Task Delete([FromBody] DeleteInput input)
    {
        // 检查是否有子分类
        var hasChildren = await _categoryRep.IsAnyAsync(c => c.Pid == input.Id);
        if (hasChildren)
            throw Oops.Oh("请先删除子分类");

        // 检查是否有关联产品
        var hasProducts = await _productRep.IsAnyAsync(p => p.CategoryId == input.Id);
        if (hasProducts)
            throw Oops.Oh("该分类下有产品,无法删除");

        await _categoryRep.DeleteByIdAsync(input.Id);
    }
}

/// <summary>
/// 添加分类输入
/// </summary>
public class AddCategoryInput
{
    /// <summary>
    /// 父级Id
    /// </summary>
    public long Pid { get; set; }

    /// <summary>
    /// 分类名称
    /// </summary>
    [Required(ErrorMessage = "分类名称不能为空")]
    [MaxLength(64)]
    public string Name { get; set; }

    /// <summary>
    /// 分类编码
    /// </summary>
    public string? Code { get; set; }

    /// <summary>
    /// 排序
    /// </summary>
    public int OrderNo { get; set; } = 100;

    /// <summary>
    /// 备注
    /// </summary>
    public string? Remark { get; set; }
}

/// <summary>
/// 更新分类输入
/// </summary>
public class UpdateCategoryInput : AddCategoryInput
{
    /// <summary>
    /// Id
    /// </summary>
    [Required]
    public long Id { get; set; }
}

4. 创建API接口

服务类实现IDynamicApiController接口后,Furion会自动生成API接口,无需手动创建Controller。

生成的API路径规则

自定义API配置

// 自定义API分组
[ApiDescriptionSettings("产品管理", Name = "Product", Order = 200)]
public class ProductService : IDynamicApiController, ITransient
{
    // 自定义方法名
    [ApiDescriptionSettings(Name = "List")]
    public async Task<List<Product>> GetList() { }
    
    // 自定义HTTP方法
    [HttpGet]
    public async Task<Product> GetDetail(long id) { }
    
    // 不生成API
    [NonAction]
    public void PrivateMethod() { }
}

5. 前端页面开发

5.1 创建API接口

// api/product/index.ts
import request from '@/utils/request'

export interface ProductQuery {
  code?: string
  name?: string
  categoryId?: number
  status?: number
  page: number
  pageSize: number
}

export interface ProductInfo {
  id: number
  code: string
  name: string
  categoryId: number
  categoryName?: string
  specification?: string
  unit?: string
  price: number
  stockQty: number
  imageUrl?: string
  status: number
  createTime?: string
}

export interface AddProductParams {
  code: string
  name: string
  categoryId: number
  specification?: string
  unit?: string
  price: number
  stockQty: number
  imageUrl?: string
  description?: string
  status: number
  orderNo: number
  remark?: string
}

export const productApi = {
  // 分页查询
  getPage(params: ProductQuery) {
    return request({
      url: '/api/product/page',
      method: 'get',
      params
    })
  },
  
  // 获取详情
  getDetail(id: number) {
    return request({
      url: '/api/product/detail',
      method: 'get',
      params: { id }
    })
  },
  
  // 新增
  add(data: AddProductParams) {
    return request({
      url: '/api/product/add',
      method: 'post',
      data
    })
  },
  
  // 更新
  update(data: AddProductParams & { id: number }) {
    return request({
      url: '/api/product/update',
      method: 'post',
      data
    })
  },
  
  // 删除
  delete(id: number) {
    return request({
      url: '/api/product/delete',
      method: 'post',
      data: { id }
    })
  },
  
  // 修改状态
  setStatus(id: number, status: number) {
    return request({
      url: '/api/product/setStatus',
      method: 'post',
      data: { id, status }
    })
  },
  
  // 导出
  export(params: ProductQuery) {
    return request({
      url: '/api/product/export',
      method: 'post',
      data: params,
      responseType: 'blob'
    })
  }
}

// 分类API
export const productCategoryApi = {
  getTree() {
    return request({
      url: '/api/productCategory/tree',
      method: 'get'
    })
  },
  
  getList() {
    return request({
      url: '/api/productCategory/list',
      method: 'get'
    })
  },
  
  add(data: any) {
    return request({
      url: '/api/productCategory/add',
      method: 'post',
      data
    })
  },
  
  update(data: any) {
    return request({
      url: '/api/productCategory/update',
      method: 'post',
      data
    })
  },
  
  delete(id: number) {
    return request({
      url: '/api/productCategory/delete',
      method: 'post',
      data: { id }
    })
  }
}

5.2 创建列表页面

<!-- views/product/index.vue -->
<template>
  <div class="product-container">
    <!-- 搜索栏 -->
    <el-card class="search-card">
      <el-form :model="queryParams" inline>
        <el-form-item label="产品编码">
          <el-input v-model="queryParams.code" placeholder="请输入产品编码" clearable />
        </el-form-item>
        <el-form-item label="产品名称">
          <el-input v-model="queryParams.name" placeholder="请输入产品名称" clearable />
        </el-form-item>
        <el-form-item label="产品分类">
          <el-tree-select
            v-model="queryParams.categoryId"
            :data="categoryTree"
            :props="{ label: 'name', value: 'id', children: 'children' }"
            placeholder="请选择分类"
            clearable
            check-strictly
          />
        </el-form-item>
        <el-form-item label="状态">
          <el-select v-model="queryParams.status" placeholder="请选择" clearable>
            <el-option label="启用" :value="1" />
            <el-option label="停用" :value="0" />
          </el-select>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="handleSearch">
            <el-icon><Search /></el-icon>搜索
          </el-button>
          <el-button @click="handleReset">
            <el-icon><Refresh /></el-icon>重置
          </el-button>
        </el-form-item>
      </el-form>
    </el-card>
    
    <!-- 表格 -->
    <el-card class="table-card">
      <template #header>
        <div class="toolbar">
          <el-button type="primary" v-auth="'product:add'" @click="handleAdd">
            <el-icon><Plus /></el-icon>新增
          </el-button>
          <el-button type="danger" v-auth="'product:delete'" :disabled="!selectedIds.length" @click="handleBatchDelete">
            批量删除
          </el-button>
          <el-button v-auth="'product:export'" @click="handleExport">
            <el-icon><Download /></el-icon>导出
          </el-button>
        </div>
      </template>
      
      <el-table 
        v-loading="loading" 
        :data="tableData" 
        border 
        @selection-change="handleSelectionChange"
      >
        <el-table-column type="selection" width="55" />
        <el-table-column prop="code" label="产品编码" width="120" />
        <el-table-column prop="name" label="产品名称" min-width="150" />
        <el-table-column prop="categoryName" label="所属分类" width="120" />
        <el-table-column prop="specification" label="规格型号" width="120" />
        <el-table-column prop="unit" label="单位" width="80" />
        <el-table-column prop="price" label="单价" width="100">
          <template #default="{ row }">
            ¥
          </template>
        </el-table-column>
        <el-table-column prop="stockQty" label="库存" width="80" />
        <el-table-column prop="status" label="状态" width="80">
          <template #default="{ row }">
            <el-switch 
              v-model="row.status" 
              :active-value="1" 
              :inactive-value="0"
              @change="handleStatusChange(row)"
            />
          </template>
        </el-table-column>
        <el-table-column prop="createTime" label="创建时间" width="160" />
        <el-table-column label="操作" width="150" fixed="right">
          <template #default="{ row }">
            <el-button type="primary" link v-auth="'product:edit'" @click="handleEdit(row)">
              编辑
            </el-button>
            <el-popconfirm title="确定要删除吗?" @confirm="handleDelete(row.id)">
              <template #reference>
                <el-button type="danger" link v-auth="'product:delete'">删除</el-button>
              </template>
            </el-popconfirm>
          </template>
        </el-table-column>
      </el-table>
      
      <el-pagination
        v-model:current-page="queryParams.page"
        v-model:page-size="queryParams.pageSize"
        :page-sizes="[10, 20, 50, 100]"
        :total="total"
        layout="total, sizes, prev, pager, next, jumper"
        @size-change="getList"
        @current-change="getList"
      />
    </el-card>
    
    <!-- 表单对话框 -->
    <ProductForm 
      v-model="formVisible" 
      :data="currentRow"
      :category-tree="categoryTree"
      @success="getList"
    />
  </div>
</template>

<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { productApi, productCategoryApi } from '@/api/product'
import type { ProductInfo, ProductQuery } from '@/api/product'
import { ElMessage, ElMessageBox } from 'element-plus'
import ProductForm from './form.vue'

// 查询参数
const queryParams = reactive<ProductQuery>({
  code: '',
  name: '',
  categoryId: undefined,
  status: undefined,
  page: 1,
  pageSize: 10
})

// 表格数据
const tableData = ref<ProductInfo[]>([])
const total = ref(0)
const loading = ref(false)
const selectedIds = ref<number[]>([])

// 分类树
const categoryTree = ref([])

// 表单
const formVisible = ref(false)
const currentRow = ref<ProductInfo | null>(null)

// 获取列表
const getList = async () => {
  loading.value = true
  try {
    const res = await productApi.getPage(queryParams)
    tableData.value = res.data.items
    total.value = res.data.total
  } finally {
    loading.value = false
  }
}

// 获取分类树
const getCategoryTree = async () => {
  const res = await productCategoryApi.getTree()
  categoryTree.value = res.data
}

// 搜索
const handleSearch = () => {
  queryParams.page = 1
  getList()
}

// 重置
const handleReset = () => {
  queryParams.code = ''
  queryParams.name = ''
  queryParams.categoryId = undefined
  queryParams.status = undefined
  queryParams.page = 1
  getList()
}

// 选择变化
const handleSelectionChange = (rows: ProductInfo[]) => {
  selectedIds.value = rows.map(r => r.id)
}

// 新增
const handleAdd = () => {
  currentRow.value = null
  formVisible.value = true
}

// 编辑
const handleEdit = (row: ProductInfo) => {
  currentRow.value = row
  formVisible.value = true
}

// 删除
const handleDelete = async (id: number) => {
  try {
    await productApi.delete(id)
    ElMessage.success('删除成功')
    getList()
  } catch (error) {
    console.error(error)
  }
}

// 批量删除
const handleBatchDelete = async () => {
  try {
    await ElMessageBox.confirm('确定要删除选中的产品吗?', '提示')
    // 调用批量删除接口
    ElMessage.success('删除成功')
    getList()
  } catch {
    // 取消
  }
}

// 修改状态
const handleStatusChange = async (row: ProductInfo) => {
  try {
    await productApi.setStatus(row.id, row.status)
    ElMessage.success('修改成功')
  } catch {
    row.status = row.status === 1 ? 0 : 1
  }
}

// 导出
const handleExport = async () => {
  try {
    const res = await productApi.export(queryParams)
    const blob = new Blob([res as any])
    const url = window.URL.createObjectURL(blob)
    const link = document.createElement('a')
    link.href = url
    link.download = '产品列表.xlsx'
    link.click()
    window.URL.revokeObjectURL(url)
  } catch (error) {
    console.error(error)
  }
}

onMounted(() => {
  getList()
  getCategoryTree()
})
</script>

<style scoped lang="scss">
.product-container {
  padding: 16px;
  
  .search-card {
    margin-bottom: 16px;
  }
  
  .toolbar {
    display: flex;
    gap: 10px;
  }
  
  .el-pagination {
    margin-top: 16px;
    justify-content: flex-end;
  }
}
</style>

6. 权限配置

6.1 添加菜单

在系统管理-菜单管理中添加产品管理菜单:

-- 目录
INSERT INTO sys_menu (id, pid, type, title, name, path, component, icon, order_no, status)
VALUES (2000000001, 0, 1, '产品管理', 'product', '/product', '', 'product', 100, 1);

-- 菜单-产品列表
INSERT INTO sys_menu (id, pid, type, title, name, path, component, permission, icon, order_no, status)
VALUES (2000000002, 2000000001, 2, '产品列表', 'productList', '/product/list', 'product/index', 'product:list', '', 1, 1);

-- 按钮权限
INSERT INTO sys_menu (id, pid, type, title, permission, order_no, status)
VALUES 
(2000000003, 2000000002, 3, '新增产品', 'product:add', 1, 1),
(2000000004, 2000000002, 3, '编辑产品', 'product:edit', 2, 1),
(2000000005, 2000000002, 3, '删除产品', 'product:delete', 3, 1),
(2000000006, 2000000002, 3, '导出产品', 'product:export', 4, 1);

6.2 配置路由

// router/modules/product.ts
export default {
  path: '/product',
  name: 'Product',
  component: Layout,
  redirect: '/product/list',
  meta: { title: '产品管理', icon: 'product' },
  children: [
    {
      path: 'list',
      name: 'ProductList',
      component: () => import('@/views/product/index.vue'),
      meta: { title: '产品列表', permission: 'product:list' }
    },
    {
      path: 'category',
      name: 'ProductCategory',
      component: () => import('@/views/product/category/index.vue'),
      meta: { title: '产品分类', permission: 'productCategory:list' }
    }
  ]
}

7. 代码生成器使用

7.1 配置代码生成

  1. 登录系统,进入”开发工具”-“代码生成”
  2. 点击”新增”,配置生成参数:
    • 数据库:选择目标数据库
    • 表名:选择要生成的表
    • 功能名称:填写中文功能名
    • 业务名称:填写英文业务名
    • 命名空间:如MyCompany.Application
    • 生成方式:压缩包/直接生成
  3. 配置字段属性:
    • 显示名称
    • 是否列表显示
    • 是否表单显示
    • 查询方式
    • 控件类型

7.2 生成代码

点击”预览”查看生成效果,确认无误后点击”生成”。

生成的代码包括:


8. 完整示例:订单管理模块

8.1 实体设计

// 订单表
[SugarTable(null, "订单表")]
public class Order : EntityTenant
{
    [SugarColumn(ColumnDescription = "订单编号", Length = 32)]
    public string OrderNo { get; set; }
    
    [SugarColumn(ColumnDescription = "客户Id")]
    public long CustomerId { get; set; }
    
    [SugarColumn(ColumnDescription = "订单金额", DecimalDigits = 2)]
    public decimal TotalAmount { get; set; }
    
    [SugarColumn(ColumnDescription = "订单状态")]
    public OrderStatusEnum Status { get; set; }
    
    [SugarColumn(ColumnDescription = "备注", Length = 512)]
    public string? Remark { get; set; }
    
    [Navigate(NavigateType.OneToMany, nameof(OrderItem.OrderId))]
    public List<OrderItem> Items { get; set; }
}

// 订单明细表
[SugarTable(null, "订单明细表")]
public class OrderItem : EntityTenant
{
    [SugarColumn(ColumnDescription = "订单Id")]
    public long OrderId { get; set; }
    
    [SugarColumn(ColumnDescription = "产品Id")]
    public long ProductId { get; set; }
    
    [SugarColumn(ColumnDescription = "产品名称", Length = 128)]
    public string ProductName { get; set; }
    
    [SugarColumn(ColumnDescription = "单价", DecimalDigits = 2)]
    public decimal Price { get; set; }
    
    [SugarColumn(ColumnDescription = "数量")]
    public int Qty { get; set; }
    
    [SugarColumn(ColumnDescription = "小计", DecimalDigits = 2)]
    public decimal Amount { get; set; }
}

// 订单状态枚举
public enum OrderStatusEnum
{
    [Description("待确认")]
    Pending = 0,
    
    [Description("已确认")]
    Confirmed = 1,
    
    [Description("已发货")]
    Shipped = 2,
    
    [Description("已完成")]
    Completed = 3,
    
    [Description("已取消")]
    Cancelled = 9
}

8.2 订单服务

[ApiDescriptionSettings("订单管理", Order = 100)]
public class OrderService : IDynamicApiController, ITransient
{
    private readonly SqlSugarRepository<Order> _orderRep;
    private readonly SqlSugarRepository<OrderItem> _itemRep;
    private readonly SqlSugarRepository<Product> _productRep;
    private readonly ISqlSugarClient _db;

    public OrderService(
        SqlSugarRepository<Order> orderRep,
        SqlSugarRepository<OrderItem> itemRep,
        SqlSugarRepository<Product> productRep,
        ISqlSugarClient db)
    {
        _orderRep = orderRep;
        _itemRep = itemRep;
        _productRep = productRep;
        _db = db;
    }

    /// <summary>
    /// 创建订单
    /// </summary>
    [HttpPost]
    public async Task<long> Create(CreateOrderInput input)
    {
        // 生成订单号
        var orderNo = $"ORD{DateTime.Now:yyyyMMddHHmmss}{new Random().Next(1000, 9999)}";
        
        // 计算订单金额
        decimal totalAmount = 0;
        var items = new List<OrderItem>();
        
        foreach (var item in input.Items)
        {
            var product = await _productRep.GetByIdAsync(item.ProductId);
            if (product == null)
                throw Oops.Oh($"产品不存在:{item.ProductId}");
            
            if (product.StockQty < item.Qty)
                throw Oops.Oh($"产品库存不足:{product.Name}");
            
            var orderItem = new OrderItem
            {
                ProductId = product.Id,
                ProductName = product.Name,
                Price = product.Price,
                Qty = item.Qty,
                Amount = product.Price * item.Qty
            };
            items.Add(orderItem);
            totalAmount += orderItem.Amount;
        }

        // 事务处理
        try
        {
            _db.Ado.BeginTran();
            
            // 创建订单
            var order = new Order
            {
                OrderNo = orderNo,
                CustomerId = input.CustomerId,
                TotalAmount = totalAmount,
                Status = OrderStatusEnum.Pending,
                Remark = input.Remark
            };
            await _orderRep.InsertAsync(order);
            
            // 创建订单明细
            items.ForEach(i => i.OrderId = order.Id);
            await _itemRep.InsertRangeAsync(items);
            
            // 扣减库存
            foreach (var item in input.Items)
            {
                await _db.Updateable<Product>()
                    .SetColumns(p => p.StockQty == p.StockQty - item.Qty)
                    .Where(p => p.Id == item.ProductId)
                    .ExecuteCommandAsync();
            }
            
            _db.Ado.CommitTran();
            return order.Id;
        }
        catch
        {
            _db.Ado.RollbackTran();
            throw;
        }
    }
}

总结

本章详细介绍了Admin.NET二次开发的完整流程:

  1. 开发模式:创建独立应用层项目
  2. 实体设计:继承基类、添加特性
  3. 服务开发:实现业务逻辑
  4. API接口:动态API自动生成
  5. 前端开发:API封装、页面组件
  6. 权限配置:菜单和按钮权限
  7. 代码生成:使用生成器加速开发
  8. 完整示例:订单管理模块

掌握这些内容后,你可以独立完成业务模块的开发。在下一章中,我们将学习更多高级功能的扩展开发。