尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

ASP.NET旅行社旅游管理系统:订单建模、安全加固与部署实践

ASP.NET旅行社旅游管理系统:订单建模、安全加固与部署实践 简介基于ASP.NET的旅行社旅游管理信息系统设计与实现文档是一份面向计算机相关专业毕业设计或课程设计的完整参考方案。内容覆盖系统开发全流程包括开发背景、需求分析、业务流程分析、系统架构设计、数据库设计等关键环节并以前台会员、后台管理员两大模块为主线详细说明了旅游线路查询、在线预订、订单管理、用户管理等功能的实现思路。文档基于B/S架构采用ASP.NET技术与SQL Server数据库既展示了如何搭建数据一致性和安全性良好的后台数据库也讲解了前台应用程序中用户体验与功能实现的要点并涉及系统测试与运行效果分析强调操作简便、功能全面和安全性高等特点。资源包为1个docx文档大小约1.84MB便于查阅和修改。目前已有180人学习下载适合需要快速掌握旅游管理信息系统设计思路、撰写毕业论文或准备答辩的读者参考能帮助理解系统模块划分与数据库设计要点。1. 基于ASP.NET的旅行社旅游管理信息系统到底要解决什么一家只有十几个门店的中小旅行社最痛苦的往往不是没有客户而是线路报价单还散落在Excel里同一个团期被两个销售重复占位财务月底对账时才发现订单状态和实收金额对不上。所谓“基于ASP.NET旅行社旅游管理信息系统”就是用ASP.NET WebForms或MVC搭一套内部业务系统把线路发布、团期库存、游客报名、订单收款、退团变更这些动作从线下挪到浏览器里让销售、计调、财务共用同一份数据。它对5年以上开发者的价值不在CRUD本身而在于老旧的WebForms项目如何在.NET Framework时代把业务边界、事务控制、安全基线做对——尤其是ViewState反序列化RCE风险在近十年里反复出现很多老项目至今还带着默认配置裸奔。这篇文章按“建模-实现-加固-部署”的顺序给出一套可直接落地的设计路径。2. 旅行社业务建模从线路到订单的表设计2.1 三个核心实体线路、团期、订单旅行社业务和普通电商不同订单不是直接挂在“商品”上的而是挂在“团期”上的。一条线路比如“昆明-大理-丽江6日游”会有多个发团日期每个团期有独立的余位和价格。因此核心实体必须拆开不能只建一张线路表。常见的做法是三张主表加两张从表TravelRoute线路表存线路名称、行程天数、出发地、目的地简介、封面图、线路状态。TourGroup团期表存线路ID、发团日期、成人价格、儿童价格、总座位数、已售座位数、余位、销售状态。TravelOrder订单表存订单号、团期ID、联系人姓名、电话、订单总价、支付金额、订单状态、支付状态、创建人。Tourist游客表一个订单对应多个出游人存姓名、证件号码、手机号、紧急联系人。OrderLog操作日志表记录订单的创建、改期、退款、取消等状态变更方便售后退责。这种拆法能直接回答两个业务问题某个团期还剩几个位子某个订单关联了哪些游客如果把游客信息直接塞进订单表后面做订单改签或者出票名单导出就会非常痛苦。2.2 SQL脚本落地表结构状态字段和软删除的取舍下面是SQL Server的表结构脚本省略了部分非关键字段保留核心约束。注意所有金额用decimal(18,2)不用float状态字段用int加注释后续在代码里定义枚举。CREATE TABLE TravelRoute ( RouteId INT IDENTITY(1,1) PRIMARY KEY, RouteName NVARCHAR(200) NOT NULL, Days INT NOT NULL DEFAULT 1, StartCity NVARCHAR(100) NOT NULL, RouteDesc NVARCHAR(MAX) NULL, CoverImageUrl NVARCHAR(500) NULL, Status INT NOT NULL DEFAULT 1, -- 0下架 1上架 2待审核 CreatedAt DATETIME NOT NULL DEFAULT GETDATE(), UpdatedAt DATETIME NOT NULL DEFAULT GETDATE() ); CREATE TABLE TourGroup ( GroupId INT IDENTITY(1,1) PRIMARY KEY, RouteId INT NOT NULL FOREIGN KEY REFERENCES TravelRoute(RouteId), DepartureDate DATE NOT NULL, AdultPrice DECIMAL(18,2) NOT NULL, ChildPrice DECIMAL(18,2) NOT NULL DEFAULT 0, TotalSeats INT NOT NULL DEFAULT 30, SoldSeats INT NOT NULL DEFAULT 0, GroupStatus INT NOT NULL DEFAULT 0, -- 0收客中 1已满员 2已发团 3已取消 IsDeleted BIT NOT NULL DEFAULT 0 ); CREATE TABLE TravelOrder ( OrderId INT IDENTITY(1,1) PRIMARY KEY, OrderNo NVARCHAR(32) NOT NULL UNIQUE, GroupId INT NOT NULL FOREIGN KEY REFERENCES TourGroup(GroupId), ContactName NVARCHAR(50) NOT NULL, ContactPhone NVARCHAR(20) NOT NULL, TotalAmount DECIMAL(18,2) NOT NULL, PaidAmount DECIMAL(18,2) NOT NULL DEFAULT 0, AdultCount INT NOT NULL DEFAULT 1, ChildCount INT NOT NULL DEFAULT 0, OrderStatus INT NOT NULL DEFAULT 0, -- 0待支付 1已支付 2已出票 3已取消 4已退款 PaymentStatus INT NOT NULL DEFAULT 0, -- 0未支付 1部分支付 2已支付 CreatedBy NVARCHAR(50) NULL, CreatedAt DATETIME NOT NULL DEFAULT GETDATE() ); CREATE TABLE Tourist ( TouristId INT IDENTITY(1,1) PRIMARY KEY, OrderId INT NOT NULL FOREIGN KEY REFERENCES TravelOrder(OrderId), TouristName NVARCHAR(50) NOT NULL, IdCardNo NVARCHAR(18) NOT NULL, Mobile NVARCHAR(20) NULL, TouristType INT NOT NULL DEFAULT 0 -- 0成人 1儿童 );这里几个设计点值得展开。团期表里的SoldSeats是冗余字段每次有效订单确认后要同步更新好处是查询余位不用每次SUM子查询坏处是需要在事务里保证一致。OrderNo用“日期随机数”或数据库序列生成唯一约束兜底。IsDeleted做软删除只用在TourGroup上因为历史订单需要追溯当时的团期信息直接物理删除会破坏订单关联TravelRoute本身的Status字段已经足够屏蔽下线路不需要再引入删除标识。状态字段为什么不用字符串字符串可读性好但脏数据难以控制计数台系统里“收客中”能出现“收客中 ”“收客中”这种半角变体。int加代码枚举能保证状态迁移逻辑集中在一个地方。给每个状态迁移画一个简单的状态机比如OrderStatus只有0→1→2和0→3→4前端操作按钮按当前状态禁用比后端无条件改状态安全得多。2.3 订单号生成与余位扣减的一致性订单提交不是简单INSERT它涉及两个动作往TravelOrder插入订单、扣减TourGroup的SoldSeats。这两步必须在一个数据库事务里完成。常见错误是先更新余位再插入订单插入失败时余位已经扣了或者先插入订单再扣位高并发下两个事务同时读到相同余位导致超卖。我一般使用SQL Server的Read Committed隔离级别配合UPDATE行锁来扣减余位BEGIN TRANSACTION; UPDATE TourGroup SET SoldSeats SoldSeats AdultCount ChildCount, GroupStatus CASE WHEN SoldSeats AdultCount ChildCount TotalSeats THEN 1 ELSE GroupStatus END WHERE GroupId GroupId AND GroupStatus 0 AND SoldSeats AdultCount ChildCount TotalSeats; IF ROWCOUNT 0 BEGIN ROLLBACK TRANSACTION; THROW 51000, 该团期已满员或已关闭, 1; END INSERT INTO TravelOrder(OrderNo, GroupId, ContactName, ContactPhone, TotalAmount, AdultCount, ChildCount, OrderStatus, PaymentStatus) VALUES(OrderNo, GroupId, ContactName, ContactPhone, TotalAmount, AdultCount, ChildCount, 0, 0); COMMIT TRANSACTION;这段脚本的关键在于UPDATE语句里的WHERE条件同时充当余额校验。SQL Server在更新已索引的GroupId时会对该行加X锁第二个并发事务会阻塞等待直到第一个事务提交或回滚。这样不会出现“读到同样余位”的情况。业务代码里如果只用SELECT判断余位再执行INSERT和UPDATE就一定要在UPDATE后检查ROWCOUNT否则等于没保护。3. ASP.NET WebForms实现核心业务线路展示与订单提交3.1 页面生命周期与三层架构的边界老一代ASP.NET开发者习惯把所有逻辑堆在.aspx.cs的Page_Load里。对于一个要给计调和销售用的系统我建议还是按“UI-业务-数据”拆三层但不要过度设计。WebForms的页面生命周期里最容易被忽略的是IsPostBack判断每次回发都会重新执行Page_Load如果不加判断GridView重新绑定数据会把用户刚选择的筛选条件覆盖掉。三层划分上我习惯这样分UI层.aspx和.aspx.cs只做控件取值、页面跳转、校验。BLL层业务逻辑类比如OrderManager、RouteManager处理状态判空、事务调用。DAL层用ADO.NET或Dapper操作SQL Server返回DataTable或实体集合。不必引入Entity Framework旅行社这种CRUD密集、关系清晰的中小系统Dapper加上手写SQL反而更容易排查问题。需要JOIN多表的报表查询Dapper的QueryAsync和参数化机制已经够用。3.2 线路管理的列表页GridView绑定与字段格式化线路列表页是内部员工的后台管理入口要求能按状态筛选、搜索线路名并以表格展示。下面是最小可运行的Dapper查询示例public class RouteService { private readonly string _connectionString; public RouteService(string connectionString) { _connectionString connectionString; } public IEnumerableTravelRoute SearchRoutes(string keyword, int status) { using var conn new SqlConnection(_connectionString); var sql SELECT RouteId, RouteName, Days, StartCity, Status, CreatedAt FROM TravelRoute WHERE (Keyword OR RouteName LIKE % Keyword %) AND (Status -1 OR Status Status) ORDER BY CreatedAt DESC; return conn.QueryTravelRoute(sql, new { Keyword keyword ?? , Status status }); } }页面后台代码里调用这个Service在非回发状态下绑定protected void Page_Load(object sender, EventArgs e) { if (!IsPostBack) { BindRoutes(); } } private void BindRoutes() { var svc new RouteService(ConfigurationManager.ConnectionStrings[TravelDB].ConnectionString); var list svc.SearchRoutes(txtKeyword.Text.Trim(), int.Parse(ddlStatus.SelectedValue)); gvRoutes.DataSource list; gvRoutes.DataBind(); }这里有几个参数细节。搜索关键字用的是LIKE如果允许用户输入通配符%会造成意外结果所以在拼SQL时可以把反斜杠转义或者用CHARINDEX替代LIKE提高确定性。状态筛选下拉框的值是-1表示全部这个约定要在代码注释里写清楚不然后来接手的人会困惑为什么传-1。Page_Load里的!IsPostBack包裹是必须的不然每次按钮点击触发回发后GridView会重新绑定搜索框里输入的关键字和列表内容就不同步了。3.3 订单提交页使用事务和参数化订单提交有独立页面不放在列表页的GridView编辑行里因为游客信息是变长的一个人到六个人不等需要动态添加TextBox。提交时的关键代码public int CreateOrder(OrderModel order, IListTouristModel tourists) { using var conn new SqlConnection(_connectionString); conn.Open(); using var tran conn.BeginTransaction(); try { // 生成订单号例如 20250514 8位随机 var orderNo GenerateOrderNo(); var sql UPDATE TourGroup SET SoldSeats SoldSeats Seats, GroupStatus CASE WHEN SoldSeats Seats TotalSeats THEN 1 ELSE GroupStatus END WHERE GroupId GroupId AND GroupStatus 0 AND SoldSeats Seats TotalSeats; IF ROWCOUNT 0 THROW 51000, 余位不足或团期关闭, 1; INSERT INTO TravelOrder(OrderNo, GroupId, ContactName, ContactPhone, TotalAmount, AdultCount, ChildCount, OrderStatus, PaymentStatus) OUTPUT INSERTED.OrderId VALUES(OrderNo, GroupId, ContactName, ContactPhone, TotalAmount, AdultCount, ChildCount, 0, 0);; var orderId conn.ExecuteScalarint(sql, new { Seats order.AdultCount order.ChildCount, order.GroupId, orderNo, order.ContactName, order.ContactPhone, order.TotalAmount, order.AdultCount, order.ChildCount }, tran); foreach (var t in tourists) { conn.Execute( INSERT INTO Tourist(OrderId, TouristName, IdCardNo, Mobile, TouristType) VALUES(OrderId, TouristName, IdCardNo, Mobile, TouristType), new { OrderId orderId, t.TouristName, t.IdCardNo, t.Mobile, t.TouristType }, tran); } tran.Commit(); return orderId; } catch { tran.Rollback(); throw; } }这段代码里订单头和游客明细在一个事务里插入任何一条游客信息插入失败都会整体回滚避免出现“订单存在但游客名单缺失”的脏数据。事务要尽早提交不要在事务里做远程接口调用或发送通知邮件这些操作应该放在事务提交之后。订单号的生成要避免依赖RN号导致的重复最简单是用日期自增序列或者在业务上允许重复扫描注册时用数据库唯一约束兜底。3.4 参数说明与运行时常见错误WebForms页面上的控件在回发后会被重新构建视图状态ViewState会保存控件的状态但也带来两个常见坑。第一个坑是“回发或回调参数无效”。这个错误通常发生在事件验证阶段原因是按钮的CommandArgument里包含动态生成的值而页面在回发时无法匹配原来的控件状态。解决办法是给按钮的OnClientClick里禁用或者确认控件没有在PreRender里被移除和重建。第二个坑是ViewState过大导致页面响应慢。线路详情页如果放了大型DataGridViewState可能膨胀到几十KB。我一般会关闭不需要回发的控件的ViewStateasp:GridView IDgvRoutes runatserver EnableViewStateFalse /对于列表展示型页面关闭ViewState是安全的因为每次回发后重新绑定数据即可。只有需要记住用户当前操作行索引的场景才保留ViewState。判断一个控件是否依赖ViewState可以在页面上放一个Label和TextBox回发后看Label.Text是否保留你手动赋的值如果不保留说明控件状态没有被序列化。4. 安全性强化ViewState加密、SQL注入防护与越权控制4.1 ViewState反序列化RCE的威胁模型最近的检索热词里反复出现__viewstate反序列化RCE这不是新漏洞而是.NET Framework时代遗留的高危面。攻击者拿到一个网站的__viewstate参数如果能篡改其内容并让它被服务端反序列化就可以通过TypeConfusedDelegate这类gadget在服务器上执行任意代码。前提是站点没有配置加密或验签WebForms默认的ViewState只有Base64编码并没有MAC校验。一旦服务器被探测出ViewState未加密且machineKey为默认值就是明确的风险信号。防御的关键不在于代码逻辑而在于web.config中对ViewState的配置。一定要设置ViewStateEncryptionModeAlways和ViewStateMACtrue同时显式配置machineKey不能让运行时自动生成。下面给出一个适合生产环境的配置片段。4.2 web.config硬基线machineKey和ViewState强制加密system.web machineKey compatibilityModeFramework20SP2 validationKey你的64位十六进制密钥 decryptionKey你的32位十六进制密钥 validationSHA1 decryptionAES / pages viewStateEncryptionModeAlways enableViewStateMactrue / httpRuntime targetFramework4.8 / /system.webmachineKey的生成可以直接在服务器上用PowerShell生成$bytes New-Object byte[] 64 [Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($bytes) [BitConverter]::ToString($bytes).Replace(-, )注意这里的validationKey长度取决于是不是用SHA1如果改成AES要求不同长度但当前设置下64字节是安全的。生成后要把这个web.config里的machineKey复制到负载均衡环境中的每一台服务器上否则不同节点间ViewState无法解密用户会被随机踢下线。启用ViewStateEncryptionModeAlways后页面源码里ViewState字段依然是Base64但长度会明显增加因为内容已加密。不要依赖某个控件的事件验证因为加密只保证无法篡改如果应用代码自己把明文信息塞进ViewState仍然属于敏感信息暴露需要额外注意。4.3 参数化查询与SQL注入字符串拼接是红线ASP.NET系统里最容易被扫描工具打穿的就是SQL注入。旅行社系统里常见的注入点是线路搜索、订单查询。下面这段反面代码以后不要再出现在代码评审里var sql SELECT * FROM TravelOrder WHERE ContactPhone phone ;攻击者在phone里传 OR 11 --就能拉走全部订单顺手还能通过堆叠查询往数据库里写入恶意数据。改用参数化查询之后这些输入只被处理为字符串字面量var sql SELECT * FROM TravelOrder WHERE ContactPhone Phone; conn.QueryTravelOrder(sql, new { Phone phone });无论使用Dapper还是原生SqlCommand参数化都是必须遵守的基线。存储过程如果内部也拼SQL仍需注意因为参数化只对直接调用者有效。在代码扫描工具层面可以把“字符串拼接SQL后执行”设置为误报级别最低的阻断项任何新提交触发该规则都进不了合并请求。4.4 订单越权不能只靠前端隐藏按钮很多内部系统在菜单层控制了权限但后端接口本身没有校验导致登录用户直接改URL订单ID就能查看他人订单。旅行社系统里订单归属通常属于“创建人”而不是“客户账号”所以要在服务端检查public bool CanAccessOrder(int orderId, string userName) { var order _orderRepo.GetById(orderId); if (order null) return false; if (order.CreatedBy userName) return true; // 管理员角色直接放行 return IsAdmin(userName); }要注意的是“创建人”字段不能相信前端传过来的用户名应该从服务端会话里取。另外在操作“改期”“取消订单”时不止检查当前用户是否有权操作该订单还要检查订单当前状态是否允许这个动作。比如已出票的订单不允许直接取消必须走退款流程这个状态机逻辑放进BLL层而不是在页面里判断。5. 从开发到上线IIS部署与运维验证技巧5.1 发布配置与web.config环境切换WebForms项目发布到IIS时最容易出的问题是本地开发用的连接字符串被带到生产环境。可以在web.config里用Web.config Transform模式发布时自动替换连接字符串和错误页配置。但很多老项目升级缓慢所以我更推荐在项目里保留一个web.config.example发布说明里强制要求复制并修改连接串。下面是一个典型的连接字符串配置发布时注意修改Data Source和数据库名。connectionStrings add nameTravelDB connectionStringServertcp:your-prod-server;DatabaseTravelDB;User IDapp_user;Password****;Trusted_ConnectionFalse;TrustServerCertificateTrue; providerNameSystem.Data.SqlClient / /connectionStrings启用集成认证还是SQL账号认证要提前决定。内网系统用Windows集成认证可以减少密码在配置文件里的暴露但Web服务器和数据库服务器需要同一个域环境。云数据库一般只能用SQL认证这时要把密码放到IIS配置中心的连接字符串设置里不要写在源码库中。5.2 会话状态Session不能随便存默认模式ASP.NET默认的Session Mode是InProc也就是Session存在IIS进程内存里。问题在于发布网站时你很可能重启应用池所有登录用户强制下线如果部署负载均衡不同节点间Session不互通用户每次请求都可能落到不同节点表现为“登录成功又马上跳回登录页”。旅行社系统里Session通常用来存当前登录用户、角色、购物车临时数据。对于中小规模用StateServer或SQL Server模式更可靠。配置SQL Server模式前先执行aspnet_regsql.exe -S server -U user -P password -ssadd -sstype c建立状态数据库然后修改web.configsystem.web sessionState modeSQLServer allowCustomSqlDatabasetrue sqlConnectionStringServeryour-server;DatabaseASPState;User IDaspnet_state;Password**** cookielessfalse timeout60 / /system.webSession泄漏也要关注。如果把权限角色信息直接存Session用户通过URL欺骗或抓包篡改的可能性不大但要防止的是跨站脚本通过脚本读取Session Cookie。给网站的HttpOnly和Secure Cookie打开httpCookies httpOnlyCookiestrue requireSSLtrue sameSiteLax /5.3 日志记录别只靠Response.Write生产事故排查时最怕看到的代码是catch里空着或只写Console.WriteLine。建议引入一个轻量日志类所有异常记录用户、页面URL、堆栈写文件或数据库。我一般用Log4Net配置简单轮转策略现成。log4net appender nameRollingFile typelog4net.Appender.RollingFileAppender file valueLogs\\travel.log / appendToFile valuetrue / rollingStyle valueDate / maxSizeRollBackups value30 / layout typelog4net.Layout.PatternLayout conversionPattern value%date [%thread] %level %logger - %message%newline / /layout /appender root level valueAll / appender-ref refRollingFile / /root /log4net在Global.asax的Application_Error里统一捕获未处理异常记录后重定向到友好错误页不要把异常堆栈直接显示给最终用户。对于订单提交易出异常的场景日志里必须记录OrderNo、ContactPhone、GroupId光有时间戳没法快速定位。5.4 验证ViewState配置是否生效的一个技巧部署完成后不要只看页面能不能打开要主动验证加固项是否真的生效。打开浏览器开发者工具把页面请求的__VIEWSTATE字段整个复制出来放到一个支持Base64解码的工具里。如果解码后能直接看到类似/wEPDwUJ...开头的内容说明ViewState没有加密如果解码后是不可读的字节流或长度明显膨胀说明加密已生效。另一个可验证的点是IIS事件日志。尝试伪造一个ViewState参数请求比如把__VIEWSTATE的最后一个字符改掉观察HTTP 500返回和事件查看器里的“ViewState MAC verification failed”记录。出现这个记录说明MAC校验已开启攻击者无法轻易构造合法状态。顺便检查应用池启动时间如果启动时间距离现在超过几天说明没有频繁因为崩溃自动重启。5.5 用最小请求集压测发现并发短板旅行社在节假日前一周的并发量会突然翻倍系统能不能扛住应该在发布后做一次简单压测。不需要用复杂的LoadRunner在服务器或测试机上用Apache Bench打一个只读接口ab -n 1000 -c 50 https://your-domain/route/list重点看两个指标Requests per second和Failed requests。如果失败率超过0%先看数据库连接池是不是满了其次看应用池队列长度。WebForms页面因为ViewState和服务器的Round-Trip天然比ASP.NET Core重压测时需要把静态资源排除在外直接打一个后台查询接口才能测出业务代码的真实吞吐。本文还有配套的精品资源点击获取
返回列表