Oracle+C#网上订餐系统开发实战:从建表到避坑全指南
简介这是一份基于Oracle与C#实现的网上订餐系统课程设计资料包适合需要完成数据库大作业或了解C/S架构开发流程的高校学生。资源共80个文件压缩包约1.02MB主要包含C#源码.cs、窗体界面资源.resx/.resources、程序配置文件.config及可执行文件.exe同时附带20页Word实验报告和Oracle数据库脚本覆盖需求分析、界面实现、核心代码、数据库设计与功能演示全流程。已有3386人学习下载。资料包目录结构清晰源码与文档分离便于读者对照实验报告理解项目从表结构设计到业务功能编码的实现思路可直接作为Oracle编程实践和C#桌面应用开发的参考模板。1. 网上订餐系统为什么选 OracleC#先把这三个前提想清楚再动手不亏很多人在网上订餐系统这个题目上卡住往往不是 C# 界面写不出来而是栽在 Oracle 的数据访问上。一个很常见的场景A 同学花了两三天把用户注册、菜品展示、购物车界面全部画完程序一运行却报 ORA-12541数据库监听没起来后面的事全停摆。这个题目的技术关键不在控件拖拽而在 Oracle 的表设计、存储过程和事务处理这三块只要有一个没理顺后面写多少界面都白搭。网上订餐系统本质上就是三个实体用户、菜品、订单。C# 负责把这些实体的增删改查做成界面和交互Oracle 负责数据落地和业务规则约束。选这个组合最常见的原因是课程指定了 Oracle 数据库也有不少人是想通过大作业把企业级数据库的常用能力走一遍序列、触发器、存储过程、事务这些在答辩和面试里都经常被问到提前自己动手做一遍比背概念有用得多。这篇文章从选型讲起先帮你确认架构和功能边界再给完整的建表脚本、C# 连接与业务代码最后把最容易翻车的几个点逐个排查一遍。无论你是刚拿到题目还是已经写了一半卡住了照着这套框架走能省下不少折腾时间。2. 系统架构与功能拆分把复杂度降下来再做思路就顺了选型这件事很多新手一上来就纠结 C/S 还是 B/S然后陷入技术选型的黑洞反倒把大作业最核心的业务逻辑丢在一边。我给你的建议是如果课程没有强制要求 Web 页面直接选 WinForms 客户端直连 Oracle这是投入产出比最高的方案。2.1 C/S 还是 B/S大作业场景下为什么首选 WinForms大作业的场景有三个特点开发周期短、需要现场演示、以功能正确性为第一优先。WinForms 在这三点上都有明显优势。C/S 架构下你拖一个 DataGridView 绑定数据源双击控件写事件F5 就能跑起来遇到问题可以直接断点调试定位非常快。而在 B/S 架构里你要额外处理 IIS 部署、浏览器兼容、异步请求这些和业务无关的问题对第一次接触 Oracle 的人来说这些干扰项会让核心业务的学习曲线变得更陡。从答辩角度讲C/S 程序演示时直接打开 exe点几下就能把整个业务流程走完老师看着直观。B/S 的话还要确保 IIS 服务和端口在答辩机器上都正常多一道风险。我见过太多人 Web 版页面写了一半最后因为部署问题被迫重写客户端的情况。如果课程确实指定 Web那是另一条路线但这篇文章接下来的内容以最常见的 WinForms 直连 Oracle 为主线。下表把两种方案的差异列清楚你按自己的课程要求对号入座对比项WinForms 直连 OracleASP.NET Web 连 Oracle开发效率拖控件事件驱动上手快需要处理 HTTP、Session、部署调试方式断点直接定位到窗体逻辑需要结合浏览器开发者工具答辩演示启动 exe 即可环境依赖少依赖 IIS 和端口配置适用场景绝大多数课设大作业课程强制要求或想走前端路线选 WinForms 还有一个隐性好处Oracle 驱动我们只用托管驱动部署到答辩机器上不需要额外装 Oracle 客户端这个在第五章会展开讲能帮你避开一个大坑。2.2 功能模块清单用户端与管理端怎么划分网上订餐系统听起来功能很多但拆开看就是两个角色、五六个模块。用户端做注册登录、浏览菜品、加入购物车、下单、查看历史订单管理端做菜品管理、订单管理、用户管理。把这两个角度分开数据库表结构和代码分层不会乱。我一般会先画一张模块清单把每个模块要做什么、涉及哪几张表列出来再开始写代码。大致如下角色模块核心功能点涉及表用户账号管理注册、登录、密码加密存储t_user用户菜品展示分类浏览、分页、搜索t_dish用户购物车加菜、改数量、删除内存集合用户下单支付提交订单、生成订单明细、扣减t_order, t_order_item用户历史订单查看自己的订单列表和详情t_order, t_order_item管理员菜品管理新增、修改价格、上下架t_dish管理员订单处理查看所有订单、修改状态t_order管理员用户管理查看用户列表、禁用用户t_user功能优先级上我的建议是先把“用户注册登录”“菜品展示”“下单”这条主链路做通再去做购物车和管理端。因为这一条链路走通意味着你把 Oracle 的增删改查、事务、参数化查询全部练了一遍后面都是重复操作。2.3 分三层写代码UI、业务、数据访问分开别写成一坨大作业只有一个人写很多人图省事把 SQL 直接写在按钮的点击事件里一个窗口几百行。这样不是不能跑但后面加功能会非常痛苦尤其是订单逻辑出问题时你根本不知道去哪一行查。我的习惯是分三层WinForms 窗体只负责界面展示和事件分发业务规则放 BLL 层数据库操作全部收拢到 DAL 层。这样层次清楚哪个环节出问题就去哪层找。具体到代码组织我习惯这样分文件OrderSystem.sln ├── OrderSystem.UI // WinForms 窗体、控件事件 │ ├── LoginForm.cs │ ├── MainForm.cs │ └── OrderForm.cs ├── OrderSystem.BLL // 业务逻辑例如订单金额计算、状态校验 │ ├── UserService.cs │ ├── DishService.cs │ └── OrderService.cs ├── OrderSystem.DAL // 数据访问只做 SQL 和参数封装 │ ├── OracleHelper.cs │ ├── UserDao.cs │ ├── DishDao.cs │ └── OrderDao.cs └── OrderSystem.Model // 实体类 ├── User.cs ├── Dish.cs ├── Order.cs └── OrderItem.cs业务逻辑放在 BLL 层比如下单时要先校验菜品是否存在、计算总金额、再调用 DAL 写入数据库数据访问层只负责把 SQL 发出去、把结果集塞进实体。这样分工答辩时老师问你“订单金额哪里算的”你能直接说出在哪一层、哪一行得分点就拿到了。2.4 先封装一个 OracleHelper后面的代码全靠它写数据访问之前我建议先做一个简单的 OracleHelper把连接管理、通用查询、执行非查询这三块封装好。然后所有 DAO 都走这个入口统一管理连接字符串和异常处理。这样写代码的时候不会到处都是连接字符串。NuGet 搜索 Oracle.ManagedDataAccess 并把引用装好之后项目里直接用这个类using System.Data; using Oracle.ManagedDataAccess.Client; namespace OrderSystem.DAL { public static class OracleHelper { private static string connStr Data Source(DESCRIPTION(ADDRESS(PROTOCOLTCP)(HOST127.0.0.1)(PORT1521)) (CONNECT_DATA(SERVICE_NAMEXE)));User Idorder_sys;Password123456;; public static OracleConnection GetConnection() { var conn new OracleConnection(connStr); conn.Open(); return conn; } public static int ExecuteNonQuery(string sql, params OracleParameter[] paras) { using (var conn GetConnection()) using (var cmd new OracleCommand(sql, conn)) { if (paras ! null) cmd.Parameters.AddRange(paras); return cmd.ExecuteNonQuery(); } } public static DataTable ExecuteDataTable(string sql, params OracleParameter[] paras) { using (var conn GetConnection()) using (var cmd new OracleCommand(sql, conn)) { if (paras ! null) cmd.Parameters.AddRange(paras); using (var adapter new OracleDataAdapter(cmd)) { var dt new DataTable(); adapter.Fill(dt); return dt; } } } } }这段代码里连接字符串的 HOST 是数据库所在机器的 IP本机就写 127.0.0.1PORT 默认是 1521SERVICE_NAME 是 Oracle 的服务名。这里有一个新手最容易迷糊的地方Express 版默认服务名是 XE完整版可能是 orcl 或者你在安装时自定义的全局数据库名写错了会报 ORA-12514。先按你本机 Oracle 的实际配置来后面第 5 章会给排查方法。GetConnection 里主动 Open 了一下是让调用方拿到的就是可用连接。这里有个细节OracleHelper 里的方法都用 using 包裹保证连接用完后被释放不会把连接池耗空。很多大作业跑一段时间就报连接池满多数是因为连接没关留在那里占着坑。3. Oracle 数据库设计四张表和一份存储过程撑起整个订餐系统数据库设计是这篇大作业的地基。网上订餐系统的核心就是订单订单牵扯用户和菜品所以最少需要四张表用户表、菜品表、订单表、订单明细表。这四张表的关系很清晰订单属于用户订单明细属于订单订单明细又引用菜品。3.1 四张核心表从用户到订单明细字段这样设计就够用我给你的这组建表脚本是大作业最标准的模板字段数量不多不少刚好满足功能需求也不会有设计冗余。直接复制到 SQL Plus 或 PL/SQL Developer 里执行就可以。CREATE TABLE t_user ( user_id NUMBER PRIMARY KEY, username VARCHAR2(50) NOT NULL UNIQUE, password VARCHAR2(64) NOT NULL, real_name VARCHAR2(50), phone VARCHAR2(20), address VARCHAR2(200), create_time DATE DEFAULT SYSDATE ); CREATE TABLE t_dish ( dish_id NUMBER PRIMARY KEY, dish_name VARCHAR2(100) NOT NULL, price NUMBER(10,2) NOT NULL CHECK (price 0), category VARCHAR2(50), image_path VARCHAR2(200), is_active NUMBER(1) DEFAULT 1 ); CREATE TABLE t_order ( order_id NUMBER PRIMARY KEY, user_id NUMBER NOT NULL REFERENCES t_user(user_id), total_amount NUMBER(10,2) NOT NULL, status VARCHAR2(20) DEFAULT 已支付, address VARCHAR2(200), create_time DATE DEFAULT SYSDATE ); CREATE TABLE t_order_item ( item_id NUMBER PRIMARY KEY, order_id NUMBER NOT NULL REFERENCES t_order(order_id), dish_id NUMBER NOT NULL REFERENCES t_dish(dish_id), quantity NUMBER NOT NULL, price NUMBER(10,2) NOT NULL, subtotal NUMBER(10,2) NOT NULL );几个字段设计上的说明t_user 的 password 用 VARCHAR2(64)是因为我们准备用 SHA256 摘要之后存十六进制字符串长度固定 64 位如果你用 MD5 是 32 位用 VARCHAR2(64) 也可以覆盖。t_order 里存一个 address 字段是用户下单时地址的快照这样哪怕用户后面改了收货地址历史订单依然保留当时的地址信息。t_order_item 里的 price 和 subtotal同样是快照菜品价格以后会调整但订单明细必须保留下单那一刻的单价和总价。t_dish 里有个 is_active 字段这是我在大作业里比较推荐的做法相当于软删除。菜品下架时把它的值改成 0界面就不显示但订单明细里的外键引用不会被破坏。这比物理删除菜品更安全后面讲解题技巧时还会专门提。3.2 序列加触发器Oracle 里表达主键自增的“正规姿势”用过 SQL Server 或 MySQL 的人刚转到 Oracle 时最不习惯的就是没有自增列。MySQL 里一句 AUTO_INCREMENT 就搞定的事Oracle 里要用序列加触发器组合实现。这个知识点几乎是 Oracle 面试必问也是答辩时老师大概率会问的在这里写清楚。先看序列它就是一个独立的递增计数器不属于某一张表可以被任何表或语句调用。再看触发器在往 t_order 表插入数据之前自动从序列里取下一个值塞给主键字段。CREATE SEQUENCE seq_order_id START WITH 1 INCREMENT BY 1 NOCACHE; CREATE OR REPLACE TRIGGER trg_order_id BEFORE INSERT ON t_order FOR EACH ROW BEGIN SELECT seq_order_id.NEXTVAL INTO :NEW.order_id FROM DUAL; END; /触发器的逻辑很简单BEFORE INSERT 表示插入动作发生之前触发:NEW.order_id 是即将写入表的那一行数据的 order_id 字段的引用我们在这里把序列的下一个值赋给它。对于 t_user、t_dish、t_order_item你需要创建同样模式的序列和触发器只要把对象名和表名字段对应改一下。有一个小细节要注意序列的 NOCACHE 表示不预取序列值到内存不会跳号如果默认带 CACHE数据库重启后可能出现主键跳号虽然不影响功能但答辩时如果被追问解释起来比较费劲。另外往表里插入数据的时候不要再显式写主键值否则触发器没有机会介入你手工给的主键可能与序列维护的值冲突报出那个经典的 ORA-00001 主键冲突错误。3.3 存储过程把一次下单做成一个原子的数据库事务下单这个动作至少要做两件数据库操作往 t_order 插一条订单主记录再往 t_order_item 插若干条明细。如果第一条插成功、第二条明细插入时失败订单头就成了孤儿数据用户没收到订单却占了一个订单号。所以这两件事必须包在同一个事务里要么全成功要么全失败。用存储过程处理是最“Oracle”的方式后端调用一个存储过程数据库内部保证原子性。既然题目强调基于 Oracle 实现我强烈建议这一块用存储过程来做这是整个项目最闪亮的加分点。这里给出一个完整的存储过程示例使用了 Oracle 的嵌套表类型来接收多条明细数据CREATE OR REPLACE TYPE order_item_t AS OBJECT ( dish_id NUMBER, quantity NUMBER ); / CREATE OR REPLACE TYPE order_item_tt AS TABLE OF order_item_t; / CREATE OR REPLACE PROCEDURE proc_create_order( p_user_id IN t_order.user_id%TYPE, p_address IN t_order.address%TYPE, p_items IN order_item_tt, p_order_id OUT t_order.order_id%TYPE ) IS v_total NUMBER : 0; v_price NUMBER(10,2); BEGIN FOR i IN 1..p_items.COUNT LOOP SELECT price INTO v_price FROM t_dish WHERE dish_id p_items(i).dish_id; v_total : v_total v_price * p_items(i).quantity; END LOOP; INSERT INTO t_order(user_id, total_amount, status, address) VALUES (p_user_id, v_total, 已支付, p_address) RETURNING order_id INTO p_order_id; FOR i IN 1..p_items.COUNT LOOP SELECT price INTO v_price FROM t_dish WHERE dish_id p_items(i).dish_id; INSERT INTO t_order_item(order_id, dish_id, quantity, price, subtotal) VALUES (p_order_id, p_items(i).dish_id, p_items(i).quantity, v_price, v_price * p_items(i).quantity); END LOOP; COMMIT; EXCEPTION WHEN OTHERS THEN ROLLBACK; RAISE; END; /先说业务逻辑过程接收用户 ID、收货地址和一个菜品明细集合。第一步遍历明细算总金额算的过程里顺便从 t_dish 查出当前单价。第二步插入订单主表通过 RETURNING 把新生成的主键返回出来。第三步循环明细集合插入明细行。最后提交事务。事务控制是这段代码的关键过程体里一旦有异常会先 ROLLBACK 回滚保证已经插入的订单头也被撤销避免脏数据然后 RAISE 把异常继续抛出给调用方C# 端就能感知到失败。这个“要么全成功要么全失败”的机制正是校园大作业里很多人没做、但老师特别在意的一个点。3.4 菜品分页查询Oracle 的 ROWNUM 写法与注意事项网上订餐系统的菜品数量一多一次性全部加载到界面上会很卡也显得不专业。Oracle 分页和 SQL Server 的 OFFSET FETCH、MySQL 的 LIMIT 都不一样它用的是 ROWNUM 这个伪列。很多第一次写 Oracle 的人在这里会困惑直接把 MySQL 的 LIMIT 搬过来然后报 ORA-00933。正确的三层嵌套写法是这样的SELECT * FROM ( SELECT t.*, ROWNUM rn FROM ( SELECT * FROM t_dish ORDER BY dish_id ) t WHERE ROWNUM :pageSize * :pageIndex ) WHERE rn (:pageIndex - 1) * :pageSize;解释一下这三层嵌套的用途最内层先按 dish_id 排序得到一个稳定的顺序中间层给每一行编上一个从 1 开始的 ROWNUM 序号同时用 WHERE ROWNUM pageSize * pageIndex 把范围上限截住最外层再用 rn (pageIndex - 1) * pageSize 把当前页之前的行滤掉。注意 ROWNUM 是在 Oracle 内部生成的行号不能再直接对 ROWNUM 做大于判断否则查不出数据这是新手最容易踩的坑。如果你用分页控件把 pageIndex 和 pageSize 作为绑定参数传给 OracleCommand 就行。这个 SQL 放在 DAO 层前面第 2 章封装的 OracleHelper 刚好可以直接调用。分页逻辑虽然看起来绕但它确实是 Oracle 的标准答案答辩时能把这段讲清楚比你说“我会用框架”更能证明你懂数据库。4. C# 连接 Oracle 与核心业务实现从登录到下单的完整代码数据库表建好了现在进入 C# 端。连接 Oracle 有两条路线老式的是引用 Oracle.DataAccess要求本机安装 Oracle 客户端新式的是用 Oracle.ManagedDataAccess纯托管程序集不需要额外安装客户端。这一章所有代码都基于托管驱动也是我在大作业里推荐的做法。4.1 连接三种方式托管驱动、连接字符串与服务名的坑先说为什么不用 Oracle.DataAccess。这个老驱动本身依赖本机装 Oracle 客户端不同版本还有 32 位和 64 位之分。你开发机上装了客户端能跑拷贝 exe 到答辩机器的纯净环境里大概率报找不到 Oracle.DataAccess.dll 或者版本不匹配。而 Oracle.ManagedDataAccess 只是一个托管 DLL项目打包时把它复制过去就能跑省去一大堆环境问题。连接字符串的写法有两种风格。一种是引用了 tnsnames.ora 里的服务名一种是直接把地址写全。我建议你在代码里直接用完整描述不依赖 tnsnames.ora 配置这样换一台电脑部署只要数据库 IP 和端口对代码就能连上。using Oracle.ManagedDataAccess.Client; class ConnTest { static void Main() { string connStr Data Source(DESCRIPTION(ADDRESS(PROTOCOLTCP) (HOST127.0.0.1)(PORT1521)) (CONNECT_DATA(SERVICE_NAMEXE))); User Idorder_sys;Password123456;; using (var conn new OracleConnection(connStr)) { conn.Open(); Console.WriteLine(数据库连接成功); } } }这段代码里HOST 是数据库所在机器的 IPPORT 默认 1521SERVICE_NAME 是数据库服务名这个值非常容易出错。你可以在数据库机器上执行SELECT value FROM v$parameter WHERE name service_names;查看真实的服务名也可以直接查看 tnsnames.ora 里的配置。注意 SERVICE_NAME 和 SID 是两个概念连接串里写的是服务名不要填 SID。很多人在这里把 orcl 写成 sid 形式导致连接失败这是个高频问题。4.2 登录与注册参数化查询是底线密码要哈希用户登录是 C# 端最基本的业务。这里最重要的不是怎么写 SQL而是怎么防 SQL 注入。大作业里经常见到这样的写法把文本框里的内容直接拼进 SQL 字符串这非常危险。我给出的写法是使用 OracleParameter 参数化查询这是数据访问的底线要求也是答辩老师可能会追问的点。先看登录的 DAO 代码public User Login(string username, string password) { string sql SELECT user_id, username, password, real_name, address FROM t_user WHERE username :uname AND password :pwd; using (var conn OracleHelper.GetConnection()) using (var cmd new OracleCommand(sql, conn)) { cmd.Parameters.Add(:uname, OracleDbType.Varchar2).Value username; cmd.Parameters.Add(:pwd, OracleDbType.Varchar2).Value password; using (var reader cmd.ExecuteReader()) { if (reader.Read()) { return new User { UserId reader.GetInt32(0), Username reader.GetString(1), RealName reader.GetString(3), Address reader.GetString(4) }; } } } return null; }这里用到的核心点就是SQL 语句里的:uname、:pwd是绑定参数实际值由 Oracle 驱动在服务器端绑定而不是简单拼接字符串。这样即使输入框里输入了 or 11 --它也只是一个普通字符串成不了攻击 payload。参数名上的冒号是 Oracle 驱动的习惯写不写冒号都行但我建议写上看着清楚。接下来是注册时的密码处理。我强烈建议不要在数据库里存明文密码而是用 SHA256 算摘要后存进 t_user 表。用 C# 做哈希只需要几行代码using System.Security.Cryptography; using System.Text; public static string HashPassword(string rawPassword) { using (var sha SHA256.Create()) { byte[] bytes sha.ComputeHash(Encoding.UTF8.GetBytes(rawPassword)); var sb new StringBuilder(); foreach (byte b in bytes) { sb.Append(b.ToString(x2)); } return sb.ToString(); } }注册时把哈希后的结果作为密码写入 t_user登录时先对用户输入的密码做同样的哈希再和数据库里存的值比对。这样做的好处是哪怕你的数据库文件被拷走别人也无法直接拿到用户的原始密码。这个点在答辩时说出来项目整体档次一下就上去了。4.3 下单业务用 OracleTransaction 包住整个操作存储过程虽然能包事务但如果你在 3.3 那一步还没完全消化C# 端也有一个相对简单可靠的替代方案用 OracleTransaction 在同一个连接里开启事务手动控制订单头和明细的插入。这种做法的好处是逻辑都在 C# 里调试方便每一步都能用断点观察。public int CreateOrder(int userId, string address, ListDish dishList, Listint quantityList) { using (var conn OracleHelper.GetConnection()) { using (var tx conn.BeginTransaction()) { try { int orderId; string sqlHead INSERT INTO t_order(user_id, total_amount, status, address) VALUES (:uid, :total, 已支付, :addr) RETURNING order_id INTO :orderId; using (var cmdHead new OracleCommand(sqlHead, conn)) { cmdHead.Transaction tx; decimal total 0; for (int i 0; i dishList.Count; i) { total dishList[i].Price * quantityList[i]; } cmdHead.Parameters.Add(:uid, OracleDbType.Int32).Value userId; cmdHead.Parameters.Add(:total, OracleDbType.Decimal).Value total; cmdHead.Parameters.Add(:addr, OracleDbType.Varchar2).Value address; var pOrderId new OracleParameter(:orderId, OracleDbType.Int32) { Direction System.Data.ParameterDirection.Output }; cmdHead.Parameters.Add(pOrderId); cmdHead.ExecuteNonQuery(); orderId Convert.ToInt32(pOrderId.Value); } string sqlItem INSERT INTO t_order_item(order_id, dish_id, quantity, price, subtotal) VALUES (:orderId, :dishId, :qty, :price, :subtotal); using (var cmdItem new OracleCommand(sqlItem, conn)) { cmdItem.Transaction tx; for (int i 0; i dishList.Count; i) { cmdItem.Parameters.Clear(); cmdItem.Parameters.Add(:orderId, OracleDbType.Int32).Value orderId; cmdItem.Parameters.Add(:dishId, OracleDbType.Int32).Value dishList[i].DishId; cmdItem.Parameters.Add(:qty, OracleDbType.Int32).Value quantityList[i]; cmdItem.Parameters.Add(:price, OracleDbType.Decimal).Value dishList[i].Price; cmdItem.Parameters.Add(:subtotal, OracleDbType.Decimal) .Value dishList[i].Price * quantityList[i]; cmdItem.ExecuteNonQuery(); } } tx.Commit(); return orderId; } catch { tx.Rollback(); throw; } } } }这段代码有三个关键点。第一cmdHead 的 INSERT 语句里用了 RETURNING order_id INTO :orderId这是 Oracle 专有的语法用来在执行插入后直接把新订单号取出来比再查一次序列优雅得多。第二cmdHead 和 cmdItem 都显式设置了Transaction tx让它们运行在同一个数据库事务里而不是自动提交。第三catch 里一旦有异常就 Rollback保证订单头和明细不会出现只写一半的情况。如果你已经写好了 3.3 的 proc_create_order那么在 C# 里调用存储过程的思路也类似把嵌套表当成参数传进去驱动会自动把内容组装成数组。存储过程方案比分两步插入更内聚但代码复杂度高一些我建议先把上面的事务版本跑通再考虑升级。4.4 菜品列表与 DataGridView绑定数据源注意只查当前页菜品展示页是用户看到的第一个界面多数人用 DataGridView 绑定数据表。常见做法是查询全部菜品然后直接绑定但数据量一上来就会卡。结合 3.4 的分页 SQL在 DAL 里写一个带页码的查询方法更专业。public DataTable GetDishPage(int pageIndex, int pageSize) { string sql SELECT * FROM ( SELECT t.*, ROWNUM rn FROM ( SELECT * FROM t_dish ORDER BY dish_id ) t WHERE ROWNUM :pageSize * :pageIndex ) WHERE rn (:pageIndex - 1) * :pageSize; return OracleHelper.ExecuteDataTable(sql, new OracleParameter(:pageSize, OracleDbType.Int32) { Value pageSize }, new OracleParameter(:pageIndex, OracleDbType.Int32) { Value pageIndex }); }这里把 pageSize 和 pageIndex 作为参数传入避免每次拼接整数字符串也顺手防住了 SQL 注入。界面上放一个上一页、下一页按钮每点一次换个 pageIndex 重新查询绑定即可。每次只查当前页的数据比把全表都拖回来再用控件过滤靠谱得多。DataGridView 绑定这段比较直白把 GetDishPage 返回的 DataTable 赋值给 DataSource再设置一下列头显示的文本例如把 dish_name 显示为“菜品名称”。注意列名如果有下划线可以单独配置 HeaderText不要偷懒直接显示英文字段名。用户看着网感好一点答辩时也显得用心。5. 避坑指南OracleC#大作业最容易翻车的 5 个场景这一章说的都是我自己和周围人做大作业时真实踩过的坑。先给结论遇到问题不要先怀疑代码逻辑先查数据库能不能连上、字符集对不对、SQL 方言有没有用错。下面按现象、原因、解决的方式来写方便你排查时对照。5.1 程序启动连接报 ORA-12541监听还没起来现象是 C# 里一 Open 就抛 ORA-12541: TNS: 监听程序无法启动或无法识别。原因可能是 Oracle 监听服务没启动或者连接字符串里的端口和服务名和实际不符。我见过有人折腾了两个小时改代码结果只是监听服务没开。解决分三步第一步在 Windows 服务里找到 Oracle 开头的 TNSListener 服务右键启动或者直接在命令行执行lsnrctl start第二步在数据库本机用tnsping 127.0.0.1验证监听是否响应第三步确认连接字符串的 PORT 是 1521SERVICE_NAME 和你数据库真实服务名一致。如果本机 tnsping 通了代码还报错把服务名核对一遍基本能定位。5.2 插入的中文变成问号或乱码字符集不一致现象是界面输入中文写入数据库后变成一串问号或者读出来乱码。原因是数据库字符集与客户端字符集不一致。Oracle 服务端可能是 AL32UTF8而客户端环境变量 NLS_LANG 没设对或者 C# 驱动的字符集配置和数据库对不上。最简单的排查方法执行SELECT USERENV(language) FROM DUAL;看数据库当前语言环境再对比你连接串里的配置。常见的做法是在连接字符串里加上;UnicodeTrue让托管驱动按 UTF-8 处理。很多老资料推荐的修改 NLS_LANG 环境变量在新版的托管驱动下已经不太依赖优先检查连接串和数据库字符集即可。装数据库时如果不是特殊要求尽量选 AL32UTF8能少很多麻烦。5.3 ORA-00933SQL 命令未正确结束多半是方言没换过来现象是明明感觉 SQL 写对了执行就报 ORA-00933。原因是把 MySQL 或 SQL Server 的习惯带到了 Oracle。最常见就是 SQL 语句末尾多了一个分号在 C# 的 OracleCommand 里一句命令末尾不要加;加了就会报这个错。另一个高频错误是分页用了LIMIT ? OFFSET ?Oracle 不认必须换成 3.4 节的 ROWNUM 写法。还有一个容易忽略的地方ORDER、USER、COMMENT这类词在 Oracle 里是保留字如果表名或字段名直接用了它们查询也会报错。处理办法是给表和字段加上双引号或者干脆在表设计阶段避开这些词。我们在 3.1 节用的是 t_user、t_dish、t_order 这种带前缀的表名就是要规避这一类问题现在你应该明白这样设计并不是随手起的名字。5.4 插入数据时报 ORA-00001主键冲突现象是第一次插入成功第二次插入同样的数据就报主键冲突。原因通常是插入语句里显式写了主键值而序列并没有同步递增下一次插入了同一个值。另一个常见情况是触发器没有建或者触发表和序列搞错了对象。解决方法是检查 t_user、t_dish、t_order、t_order_item 各表是否都有对应的序列和触发器并且插入语句不要给主键赋值。如果你确实需要手动指定主键那就要同步把序列的当前值ALTER SEQUENCE ... RESTART但这在代码里非常不优雅。大作业里最省心的方案就是每个表配一个独立序列和同名表触发器插入时彻底忘记主键字段。5.5 换电脑运行报“找不到 Oracle.DataAccess”驱动依赖问题现象是开发机正常把整个项目拷到答辩机器上运行报找不到 Oracle.DataAccess 或版本不匹配。原因是你用了老式驱动它依赖本机安装的 Oracle 客户端而答辩环境没有装。这个坑每年都有不少人掉进去。解决方法是把项目里的 Oracle.DataAccess 引用去掉换成 Oracle.ManagedDataAccess。替换之后不需要安装任何客户端代码逻辑和前几节完全一样只是 using 的命名空间和引用路径变一下。从 NuGet 安装之后在发布时把对应的 DLL 复制进输出目录即可。这个替换能一次性解决 90% 的部署类问题强烈建议一上来就用托管驱动别走老路。6. 从“能跑”到“能讲好”演示预案与三个加分项代码写到这里系统已经能跑通完整流程了。但我见过不少人在答辩时栽在演示环节不是因为功能有问题而是演示顺序乱、操作不连贯被老师误以为没完成。我建议你把演示脚本当成一个测试用例来走启动程序注册新用户登录浏览菜品把一个菜加入购物车提交订单然后切到管理员账号进入订单管理看到刚才的订单修改状态最后回到菜品管理新增一个菜品。整个过程控制在五分钟以内不要临场乱点。演示之前把数据库服务、监听、C# 程序的连接字符串全部检查一遍最好提前一天在答辩机器上完整跑一遍流程。这是老生常谈但每年都有人因为答辩机器没装服务、数据库没启动而当场翻车。我自己的习惯是做一个启动自检脚本先把lsnrctl status跑一遍确认监听正常再打开程序。加分项有三个方向。第一是 3.4 节的分页查询让菜品列表不卡顿第二是软删除字段 is_active保留历史订单的数据完整性第三是登录密码做哈希存储体现安全意识。这三个点都不复杂但能向老师传递一个信号你不只是把功能拼出来了而是在认真思考数据安全和系统设计。如果还有余力可以把订单状态做成一个独立枚举表维护而不是散落在业务代码里。讲存储过程的时候不要只背语法可以按这个思路组织先讲下单需要同时写订单头和明细再讲这样存在数据一致性问题最后点出用存储过程加事务解决。老师追问异常分支时就回答EXCEPTION WHEN OTHERS THEN ROLLBACK RAISE说明失败会回滚同时把异常抛回调用端。这个逻辑链条清晰比背十句概念都有说服力。答辩前一天把数据库里造好的测试数据清掉一部分只保留三四个菜品、一两条订单演示页面干净清爽。我吃过一次亏演示时菜品太多翻了几页找不到需要的菜场面略显尴尬。从那以后每次答辩前我都会把演示数据精简到正好能覆盖演示路径界面清爽讲解反而更顺畅。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

