ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

C#固定资产管理系统开发实战:数据库设计与核心功能实现解析

C#固定资产管理系统开发实战:数据库设计与核心功能实现解析 简介C#固定资产管理系统是一套面向计算机专业毕业设计或课程设计的完整源码工程基于Visual Studio与SQL Server开发采用C/S架构覆盖资产分类、资产增加、更新、折旧核算、资产清理等典型业务模块可帮助学习者理解固定资产管理流程及WinForm程序的组织方式。压缩包共90个文件核心为36个cs源码文件与16个resx界面资源另有29个ico图标、sln解决方案、数据库mdf与ldf文件及doc使用说明文档整体仅570KB结构紧凑、便于查阅。系统内置操作员管理、本单位信息、资产编号规则等设置功能菜单清晰适合作为课程设计模板或毕业设计参考。目前已有321人学习下载配有使用说明可快速运行和二次开发对希望掌握C/S数据库应用开发的读者具有实用价值。1. 为什么毕业设计总是绕不开C#固定资产管理系统每年这个时候总会有一批人被同一个题目卡住。老师丢过来一句“做个固定资产管理系统”要求是C#、能连数据库、有增删改查末尾还要有一份能看的源码。去网上搜一圈十个资源九个是半成品第三个还带密码。固定资产管理系统之所以被反复布置是因为它把数据库设计、三层架构、Windows客户端界面、报表导出这些课程知识点串成了一条完整的线从资产入库到领用、归还、折旧、报废每一步都在和数据打交道。这是一个麻雀虽小五脏俱全的业务系统既是课程设计里的常青树也是最容易讲清楚、讲出亮点的题目。下面按我完整做完一套的方案给你拆透从技术选型、表结构、核心代码到踩过的坑以及答辩前怎么把它从“能跑”调到“能讲”。2. 先把功能和边界想清楚固定资产管理系统的数据流与C#技术选型一个固定资产管理系统从使用者的视角看就是行政老师手里的一个Windows客户端。但从课程设计角度看它必须覆盖一条完整生命周期资产采购入库后经历在库、领用、归还、维修、报废每一步都要可追溯。把这条状态流转做通系统的主干就立住了。这章先把功能边界讲清楚再讲C#这边的选型——选型本身不难但选错了后面每一步都在还债。2.1 功能边界六个模块构成的最小闭环先说结论一份拿得出手的固定资产管理系统源码至少要有六个功能模块。第一个是资产信息管理负责资产的增删改查核心字段包括资产编号、名称、分类、所属部门、原值、净值、购置日期。第二个是分类管理资产分类做单级就够不做树形避免界面和递归查询失控。第三个是部门管理部门与资产是一对多关系领用记录里冗余部门名称省得每次都关联查。第四个是领用归还这是业务的“高光点”因为它涉及状态变更和记录写入能顺势讲出数据库事务。第五个是折旧计算这是答辩老师最爱问的地方。第六个是统计查询至少要有资产总台账和按部门、按状态的汇总页。这一套模块背后有一条核心状态链在库、在用、维修、报废。状态字段建议用整数存0在库、1在用、2维修、3报废不要直接存中文理由是整数索引快、显示名想改就改界面下拉框的数据源也不必跟着数据库动。如果以后要扩展盘点和借用功能状态枚举还能继续往后加。2.2 WinForms还是WPF不同选型背后的答辩风险C#做Windows桌面客户端两个壳二选一WinForms或WPF。课程设计我推荐WinForms原因很朴素学习曲线和排错成本都低得多。如果你还在C#入门阶段最该先把DataGridView、事件处理器和ADO.NET这套组合练熟而不是一上来就跟XAML搏斗。WPF的布局用XAML数据绑定要理解依赖属性、数据模板样式、触发器、命令系统一套下来课程设计周期内很难吃透。而且WPF的报错经常是运行到一半才弹出来不像WinForms那样编译期就能拦下一部分问题定位成本高一个量级。WinForms是拖控件、双击写事件、DataGridView直接绑DataTable几乎直觉式开发。把调界面的时间省下来留给业务逻辑的时间自然就多了。从答辩角度讲WinForms的事件链更直白老师问“这个按钮点下去执行了什么”你可以一路从按钮事件讲到SQL语句。WPF里按钮和数据更新往往被绑定机制打散讲不清反而扣分。我见过不少因为选WPF熬夜调DataGrid样式最后来不及做业务功能的翻车案例。如果你已经对WPF很熟非要用也行前提是能把MVVM里哪个ViewModel对应哪块界面讲明白——讲不明白很容易被怀疑是照抄来的。2.3 数据库选型SQL Server Express是默认答案SQLite是保底方案课程设计最常见的组合是C# SQL Server Express或LocalDB。好处是Visual Studio自带创建本地数据库的模板可以直接在解决方案里建一个.mdf文件连接串短也不要求机器上装了完整版SQL Server。Express版10GB的容量上限对课程设计那点数据量来说绰绰有余。如果你的机器死活装不上SQL ServerSQLite是第二选择。C#连SQLite用System.Data.SQLite或Microsoft.Data.Sqlite用法和ADO.NET几乎一样把SqlConnection换成SQLiteConnection即可。参数名写法也基本一致SQLite支持、$和:三种参数前缀统一用就行。但SQLite有个坑类型亲和性比较松日期时间存进去可能是文本取出时不做Convert.ToDateTime就会抛异常。下面是LocalDB连接串模板后面所有代码都基于它public static string ConnStr Data Source(LocalDB)\MSSQLLocalDB;AttachDbFilename|DataDirectory|\Assets.mdf;Integrated SecurityTrue;MultipleActiveResultSetsTrue;四个关键参数要能讲得出来。Data Source指定LocalDB实例AttachDbFilename指向mdf物理文件|DataDirectory|在WinForms里默认是程序运行目录下的bin文件夹你要把mdf放进项目根目录并在文件属性里设为“如果较新则复制”Integrated Security用Windows身份认证课程设计不用动SQL Server账号MultipleActiveResultSets设为True避免连续查询时出现“已有打开的与此命令关联的连接”。提示本机开发时Data Source写成(LocalDB)\MSSQLLocalDB别顺手写成.\SQLEXPRESS后者是独立SQL Server服务的实例名本地没装就连接失败。这两个实例名经常被混着抄是课程设计里最高频的连接失败原因。选型定下来之后下一步就是把数据库表建起来。表结构设计得好不好直接决定后面写业务代码时要不要反复返工下一章专门讲这个。3. 从表结构到数据访问层五张核心表和一个通用Helper3.1 五张表的字段设计表结构是系统的一半第2章里已经给了Asset表的雏形这边把它补全成一套。全套共五张表资产管理表、分类表、部门表、领用记录表、报废记录表。关系不多但足够说明外键和连接查询的用法。CREATE TABLE [dbo].[Asset] ( [Id] INT IDENTITY (1, 1) NOT NULL, [AssetCode] NVARCHAR (50) NOT NULL, [Name] NVARCHAR (100) NOT NULL, [CategoryId] INT NOT NULL, [DepartmentId] INT NOT NULL, [PurchaseDate] DATETIME NOT NULL, [OriginalValue] DECIMAL (18, 2) NOT NULL, [NetValue] DECIMAL (18, 2) NOT NULL, [Status] INT NOT NULL DEFAULT 0, [LastDepreciationMonth] NVARCHAR (6) NULL, [Remark] NVARCHAR (200) NULL, CONSTRAINT [PK_Asset] PRIMARY KEY CLUSTERED ([Id] ASC), CONSTRAINT [UQ_AssetCode] UNIQUE NONCLUSTERED ([AssetCode] ASC) );AssetCode加唯一约束从数据库层面挡住重复编号。OriginalValue和NetValue分开存原值在购置后不变净值每月折旧后回写。Status默认值0直接落在数据库比每次插入时手动赋值少一个漏网之鱼。LastDepreciationMonth是给批量折旧去重用的存“202406”这种格式后面章节会用到。分类表和部门表结构类似是典型的字典表CREATE TABLE [dbo].[Category] ( [Id] INT IDENTITY (1, 1) NOT NULL, [Name] NVARCHAR (50) NOT NULL, CONSTRAINT [PK_Category] PRIMARY KEY CLUSTERED ([Id] ASC) ); CREATE TABLE [dbo].[Department] ( [Id] INT IDENTITY (1, 1) NOT NULL, [Name] NVARCHAR (50) NOT NULL, CONSTRAINT [PK_Department] PRIMARY KEY CLUSTERED ([Id] ASC) );这两张表数据量小、被Asset频繁引用实际工作中建议给Name加唯一索引防止管理员手滑建出两个“财务部”。领用记录表是另一个关键表CREATE TABLE [dbo].[UsageRecord] ( [Id] INT IDENTITY (1, 1) NOT NULL, [AssetId] INT NOT NULL, [UserName] NVARCHAR (50) NOT NULL, [DepartmentId] INT NOT NULL, [UseDate] DATETIME NOT NULL DEFAULT GETDATE(), [ReturnDate] DATETIME NULL, CONSTRAINT [PK_UsageRecord] PRIMARY KEY CLUSTERED ([Id] ASC) );ReturnDate允许为空一次完整领用周期里归还动作发生前它就是NULL。AssetId上加普通索引因为查询某台资产的历史领用记录是最常见的操作。还要一张报废记录表Disposal记录报废日期、原因、经办人。表面看可以在Asset表上直接改Status但独立表能保留资产的历史轨迹答辩时多一个“可追溯”的讲法后面避坑章会细说。3.2 实体类与DataRow转换DBNull是初学者第一道坎表建好以后C#这边要有对应的实体类。课程设计不需要引入ORM一个简单的POCO类就够放在单独的Models文件里。public class Asset { public int Id { get; set; } public string AssetCode { get; set; } public string Name { get; set; } public int CategoryId { get; set; } public int DepartmentId { get; set; } public DateTime PurchaseDate { get; set; } public decimal OriginalValue { get; set; } public decimal NetValue { get; set; } public int Status { get; set; } public string Remark { get; set; } }定义类本身不难难在从DataRow里取值时经常碰到DBNull。比如Remark字段允许为空从DataTable里读出来是DBNull而不是null直接ToString没问题但如果一个可空数字字段没值Convert.ToDecimal就会当场炸。我的习惯是写一个通用的取值辅助方法public static T GetValueT(DataRow row, string columnName) { if (row.IsNull(columnName)) return default(T); return (T)row[columnName]; }这个泛型方法放哪里都行但既然被多层共用建议放在单独的工具类里比如Common层。调用时写成GetValue (row, NetValue)可空字段取出来就是default(T)。课程设计里数据量小不会有性能问题但能把类型转换的代码统一收住少写一堆if判空。3.3 数据访问层一个Helper搞定连接、查询、事务数据访问层不搞花活一个静态类加三个方法就够ExecuteDataTable用于查询ExecuteNonQuery用于增删改ExecuteScalar用于取单个值。在这套系统里高频查询用DataTable直接绑给DataGridView复杂规则判断在BLL层再转实体。public static class DbHelper { public static string ConnStr Data Source(LocalDB)\MSSQLLocalDB;AttachDbFilename|DataDirectory|\Assets.mdf;Integrated SecurityTrue;MultipleActiveResultSetsTrue; public static DataTable ExecuteDataTable(string sql, params SqlParameter[] pars) { using (SqlConnection conn new SqlConnection(ConnStr)) using (SqlDataAdapter da new SqlDataAdapter(sql, conn)) { if (pars ! null) da.SelectCommand.Parameters.AddRange(pars); DataTable dt new DataTable(); da.Fill(dt); return dt; } } public static int ExecuteNonQuery(string sql, params SqlParameter[] pars) { using (SqlConnection conn new SqlConnection(ConnStr)) { conn.Open(); SqlCommand cmd new SqlCommand(sql, conn); if (pars ! null) cmd.Parameters.AddRange(pars); return cmd.ExecuteNonQuery(); } } public static object ExecuteScalar(string sql, params SqlParameter[] pars) { using (SqlConnection conn new SqlConnection(ConnStr)) { conn.Open(); SqlCommand cmd new SqlCommand(sql, conn); if (pars ! null) cmd.Parameters.AddRange(pars); return cmd.ExecuteScalar(); } } }三个方法的调用规则只有一条SQL里有需要动态拼的值就new SqlParameter塞进参数数组永远别用字符串拼接。params关键字让调用时可以灵活传任意数量的参数。using包住连接和适配器方法结束就释放连接避免把连接池拖垮。提示SqlDataAdapter的Fill会自行打开和管理连接不需要先调用conn.Open。但如果同一个连接上既要Fill又要执行更新请确保MultipleActiveResultSets是打开的否则第二次查询会报“连接正忙”。3.4 BLL层别写成空壳规则判断才是业务层存在的意义有同学搭了三层架构BLL层却只把DAL方法又转发一遍等于没写。业务层存在的意义是跨表规则和状态流转判断。以领用为例DAL只需要提供“把状态改成在用”和“插入一条UsageRecord”两个原子操作BLL要做的则是先查资产当前状态不是“在库”就拒绝是“在库”则先插记录再改状态两步合成一个事务。规则写进BLLUI层的按钮事件才能保持清爽答辩时老师问“业务逻辑写在哪”你能直接指到BLL层。4. 核心功能逐段落地资产登记、领用归还、折旧计算与台账导出4.1 资产登记插入记录前先做三项校验资产登记界面通常是一个表单资产编号、名称、分类下拉框、部门下拉框、购置日期、原值、备注。保存按钮的流程是固定的界面必填校验、编号唯一性校验、插入每一步都不能省。private void btnSave_Click(object sender, EventArgs e) { if (string.IsNullOrWhiteSpace(txtAssetCode.Text.Trim())) { MessageBox.Show(资产编号不能为空); return; } if (txtAssetCode.Text.Trim().Length 50) { MessageBox.Show(资产编号长度不能超过50); return; } object exists DbHelper.ExecuteScalar( SELECT COUNT(1) FROM Asset WHERE AssetCode code, new SqlParameter(code, txtAssetCode.Text.Trim())); if (Convert.ToInt32(exists) 0) { MessageBox.Show(该资产编号已存在); return; } string sql INSERT INTO Asset(AssetCode, Name, CategoryId, DepartmentId, PurchaseDate, OriginalValue, NetValue, Status, Remark) VALUES(code, name, categoryId, departmentId, purchaseDate, originalValue, originalValue, 0, remark); int rows DbHelper.ExecuteNonQuery(sql, new SqlParameter(code, txtAssetCode.Text.Trim()), new SqlParameter(name, txtName.Text.Trim()), new SqlParameter(categoryId, cmbCategory.SelectedValue), new SqlParameter(departmentId, cmbDepartment.SelectedValue), new SqlParameter(purchaseDate, dtpPurchase.Value.Date), new SqlParameter(originalValue, nudOriginal.Value), new SqlParameter(remark, txtRemark.Text.Trim())); if (rows 0) MessageBox.Show(保存成功); }逻辑说明先用必填和长度把无效输入挡住再用ExecuteScalar查计数比把整条记录Select出来再判断更省。插入时NetValue直接等于OriginalValue因为新资产还没开始折旧Status固定写0表示在库。原值输入控件用NumericUpDown而不是TextBox能天然挡住非数字输入。参数说明SqlParameter里的参数名和SQL占位符必须完全一致这是靠名字匹配而不是靠顺序所以SQL里字段顺序调整时不用担心参数错位。PurchaseDate传的是.Value.Date把时间部分截掉只存日期。4.2 领用与归还状态更新和记录写入必须被事务绑在一起领用本质上是两件事把资产状态改为“在用”同时插入一条UsageRecord。两件事缺一不可用事务保证一致性。如果不用事务会出现“状态改成在用了但没记录”或者“有记录但状态还在库”的半截数据。public bool UseAsset(int assetId, string userName, int departmentId) { string sqlCheck SELECT Status FROM Asset WHERE Id assetId; object status DbHelper.ExecuteScalar(sqlCheck, new SqlParameter(assetId, assetId)); if (status null) return false; if (Convert.ToInt32(status) ! 0) return false; using (SqlConnection conn new SqlConnection(DbHelper.ConnStr)) { conn.Open(); SqlTransaction trans conn.BeginTransaction(); try { string sqlUpdate UPDATE Asset SET Status 1 WHERE Id assetId AND Status 0; string sqlInsert INSERT INTO UsageRecord(AssetId, UserName, DepartmentId, UseDate) VALUES(assetId, userName, departmentId, GETDATE()); SqlCommand cmd new SqlCommand(sqlUpdate, conn, trans); cmd.Parameters.AddWithValue(assetId, assetId); int rows cmd.ExecuteNonQuery(); if (rows ! 1) throw new Exception(资产状态已变化请刷新后重试); cmd new SqlCommand(sqlInsert, conn, trans); cmd.Parameters.AddWithValue(assetId, assetId); cmd.Parameters.AddWithValue(userName, userName); cmd.Parameters.AddWithValue(departmentId, departmentId); cmd.ExecuteNonQuery(); trans.Commit(); return true; } catch { trans.Rollback(); return false; } } }逻辑说明事务开始前先用Select校验一次状态UPDATE里再带一次AND Status 0做并发下的二次判断。ExecuteNonQuery返回值是受影响行数只有等于1才算真的更新成功等于0说明别的操作抢先改了状态直接抛异常回滚。插入和更新在同一事务里任何一步失败另一部也会被一起撤销。参数说明AddWithValue在参数类型上不够精确遇到NVARCHAR字段偶尔会推断成NVARCHAR(4000)导致索引失效。如果要对上扣这个细节用new SqlParameter(userName, SqlDbType.NVarChar, 50) { Value userName }显式指定长度。课程设计数据量小影响不大但答辩时能说出来就显得功底不一样。归还的逻辑是镜像的把状态改回0同时把当前未关闭的记录填上ReturnDate。注意一个关键条件public bool ReturnAsset(int assetId) { using (SqlConnection conn new SqlConnection(DbHelper.ConnStr)) { conn.Open(); SqlTransaction trans conn.BeginTransaction(); try { string sqlUpdate UPDATE Asset SET Status 0 WHERE Id assetId AND Status 1; string sqlReturn UPDATE UsageRecord SET ReturnDate GETDATE() WHERE AssetId assetId AND ReturnDate IS NULL; SqlCommand cmd new SqlCommand(sqlUpdate, conn, trans); cmd.Parameters.AddWithValue(assetId, assetId); int rows cmd.ExecuteNonQuery(); if (rows ! 1) throw new Exception(资产不是在用状态无法归还); cmd new SqlCommand(sqlReturn, conn, trans); cmd.Parameters.AddWithValue(assetId, assetId); cmd.ExecuteNonQuery(); trans.Commit(); return true; } catch { trans.Rollback(); return false; } } }逻辑说明ReturnDate IS NULL这个条件很容易被漏掉。如果不加它一次归还会把这台资产所有历史记录的ReturnDate全部覆盖加了它只会关闭当前这一条“未归还”记录。如果同一资产被连续领用多次只有ReturnDate为空的那条才是要关闭的思路就清楚了。4.3 折旧计算平均年限法写成带去重的批量计提固定资产折旧通常用平均年限法公式是月折旧额 原值 × (1 - 残值率) / (使用年限 × 12)。残值率一般取5%使用年限按资产类别而定课程设计里固定写5年就行。批量计提要考虑两件事一是净值不能折到残值以下二是同一个月份不能重复计提。public void CalculateDepreciation(DataTable assetTable) { decimal rate 0.05m; int years 5; int months years * 12; string currentMonth DateTime.Now.ToString(yyyyMM); foreach (DataRow row in assetTable.Rows) { if (row[LastDepreciationMonth] ! DBNull.Value row[LastDepreciationMonth].ToString() currentMonth) continue; decimal originalValue Convert.ToDecimal(row[OriginalValue]); decimal netValue Convert.ToDecimal(row[NetValue]); decimal monthly originalValue * (1 - rate) / months; decimal newNet netValue - monthly; if (newNet originalValue * rate) newNet originalValue * rate; string sql UPDATE Asset SET NetValue netValue, LastDepreciationMonth month WHERE Id id; DbHelper.ExecuteNonQuery(sql, new SqlParameter(netValue, newNet), new SqlParameter(month, currentMonth), new SqlParameter(id, Convert.ToInt32(row[Id]))); } }逻辑说明当月已经计提过的资产直接跳过这是靠LastDepreciationMonth实现的它存的是“202406”这种年月串。每月净额递减净值最低只能到残值残值等于原值乘残值率不能折成负数。如果资产中途提前报废净值应该一次性清零那是另一个流程不放在这个批量方法里。参数说明rate和years也可以做成界面可配置存进数据库配置表但课程设计里写死反而好讲。答辩老师问为什么是5年你回答“参考企业会计估计的常用年限”就已经够了。如果要更规范可以在Asset表里给不同分类设置不同年限但这一版的表结构已经能撑起核心演示了。4.4 台账导出不用第三方库的CSV方案与编码坑核心功能都落地后一个导出功能能让整个系统的完整度提升一截。导出不一定要用Office组件CSV就能被Excel直接打开而且不依赖目标机器装没装Office。这里用一个SaveFileDialog配合字符串拼接实现private void btnExport_Click(object sender, EventArgs e) { SaveFileDialog sfd new SaveFileDialog(); sfd.Filter CSV文件|*.csv; if (sfd.ShowDialog() ! DialogResult.OK) return; DataTable dt DbHelper.ExecuteDataTable( SELECT AssetCode AS 资产编号, Name AS 资产名称, OriginalValue AS 原值, NetValue AS 净值, Status AS 状态 FROM Asset); StringBuilder sb new StringBuilder(); foreach (DataColumn col in dt.Columns) sb.Append(col.ColumnName).Append(,); sb.AppendLine(); foreach (DataRow row in dt.Rows) { for (int i 0; i dt.Columns.Count; i) { sb.Append(row[i]).Append(,); } sb.AppendLine(); } File.WriteAllText(sfd.FileName, sb.ToString(), new UTF8Encoding(true)); MessageBox.Show(导出完成); }逻辑说明SQL查询时直接用AS把列名改成中文导出文件的第一行就是表头。DataColumn遍历生成表头DataRow遍历生成数据行CSV的结构就是“每行一条记录逗号分隔”。导出后双击文件Excel能直接打开。编码是这个方案的第一个坑Excel打开UTF-8无BOM的CSV时会把中文显示成乱码所以要显式new UTF8Encoding(true)带BOM写文件。另一个坑是字段内容里如果含逗号或引号CSV会被错切课程设计数据一般不会出现这种情况真遇到就替换成中文逗号属于可接受的简化。5. 源码调试避坑指南五个让你反复查代码的经典场景5.1 DataGridView绑定不上数据不是数据源空就是列名对不上现象DataGridView设计好了列运行时整个表格是白的一行数据都没有也不报错。原因最常见两种。一是查询结果本身是空的SQL条件写错或者表里就没数据二是DataGridView列的DataPropertyName和DataTable的列名对不上数据其实查到了但绑不上。解决先在SQL Server管理工具里跑一遍SQL确认有数据再回到代码。然后逐列检查DataGridView的DataPropertyName必须和DataTable列名完全一致。省事的做法是在窗体Load里先把DataTable绑上去让DataGridView自动生成列再按需隐藏比手工配列稳得多。这个场景对C#入门阶段的同学几乎是必经之路记住一个判断原则没报错就有救从“有没有数据”和“列名对不对”两头一起查。5.2 LocalDB连不上或文件被占用先查连接串再查实例名现象程序第一次跑正常第二次跑报“无法附加数据库”或者把项目从A机器拷到B机器后连不上。原因LocalDB的mdf文件被上一次连接占用或者AttachDbFilename里的路径在B机器上不存在。还有一种是自己装了独立SQL Server却把Data Source写成了.\SQLEXPRESS而本机实例实际是LocalDB。解决把mdf的“复制到输出目录”设为“如果较新则复制”确认bin目录下真的生成了这个文件。再把连接串里的|DataDirectory|换成绝对路径试一次能通就说明是路径解析问题。如果是文件占用不要直接在任务管理器里乱杀sqlservr.exe那会把本机其他SQL Server服务一起干掉。正确做法是打开命令行执行sqllocaldb stop MSSQLLocalDB再sqllocaldb start MSSQLLocalDB重启实例。5.3 中文乱码字段类型和排序规则要统一现象界面输入中文保存后再查出来变成“?”或乱码。原因字段类型不是NVARCHAR。VARCHAR在中文Windows下走的是GBK编码写入端和读取端只要有一环不匹配中文字符就保不住。另外SQL Server的排序规则如果建库时选错也会出现排序和显示不一致的问题。解决所有字符串字段统一用NVARCHAR这是血泪经验。已经建好的表把VARCHAR字段改成NVARCHAR再重建数据不要想着靠连接串加什么Encoding去解决——SqlConnection不认这个关键字加了反而报错。数据已经乱了也别心疼删掉重插越早统一类型越省事。5.4 领用了状态没变但记录多了一条忘了事务现象第一次写领用功能时先更新状态再插入记录两步分别执行。结果前面那步成功了后面那步报错资产状态变了但记录没有或者反过来。原因没有把两次数据库写操作放进同一个事务。任何一步失败另一步不会自动回滚。很少有人能一次写对这个坑几乎每个人都会踩一遍。解决回到第4章的事务模板把两个SqlCommand挂到同一个SqlTransaction上。判定标准很简单一件事里只要包含多次数据库写操作无条件进事务。答辩时主动说一句“这里用事务保证了一致性”是标准加分句。5.5 折旧把净值折成负数缺残值下限和月份去重现象点了多次批量计提净值一路往下减最后变成负数。原因折旧代码里没有设置净值下限。财务逻辑上净值最多折到残值残值 原值 × 残值率。另外批量计提没有判断“这个月是不是已经提过了”同一批资产被反复扣减。解决回写前加一行判断if (newNet originalValue * rate) newNet originalValue * rate;净值永远不会低于残值。月份去重要靠第3章预留的LastDepreciationMonth字段每次计提后把当期月份写进去下一次执行时发现月份相同就跳过。这两个问题不解决数据一乱演示当场翻车。6. 答辩前夜的验证顺序把演示路径固定成一条业务主线答辩演示最忌临场乱点。我习惯把演示路径固定成一条业务主线先登记一台新资产再领用出去归还执行一次折旧最后导出台账。这条线正好覆盖核心功能老师跟着你的操作走思路不会散。步骤演示动作预期结果对应功能1新增分类“电子设备”、部门“技术部”下拉框出现新选项基础数据2登记资产编号A001原值5000台账出现一行净值为5000资产登记3再次登记A001弹出“该资产编号已存在”业务校验4把A001领用给张三状态变为“在用”领用记录新增一条领用管理5执行一次折旧净值变为5000 × (1 - 5%) / 60 ≈ 79.17扣减后是4920.83折旧计算6归还A001状态回到“在库”ReturnDate写入归还管理7导出CSV并用Excel打开中文不乱码台账导出演示前还要做三个静态检查查数据库连接串里的路径在本机是否有效确认所有下拉框的SelectedValue不会落在空值上把bin目录里的exe直接双击运行一次而不是依赖VS调试器启动。很多项目在VS里能跑拔出VS就崩多半是mdf没复制过去或者路径写死这个细节在答辩现场很容易让人措手不及。有一个加分小技巧在界面上留一个只读文本框显示当前时间和当前操作人。系统虽然没做登录模块但这个区域能暗示“权限扩展点预留在这里”。老师问起来你就说用全局变量模拟了当前用户后续可以接Login表做权限控制。这个说法诚实而且不丢分。最后说个我每届都强调的习惯把SQL建表脚本、源码包、答辩PPT三样东西放进同一个目录SQL里每行都写上注释。老师拷走源码时第一眼看的就是工程整洁度这比代码里多一个花哨动画实在得多。这个习惯我是吃了不少亏才养成的希望帮到你。本文还有配套的精品资源点击获取
返回列表