ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

电力收费数据库实战模板:SQL Server建模与触发器工程实践

电力收费数据库实战模板:SQL Server建模与触发器工程实践 简介本资源是一份面向高校数据库课程初学者的完整课程设计报告聚焦电力公司收费管理信息系统的数据库建模与实现适用于数据库原理、SQL编程及OracleC#综合实践教学场景。文档详细涵盖系统需求分析、E-R模型设计、6张核心数据表客户、用电类型、员工、用电信息、费用管理、收费登记的结构定义与SQL建表语句、外键约束与关系说明并实现了触发器自动更新收费标志与结余、存储过程月度应收费统计、欠费用户查询及规则月份格式校验等高级数据库对象。资源为单个261KB的Word文档.doc内容包含系统概要设计、数据流程图、功能模块图、程序流程图、完整SQL脚本及实验环境配置VS2023SQL Server2023结构规范、注释清晰可直接用于课程报告提交或教学参考。目前已有239人学习下载是掌握数据库设计全流程、理解业务逻辑到SQL落地转化的典型范例。1. 这不是一份“交差作业”而是一套能跑通、能改、能上线的电力收费数据库实战模板你手头这份《数据库课程设计电力公司收费系统.doc》表面看是高校课程设计文档但拆开来看——它是一份完整闭环的行业级收费业务建模样本从客户档案、用电分类、度数采集到费用生成、收费登记、结余计算再到触发器自动更新、存储过程批量查询、规则约束月份格式全链路用 SQL Server C# 实现。它不依赖 Oracle原文虽提 Oracle但所有 DDL/DML/触发器/存储过程代码均基于 SQL Server 语法也不靠虚构数据糊弄而是用真实字段名客户号、类别号、月份、真实业务逻辑“未收→已收”状态流转、“应收费用-实收费用结余”、真实约束PRIMARY KEY (客户号,类别号,月份)扎扎实实搭出一个可运行的最小可行系统。适合三类人大三学生照着文档建库、写触发器、连 C# 窗体两周内交出有交互、有逻辑、有错误提示的完整系统刚入职的 DBA 或后端开发把用电信息→费用管理→收费登记→结余登记这条数据流当教科书理解“业务动作如何驱动数据库状态变更”小型供电所或物业收费员删掉员工号表、简化用电类型直接拿去改成本地收费台账——我见过乡镇电管站真用这套结构导出 Excel 做月度报表。关键不在“课程设计”四个字而在它把抽象的数据库理论范式、触发器、存储过程焊死在电费这个具体场景里电价×度数费用、收费后自动改标志、未缴费客户按结余排序催收……每一步都有 SQL 可查、有窗体可点、有错误可 debug。这不是玩具是带血丝的实战切片。2. 从 E-R 图到七张表为什么这样建模字段怎么选主键为何必须复合2.1 业务实体拆解客户、用电类型、员工不是并列关系而是分层依赖课程设计里列出的七张表客户、用电类型、员工、用电信息、费用管理、收费登记、结余登记表面平铺实则存在三层依赖顶层静态主数据客户唯一标识用户、用电类型定义电价规则、员工操作人——这三张表独立存在无外键依赖中层业务事实表用电信息记录某客户某月某类用电多少度——它同时依赖客户客户号外键和用电类型类别号外键是连接静态数据与动态费用的枢纽底层状态流转表费用管理生成费用标记收/未收、收费登记记录谁收了多少钱、结余登记计算差额——它们全部依赖用电信息的产出且彼此间存在强时序先有用电度数 → 才能算费用 → 才能登记收费 → 才能算结余。提示这种分层不是教科书硬套而是业务本质决定的。比如用电类型的电价字段必须是money类型非float因为电费计算要求精确到分SQL Server 的money类型内部用 4 字节整数存储万分之一元避免浮点误差。你若改成decimal(10,2)也行但float会导致100.5 * 1.23 123.61499999999999这种玄学结果。2.2 主键设计为什么用电信息必须用(客户号,类别号,月份)复合主键看原文建表语句Create table 用电信息( 客户号 char(5), 类别号 char(10), 月份 date, 用电度数 char(8), primary key (客户号,类别号,月份), foreign key (客户号) references 客户(客户号), foreign key (类别号) references 用电类型(类别号) );这里primary key (客户号,类别号,月份)是铁律原因有三业务唯一性同一客户在同一个月可能使用多种用电类型如家庭商铺或同一类型在不同月份用量不同单字段主键无法区分外键完整性费用管理表的主键(客户号,月份)是它的子集收费登记同理——复合主键让下游表能精准锚定到某次用电行为而非模糊的“某客户某月”索引效率SQL Server 对复合主键自动生成聚集索引查询WHERE 客户号00001 AND 类别号ABC AND 月份2023-12-01时B树直接定位比WHERE 客户号00001 AND 月份2023-12-01需扫描该客户所有类别快一个数量级。注意用电度数字段用char(8)是历史遗留问题早期为兼容老系统文本导入实际应改为decimal(10,2)。char(8)存123.45会占满8位存5却浪费7位空格且无法做数值运算如SUM(用电度数)需先CAST。我在本地测试时已统一改为decimal(10,2)后续所有计算脚本均基于此。2.3 月份字段date类型 vs 字符串规则为什么选前者课程设计要求“月份符合格式‘××××年××月’”原文用规则Rule绑定但实操中我弃用规则改用date类型 计算列-- 原文规则已弃用 CREATE RULE month_format AS value LIKE [0-9][0-9][0-9][0-9]年[0-1][0-9]月; EXEC sp_bindrule month_format, 用电信息.月份; -- 实际采用方案推荐 ALTER TABLE 用电信息 ADD 月份_年月 AS FORMAT(月份, yyyyMM) PERSISTED; -- 持久化计算列值为202312 CREATE INDEX IX_用电信息_年月 ON 用电信息(月份_年月); -- 按年月快速聚合理由规则Rule是 SQL Server 2000 遗留特性SQL Server 2012 已废弃新环境部署会报错date类型天然支持日期函数YEAR(月份)、MONTH(月份)比字符串解析SUBSTRING(月份,1,4)更可靠FORMAT(月份, yyyyMM)计算列可直接用于分组统计如GROUP BY 月份_年月且PERSISTED属性让索引体积更小、查询更快若必须显示“2023年12月”C# 窗体用DateTime.ToString(yyyy年MM月)格式化数据库只存标准date。2.4 外键陷阱收费登记表的员工号外键为何不能设为ON DELETE CASCADE原文建表Create table 收费登记( 客户号 char(5), 月份 date, 应收费用 money, 实收费用 money, 员工号 char(5), PRIMARY KEY (客户号,月份), foreign key (员工号) references 员工(员工号) );注意foreign key (员工号) references 员工(员工号)未指定ON DELETE行为。若设为ON DELETE CASCADE删除员工时会连带清空其所有收费记录——这违反审计要求电费记录必须永久留存哪怕业务员离职。正确做法是设为ON DELETE NO ACTION默认删除员工前SQL Server 强制检查收费登记中是否存在该员工记录存在则报错逼你手动归档或转交补充ON UPDATE CASCADE员工号变更时极少见自动同步到收费登记避免数据断裂。-- 正确外键声明 ALTER TABLE 收费登记 ADD CONSTRAINT FK_收费登记_员工 FOREIGN KEY (员工号) REFERENCES 员工(员工号) ON DELETE NO ACTION ON UPDATE CASCADE;3. 触发器四连击从费用生成到结余计算每一步都踩过坑3.1 触发器 1用电信息插入时自动生成费用——为什么必须用INSERTED而非SELECT * FROM 用电信息原文触发器create trigger change_trigger1 on 用电信息 for insert as insert into 费用管理 (客户号,月份,费用) SELECT inserted.客户号,inserted.月份, inserted.用电度数*(select 电价 from 用电类型,inserted where 用电类型.类别号inserted.类别号) FROM inserted;这段代码看似正确但暗藏致命缺陷SELECT * FROM 用电类型,inserted是隐式笛卡尔积若用电类型表有 N 条记录inserted有 M 条结果集就是 N×M 行。正确写法必须显式JOINCREATE TRIGGER change_trigger1 ON 用电信息 FOR INSERT AS BEGIN INSERT INTO 费用管理 (客户号, 月份, 费用) SELECT i.客户号, i.月份, CAST(i.用电度数 AS DECIMAL(10,2)) * et.电价 AS 费用 FROM inserted i INNER JOIN 用电类型 et ON i.类别号 et.类别号; END;CAST(i.用电度数 AS DECIMAL(10,2))确保度数参与乘法时精度可控避免char转money的隐式转换误差INNER JOIN明确关联条件杜绝笛卡尔积BEGIN...END包裹多行逻辑防止SET NOCOUNT ON等设置干扰。3.2 触发器 2插入费用时自动设“未收”——为什么UPDATE不能直接写SET 收费标志未收原文create trigger change_trigger on 费用管理 for insert as update 费用管理 set 收费标志未收;这是典型翻车代码它会更新整张费用管理表的所有行而非仅刚插入的行。正确做法是关联INSERTEDCREATE TRIGGER change_trigger2 ON 费用管理 FOR INSERT AS BEGIN UPDATE fm SET 收费标志 未收 FROM 费用管理 fm INNER JOIN inserted i ON fm.客户号 i.客户号 AND fm.月份 i.月份; END;关键是INNER JOIN inserted i将更新范围严格限定在本次插入的记录上若费用管理主键是(客户号,月份)则i.客户号 i.月份能唯一定位目标行。3.3 触发器 3收费登记更新时改“已收”——为什么UPDATE语句要加WHERE条件原文create trigger change_trigger3 on 收费登记 for update as update 费用管理 set 收费标志已收 from 费用管理,inserted where 费用管理.客户号inserted.客户号 and 费用管理.月份inserted.月份;这段代码逻辑正确但存在两个隐患未处理inserted中多行情况一次更新多条收费记录时inserted是表WHERE条件必须匹配所有行未校验费用管理是否存在对应记录若收费登记插入了不存在于费用管理的(客户号,月份)UPDATE会静默失败影响行数为0但业务上应报错。加固版CREATE TRIGGER change_trigger3 ON 收费登记 FOR UPDATE AS BEGIN -- 先检查费用管理中是否存在对应记录 IF NOT EXISTS ( SELECT 1 FROM inserted i LEFT JOIN 费用管理 fm ON i.客户号 fm.客户号 AND i.月份 fm.月份 WHERE fm.客户号 IS NULL ) BEGIN UPDATE fm SET 收费标志 已收 FROM 费用管理 fm INNER JOIN inserted i ON fm.客户号 i.客户号 AND fm.月份 i.月份; END ELSE BEGIN RAISERROR(收费登记的客户号月份在费用管理表中不存在请先生成费用, 16, 1); ROLLBACK TRANSACTION; END END;3.4 触发器 4收费登记更新时写入结余——为什么结余费用 实收费用 - 应收费用而非实收费用 - 费用原文create trigger change_trigger4 on 收费登记 for update as insert into 结余登记 select inserted.客户号, inserted.月份, inserted.应收费用,inserted.实收费用, inserted.实收费用-inserted.应收费用 from inserted;这里结余费用 实收费用 - 应收费用是业务核心应收费用来自收费登记表业务员录入的应收金额费用来自费用管理表系统根据度数×电价算出的理论应收二者可能不等如减免、阶梯电价调整。结余必须反映实际操作结果而非理论值。血泪经验某次调试发现结余登记数据异常追查发现业务员在收费登记中把应收费用录成0误操作导致结余费用为负值。后来我在 C# 窗体加了校验应收费用 0 AND 实收费用 0 AND 实收费用 应收费用 * 1.2允许20%浮动再提交到数据库。4. 存储过程与 C# 窗体如何让 SQL 逻辑真正被用户看见4.1 存储过程ch_procedure01查指定月份应收/实收——为什么参数用date而不用varchar原文create procedure ch_procedure01 month date as begin select 月份,应收费用,实收费用 from 收费登记 where 收费登记.月份month end用date类型传参是正确选择原因避免字符串解析开销若用month varchar(10)需WHERE 月份 CAST(month AS date)每次调用都执行转换利用索引月份列是date类型WHERE 月份month可走索引WHERE 月份CONVERT(date,month)则无法使用索引防注入date参数天然过滤非法字符varchar参数若拼接进动态 SQL 易遭注入虽此处未用动态 SQL但习惯要养。调用示例C# 中// 正确传 DateTime 对象 cmd.Parameters.Add(month, SqlDbType.Date).Value dateTimePicker1.Value.Date; // 错误传字符串 2023-12-01需额外转换 cmd.Parameters.Add(month, SqlDbType.VarChar).Value 2023-12-01;4.2 存储过程ch_procedure02查未缴费客户——为什么ORDER BY 结余费用要加DESC原文create procedure ch_procedure02 month date as begin select 客户号,月份,结余费用 from 结余登记 where 结余登记.月份month order by 结余费用 endORDER BY 结余费用默认升序ASC即结余最少的排前面。但催费场景需要结余最大欠费最多的客户优先处理所以必须ORDER BY 结余费用 DESC否则窗体显示时客户号00001欠费1000元排在最后业务员一眼看不到重点。我在Form5_Load中调用此存储过程后还加了高亮// C# 中对欠费 500 的行标红 foreach (DataGridViewRow row in dataGridView1.Rows) { if (row.Cells[结余费用].Value ! null Convert.ToDecimal(row.Cells[结余费用].Value) 500) { row.DefaultCellStyle.BackColor Color.LightCoral; } }4.3 C# 窗体数据操作为什么button1_Click的插入逻辑要两次查询原文插入代码核心逻辑先SELECT * FROM 用电信息 WHERE ...检查是否重复若无重复再INSERT INTO 用电信息 VALUES (...)。这是必须的业务校验而非冗余操作。原因用电信息主键是(客户号,类别号,月份)但 C# 窗体未对这三个字段做组合唯一性校验textBox1、textBox2、textBox3是独立输入框用户可能输入客户号00001、类别号ABC、月份2023-12-01而该记录已存在直接INSERT会因主键冲突报错用户体验差先查后插能在 UI 层提前拦截弹出MessageBox.Show(已经存在)比数据库报错更友好。但原文代码有严重漏洞strSqls string.Format(SELECT * FROM 用电信息 where 客户号 textBox1.Text.Trim() and 类别号 textBox2.Text.Trim() and 月份 textBox3.Text.Trim() ;);这是典型的SQL 注入温床textBox1.Text若输入 OR 11整个WHERE条件恒真。修复方案// 正确用参数化查询 string checkSql SELECT COUNT(1) FROM 用电信息 WHERE 客户号custId AND 类别号typeId AND 月份month; SqlCommand checkCmd new SqlCommand(checkSql, sqlcon); checkCmd.Parameters.Add(custId, SqlDbType.Char, 5).Value textBox1.Text.Trim(); checkCmd.Parameters.Add(typeId, SqlDbType.Char, 10).Value textBox2.Text.Trim(); checkCmd.Parameters.Add(month, SqlDbType.Date).Value DateTime.Parse(textBox3.Text.Trim()).Date; int count (int)checkCmd.ExecuteScalar(); if (count 0) { MessageBox.Show(已经存在, 信息提醒, MessageBoxButtons.OK, MessageBoxIcon.Error); return; }4.4 修改逻辑的致命缺陷button2_Click中oldDate和oldLeibie取值方式为何危险原文修改代码string oldDate dataGridView1.Rows[tnum].Cells[月份].Value.ToString(); string oldLeibie dataGridView1.Rows[tnum].Cells[类别号].Value.ToString(); // ... strSqls string.Format(update 用电信息 set 类别号...,月份...,用电度数... where 客户号... and 类别号 oldLeibie and 月份 oldDate ;);问题在于dataGridView1.Rows[tnum].Cells[月份].Value返回的是DateTime对象ToString()默认格式为2023/12/1 0:00:00而数据库月份字段是date类型值为2023-12-01字符串比较必然失败oldLeibie若含空格或单引号如类别号AB C拼接 SQL 会语法错误。正确做法// 从 DataGridView 获取原始值非字符串 DateTime oldDate (DateTime)dataGridView1.Rows[tnum].Cells[月份].Value; string oldLeibie dataGridView1.Rows[tnum].Cells[类别号].Value.ToString().Trim(); // 参数化 UPDATE string updateSql UPDATE 用电信息 SET 类别号newType, 月份newMonth, 用电度数newKwh WHERE 客户号custId AND 类别号oldType AND 月份oldMonth; SqlCommand updateCmd new SqlCommand(updateSql, sqlcon); updateCmd.Parameters.Add(newType, SqlDbType.Char, 10).Value textBox2.Text.Trim(); updateCmd.Parameters.Add(newMonth, SqlDbType.Date).Value DateTime.Parse(textBox3.Text.Trim()).Date; updateCmd.Parameters.Add(newKwh, SqlDbType.Decimal).Value decimal.Parse(textBox4.Text.Trim()); updateCmd.Parameters.Add(custId, SqlDbType.Char, 5).Value textBox1.Text.Trim(); updateCmd.Parameters.Add(oldType, SqlDbType.Char, 10).Value oldLeibie; updateCmd.Parameters.Add(oldMonth, SqlDbType.Date).Value oldDate.Date; // .Date 去掉时间部分5. 避坑指南那些让课程设计卡住三天的常见问题与血泪解法5.1 现象SQL Server 连接字符串报错 “Login failed for user xxx”原因课程设计文档中连接字符串Data SourceGXW-PC;Initial Catalogliqiuyue0;Integrated SecurityTrue里的GXW-PC是作者电脑名liqiuyue0是作者数据库名你的机器既没有GXW-PC这台服务器也没有liqiuyue0这个库。解决打开 SQL Server Management Studio (SSMS)右键服务器 → 属性 → “常规”页查看“服务器名称”通常是你的电脑名\SQLEXPRESS或localhost在 SSMS 中新建数据库命名为dianfei中文名易出错用英文连接字符串改为Data Sourcelocalhost\\SQLEXPRESS;Initial Catalogdianfei;Integrated SecurityTrue\\是转义实际写localhost\SQLEXPRESS若用 Windows 身份验证失败改用 SQL Server 身份验证Data Sourcelocalhost\\SQLEXPRESS;Initial Catalogdianfei;User IDsa;Password你的密码。5.2 现象触发器执行后费用管理表数据为空或收费标志始终为 NULL原因费用管理表建表时未设收费标志默认值且触发器change_trigger2在INSERT后才更新若INSERT语句本身没给收费标志赋值该字段为NULL触发器更新时UPDATE语句找不到NULL值的行WHERE 收费标志NULL永假。解决建表时强制设默认值Create table 费用管理( 客户号 char(5), 月份 date, 费用 money, 收费标志 varchar(50) DEFAULT 未收, -- 关键加 DEFAULT PRIMARY KEY (客户号,月份), foreign key (客户号) references 客户(客户号) );或在触发器中用ISNULL安全更新UPDATE fm SET 收费标志 未收 FROM 费用管理 fm INNER JOIN inserted i ON fm.客户号i.客户号 AND fm.月份i.月份 WHERE ISNULL(fm.收费标志, ) 未收;5.3 现象C# 窗体点击按钮无反应或dataGridView1不显示数据原因Form5_Load事件中if (ds.Tables[0].Rows.Count 1)判断错误——Rows.Count为 0 时表为空为 1 时有一行1意味着至少两行才显示导致单行数据不渲染。解决改为if (ds.Tables[0].Rows.Count 0)只要有表就绑定更稳妥写法if (ds.Tables.Count 0 ds.Tables[0].Rows.Count 0) { dataGridView1.DataSource ds.Tables[0]; } else { dataGridView1.DataSource null; // 清空旧数据 MessageBox.Show(暂无数据); }5.4 现象插入用电信息后费用管理表没新增记录触发器change_trigger1无声失效原因触发器中SELECT inserted.客户号,inserted.月份,... FROM inserted语句若inserted表为空如INSERT语句因外键约束失败回滚触发器不会执行或用电类型表中类别号与inserted.类别号不匹配大小写、空格JOIN结果为空。解决在 SSMS 中手动测试触发器-- 先确认用电类型存在 SELECT * FROM 用电类型 WHERE 类别号 ABC; -- 再模拟插入 INSERT INTO 用电信息 (客户号,类别号,月份,用电度数) VALUES (00001,ABC,2023-12-01,100); -- 查看费用管理 SELECT * FROM 费用管理 WHERE 客户号00001 AND 月份2023-12-01;在触发器开头加日志临时CREATE TRIGGER change_trigger1 ON 用电信息 FOR INSERT AS BEGIN -- 临时日志检查 inserted 内容 SELECT 触发器启动inserted 行数 CAST((SELECT COUNT(*) FROM inserted) AS VARCHAR(10)); INSERT INTO 费用管理 (客户号,月份,费用) SELECT i.客户号,i.月份,CAST(i.用电度数 AS DECIMAL(10,2)) * et.电价 FROM inserted i INNER JOIN 用电类型 et ON i.类别号 et.类别号; END;5.5 现象存储过程 ch_procedure01执行返回空结果集但收费登记表明明有数据原因month参数传入的是DateTime.Now含时分秒而收费登记.月份是date类型只有年月日WHERE 月份month比较时month的时间部分导致不匹配。解决C# 调用时传dateTimePicker1.Value.Date.Date属性截取日期部分或在存储过程中强制截断ALTER PROCEDURE ch_procedure01 month date AS BEGIN DECLARE monthOnly date CAST(month AS date); -- 确保是纯日期 SELECT 月份,应收费用,实收费用 FROM 收费登记 WHERE CAST(月份 AS date) monthOnly; -- 两边都转 date END;6. 进阶技巧把课程设计变成可维护的生产级收费台账6.1 用视图替代重复查询一键生成月度收费汇总报表课程设计只要求查单月数据但实际业务需看“某月各用电类型总用电量/总费用”。手写 SQL 太累建视图一劳永逸CREATE VIEW v_月度收费汇总 AS SELECT ei.月份, et.类别名, COUNT(*) AS 客户数, SUM(ei.用电度数) AS 总用电度数, SUM(fm.费用) AS 总应收, SUM(pd.实收费用) AS 总实收, SUM(pd.实收费用) - SUM(fm.费用) AS 净结余 FROM 用电信息 ei INNER JOIN 用电类型 et ON ei.类别号 et.类别号 INNER JOIN 费用管理 fm ON ei.客户号 fm.客户号 AND ei.月份 fm.月份 INNER JOIN 收费登记 pd ON ei.客户号 pd.客户号 AND ei.月份 pd.月份 GROUP BY ei.月份, et.类别名;调用-- 查 2023 年 12 月汇总 SELECT * FROM v_月度收费汇总 WHERE 月份 2023-12-01;从那以后我每次交付课程设计都强制在最后加一个v_XXX视图——它不增加代码量却让老师一眼看到“这学生懂业务不是堆砌语法”。而且视图可直接绑定到 C# 的DataGridView比写SELECT语句快十倍。6.2 用 CHECK 约束替代规则让“××××年××月”校验更健壮原文用Rule绑定月份格式但Rule已废弃。改用CHECK约束且支持更严苛校验-- 删除旧规则若存在 IF EXISTS (SELECT * FROM sys.rules WHERE name month_format) DROP RULE month_format; -- 为 用电信息.月份 添加 CHECK 约束 ALTER TABLE 用电信息 ADD CONSTRAINT CK_用电信息_月份格式 CHECK (YEAR(月份) BETWEEN 2000 AND 2100 AND MONTH(月份) BETWEEN 1 AND 12);YEAR(月份) BETWEEN 2000 AND 2100防止录入0001-01-01这类无效年份MONTH(月份) BETWEEN 1 AND 12确保月份合法CHECK约束在INSERT/UPDATE时实时生效比应用层校验更可靠。6.3 C# 窗体增强添加 Excel 导出功能让课程设计真能用课程设计文档没提导出但老师最想看到“这系统能干实事”。加一个按钮导出当前dataGridView1数据到 Excelprivate void btnExportExcel_Click(object sender, EventArgs e) { if (dataGridView1.Rows.Count 0) return; Microsoft.Office.Interop.Excel.Application excel new Microsoft.Office.Interop.Excel.Application(); excel.Visible false; Workbook wb excel.Workbooks.Add(XlWBATemplate.xlWBATWorksheet); Worksheet ws (Worksheet)wb.ActiveSheet; // 写表头 for (int i 0; i dataGridView1.Columns.Count; i) { ws.Cells[1, i 1] dataGridView1.Columns[i].HeaderText; } // 写数据 for (int i 0; i dataGridView1.Rows.Count; i) { for (int j 0; j dataGridView1.Columns.Count; j) { ws.Cells[i 2, j 1] dataGridView1.Rows[i].Cells[j].Value?.ToString() ?? ; } } // 保存 SaveFileDialog sfd new SaveFileDialog(); sfd.Filter Excel files (*.xlsx)|*.xlsx; sfd.FileName $电费数据_{DateTime.Now:yyyyMMddHHmmss}.xlsx; if (sfd.ShowDialog() DialogResult.OK) { wb.SaveAs(sfd.FileName); MessageBox.Show(导出成功); } wb.Close(); excel.Quit(); }注意需在项目引用中添加Microsoft.Office.Interop.ExcelCOM 组件发布时目标机器需装 Office。若不想依赖 Office可用EPPlusNuGet 包轻量、免 Office代码更简洁。希望帮到你。本文还有配套的精品资源点击获取
返回列表