从 0 到 1 构建销售 AI Agent Harness Engineering:线索生成、客户画像与转化预测实战(TaoToken 统一 Key 接入篇)

从 0 到 1 构建销售 AI Agent Harness Engineering:线索生成、客户画像与转化预测实战(TaoToken 统一 Key 接入篇)

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 3:15:51 阅读更多 →
生产级LLM API网关实践:用litellm实现多模型接入与故障转移

生产级LLM API网关实践:用litellm实现多模型接入与故障转移

凌晨一点,我盯着监控面板上满屏的504报错,心情复杂。白天刚把新模型切到线上,晚上就给我颜色看——下游服务商接口超时,业务侧所有请求像约定好了一样一起超时。那个瞬间我意识到,直接在业务代码里写死模型API的调用&a…

2026/10/12 3:14:50 阅读更多 →
Django+Flask双框架协作:考研服务系统架构与实现

Django+Flask双框架协作:考研服务系统架构与实现

1. 项目概述与需求拆解考研服务系统,说白了就是给备考学生提供一个“信息工具交流”的一站式平台。这一点我需要先强调,因为很多同学在做这类系统时容易跑偏——要么把它做成纯资讯网站,要么做成纯笔记工具,忽略了考研群体真正的使…

2026/10/12 3:14:50 阅读更多 →

