资讯详情

资讯详情

ASP.NET与MySQL打造待办任务系统:从表结构到CRUD完整实践

简介一份基于ASP.NET Core、MySQL与Vue.js构建的待办任务清单系统完整项目资料面向需要课程设计参考或前后端分离实战练习的开发者。资源后端提供RESTful API接口与CRUD操作前端采用Vue.js组件化开发数据库脚本、接口文档、运行截图一应俱全覆盖身份验证、API设计、错误处理、数据库优化等关键知识点。包体共37个文件包含C#后端代码5个cs、Vue前端3个vue及js配置、SQL建表脚本、接口文档、截图png等整体压缩包仅1.83MB结构清晰便于快速检索。目前已有379人学习下载。通过实际操作可完整理解ASP.NET Core依赖注入、中间件、MySQL数据访问、Vue组件通信等环节并可直接基于此二次开发。适合作为毕业设计、课程设计或自学Web全栈的参考项目。1. 一个待办清单而已为什么非要 ASP.NET MySQL 认真做一遍把任务存进数组也能跑通演示可一旦清单每天被真人在用立刻会冒出三个问题刷新丢数据、多人同时勾选状态互相覆盖、想按截止时间排序只能重写逻辑。把存储换成 MySQL再用 ASP.NET 的 MVC 路由和模型绑定包一层 HTTP 入口这套待办任务清单系统的 CRUD 才真正变成可维护的代码而不是一次性脚本。SQLite 在并发写入上偏弱SQL Server 部署成本又高ASP.NET MySQL 是中小团队和内部自建工具里最顺手的组合Windows 上一套安装包、一个连接串就能跑。下文从表结构设计开始一路做到 CRUD、筛选分页和部署备份新手能照着搭完熟手可以重点看索引和乐观锁那两节。2. 待办任务系统的数据模型与 MySQL 表结构设计2.1 引擎、字符集与排序规则建表前先定这三件事MySQL 里同一份建表语句可以落在不同引擎上。InnoDB 支持事务、行级锁和外键待办状态切换本质是一行数据的并发更新行级锁能避免两条请求同时改同一条记录时互相踩踏MyISAM 用表级锁在写多读少的任务表上会整体放大延迟。排序规则选 utf8mb4_unicode_ci任务标题里可能出现 emoji、中文和日文假名混排utf8mb3别名 utf8存不了 4 字节字符而 unicode_ci 对日常查询的相等判断和排序足够用。MySQL 8.0 默认字符集已经是 utf8mb4但显式写在 DDL 里能让团队里每个人都不依赖服务器默认值。2.2 tasks 表 DDL状态、优先级、截止时间都该用什么类型CREATE DATABASE IF NOT EXISTS todo_app DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; USE todo_app; CREATE TABLE IF NOT EXISTS tasks ( id INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键, title VARCHAR(200) NOT NULL COMMENT 任务标题, description TEXT NULL COMMENT 详细描述, status TINYINT NOT NULL DEFAULT 0 COMMENT 0-待办 1-已完成 2-已取消, priority TINYINT NOT NULL DEFAULT 1 COMMENT 0-低 1-普通 2-高, due_date DATETIME NULL COMMENT 截止时间, 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), KEY idx_status_due (status, due_date), KEY idx_priority (priority) ) ENGINEInnoDB COMMENT待办任务表;status用 TINYINT 而不是 VARCHAR状态机只有三四个取值数字占 1 字节相等判断和排序都比字符串快也不会出现todo和TODO两种写法并存的问题。priority同理0/1/2 映射低/普通/高显示层的文案交给 ASP.NET 视图去翻译。due_date允许 NULL因为不是每个任务都有截止时间把它强行设成默认值反而会让查询里多出一堆IS NOT NULL判断。两个索引是按真实查询习惯建的。最常跑的是「列出所有未完成任务并按截止时间升序」对应WHERE status0 ORDER BY due_date联合索引idx_status_due能同时覆盖过滤和排序单独按优先级过滤的场景少但建一个轻量索引成本也低。注意联合索引的最左前缀原则哪天想按due_date单独查这个联合索引帮不上忙得再建due_date单列索引。下面这张映射表是后面所有 CRUD 代码的共同约定字段取值含义ASP.NET 侧映射status0 / 1 / 2待办 / 已完成 / 已取消下拉框选中值priority0 / 1 / 2低 / 普通 / 高排序权重due_dateNULL 或时间无截止时间或截止时刻DateTime?可空类型建表脚本可以直接在 MySQL Workbench 里执行也可以用图形界面建完表后右键复制 CREATE 语句留档。Workbench 的 SQL Editor 同时能跑建库和后续的权限语句比命令行一个个敲更直观。2.3 别把业务规则压进数据库存储过程在这里不是首选待办系统的状态流转只有「待办→已完成」「待办→已取消」两条用应用层的一条 UPDATE 加参数就能表达没必要引入存储过程。存储过程适合多个系统共用、需要把复杂事务封装在数据库内部的项目单应用场景里存储过程反而让调试链变长C# 侧报错、MySQL 日志、权限配置三处要对齐。索引也不是越多越好每个索引都会拖慢写入tasks 表在十万行以内时上面两个索引已经足够不要为每个字段单独加索引。3. ASP.NET 连接 MySQL 与任务 CRUD 的最小实现3.1 连接字符串与专用账号先别急着用 root装完 MySQL 8.0 之后第一件事不是写代码而是建一个应用专用账号。用 root 连接会让权限边界完全消失一旦代码里出现注入点攻击者能直接删库。在 Workbench 或命令行里执行CREATE USER todo_app% IDENTIFIED BY ChangeMe_2024; GRANT SELECT, INSERT, UPDATE, DELETE ON todo_app.* TO todo_app%; FLUSH PRIVILEGES;账号只授 DML 权限没有 DDL 和 FILE 权限所以即使 SQL 写错也破坏不了表结构。连接字符串放在 appsettings.json 的 ConnectionStrings 节点{ ConnectionStrings: { TodoDb: Server127.0.0.1;Port3306;Databasetodo_app;User Idtodo_app;PasswordChangeMe_2024;Charsetutf8mb4;SslModePreferred } }这里特意用127.0.0.1而不是localhost在 Linux 上 localhost 可能走 unix socket而 ASP.NET 里的 MySQL 连接库默认走 TCP两者指的不是同一条通道排查时容易绕圈子。Charsetutf8mb4保证从客户端到服务端全程用四字节字符集写入中文或 emoji 不乱码。MySQL 8.0 默认认证插件是caching_sha2_password新版 MySqlConnector 原生支持如果被迫用旧驱动连接串里需要加AllowPublicKeyRetrievalTrue并且 SslMode 不能是 Disabled。下面这张表是连接串参数的速查参数示例值说明Server127.0.0.1走 TCP避免 unix socket 混淆Port3306MySQL 默认端口Charsetutf8mb4连接层字符集和表字符集一致SslModePreferred8.0 默认要求安全连接优先协商3.2 Program.cs 注册服务连接按请求创建比全局复用可靠ASP.NET 里操作 MySQL 有两个常用驱动Oracle 官方的 MySql.Data 和社区维护的 MySqlConnector。MySqlConnector 对异步支持、连接缓存和 .NET 的兼容性更省心下面按它来写。在 Program.cs 里把连接注册成作用域服务var builder WebApplication.CreateBuilder(args); builder.Services.AddControllersWithViews(); var connStr builder.Configuration.GetConnectionString(TodoDb); builder.Services.AddScopedMySqlConnection(_ new MySqlConnection(connStr)); var app builder.Build(); app.MapControllerRoute( name: default, pattern: {controllerTask}/{actionIndex}/{id?}); app.Run();AddScoped意味着每个 HTTP 请求拿到一个新的 MySqlConnection请求结束由容器释放连接并归还连接池。不要注册成单例MySqlConnection 不是线程安全的多个请求同时打开同一个连接会互相污染会话状态出现数据串号。路由模板里的{controllerTask}把默认控制器指向 TaskController访问根路径/时直接落到待办列表页。常见误用是在 Controller 里手动new一个连接再 Open忘了 Dispose连接池占满后报Too many connections请求作用域注册配合仓储层的await using才能保证连接一定归还。3.3 参数化增删改写入类 SQL 最容易翻车的一步新增和更新都必须用参数化 SQL而不是字符串拼接。看一个新增任务的仓储方法public class TaskRepository { private readonly MySqlConnection _conn; public TaskRepository(MySqlConnection conn) _conn conn; public async Task CreateAsync(string title, string description, DateTime? dueDate) { const string sql INSERT INTO tasks (title, description, status, priority, due_date) VALUES (title, description, 0, 1, dueDate); await using var cmd new MySqlCommand(sql, _conn); cmd.Parameters.AddWithValue(title, title); cmd.Parameters.AddWithValue(description, (object?)description ?? DBNull.Value); cmd.Parameters.AddWithValue(dueDate, (object?)dueDate ?? DBNull.Value); await _conn.OpenAsync(); await cmd.ExecuteNonQueryAsync(); } }AddWithValue传 null 时必须先转成DBNull.Value否则 MySQL 端收到的是 C# 的 null字段写入直接报错。description和due_date都允许为空所以这两行特意做了空值转换。SQL 里的status0, priority1是新增任务的固定初始值如果业务允许建任务时指定优先级把priority也改成参数传入即可。更新已完成状态时最常见的错误是「先 SELECT 查状态再根据结果决定更新哪条」。两个人同时操作时后读到的一方拿到的是过期状态。改成一条原子更新public async Taskbool ToggleAsync(int id, byte currentStatus) { const string sql UPDATE tasks SET status 1 WHERE id id AND status currentStatus; await using var cmd new MySqlCommand(sql, _conn); cmd.Parameters.AddWithValue(id, id); cmd.Parameters.AddWithValue(currentStatus, currentStatus); await _conn.OpenAsync(); var affected await cmd.ExecuteNonQueryAsync(); return affected 0; }WHERE 里带上当前状态做乐观锁受影响行数为 0 就说明状态已被别人改过前端提示刷新即可。这样既不会丢更新也不需要手动LOCK TABLE或SELECT ... FOR UPDATEInnoDB 的行级锁在这里把并发写串行化不会出现两个请求同时把状态改到同一结果的问题。4. MVC 路由、筛选与分页让待办列表真正可用的关键配置4.1 路由与模型绑定URL 到 Action 参数是怎么对上的ASP.NET MVC 收到请求后先按路由模板解析控制器和动作名再把 URL 里的查询字符串和表单字段绑定到 Action 参数上。上一章的路由模板{controllerTask}/{actionIndex}/{id?}意味着/Task/Edit/3命中 TaskController 的 Edit 方法并传入id3/Task/Index?status1把 status 作为查询参数交给 Index 方法。理解这一层后续写筛选分页才不会把参数散落到 ViewData 里手工搬运。下面这张表是这套系统里最常用的三组路由对应关系URL 示例命中的动作绑定结果/TaskController.Index走默认 controllerTask/Task/Index?status1TaskController.Indexstatus1/Task/ToggleTaskController.Toggle表单字段 id、currentStatus4.2 Index Action状态、关键字、分页三个参数的接法与 SQL 拼装public async TaskIActionResult Index(int status -1, string keyword , int page 1) { const int pageSize 10; var tasks await _repo.QueryAsync(status, keyword, page, pageSize); ViewData[CurrentStatus] status; ViewData[CurrentKeyword] keyword; ViewData[CurrentPage] page; return View(tasks); }status默认 -1 表示「全部」避免 0 被当作「未选中」keyword默认空字符串。仓储层的 SQL 是动态拼装的但拼的只是固定的 WHERE 片段值全部走参数public async TaskListTaskItem QueryAsync(int status, string keyword, int page, int pageSize) { var sql new StringBuilder(SELECT * FROM tasks WHERE 11); var ps new ListMySqlParameter(); if (status 0) { sql.Append( AND status status); ps.Add(new MySqlParameter(status, status)); } if (!string.IsNullOrEmpty(keyword)) { sql.Append( AND title LIKE CONCAT(%, keyword, %)); ps.Add(new MySqlParameter(keyword, keyword)); } sql.Append( ORDER BY status ASC, due_date ASC LIMIT offset, pageSize); ps.Add(new MySqlParameter(offset, (page - 1) * pageSize)); ps.Add(new MySqlParameter(pageSize, pageSize)); await using var cmd new MySqlCommand(sql.ToString(), _conn); cmd.Parameters.AddRange(ps.ToArray()); await _conn.OpenAsync(); var result new ListTaskItem(); await using var reader await cmd.ExecuteReaderAsync(); while (await reader.ReadAsync()) { result.Add(new TaskItem { Id reader.GetInt32(id), Title reader.GetString(title), Status reader.GetByte(status), DueDate reader.IsDBNull(reader.GetOrdinal(due_date)) ? null : reader.GetDateTime(due_date) }); } return result; }两个细节值得说明。LIKE 用CONCAT(%, keyword, %)而不是把%直接拼进参数参数里不嵌通配符同一份值以后还能用于精确匹配LIMIT的 offset 在 C# 侧算好再传参避免在 SQL 里做乘法。标题的模糊查询对 tasks 这种小表够用等数据量上去再考虑全文索引——以%开头的 LIKE 无论怎样都用不上普通索引这点要心里有数。4.3 视图层的复选框与分页链接一次状态变更对应一次 POST视图里状态筛选用 GET 表单勾选完成用 POST 表单。GET 表单让筛选条件能收藏、能刷新而不弹「确认重新提交」POST 用于改数据是 HTTP 语义的基本要求浏览器也不会把 POST 地址误缓存。Razor 里两个表单的示意如下form methodget action/Task/Index select namestatus option value-1全部/option option value0 ((int)ViewData[CurrentStatus] 0 ? selected : null)待办/option option value1 ((int)ViewData[CurrentStatus] 1 ? selected : null)已完成/option /select input typetext namekeyword valueViewData[CurrentKeyword] / button typesubmit筛选/button /form foreach (var t in Model) { form methodpost action/Task/Toggle input typehidden nameid valuet.Id / input typehidden namecurrentStatus valuet.Status / button typesubmit勾选/button spant.Title/span /form }(bool ? selected : null)是 Razor 里条件渲染属性的标准写法条件为假时不输出该属性。每个任务一行一个独立表单勾选提交后走 Toggle 的乐观锁更新更新失败时受影响行数为 0前端再提示刷新。分页链接必须把当前筛选参数全部带上if ((int)ViewData[CurrentPage] 1) { a href/Task/Index?statusViewData[CurrentStatus]keywordViewData[CurrentKeyword]page((int)ViewData[CurrentPage]-1)上一页/a }分页和筛选参数同时传递否则切页后筛选条件丢失——这是待办列表最常见的体验坑。5. 交付前的四项检查字符集、备份 bat、索引与并发5.1 连不上库的两个常见报错Linux 上报error 2002 (HY000): cant connect to local MySQL server through socket先确认 MySQL 服务是否在运行systemctl status mysql再检查连接字符串里是否写了 localhost——它触发 unix socket 路径ASP.NET 应用应写127.0.0.1走 TCP。报 2059 认证失败时优先升级 MySqlConnector 驱动短期内想绕开可以执行ALTER USER todo_app% IDENTIFIED WITH mysql_native_password BY ...但后续 MySQL 版本对旧认证插件的支持会持续收紧不是长久方案。5.2 bat 每日自动备份Windows 上最常用的方案是计划任务调用 mysqldumpecho off set ts%date:~0,4%%date:~5,2%%date:~8,2% mysqldump -utodo_app -pChangeMe_2024 --databases todo_app --default-character-setutf8mb4 D:\backup\todo_%ts%.sql forfiles /P D:\backup /M *.sql /D -30 /C cmd /c del path 2nul--databases会把 CREATE DATABASE 也写进备份文件恢复时直接mysql -uroot -p todo_20241001.sql就完成建库加导入。forfiles只保留 30 天内的备份防止磁盘被积压文件占满。bat 里明文写密码在内部服务器可接受更稳妥的做法是把凭据放进 my.cnf 的[client]段命令行只写mysqldump --defaults-extra-fileC:\backup\my.cnf ...。5.3 四项自查顺序插入一条含 emoji 的标题看落库是否乱码开两个浏览器同时勾选同一任务确认只有一个请求生效——乐观锁直接影响这一条对最常跑的筛选语句执行EXPLAIN SELECT * FROM tasks WHERE status0 ORDER BY due_date确认 key 列出现idx_status_due最后把备份恢复到todo_restore_test库比对行数一致后删掉临时库这套备份方案才能在真正出事故那天放心使用。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →