☰
基于.NET 10+Vue3+Element Plus的图书管理系统全栈实战
2026/10/10 9:47:30 网站建设 项目流程

去年我接手一个内部图书管理系统的重构任务,网上翻了一圈全栈教程,铺天盖地全是 Java 和 Node 的内容,.NET 的声音少得可怜。可实际在企业里,尤其是 Windows 服务器生态占主导、技术团队又不大的一线环境,.NET 才是最有性价比的选择。这套基于 .NET 10 + Vue3 + Element Plus + MySQL 的全栈图书管理系统,就是我在这个背景下重新梳理出来的完整方案。它能覆盖什么范围?图书录入、检索、借阅、归还、用户管理、权限控制,最常见的图书馆和内部资料室场景都能落进去;适合谁参考?刚学完 C# 和前端基础、想做一个能写进简历的完整全栈项目的人,或者在团队里需要快速交付一个管理系统、不想被繁琐框架折腾的人。这篇文章我会按一条真实项目的推进顺序,从技术选型、数据库设计、后端 API、前端页面到最终部署,把每一步的关键细节和踩坑记录展开讲清楚。

1. 技术选型:为什么是 .NET 10 而不是 Spring Boot 或 Node

先亮结论:这套组合的核心优势是后端强类型、编译期检查多、开发效率高,前端借着 Vue3 的生态能快速搭出中后台管理界面,MySQL 又是普及度最高的数据库。三者组合在一起,正好避开我遇到过的几个痛点:教程碎片化、前后端联调无法无天、数据库字段改一个连带着改三天代码。

1.1 我对比过的几套全栈方案,为何最终定下这一套

在决定用 .NET 之前,我实际对比过三套全栈组合,分别拿图书管理这类中后台业务跑过 demo。结果如下表:

对比维度.NET 10 + Vue3Spring Boot + Vue3Node.js + Vue3
后端语言C#JavaJavaScript/TypeScript
学习曲线中等偏高较低
类型安全强类型,编译期检查强类型弱类型(选 TS 才可选)
国内资料官方文档完善最多前端生态丰富
部署环境Windows/Linux 都轻松依赖 JVM 调优Docker 化容易
数据库驱动Pomelo EF Core 成熟MyBatis/JPA 成熟Sequelize/Prisma 略折腾

Spring Boot 不是不好,而是对一个图书管理系统来说,Java 环境的配置成本、Maven 依赖的解析速度、JVM 参数的调优,对企业内部技术团队其实是负担。Node.js 的上手速度确实快,但业务代码一旦超过十几个接口,弱类型带来的字段名拼写错误、参数类型不确定这些小坑会消耗大量联调时间。C# 这边,Visual Studio 社区版免费就能开发,dotnet 命令一行就能建项目,EF Core 操作 MySQL 的链路也非常顺,整体体验最接近“把注意力放在业务上”。

1.2 .NET 10 对业务开发最直观的几个变化

很多教程一提到 .NET 就只讲框架本身,其实对业务开发影响最大的是语言和生态的迭代。.NET 10 这个版本,有几个地方在实际写代码时会明显感受到:

  • C# 13 的集合表达式,类似List<int> nums = [1, 2, 3],比原来new List<int> { 1, 2, 3 }简洁不少,写初始化逻辑时很顺手。
  • Web API 模板默认集成了 OpenAPI 文档,dotnet new webapi出来就能看到接口文档页面,省去了单独配 Swagger 的步骤。
  • 运行时和 GC 做了持续优化,对图书管理系统这种中小并发场景绰绰有余。
  • EF Core 在查询翻译上有不少改进,复杂where和连表在 MySQL 上生成的 SQL 更精准。

需要提醒一句:.NET 版本有 LTS 与 STS 的策略区分,如果所在团队对版本生命周期有严格要求,正式立项前务必确认好选型版本。个人项目和内部系统用 .NET 10 完全没问题,但如果是交付给外部客户,选择上一个 LTS 版本也许更稳妥。

1.3 Vue3 + Element Plus 的组合为什么比 Vue2 更划算