最新新闻

【I2C的七宗罪】深度总结:从协议原理到硬件故障排查

【I2C的七宗罪】深度总结:从协议原理到硬件故障排查

前言:最近在网上淘到了一篇讲解IIC的文章,讲的是一个老工程师的多年IIC调试工作的总结,看了之后感觉覆盖了几乎所有的IIC调试过程可能遇到的问题,由于在CSDN平台上面没有发现类似的完整的文章,现就此重新加工编辑一下&…

2026/10/13 7:01:55 阅读更多 →
新品发布后多久才能被AI推荐?——以iPhone Duo为例

新品发布后多久才能被AI推荐?——以iPhone Duo为例

新品发布后多久才能被AI推荐?——以iPhone Duo为例【摘要】Top1推荐率为0、总提及率仅7.1%:基于4款主流大模型、140个有效回答、9个品牌实测数据,拆解新品从「被报道」到「被AI推荐」的上架时差,将AI上架拆分为3道门槛&#xff0c…

2026/10/13 7:01:55 阅读更多 →
OS_labs内核沙盒:从页表映射到进程切换的可调试x86-64实操指南

OS_labs内核沙盒:从页表映射到进程切换的可调试x86-64实操指南

简介:本资源是一套面向高校操作系统课程学习者的C语言实验项目合集,聚焦进程管理、内存调度、文件系统与系统调用等核心原理的动手实践,适用于具备基础C编程能力的本科生及系统编程初学者。压缩包共12个文件,含5个C源码文件&#…

