资讯动态

ASP.NET Core值班管理系统实战:排班换班与交接记录设计

发布时间:2026/10/6 3:40:56 来源:尧图企业网站定制
简介这套基于ASP.NET的值班管理系统源码面向需要快速搭建内部值班排班、交接班、人员与部门管理的开发者或中小机构采用ASPX页面配合Access数据库并涉及SQL Server数据文件适合具备一定.NET基础的学习者参考。压缩包共77个文件包含31个aspx页面、JS与CSS前端资源、6个DLL程序集、2个ascx用户控件以及mdb数据库、mdf/ldf日志文件等整体约8.39MB目录按页面、脚本、样式、数据库等模块划分便于定位。已有491人学习下载适合课程设计或内部系统起步参考。资源中可看到登录认证、用户管理、值班列表、历史记录、部门与班次维护、Ajax部门管理等典型功能页面配合说明.txt与Web.config能较完整地展示ASP.NET Web Forms项目的分层结构、用户控件复用方式、Ajax局部刷新以及多数据库访问思路利于二次开发与功能扩展。开发环境为Windows Server 2003 SP2与Visual Studio 2010数据库使用Access 2010对旧版.NET项目维护也有参考价值。1. 别再让Excel替你排班ASP.NET值班管理系统到底解决什么问题很多单位安排值班还停留在“群里发Excel模板”的阶段谁临时有事就在群里喊换班月底统计值班学时Excel早就被人手动改过好几轮对不上账是常态。ASP.NET值班管理系统就是把这块最容易扯皮的内部事务搬进Web应用排班、换班、交接记录、学时统计都在一套流程里完成每一步操作都留痕。这个标题听起来像毕业设计但真做过这类系统的同事应该认同难点不在生成轮转表的算法而在换班和交接这个环节怎么让人愿意把“账”交到系统里。适合动手的人主要有两类一类是手上有老ASP.NET排班代码、想盘活或迁移的维护者另一类是打算用ASP.NET Core MVC给所在部门做内部小系统的开发者。不用急着堆功能先把登录、排班、换班、交接四个环节跑通这个系统就成了。2. 开工前定两件事ASP.NET Core版本怎么选值班表数据模型怎么建做这种内部系统最容易犯的错是一上来就写页面。排班页面看着简单但数据模型改起来特别伤筋动骨。我建议动手前先把两个朴素的问题定下来第一项目跑在哪个ASP.NET版本上第二值班数据到底怎么组织。这两件事定了后面写起来才不用返工。2.1 版本选型老框架、ASP.NET Core MVC以及你们服务器的真实状况老的ASP.NET系统指的是跑在.NET Framework上的Web Forms或ASP.NET MVC 5这类应用它们通常依赖Windows和IIS。ASP.NET Core则是从.NET 5开始统一后的技术栈拥有跨平台能力也支持Docker部署。如果单位服务器上已经有一套跑了好几年的值班排班系统业务逻辑全是Web Forms里的事件驱动写法那“推倒重来上Core”和“在原框架上接着加功能”是两种完全不同的成本。我一般用下面这张对照表帮自己做判断判断维度ASP.NET FrameworkMVC5 / Web FormsASP.NET Core MVC运行环境Windows IIS部署简单直接Windows / Linux / 容器均可依赖注入需要引入Unity等第三方容器内置IServiceCollection开箱即用单元测试可测但HttpContext等对象难mock抽象更好测试相对顺手适合场景老系统维护、不想动基础设施新项目、长期维护、考虑Docker学习成本资料老新人不一定能看懂社区活跃模板和示例多每换一批.NET大版本Core项目就要面临升级所以新项目尽量选LTS长期支持版本别用STS过渡版。另一条经验是如果老系统里只有一个人能维护业务报表又特别多那暂时别急着迁移。把一套能跑的系统从旧框架迁到Core不是把代码搬一遍那么简单页面控件、过滤器、配置文件、第三方包的替换都是工作量。硬切过去只会把一个能用的系统变成两个不能用的系统这不是技术能力问题是项目管理的判断问题。反过来如果团队本来就在学新框架服务器也想迁到Docker那直接选ASP.NET Core MVC是更合适的起点。2.2 数据模型几张表管住排班、换班和交接值班数据的核心不是“排班表”一张表而是人员、班次、排班、换班单、交接记录五个部分。我用表格把职责理一下避免写着写着把换班记录塞进排班表里表名职责关键字段Users值班人员与角色Account、Name、Role、EnabledShifts班次定义StartTime、EndTime、IsCrossDayDutySchedule某天谁值什么班UserId、ShiftDate、ShiftId、StatusShiftChange换班申请与审批ScheduleId、FromUserId、ToUserId、StatusHandoverLog交接班记录ScheduleId、Content、CreatedAt这里最容易被忽略的是Shifts表里的IsCrossDay。值班系统里常有一组班是下午到第二天上午的如果只用StartTime和EndTime跨天班次怎么算时长会变成一团乱麻。加一个IsCrossDay字段配合DurationHours做小时数报表统计就不会把半夜的班算到错误日期上。建表时有一组约束需要注意DutySchedule用SQL Server建参考下面这个样子CREATE TABLE dbo.DutySchedule ( Id INT IDENTITY PRIMARY KEY, UserId INT NOT NULL REFERENCES dbo.Users(Id), ShiftDate DATE NOT NULL, ShiftId INT NOT NULL REFERENCES dbo.Shifts(Id), Status TINYINT NOT NULL DEFAULT 0, Remark NVARCHAR(200) NULL, RowVersion ROWVERSION, UNIQUE (ShiftDate, ShiftId, UserId) );这段DDL里Status用TINYINT存状态码对应关系是0草稿、1已确认、2已换班、3已取消。用数字存状态而不是直接存“已确认”三个字是为了报表统计时不出现中文大小写和别字问题。UNIQUE约束放在ShiftDate、ShiftId、UserId三列上防止同一个人在同一个班次同一天被插入两次。如果你们的业务一天只有一个班次可以在代码层保证每天只生成一条数据库这个唯一组合能兜底重复生成的脏数据。用EF Core写实体时我会把Status映射成枚举public class DutySchedule { public int Id { get; set; } public int UserId { get; set; } public DateTime ShiftDate { get; set; } public int ShiftId { get; set; } public DutyStatus Status { get; set; } public string? Remark { get; set; } public byte[] RowVersion { get; set; } []; public User User { get; set; } null!; } public enum DutyStatus { Draft 0, Confirmed 1, Swapped 2, Canceled 3 }这段实体代码里有几个参数值得说清楚。ShiftDate用DateTime而不是string是为了在LINQ里直接比较大小按周查、按月查都省去字符串转换。RowVersion类型在EF Core里会映射成SQL Server的rowversion列这是后面处理并发换班的关键第5章会细讲。User导航属性可以留着方便连表查询时直接取人名但返回接口时不要直接把实体序列化给前端否则会出现循环引用。换班记录不直接改排班表而是先走ShiftChange这种“单据”审批通过后更新DutySchedule.UserId。这是我在内部系统里比较推荐的写法值班表上“谁值班”这个核心字段不要被到处直接修改所有变化都从单据衍生出了问题能追溯。建表时给CreatedAt默认值用SYSUTCDATETIME()不要用GETDATE()服务器时区设置不统一的时候UTC时间至少能保证全球范围内的记录先后关系不混乱。3. 用ASP.NET Core MVC跑通排班核心建项目、生成月度表、按周查询新项目我偏向直接走ASP.NET Core MVC而不是Core Web API配一个前端框架。值班系统页面多、交互不复杂Razor服务端渲染足够要做得复杂再局部加Vue或React也不迟。下面按一套最小又完整的路径走一遍核心是把排班生成和查询落地。3.1 用dotnet CLI建项目并还原EF Core包如果你还没装好模板用Visual Studio建“ASP.NET Core Web应用模型-视图-控制器”也一样。命令行方式更直接dotnet new mvc -n DutySystem cd DutySystem dotnet add package Microsoft.EntityFrameworkCore.SqlServer dotnet add package Microsoft.EntityFrameworkCore.Design命令里dotnet new mvc生成的标准模板包含Controllers、Views和Program.cs。EF Core的两个包分别是SqlServer驱动和设计时工具设计时包用于执行Add-Migration等迁移命令。如果你们准备直接用SQL脚本建表不跑EF迁移第二个包可以省掉。建完项目后打开appsettings.json把连接字符串和排班配置放进去{ ConnectionStrings: { DutyDb: Serverlocalhost;DatabaseDutySystem;User Idsa;PasswordYourPassword;TrustServerCertificateTrue; }, Duty: { IncludeWeekend: true } }这里有一个参数特别提醒TrustServerCertificateTrue只适合开发环境连接测试库时用生产环境证书应该由服务器正式配置否则等于把加密链路的安全检查关掉了。IncludeWeekend是给排班生成算法用的设置为true代表周末也排false则跳过周六日。3.2 排班生成算法先算月天数再按人员顺序落稿排班逻辑不要直接写在Controller里我习惯单独建一个DutyService。Controller只负责接收请求、返回视图业务计算放到服务层后面写单元测试会舒服很多。以单班制为例生成一整个月排班的核心循环是这样public class DutyService { private readonly DutyDbContext _db; private readonly IConfiguration _config; public DutyService(DutyDbContext db, IConfiguration config) { _db db; _config config; } public int GenerateMonth(int year, int month) { var users _db.Users .Where(u u.Enabled) .OrderBy(u u.Id) .ToList(); if (users.Count 0) return 0; var daysInMonth DateTime.DaysInMonth(year, month); var firstDay new DateTime(year, month, 1); var includeWeekend _config.GetValuebool(Duty:IncludeWeekend); var index 0; for (var day 1; day daysInMonth; day) { var date firstDay.AddDays(day - 1); if (!includeWeekend (date.DayOfWeek DayOfWeek.Saturday || date.DayOfWeek DayOfWeek.Sunday)) { continue; } var exists _db.Schedules.Any(s s.ShiftDate date); if (exists) continue; var user users[index % users.Count]; _db.Schedules.Add(new DutySchedule { ShiftDate date, UserId user.Id, ShiftId 1, Status DutyStatus.Draft, Remark 系统生成 }); index; } _db.SaveChanges(); return index; } }这段代码有三个参数含义要看清。DateTime.DaysInMonth(year, month)直接告诉这个月有几天比“从1号AddDays到31号”安全得多2月不会多排30天月份也不会少排。index % users.Count实现轮流分配index并不是Day而是“已经排出去的班次计数”跳过的周末不会让人员顺序错位。exists判断是幂等保护重复点击“生成排班”按钮不会重复插入数据。如果哪天需要支持“一天多个班次”把Add操作包在一个班次循环里再根据ShiftId生成多条记录。不过内部系统不建议一上来就做多班次先跑通单班模式多班次作为后续扩展会少很多返工。3.3 按周查询“这周谁值班”周一计算别数错排班页面默认展示本周排班这比让用户选“第几周”更符合直觉。Controller里这样写public IActionResult Week(int? year, int? week) { var today DateTime.Now.Date; var daysSinceMonday ((int)today.DayOfWeek 6) % 7; var monday today.AddDays(-daysSinceMonday); var rangeStart monday; var rangeEnd monday.AddDays(7); if (year.HasValue week.HasValue) { rangeStart GetMondayOfYear(year.Value, week.Value); rangeEnd rangeStart.AddDays(7); } var items _db.Schedules .Include(s s.User) .Where(s s.ShiftDate rangeStart s.ShiftDate rangeEnd) .OrderBy(s s.ShiftDate) .ToList(); ViewBag.Start rangeStart.ToString(yyyy-MM-dd); ViewBag.End rangeEnd.AddDays(-1).ToString(yyyy-MM-dd); return View(items); } private static DateTime GetMondayOfYear(int year, int week) { var jan1 new DateTime(year, 1, 1); var daysUntilMonday ((int)DayOfWeek.Monday - (int)jan1.DayOfWeek 7) % 7; var firstMonday jan1.AddDays(daysUntilMonday); return firstMonday.AddDays((week - 1) * 7); }这里计算周一的逻辑是防止“跨周显示错位”的关键。C#里DayOfWeek的Sunday值是0直接用today.DayOfWeek做偏移会把周一算成周二所以取((int)today.DayOfWeek 6) % 7结果是周一为0、周日为6减掉这个偏移量就回到本周一。GetMondayOfYear则是把1月1日推到最近的周一再按周序号加七天但要注意这个算法不是严格ISO周标准如果12月31日落在下一年的第1周查询会显示空白内部系统多数情况不在意这个细节。查询数据时用 rangeStart rangeEnd这样查询天然包含边界不会漏掉晚上0点的记录。ViewBag里的Start和End是给页面顶部的日期区间显示用的我习惯把End减一天因为rangeEnd本身是下周一直接显示会让人觉得多了一天。4. 登录、换班与交接记录让值班系统真正被用起来排班页面能做出来只是第一步内网系统真正决定死活的是权限和交接流程。登录做不好大家觉得不安全换班做不顺大家宁可在群里喊交接记录不做出了问题没人认账。所以这一章把落地闭环里最关键的三个动作写透。4.1 登录用Cookie认证代替手写Session值班系统这种内部应用不需要上复杂的Identity框架Cookie认证配合用户表就够了。在Program.cs里注册服务builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(options { options.LoginPath /Account/Login; options.AccessDeniedPath /Account/Denied; options.ExpireTimeSpan TimeSpan.FromHours(8); options.SlidingExpiration true; }); builder.Services.AddAuthorization(); var app builder.Build(); app.UseAuthentication(); app.UseAuthorization();这段配置里ExpireTimeSpan是登录有效期8小时SlidingExpiration设为true表示用户只要有操作过期时间就自动顺延防止值班人员填个交接记录填到一半就被踢下线。LoginPath和AccessDeniedPath分别处理未登录和没权限的跳转。登录Action不要只校验账号密码还要把用户角色放进Claim[HttpPost] [ValidateAntiForgeryToken] public async TaskIActionResult Login(LoginViewModel model) { var user _db.Users .FirstOrDefault(u u.Account model.Account u.Enabled); if (user null || !PasswordHasher.Verify(model.Password, user.PasswordHash, user.PasswordSalt)) { ModelState.AddModelError(, 账号或密码错误请重新输入); return View(model); } var claims new ListClaim { new Claim(ClaimTypes.Name, user.Id.ToString()), new Claim(ClaimTypes.NameIdentifier, user.Name), new Claim(ClaimTypes.Role, user.Role 1 ? Admin : Member) }; var identity new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); await HttpContext.SignInAsync( CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(identity)); return RedirectToAction(Week, Duty); }这里我把用户Id放进Name claims把姓名放进NameIdentifier纯粹是个人习惯你反过来放也行但一定要全项目统一。后面过滤器里取“当前操作人是谁”时都从同一个claims键里读不统一就会踩坑。密码校验用PasswordHasher.Verify内部系统也别存明文密码用Rfc2898DeriveBytes做PBKDF2迭代至少十万次或者直接用BCrypt库安全底线不能省。4.2 换班把班次做成状态机不要直接改人换班是值班系统里纠纷最多的地方。我的做法是给DutySchedule加Status字段只有状态为Confirmed的班次才允许换。换班时先查状态再更新更新过程要处理并发冲突[HttpPost] [Authorize(Roles Admin)] [ValidateAntiForgeryToken] public async TaskIActionResult Swap(int scheduleId, int targetUserId) { var schedule await _db.Schedules .FirstOrDefaultAsync(s s.Id scheduleId s.Status DutyStatus.Confirmed); if (schedule null) { TempData[Error] 该班次不存在或当前状态不能换班; return RedirectToAction(Week, Duty); } schedule.UserId targetUserId; schedule.Status DutyStatus.Swapped; schedule.Remark ${DateTime.Now:yyyy-MM-dd HH:mm} 从原值班人换班给用户#{targetUserId}; try { await _db.SaveChangesAsync(); TempData[Success] 换班完成; } catch (DbUpdateConcurrencyException) { TempData[Error] 该班次刚被其他人修改请刷新后重试; } return RedirectToAction(Week, Duty); }这里的状态机设计是Draft草稿不能直接换必须确认成Confirmed换过一次后变成Swapped不能再被换第二次Canceled是取消单。[Authorize(Roles Admin)]表示这一步只能管理员操作如果你们单位是值班人自己协商好再找管理员换班权限这样分是合理的。换班完成后把原值班人、目标人、时间写进Remark这行备注就是后来对账时最直接的证据。4.3 统一操作留痕用ActionFilter给整个控制器记账值班系统里比排班算法更重要的是“谁在什么时候改过谁”。如果在每个Action里手写日志代码会迅速失控而且容易漏。ASP.NET Core的过滤器机制正好用来做这件事我把写日志的逻辑收敛到一个全局过滤器里using Microsoft.AspNetCore.Mvc.Filters; public class DutyLogFilter : IAsyncActionFilter { private readonly ILoggerDutyLogFilter _logger; public DutyLogFilter(ILoggerDutyLogFilter logger) { _logger logger; } public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { var user context.HttpContext.User.Identity?.Name ?? 匿名; var path context.HttpContext.Request.Path; var method context.HttpContext.Request.Method; var startedAt DateTime.UtcNow; var result await next(); var ms (DateTime.UtcNow - startedAt).TotalMilliseconds; if (result.Exception ! null) { _logger.LogError(用户 {User} 执行 {Method} {Path} 失败{Error}, user, method, path, result.Exception.Message); } else { _logger.LogInformation(用户 {User} 执行 {Method} {Path} 成功耗时 {Ms} 毫秒, user, method, path, ms); } } }这段过滤器是在Action执行前后各插入一段逻辑。await next()之前的代码在Action进入前运行之后的代码在Action返回后运行。result.Exception用来判断是否抛了异常能抓到异常日志就能少跑很多次服务器日志。注册时要在Program.cs里把它加到全局过滤器builder.Services.AddScopedDutyLogFilter(); builder.Services.AddControllersWithViews(options { options.Filters.AddDutyLogFilter(); });为什么用IAsyncActionFilter而不是直接继承ActionFilterAttribute老ASP.NET Web API时代System.Web.Http.Filters.ActionFilterAttribute是常见的写法现在老代码里还能看到这串全名。但ASP.NET Core里命名空间改成了Microsoft.AspNetCore.Mvc.Filters而且Attribute过滤器如果用特性方式挂载构造函数里没法用依赖注入。全局过滤器注册成Scoped既能拿ILogger又能让每个请求拿到独立的实例。过滤器上线后登录Action的日志也会被记录日志里会出现明文密码吗不会因为Request.Path里不包含请求体但你会看到大量登录成功失败记录。如果你觉得吵可以往过滤器里加一个路径白名单把Account相关的健康检查请求过滤掉其他业务接口全部留痕。5. 值班管理系统避坑指南日期边界、并发换班、浏览器缓存与序列化这一章写一下我实际踩过的和看同事踩过的坑。里面的现象都很典型背后的原因绕开这几个设计点基本都能规避。每一条按“现象、原因、解决”的方式来写方便你直接对照检查。5.1 生成排班时按“31天”想当然2月和30天月份连续翻车现象2月生成出来29天的排班实际12月31日被排到1月里或者某个30天的月份少排了最后一天。原因代码里写死for (int i 0; i 31; i)再用DateTime.Now.AddDays(i)往日期上累加。这样2月会排到3月去4月则会跳过30号之后强行补一天。解决无论哪个年月先用DateTime.DaysInMonth(year, month)取真实天数。更稳妥的做法是把循环上界改成下个月1号的前一天这样跨年都不会错。写排班生成算法时把“月份长度”当成黑匣子不要自己算。5.2 两个管理员同时换班同一张排班被改了两次现象两个管理员同时操作一个把班换给小张另一个换给小李最终显示的是最后一次提交的人且没人知道有人换了两遍。原因第4章那个Swap代码先查一条Confirmed状态的记录再改UserId两个请求同时通过FirstOrDefaultAsync都能查到同一条数据最后后提交的覆盖先提交的。这叫第一类丢失更新本质是并发写没有冲突检测。解决给DutySchedule表加rowversion列实体里的RowVersion字段用Fluent API标记为IsRowVersionmodelBuilder.EntityDutySchedule() .Property(x x.RowVersion) .IsRowVersion();这样EF Core在SaveChanges时会自动带上WHERE RowVersion original如果另一个请求已经改动过这行更新影响行数为0EF抛DbUpdateConcurrencyException。Swap方法里已经catch了这个异常返回“请刷新后重试”。这是对付并发换班最省事的方案不需要自己加锁。5.3 发布新版本后页面还是旧的根源在静态文件没指纹现象代码改了CSS或JS重新发布到服务器后用户浏览器里还是旧样式强制刷新才正常。原因浏览器缓存了/css/duty.css这类静态文件文件名没变浏览器认为缓存还有效就不会重新下载。解决Razor里引用静态资源时加上asp-append-version标签助手link relstylesheet asp-append-versiontrue href~/css/duty.css /这个标签助手会在文件路径后自动追加版本查询参数文件内容变了版本参数就变浏览器就会重新拉取。发布时如果发现用户端还是旧样式先让他们硬刷新一次后面加了版本参数就不会反复出这个问题。5.4 服务器时间用了UTC交班记录凌晨串天现象值班人在0点5分交班交接记录却归到了前一天第二天对账时双方互相说不清。原因数据库和Web服务器可能不在一个时区代码里有的地方用DateTime.Now有的地方用DateTime.UtcNow系统里混了两套时间源。解决在系统里定义一个统一的时间帮助类所有业务代码只从这个入口取时间public static class DutyTime { public static DateTime Now TimeZoneInfo.ConvertTimeBySystemTimeZoneId(DateTime.UtcNow, China Standard Time); }这里“China Standard Time”是Windows上的时区Id换成Linux容器跑时是Asia/Shanghai更通用的做法是配置一下容器的TZ环境变量让整个进程的本地时间统一到东八区。关键是不要让业务代码各处散着写DateTime.Now管理好这一个入口凌晨交接串天的问题就没了根。5.5 返回JSON时提示“检测到循环引用”接口直接500现象写了一个排班查询接口直接返回_db.Schedules.ToList()浏览器里报A circular reference was detected页面白屏。原因DutySchedule上有User导航属性User如果也有Schedules集合属性序列化器从一根线能绕回原点Newtonsoft.Json或System.Text.Json默认都不允许循环引用。解决不要直接把EF实体返回给前端。写一个DTO只带当前展示需要的字段public class DutyScheduleDto { public int Id { get; set; } public DateTime ShiftDate { get; set; } public string UserName { get; set; } ; public string StatusText { get; set; } ; }查询时用Select投影var items _db.Schedules .Where(s s.ShiftDate start s.ShiftDate end) .Select(s new DutyScheduleDto { Id s.Id, ShiftDate s.ShiftDate, UserName s.User.Name, StatusText s.Status.ToString() }) .ToList();这样既不会循环引用也不会把整张User表数据泄露给前端响应体还变小。凡是“排班接单页要显示人名”的查询都建议写DTO而不是复用实体。6. 老系统迁到ASP.NET Core时建议按这个顺序先做这三处改造如果你手上正是一个老ASP.NET值班系统我的建议是不要从Controller一个个抄页面而是先把三个底层改造做完。第一处把操作日志从“散落各处”收敛成全局ActionFilter。老系统里常见的写法是对着数据库表直接UPDATE没有日志。迁移时先加一个全局过滤器把请求路径、操作用户、时间统一记到一张Log表里后面做换班对账、交接追溯才有底。这一步花的时间最少价值最大。第二处把排班状态从中文文本字段改成枚举加状态机。老系统可能用varchar字段存“正常”“已换班”“已取消”这种设计在报表统计时特别容易出问题因为同一个人在不同页面可能手输“已换班”和“已换班。”。迁移时把旧值做一次映射枚举值从0开始编号入库程序里再不用字符串去比较状态。这属于数据整理的活越早做越省心否则后面每个页面都要处理历史脏数据。第三处把首页改成“本周排班”视图而不是月初一张表。老系统的通病是一打开就是整月排班大表格手机上看起来很费劲。我一般用Razor直接渲染周一到周日七列每列显示当天值班人姓名和班次顶部放上一周、下一周两个按钮。这个改动的实现成本很低但用户接受度会明显提高值班人想知道“明天是不是我”打开系统一眼就能看到。迁完后一定要做的验证有两件一是跨月生成排班检查2月交接和12月到1月的周视图是否正常二是找两个人同时打开同一个班次点换班确认能被并发拦截。我迁过几次这类系统最深的感觉是排班表的数据结构一旦错了后期改动特别被动UI丑一点能忍数据和状态错乱最难补。所以先说结论再动手把状态机和日志做扎实其他页面慢慢搬都不迟。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