前端部分我直接选择了 Vue3,原因不复杂。Vue2 的 Options API 在小页面里很直观,但一旦图书管理这种页面多了,data、methods、watch分散在文件各处,做逻辑复用只能靠 mixin,项目一大就容易“找不到代码在哪”。Vue3 的组合式 API 把同一业务的代码聚在一起,用onMounted、ref、watch这些 API 组织页面逻辑,配合script setup写法,写起来清爽得多。

Element Plus 是 Element UI 面向 Vue3 的重写版本,表格、表单、弹窗、分页、消息提示这些中后台组件都齐,表单校验和表格列渲染的扩展性也足够。再配合官方提供的暗黑模式支持,做个主题切换功能都花不了十分钟,这一点后面部署部分我会演示。

至于 MySQL,选择 8.0 以上的稳定版本即可,社区资料最多、问题最容易检索到。字符集统一utf8mb4,中文和特殊符号都能正确处理,这是必选项。

2. 数据库设计骨架:图书、借阅与用户的三表联动

数据库是这一类系统最容易想当然的环节。很多人上来就建一堆表,然后发现业务根本用不上;或者反过来,只建两张表,结果借还书记录没地方存。我的建议是先别写任何代码,把完整的业务流走一遍,再回头建模。

2.1 先想清楚借阅流程再设计表

图书管理系统的核心业务可以拆成一条闭环:

  1. 用户注册并登录。
  2. 管理员录入图书信息。
  3. 用户按书名、分类或作者检索图书。
  4. 用户发起借阅,系统检查可借库存并扣减。
  5. 用户在应还日期前归还,系统加回库存。

这条闭环里的关键角色只有三个:用户、图书、借阅记录。所以核心表结构就是三张,不必额外建一堆分类表、出版社表。分类和出版社直接作为图书表的字段即可,对中小系统更实用。“先闭环、后加表”是我踩过几次坑后总结出来的原则。

2.2 三张核心表的建表 SQL 与字段说明

以 MySQL 8.0 为例,完整建表脚本如下。

用户表:

CREATE TABLE `users` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', `username` VARCHAR(50) NOT NULL COMMENT '登录名', `password_hash` VARCHAR(100) NOT NULL COMMENT '密码哈希值', `real_name` VARCHAR(50) DEFAULT NULL COMMENT '真实姓名', `email` VARCHAR(100) DEFAULT NULL COMMENT '邮箱', `role` TINYINT NOT NULL DEFAULT 0 COMMENT '0-普通用户,1-管理员', `status` TINYINT NOT NULL DEFAULT 1 COMMENT '1-正常,0-禁用', `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';

图书表:

CREATE TABLE `books` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', `isbn` VARCHAR(20) DEFAULT NULL COMMENT '国际标准书号', `title` VARCHAR(200) NOT NULL COMMENT '书名', `author` VARCHAR(100) DEFAULT NULL COMMENT '作者', `publisher` VARCHAR(100) DEFAULT NULL COMMENT '出版社', `category` VARCHAR(50) DEFAULT NULL COMMENT '分类', `cover_url` VARCHAR(255) DEFAULT NULL COMMENT '封面图地址', `total_stock` INT NOT NULL DEFAULT 0 COMMENT '总库存', `available_stock` INT NOT NULL DEFAULT 0 COMMENT '可借库存', `description` TEXT COMMENT '图书简介', `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`), KEY `idx_title` (`title`), KEY `idx_category` (`category`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='图书表';

借阅记录表:

CREATE TABLE `borrow_records` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', `user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户ID', `book_id` BIGINT UNSIGNED NOT NULL COMMENT '图书ID', `borrow_date` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '借出时间', `due_date` DATETIME NOT NULL COMMENT '应还时间', `return_date` DATETIME DEFAULT NULL COMMENT '实际归还时间', `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0-借出中,1-已归还,2-逾期', PRIMARY KEY (`id`), KEY `idx_user` (`user_id`), KEY `idx_book` (`book_id`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='借阅记录表';

几个字段设计上的关键决策:

  • 主键用BIGINT UNSIGNED,不要用INT。图书管理项目短时间内看起来几十条数据无所谓,但借阅记录会随时间持续增长,INT上限约 21 亿,索引和关联查询的性能余量也不如BIGINT。
  • 密码字段存password_hash,绝对不存明文。哪怕内部系统也不该犯这种错误。
  • role用TINYINT而不是字符串,判断逻辑简单,扩展新角色时加一个枚举值即可。
  • books表里total_stock和available_stock分开存。归还图书时只改available_stock,总数保留用于统计损耗和借出率,不用临时计算。
  • borrow_records.status初始为 0,归还时改为 1。逾期状态可以不做定时任务,查询时根据due_date < NOW()和return_date IS NULL动态判断,在接口层算出来,省去后台任务。

2.3 索引、外键与 MySQL 8.0 环境要点

索引策略上遵循一个原则:给查询的 where 条件和排序字段建索引,不要给每个字段都加索引。books表的title和category是检索高频字段,加普通索引;borrow_records的user_id、book_id、status分别加索引,支撑“某用户当前借了哪些书”“某本书现在是否可借”这类查询。

外键我建议只在特殊场景下用。图书借阅集中在高并发写入,物理外键会影响每次插入的校验成本;而且如果后期分表分库,物理外键反而成负担。项目里直接用逻辑外键,在程序层保证user_id和book_id的有效性,代码里多做一步存在性查询。

再说一个很多人卡住的环境问题。MySQL 在 Windows 上安装时,如果 MySQL Installer 界面里没有“Developer Default”选项,不必纠结,直接选Server only,装完再单独装一个 MySQL Workbench 图形客户端就够了。安装路径不要带中文和空格,否则启动服务时很容易报路径错误。习惯用 Docker 的可以直接跑docker run -p 3306:3306 --name mysql8 -e MYSQL_ROOT_PASSWORD=你的密码 -d mysql:8.0,注意镜像标签不要用latest,指定mysql:8.0更可控,必要时把数据目录挂载到宿主机,避免容器重建后数据丢失。

3. 后端 API:.NET 10 项目中容易被忽略的落地细节

数据库设计完成,后端是第一个动手写的部分。很多新手喜欢上来就写 Controller,我认为正确顺序是先搭骨架,再填充业务。骨架对了,后面的每个接口都只是机械操作。

3.1 项目结构与依赖注入,一次搭好不分心

我用的解决方案是四个项目:BookManage.Api(Web API 入口)、BookManage.Application(业务服务层)、BookManage.Domain(实体与接口)、BookManage.Infrastructure(EF Core 与仓储实现)。图书管理系统这个规模,严格来说两层也够,但四层的好处是后续加缓存、加消息队列时有明确的位置放,不会把 Api 项目堆成一团浆糊。

dotnet new sln -n BookManage dotnet new webapi -n BookManage.Api dotnet new classlib -n BookManage.Domain dotnet new classlib -n BookManage.Application dotnet new classlib -n BookManage.Infrastructure dotnet sln add BookManage.Api BookManage.Application BookManage.Domain BookManage.Infrastructure

依赖注入的核心代码在Program.cs:

var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddDbContext<BookDbContext>(options => options.UseMySql(builder.Configuration.GetConnectionString("Default"), new MySqlServerVersion(new Version(8, 0, 36)))); builder.Services.AddScoped<IBookService, BookService>(); builder.Services.AddScoped<IBorrowService, BorrowService>(); builder.Services.AddScoped<IUserService, UserService>(); var app = builder.Build(); app.MapControllers(); app.Run();

构造函数的依赖注入是 ASP.NET Core 的默认行为,控制器里声明接口参数,框架会在请求时自动实例化服务,不需要手动 new。这一点在 .NET 里非常省心,写完服务注册后,Controller 里直接用即可。

3.2 EF Core 连接 MySQL 的配置与迁移过程

EF Core 操作 MySQL 需要安装Pomelo.EntityFrameworkCore.MySql包,这是目前社区使用最广泛的驱动库,对 MySQL 的方言支持完整。连接字符串配置在appsettings.json:

{ "ConnectionStrings": { "Default": "Server=localhost;Port=3306;Database=book_manage;User=root;Password=你的密码;" } }

这里有个很容易被忽略的点:Database=book_manage这个库要先手动创建,EF Core 的迁移不会自动建库,只会在库已存在的前提下建表。如果数据库不存在,第一次database update会直接报错。

迁移工具链如下:

dotnet tool install --global dotnet-ef dotnet add BookManage.Infrastructure package Pomelo.EntityFrameworkCore.MySql dotnet add BookManage.Infrastructure package Microsoft.EntityFrameworkCore.Design dotnet ef migrations add Init -p BookManage.Infrastructure dotnet ef database update -p BookManage.Infrastructure

用迁移而不是直接同步数据库,核心价值在于保留变更历史。后面改了表结构,migrations add会生成增量脚本,代码评审能看见数据库的变化,回滚也有据可依。

3.3 统一响应模型 Result 与全局异常处理

之前做一个前后端项目时,后端接口有的返回{ code, message, data },有的直接返回数据本身,前端每个页面都要单独判断,联调效率极低。从那以后我所有项目统一用响应模型。

public class ApiResult<T> { public bool Success { get; set; } public string Message { get; set; } = string.Empty; public T? Data { get; set; } public static ApiResult<T> Ok(T data, string message = "操作成功") { return new ApiResult<T> { Success = true, Message = message, Data = data }; } public static ApiResult<T> Fail(string message) { return new ApiResult<T> { Success = false, Message = message }; } }

控制器里不需要关心统一包装,服务层直接返回业务实体,控制器统一包一层:

[HttpGet("{id}")] public async Task<IActionResult> GetBook(long id) { var book = await _bookService.GetByIdAsync(id); if (book == null) return NotFound(ApiResult<BookDto>.Fail("图书不存在")); return Ok(ApiResult<BookDto>.Ok(book)); }

配合全局异常处理中间件,遇到的未捕获异常统一返回500 + 错误信息,不用在每个方法里写 try-catch。中间件的核心逻辑是捕获Exception,记录日志后返回统一结构,前端 axios 拦截器只需要写一次错误处理。

3.4 JWT 认证,前后端分离下最稳妥的身份方案

图书管理系统必然涉及权限区分:普通用户能借书还书,管理员还能管理图书和维护用户。前后端分离架构下,JWT 是最稳妥的方案,服务端不保存会话状态,登录成功返回一个带签名的 Token,前端后续请求在请求头带Authorization: Bearer <token>即可。

Program.cs里配置认证:

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidIssuer = jwtConfig.Issuer, ValidateAudience = true, ValidAudience = jwtConfig.Audience, ValidateLifetime = true, ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes(jwtConfig.Key)) }; });

生成 Token 时,把用户 ID 和角色放进声明(Claim)里:

var claims = new List<Claim> { new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), new Claim(ClaimTypes.Role, user.Role == 1 ? "Admin" : "User") }; var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var token = new JwtSecurityToken( issuer: jwtConfig.Issuer, audience: jwtConfig.Audience, claims: claims, expires: DateTime.UtcNow.AddHours(2), signingCredentials: credentials); return new JwtSecurityTokenHandler().WriteToken(token);

需要管理员权限的接口直接加特性标注:

[HttpPost] [Authorize(Roles = "Admin")] public async Task<IActionResult> CreateBook(BookCreateDto dto)

这里有一个比较常见的坑:JWT 的密钥太短会导致运行时抛加密算法异常。建议用不少于 32 个字符的随机字符串,放在环境变量或配置文件里。其次,前端退出登录的直接做法是清掉本地 Token,因为 JWT 服务端天然无状态,不需要调用登出接口。

4. 前端工程:Vue3 组合式 API 与 Element Plus 的配合方式

前端的核心任务是把后端接口可靠地展示出来,并且给用户一个顺手的操作界面。Element Plus 的组件能省掉大量 UI 工作量,真正要花心思的是接口封装和状态管理。

4.1 用 Vite 初始化项目,换掉过时的 Vue CLI

Vue CLI 虽然还能用,但 Vite 已经是 Vue 生态默认构建工具,启动速度和热更新体验完全不是一个量级。创建项目的命令:

npm create vite@latest book-manage-web -- --template vue-ts cd book-manage-web npm install npm install element-plus @element-plus/icons-vue npm install pinia vue-router axios

--template vue-ts会生成带 TypeScript 的模板,对中后台项目非常合适,接口返回的数据结构可以用类型约束,避免字段名拼错才发现问题。

工程目录我习惯这样组织:

src/ ├── api/ # 接口层,按模块拆分 ├── components/ # 通用组件 ├── layouts/ # 布局组件 ├── router/ # 路由配置 ├── stores/ # Pinia 状态 ├── views/ # 页面组件 └── utils/ # 工具函数

api层和页面逻辑分离是我比较坚持的一点。所有后端接口调用集中在src/api目录,页面只关心数据展示,后面接口路径变了只改一个文件。

4.2 组合式 API 的图书列表页:从列表到表单的一次串通

图书管理页是这套系统里最有代表性的页面,包含筛选区、数据表格、分页和新增编辑弹窗。用组合式 API 组织逻辑,代码集中而且扩展方便:

<script setup lang="ts"> import { ref, onMounted, reactive } from 'vue' import { getBookList, createBook, updateBook, deleteBook } from '@/api/book' import { ElMessage, ElMessageBox } from 'element-plus' const loading = ref(false) const list = ref<BookItem[]>([]) const total = ref(0) const query = reactive({ page: 1, pageSize: 10, keyword: '' }) const loadData = async () => { loading.value = true try { const res = await getBookList(query) list.value = res.data.records total.value = res.data.total } finally { loading.value = false } } const handleSearch = () => { query.page = 1 loadData() } const handleDelete = async (id: number) => { await ElMessageBox.confirm('确定删除这本书吗?', '提示', { type: 'warning' }) await deleteBook(id) ElMessage.success('删除成功') loadData() } onMounted(loadData) </script>

模板部分用el-table展示数据,el-dialog承载新增编辑表单,el-pagination做分页。这里有一个值得注意的细节:el-table的data绑定的是当前页的记录列表,total是后端返回的总条数,分页组件用total来控制总页数,千万不要把当前页的list.length当成总数传给分页,否则切到第二页时会发现分页条数少一截。

表单校验方面,el-form的model和rules字段名必须一一对应,如果不一致,校验规则不会触发。这是 Element Plus 使用中最常见的隐形问题。

4.3 一套开箱即用的 axios 封装、状态管理与路由守卫

axios 封装的核心目的是统一处理 Token 注入、响应异常和 401 跳转。下面是实际项目里直接可用的request.ts:

import axios from 'axios' import { ElMessage } from 'element-plus' import router from '@/router' const service = axios.create({ baseURL: '/api', timeout: 15000 }) service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) service.interceptors.response.use( response => { const res = response.data if (!res.success) { ElMessage.error(res.message) return Promise.reject(new Error(res.message)) } return res }, error => { if (error.response?.status === 401) { localStorage.removeItem('token') router.push('/login') } ElMessage.error(error.response?.data?.message || '请求失败') return Promise.reject(error) } ) export default service

路由守卫用来做登录控制和权限判定:

router.beforeEach((to, from) => { const token = localStorage.getItem('token') if (!token && to.path !== '/login') { return '/login' } if (token && to.path === '/login') { return '/' } return true })

管理员权限用路由的meta字段标记,比如meta: { requiresAdmin: true },在守卫里判断用户角色后放行。这套逻辑对图书管理系统的权限模型足够了。

5. 联调与部署:跨域、认证、发布一步都不能省

前后端都写完,真正的考验才开始。联调阶段的跨域问题、生产部署的路径问题、上线前的体验细节,每一项都能让项目从“写完”变成“可用”。

5.1 开发环境跨域:Vite 代理与 CORS 配合使用

开发时前端跑在http://localhost:5173,后端跑在http://localhost:7000,浏览器同源策略会拦截请求。两种处理方式我建议同时配置。

后端开启 CORS:

builder.Services.AddCors(options => { options.AddPolicy("AllowVite", policy => policy.WithOrigins("http://localhost:5173") .AllowAnyHeader() .AllowAnyMethod()); }); app.UseCors("AllowVite");

前端用 Vite 代理把/api转发到后端地址,vite.config.ts:

export default defineConfig({ server: { port: 5173, proxy: { '/api': { target: 'http://localhost:7000', changeOrigin: true } } } })

这样前端请求/api/books/list时代理会转发到http://localhost:7000/api/books/list,开发环境下的请求路径和后端路由完全一致,不需要在代码里维护一长串绝对地址。

需要注意一点:默认代理转发的changeOrigin: true会把请求头里的Host改成目标地址。后端什么都没有配置的情况下也能通,但如果后端加了基于 Host 的重定向逻辑,仅靠代理可能不够,所以 CORS 也要开。

5.2 生产部署:Kestrel 与 Nginx 的分工

生产环境我采用最常见的方案:后端用 Kestrel 直接监听内部端口,前端静态文件交给 Nginx 托管,Nginx 再把/api请求反向代理到后端。

后端发布:

dotnet publish -c Release -o ./publish

在服务器上运行发布后的 dll 即可:

dotnet BookManage.Api.dll --urls http://localhost:5000

前端构建:

npm run build

构建产物在dist目录,把它复制到服务器如/var/www/book-web。Nginx 配置如下:

server { listen 80; server_name book.example.com; location / { root /var/www/book-web; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:5000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }

这里的try_files $uri $uri/ /index.html是必须的。Vue 路由用的createWebHistory模式,前端路由/books/1在刷新时如果 Nginx 找不到对应文件,会直接 404,加上try_files后所有路径都会回退到index.html,由前端路由接管。如果不想处理这个细节,也可以在路由中改用createWebHashHistory,但 URL 会带#,观感稍差。

5.3 上线前值得做的三个细节:主题切换、动态标题、批量操作

基础功能跑通后,有几个小功能能显著提升完成度。实测下来,这三个性价比最高。

第一是暗黑模式主题切换。Element Plus 官方已经内置了暗黑模式的 CSS 变量,只需要在html根节点上加darkclass:

import 'element-plus/theme-chalk/dark/css-vars.css' const isDark = ref(false) watch(isDark, (val) => { document.documentElement.classList.toggle('dark', val) localStorage.setItem('theme', val ? 'dark' : 'light') })

进入页面时从localStorage读取主题偏好,页面刷新后也能保持。登录页和内容页都配上,项目观感会立刻提一个档次。

第二是动态多标签页导航。管理系统经常需要在多个页面来回操作,单页签体验很割裂。实现思路不复杂:在 Pinia 里维护一个tabs数组,路由切换时把路径和标题推入,关闭标签时从数组移除并跳转到最后一个页面。这个功能在全栈项目展示时非常加分,对应热搜里“vue3 修改 tabs 标签页样式”的需求,本质就是维护一个可渲染数组。

第三是图书批量导入导出。如果后台上手工一条条录入图书信息,几十本就够烦躁。可以借助exceljs在前端直接生成 Excel 文件,也可以后端用 NPOI 或 MiniExcel 生成结构化文件。后端导出的优点是可以带上所有字段,前端导出的优点是减少了网络传输。我的选择是批量导入走后端解析 CSV 或 Excel,批量导出走前端exceljs生成,两者分工明确。

最后再分享一点个人体会

把整套项目从无到有跑通之后,我最大的感触是:全栈项目真正的难点不在任何一个单一技术点上,而在三层之间的衔接。数据库字段名改一下,后端所有查询就得跟着变;后端返回结构不统一,前端每个页面都要单独适配;接口路径没有提前约定,联调阶段就会被一次又一次“我这边没问题啊”卡住。所以如果你准备自己动手做这套项目,我强烈建议把顺序反过来:先把数据库建模讨论清楚,再定义ApiResult<T>这类公共规范,最后才开始写业务页面。顺序对了,剩下的工作都是体力活。这套项目做完以后,你对全栈的理解会比单看教程深得多,至少我是这么成长过来的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询