2026/10/13 7:01:55 阅读更多 →
医疗器械包装运输验证:GB/T4857标准应用与方案设计

医疗器械包装运输验证:GB/T4857标准应用与方案设计

医疗器械包装运输验证这件事,我这两年做得比较多,说句实话,大部分产品出事都出在“纸箱看起来没事,开箱后产品坏了”或者“运输途中振动导致精密部件脱落”这种不起眼的环节上。国内做医疗器械注册,包装验证经常是发补…

2026/10/13 7:01:55 阅读更多 →
Python函数参数:从提示词到temperature掌控大模型输出

Python函数参数:从提示词到temperature掌控大模型输出

我们开始上课。第14课,前面几节课,我们已经把Python基础语法过了一遍,也成功让自己的电脑接上了大语言模型的API,能写出类似“你今年几岁”这种问题,让AI回答。但是有个问题一直困扰着零基础的小朋友:同一个…

2026/10/13 7:01:55 阅读更多 →
Java实现GB28181 RTP PS流解包与RTMP推流实战

Java实现GB28181 RTP PS流解包与RTMP推流实战

1. 项目背景与整体设计思路1.1 为什么要在Java里做RTP PS流解包推RTMP做过安防流媒体对接的朋友应该都有体会,GB28181协议在国内安防行业几乎是绕不开的存在。摄像头、NVR、平台之间信令走SIP,媒体流走RTP承载PS封装,这套组合拳已经跑了很多年…

2026/10/13 7:00:55 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目,我最大的感受是,大部分坑不是出在技术前沿,而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节,最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了,很难不碰到舵机控制这道坎。做智能小车要转方向,做机械臂要抬关节,做云台要偏航俯仰,背后都是同一个东西在干活:舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”?——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台?或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜?又或者,在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 21:05:09 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 14:48:49 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →