简介基于C#的理发会员管理系统是一套面向高校计算机科学、软件工程、通信工程专业学生课程设计与毕业设计的完整项目。系统实现了会员信息添加、修改、删除与查询理发预约时间与理发师分配消费明细记录积分累计与兑换营业报表统计以及管理员与普通用户权限管理等功能贴合中小型理发店的实际经营流程。压缩包内共有二百一十个文件整体大小约二点六三兆字节其中包括C#语言编写的窗体源代码、界面资源文件、可直接运行的程序文件、依赖运行库文件同时提供了数据库文件以及数据库设计文档便于对照学习与二次开发。目前已有二百八十一人学习或下载。借助该资源学习者能够完整走一遍C#桌面应用从需求分析、数据库建模到界面编码、运行测试的流程重点理解会员表、预约表、消费记录表之间的关系设计也能为毕业设计或课程设计提供切实可用的模板与排错经验。1. 从 zip 到能跑的会员系统这类 C# 项目到底解决什么问题如果你下载过这种“基于C#的理发会员管理系统含数据库设计文档.zip”大概率遇到过同一个场景解压后源码能打开SQL 脚本却跑不通或者界面跑起来了一充值就翻车。说句实话这类项目在大作业、毕业设计和外包交付里出现频率极高核心价值就两样一段能操作的 WinForms 界面和一份能把会员、储值、消费流水解释清楚的数据库设计文档。它解决的问题非常具体办卡、查余额、充值、扣费、打流水全是理发店这种单店或小连锁的日常痛点。适合谁正在做管理信息系统课设的新手以及要从零搭一个小型会员系统、又不想在表结构上动太多脑筋的开发者和门店系统维护者。这类项目的难点从来不在界面上而在“钱”。余额怎么算、流水怎么记、操作员和会员的权限边界在哪、数据库设计文档里的表结构和代码里 SqlCommand 写的是不是一回事才是你拿到 zip 之后最该先看的东西。下面直接按这个顺序拆开讲。2. 拆解数据流向先定会员表、储值表、流水表的边界拿到数据库设计文档很多人第一件事是打开建表语句直接执行但真正值得花时间的是先把数据边界理清。理发店的会员系统核心不是“会员叫什么名字”而是“卡里还有多少钱、这钱是怎么变的”。常见做法是拆成四张核心表会员档案、储值账户、消费流水、充值流水配套再一张操作员日志表。这个拆分不是拍脑袋而是把“低频档案”和“高频金额变动”分开避免一张表既被页面查询频繁读取又被收银事务反复锁行。2.1 会员表与储值账户表先分清“档案”和“钱”最直观的设计是把余额字段直接放在会员表里比如在 Member 表上放一个 Balance 列。这种做法在小门店起步时没什么问题但一旦你需要统计“这个月累计充值多少、累计消费多少”或者要按时间段回滚历史账单就会非常被动。更常见也更稳妥的做法是拆成两张表Member 只负责会员是谁MemberAccount 负责钱怎么变。CREATE TABLE Member ( MemberId INT IDENTITY(1,1) PRIMARY KEY, CardNo VARCHAR(20) NOT NULL UNIQUE, Name NVARCHAR(50) NOT NULL, Phone VARCHAR(20) NULL, Gender TINYINT DEFAULT 0, JoinDate DATETIME DEFAULT GETDATE(), Status TINYINT DEFAULT 1, Remark NVARCHAR(200) NULL ); CREATE TABLE MemberAccount ( AccountId INT IDENTITY(1,1) PRIMARY KEY, MemberId INT NOT NULL, Balance DECIMAL(10,2) NOT NULL DEFAULT 0, TotalCharge DECIMAL(10,2) NOT NULL DEFAULT 0, TotalConsume DECIMAL(10,2) NOT NULL DEFAULT 0, LastUpdateTime DATETIME DEFAULT GETDATE(), CONSTRAINT FK_Account_Member FOREIGN KEY (MemberId) REFERENCES Member(MemberId) );代码里的几个参数值得说明一下。DECIMAL(10,2) 是金额字段的标准选型10 位总长度、2 位小数足够存百万级金额最重要的是不会像 FLOAT 那样出现精度漂移卡号CardNo 加 UNIQUE 约束是为了防止同一张卡被重复开出来Status 字段我习惯用作逻辑删除比如退卡时把 Status 置 0而不是物理删掉 Member 行否则历史流水关联不到人了。卡号生成一般用“年份流水号”比如 2025-0001在代码里查当天最大流水号再加一虽然并发量极低也建议在写卡号前先查一次唯一约束冲突给用户一个友好提示。把档案和资金拆开还有一个额外好处对账的时候可以直接对 MemberAccount 表做 SUM而不需要每次都 JOIN 流水表聚合。会员表的数据变动频率很低储值账户表每次收银都要更新分表后查询缓存也能分得更细这在门店收银高峰期有明显体感。2.2 消费流水与操作员日志轧账对不上的时候看这里有了账户表还需要记录每一笔金额变动。这就是消费流水和充值流水的职责它们的基本逻辑是只追加、不修改。和银行流水一个道理你今天消费了一笔发现当时收银员录错了金额补救方式不是去 UPDATE 那一条流水而是另外做一笔“更正”记录把差额退回账户。这种设计在数据库设计文档里写起来非常简单但真正落地时很多新手会忍不住去改历史流水这是日后对账最大的坑。CREATE TABLE ConsumeRecord ( ConsumeId INT IDENTITY(1,1) PRIMARY KEY, MemberId INT NOT NULL, ProductId INT NULL, Amount DECIMAL(10,2) NOT NULL, BalanceAfter DECIMAL(10,2) NOT NULL, OperatorId INT NOT NULL, ConsumeTime DATETIME DEFAULT GETDATE(), CONSTRAINT FK_Consume_Member FOREIGN KEY (MemberId) REFERENCES Member(MemberId) );这里有一个常被忽视的字段BalanceAfter也就是本次消费后的账户余额。它的作用在于门店和顾客发生纠纷时不需要从第一条流水开始累加推算直接看这一行就知道当时卡里还剩多少。数据库设计文档里如果写了这个字段说明建模的人对账务审计是有意识的。充值流水 ChargeRecord 的结构和消费流水基本对称多一个 PayMethod 字段记录现金、微信还是支付宝以便月底核对平台账单。操作员日志单独拎出来一张表别把日志塞在业务表里。开卡、改价、手动调整余额这类高风险操作都需要记录是谁在什么时间做的。也不需要做得太重OperatorId、ActionType、Description、LogTime 四个字段就够用。表职责变更频率生命周期Member会员档案低长期保存逻辑删除MemberAccount储值账户高与会员同生命周期ChargeRecord充值流水高只追加ConsumeRecord消费流水高只追加2.3 主外键与约束设计数据库设计文档里最值得抄的两页数据库设计文档里最容易被跳过、但对运行稳定性影响最大的是约束和索引的设计。很多 zip 里的脚本只写了主键和外键业务规则全靠代码自觉结果就是界面层忘记判断余额数据库里照样出现负数。比较实用的做法是给流水表的金额字段加 CHECK 约束比如消费金额必须大于 0给 MemberAccount 的 Balance 字段也加一个 CHECK (Balance 0)作为代码判断之外的第二道防线。注意这不是让你完全依赖数据库兜底而是在收银程序出现并发异常时数据库至少能拦住最离谱的数据。同时给流水表建立非聚集索引成员 ID 加消费时间索引顺序一般是(MemberId, ConsumeTime DESC)这样查某人的历史消费记录就是一次索引范围扫描不需要全表遍历。ALTER TABLE MemberAccount ADD CONSTRAINT CK_Account_Balance CHECK (Balance 0); ALTER TABLE ConsumeRecord ADD CONSTRAINT CK_Consume_Amount CHECK (Amount 0); CREATE INDEX IX_Consume_MemberTime ON ConsumeRecord(MemberId, ConsumeTime DESC);需要提醒的是不要把所有能想到的约束都堆上去过度约束会让业务变更变得很痛苦。比如会员手机号你加了 UNIQUE结果门店录入时发现两个顾客用同一个家庭号码售后就要改表结构。我的习惯是唯一约束只给真正不允许重复的业务标识比如 CardNo状态类字段用 TINYINT 加 DEFAULT比如 Status 默认 1 表示正常和金额有关的字段必须用 DECIMAL并尽量加上 CHECK 约束。3. 三层架构落地SqlConnection、事务与 DataGridView 的配合数据库设计文档搭建完成之后接下来就是把表结构接到 C# WinForms 界面。这一步最常见的做法是把项目拆成三个工程UI 层放窗体、BLL 层放业务规则、DAL 层放数据库访问。这是很多 C# 入门项目常采用的三层结构目的很简单你以后改页面不用去翻几十个 SqlCommand改数据库连接也不用在窗体的代码隐藏文件里到处找。3.1 项目结构、连接字符串与 SqlHelper如果是自己从头建工程我会把 DAL 单独做成一个类库项目这样 UI 层只引用 BLLBLL 只引用 DAL依赖关系是单向的。连接字符串放在 UI 工程的 App.config 里而不是写死在代码里。很多同学第一次打开这种 zip 项目连不上数据库的第一反应是去改代码里的 connectionString结果把源代码改乱了。实际上连接字符串在配置文件中修改就行而且不用重新编译。connectionStrings add nameBarberDB connectionStringData Source.\SQLEXPRESS;Initial CatalogBarberDB;Integrated SecurityTrue;Connect Timeout5 providerNameSystem.Data.SqlClient / /connectionStrings连接字符串的几个参数含义Data Source 是 SQL Server 实例名本地开发一般写.\SQLEXPRESS或.\MSSQLSERVER注意那个点代表本机Initial Catalog 是数据库名要和你建库时保持一致Integrated SecurityTrue 表示用当前 Windows 账号登录数据库。如果你用的是 SQL Server 身份验证就写成User IDsa;Passwordxxx并把 Integrated Security 去掉。Connect Timeout5 是建议加上的默认 15 秒太长数据库没开启时界面卡得像个假死。DAL 层不要每一个窗体都 new 一个 SqlConnection更不要手动拼 SQL 字符串。写一个轻量 SqlHelper 是常见做法把打开连接、执行查询、返回 DataTable 封装好参数化查询直接在业务层用。下面是可用的最小封装public static class SqlHelper { private static readonly string _connStr ConfigurationManager.ConnectionStrings[BarberDB].ConnectionString; public static DataTable ExecuteTable(string sql, params SqlParameter[] parameters) { using (SqlConnection conn new SqlConnection(_connStr)) using (SqlCommand cmd new SqlCommand(sql, conn)) { cmd.Parameters.AddRange(parameters); SqlDataAdapter adapter new SqlDataAdapter(cmd); DataTable dt new DataTable(); adapter.Fill(dt); return dt; } } public static int ExecuteNonQuery(string sql, params SqlParameter[] parameters) { using (SqlConnection conn new SqlConnection(_connStr)) using (SqlCommand cmd new SqlCommand(sql, conn)) { cmd.Parameters.AddRange(parameters); conn.Open(); return cmd.ExecuteNonQuery(); } } }注意代码里的 using 用法SqlConnection 和 SqlCommand 都实现了 IDisposable用 using 包住可以保证连接关掉频繁开关也不会吃掉数据库连接池。这也是很多跑一段时间就报“连接池已满”的根因——连接没释放。3.2 会员开卡与充值参数化查询写出基本操作开卡是会员管理系统的第一个核心操作它要做的事是往 Member 表插入档案然后往 MemberAccount 表插入一行初始余额为 0 的账户数据。两件事如果拆开执行中间机器重启或网络中断就会出现有卡没账户的脏数据。落地时我习惯在一个事务里完成或者干脆用一条 SQL 把档案和账户一起插再用 SCOPE_IDENTITY() 把会员号带出来。public int AddMember(MemberInfo m) { string sql INSERT INTO Member(CardNo, Name, Phone, Gender, Remark) VALUES(cardNo, name, phone, gender, remark); SELECT CAST(SCOPE_IDENTITY() AS INT);; using (SqlConnection conn new SqlConnection(_connStr)) using (SqlCommand cmd new SqlCommand(sql, conn)) { cmd.Parameters.Add(cardNo, SqlDbType.VarChar, 20).Value m.CardNo; cmd.Parameters.Add(name, SqlDbType.NVarChar, 50).Value m.Name; cmd.Parameters.Add(phone, SqlDbType.VarChar, 20).Value m.Phone; cmd.Parameters.Add(gender, SqlDbType.TinyInt).Value m.Gender; cmd.Parameters.Add(remark, SqlDbType.NVarChar, 200).Value m.Remark; conn.Open(); return (int)cmd.ExecuteScalar(); } }参数化查询是必须养成的习惯不管这个项目是本地单机版还是以后的 Web 版。直接用字符串拼接写 SQL一旦用户名里出现单引号就会报错更不要说 SQL 注入风险。上面代码中 SqlDbType 和长度要和表结构对齐否则 SQL Server 会做隐式转换索引可能失效NVarChar 对应 XML 和含中文的字段VARCHAR 对应卡号和电话这类不含中文的短文本。开卡成功拿到 MemberId 后再向 MemberAccount 插入一行初始数据。充值操作比开卡更值得重视因为它直接改钱。充值时必须同时做两件事更新 MemberAccount 的余额向 ChargeRecord 插入一条流水。任何一个失败都要回滚否则顾客钱到账了但流水没了月底对账会非常头痛。public bool Recharge(int memberId, decimal amount, string payMethod, int operatorId) { string update UPDATE MemberAccount SET Balance Balance amount, TotalCharge TotalCharge amount, LastUpdateTime GETDATE() WHERE MemberId memberId; string insert INSERT INTO ChargeRecord(MemberId, ChargeAmount, PayMethod, OperatorId) VALUES(memberId, amount, payMethod, operatorId); using (SqlConnection conn new SqlConnection(_connStr)) { conn.Open(); using (SqlTransaction tx conn.BeginTransaction()) using (SqlCommand updateCmd new SqlCommand(update, conn, tx)) { try { updateCmd.Parameters.Add(amount, SqlDbType.Decimal).Value amount; updateCmd.Parameters.Add(memberId, SqlDbType.Int).Value memberId; int rows updateCmd.ExecuteNonQuery(); if (rows 0) { tx.Rollback(); return false; } SqlCommand insertCmd new SqlCommand(insert, conn, tx); insertCmd.Parameters.Add(memberId, SqlDbType.Int).Value memberId; insertCmd.Parameters.Add(amount, SqlDbType.Decimal).Value amount; insertCmd.Parameters.Add(payMethod, SqlDbType.NVarChar, 20).Value payMethod; insertCmd.Parameters.Add(operatorId, SqlDbType.Int).Value operatorId; insertCmd.ExecuteNonQuery(); tx.Commit(); return true; } catch { tx.Rollback(); throw; } } } }这里最核心的细节是 SqlCommand 构造时传入了事务对象更新和插入两条命令必须挂在同一个 tx 上否则它们各自使用独立事务第二条失败时第一条不会回滚。这也是理解数据库设计文档里“流水表必须和账户表保持一致”的代码层面落实。catch 里回滚后直接 throw让上层弹窗提示不要在这里吞掉异常。3.3 消费扣费与 DataGridView 刷新扣费的逻辑和充值对称但有一个额外的业务判断余额是否够用。新手常犯的错误是先 SELECT 余额在代码里判断再 UPDATE。这种写法在单机版里风险不大但放到多台收银机的场景就会出问题——两个窗口同时扣一张卡先查完都认为余额够然后都去 UPDATE最后余额变负数。解决方式是把判断放进 UPDATE 语句里利用数据库的原子性。public bool Consume(int memberId, decimal amount, int operatorId) { string update UPDATE MemberAccount SET Balance Balance - amount, TotalConsume TotalConsume amount WHERE MemberId memberId AND Balance amount; using (SqlConnection conn new SqlConnection(_connStr)) using (SqlCommand cmd new SqlCommand(update, conn)) { cmd.Parameters.Add(amount, SqlDbType.Decimal).Value amount; cmd.Parameters.Add(memberId, SqlDbType.Int).Value memberId; conn.Open(); int rows cmd.ExecuteNonQuery(); if (rows 0) return false; // 余额不足或会员不存在 // 这里同样要用事务插入 ConsumeRecord并把 BalanceAfter 取出来写流水 return true; } }扣完费后界面上通常要用 DataGridView 显示最新余额和历史流水。直接设置 DataSource DataTable 后新手会发现列名是英文而且数据更新后界面不刷新。这两个问题都源于 DataGridView 默认的自动生成列机制。需要把 AutoGenerateColumns 设为 false然后在列配置里指定 DataPropertyName这样列显示什么由你控制不会随查询结果乱变。每次操作完成后重新查询一次 DataTable再给 DataSource 重新赋值或者用 BindingSource.ResetBindings() 强制刷新。4. 初始化数据库SQL 脚本重放出错时先查这四处无论你拿到的 zip 里数据库设计文档附的是 .sql 脚本还是 .bak 备份文件最终目标都是让本机的 SQL Server 里跑起来一个可用的库。这一步最频繁翻车而且报错信息每次都让人一头雾水。下面从零走一遍初始化流程然后把你最可能踩到的四个问题提前摆出来。4.1 从 SQL 脚本到能连上的数据库常见做法是先在 SQL Server Management Studio 里手动建一个空库然后执行脚本。库名建议和连接字符串里的 Initial Catalog 保持一致比如 BarberDB排序规则用默认的 Chinese_PRC_CI_AS避免后续中文查询排序异常。打开 SSMS用 Windows 身份或 sa 登录本机实例。右键“数据库”选择“新建数据库”名称填 BarberDB其他保持默认。打开 zip 里的 .sql 文件确认第一行不是CREATE DATABASE如果是把它注释掉否则脚本会在你已有的库里乱建。整个脚本按 F5 执行。如果脚本比较大建议一条条看错误信息而不是直接全选执行。如果你习惯命令行也可以用 sqlcmd 执行脚本。下面是一条典型命令sqlcmd -S .\SQLEXPRESS -d BarberDB -U sa -P your_password -i init.sql参数含义-S指定服务器实例名要和连接字符串的 Data Source 一致-d指定目标数据库-U和-P是 SQL Server 身份验证的账号密码-i指定要执行的脚本文件。执行完成后注意看控制台有没有输出错误消息如果没有说明脚本已重放成功。一个特别容易忽略的点是脚本文件的中文编码。如果 .sql 文件里包含中文注释或中文默认值而文件编码是 UTF-8 无 BOMSSMS 执行时可能把它按 GBK 解析结果就是中文全变成乱码。最稳妥的办法是用记事本打开脚本另存为 ANSI 编码或者存成 UTF-8 with BOM。这算是数据库设计文档配套脚本最常见的坑。4.2 不要把 .bak 当作万能后悔药有些 zip 里附带的是数据库备份文件 .bak而不是 .sql 脚本。恢复备份的操作和建库执行脚本完全不同需要在 SSMS 里右键“数据库”选择“还原数据库”来源设备选择那个 .bak 文件。这里有两个坑一是 .bak 文件内部记录的数据库文件名可能和你的实例冲突还原时会报“文件正在使用”或“逻辑文件名不一致”错误二是还原出来的库名可能和 App.config 里写的 Initial Catalog 不一致导致程序找不到库。建议还原后先用一个查询窗口执行SELECT name FROM sys.databases确认实际库名再改 App.config或者干脆把库改名对齐。如果脚本执行报“对象名无效”不要慌这是表创建顺序的问题。常见原因是脚本里先插数据后建表或者外键引用的表还没有创建。解决方式很粗暴把脚本里的所有CREATE TABLE语句剪切到最前面先执行然后再执行INSERT和约束。数据库设计文档里的脚本如果按照这种顺序组织通常就是可靠的。联调时再检查几个边界场景用同一个卡号开两张卡数据库会不会拒绝。不会的话说明 UNIQUE 约束没生效。给余额为 20 的会员扣费 30返回结果是不是 False且余额没有变化。重复点击充值按钮两次流水表有没有出现两条记录。这需要在点击后立刻置灰按钮或者在后端加唯一约束。注意如果脚本执行正常但程序连不上先不要怀疑代码先去 SSMS 里用连接字符串里同样的账号密码连一次。这条能排除一半以上的连接问题。5. 常见问题排查与避坑记录为什么别人能跑你不能这种 zip 项目最大的玄学就是“在我电脑上能跑”。实际上绝大多数运行失败都是环境或上下文问题下面这五条是我在这类项目里见得最多的也是新手最容易踩进去的坑。5.1 现象登录窗体报错“用户 sa 登录失败”第一次打开程序登录界面还没亮出来就弹这个错原因基本集中在三处SQL Server 实例名不对、SQL Server 身份验证模式没开启、密码不对。解决路径是先用 SSMS 手动连一次如果 SSMS 也连不上说明服务没启动或实例名不对去 Windows 服务里找 SQL Server 服务并启动如果 SSMS 能连但程序不行把 App.config 里的 Data Source 改成和 SSMS 登录时一模一样的实例名如果用的是 sa还要确认 SQL Server 的属性里“服务器身份验证”已经切换成“SQL Server 和 Windows 身份验证模式”改完要重启服务。最快的是先把连接字符串改成 Integrated SecurityTrue用 Windows 身份跑通再回头处理 sa 的事。5.2 现象充值后余额没变流水却多了一条这个问题的本质是事务没有包住“更新账户”和“插入流水”两步。很多入门代码是分两次 ExecuteNonQuery第一次更新余额成功了第二次插入流水失败异常被 catch 吞掉所以界面上看只是余额没变。但如果你打开 ChargeRecord 表会发现根本没有这条记录或者反过来流水存在但余额没更新。解决方法是参考上面事务的写法把两个 SqlCommand 挂在同一个 SqlTransaction 上并且先更新账户再插入流水。如果 UPDATE 返回 0说明会员不存在直接回滚流水也不会写入。这条血泪经验同样适用于消费扣费。5.3 现象DataGridView 显示不出中文列名只有 CardNo、Name 这种字段名这不是程序报错而是 DataGridView 默认把查询结果里的列名直接显示出来了。SQL 字段是英文界面自然显示英文。代码里要把 AutoGenerateColumns 设为 false再手工定义列this.dataGridView1.AutoGenerateColumns false; this.dataGridView1.Columns.Clear(); DataGridViewTextBoxColumn colName new DataGridViewTextBoxColumn(); colName.HeaderText 会员姓名; colName.DataPropertyName Name; dataGridView1.Columns.Add(colName);DataPropertyName 对应 DataTable 里的实际列名HeaderText 是界面显示的中文。不匹配时单元格会显示空白这是排查 DataGridView 乱显示问题时第一个要确认的地方。5.4 现象导出 Excel 出来的中文全是乱码市面上很多会员系统导出用的是 CSV 而不是真正的 xlsx因为 CSV 不需要安装 Excel 组件通用性也更好。但 CSV 文件在 Excel 里打开时默认按 ANSI 读取而你用 UTF-8 写出中文自然乱码。解决方式是写文件时带上 BOM 头让 Excel 知道这是 UTF-8 编码using (StreamWriter sw new StreamWriter(path, false, new UTF8Encoding(true))) { sw.WriteLine(卡号,姓名,手机号,余额); // 逐行写数据 }new UTF8Encoding(true) 里的 true 就是输出 BOM。如果这样还乱码就把编码换成 GB2312绝大多数中文 Windows 系统上都能正确识别。顺带提醒不要用 Microsoft.Office.Interop.Excel 去做导出客户机上没装 Office 就直接抛 COM 异常这种方案维护成本太高。5.5 现象部署到另一台电脑上报“数据库文件正在使用”或“文件占用”很多人图省事连接字符串里用 AttachDbFilename 指向一个本地 .mdf 文件开发机上跑得挺好拷到别的电脑上就各种文件占用、权限不足。原因在于 .mdf 附加的数据库实例是和路径强相关的换机器或者没开 SQL Server Express 服务就找不到文件。解决方式是回归正路在目标机器上装 SQL Server用前面的脚本初始化数据库把连接字符串改成指向实例。这种 zip 项目如果是 C# 入门练手倒还好真要作为交付系统数据库服务器和程序分开是必须的。6. 给项目加一个储值卡到期提醒从数据库到界面的一小时改造这类会员系统里理发店最常提的需求之一就是“快到我办的卡到期了提醒一下”。这个功能很适合作为你读完整个数据库设计文档后的第一个自主改造点因为它只涉及一张表、一个查询和一次界面调用。先给会员表加两个字段到期日期以及是否已提醒过。已提醒字段加到数据库设计文档里很实用不然你下次启动程序又会把同一个会员弹一遍提醒。ALTER TABLE Member ADD ExpireDate DATETIME NULL; ALTER TABLE Member ADD RemindSent TINYINT NOT NULL DEFAULT 0;新开卡时在 AddMember 方法里顺手算好到期时间比如一年后m.ExpireDate DateTime.Now.AddYears(1)。对已经存在的旧数据用一条 UPDATE 把过期时间补上来根据办卡日期加一年UPDATE Member SET ExpireDate DATEADD(YEAR, 1, JoinDate) WHERE ExpireDate IS NULL AND Status 1;然后在程序启动的 MainForm_Load 里查询未来 30 天内到期且未提醒过的会员private void MainForm_Load(object sender, EventArgs e) { DataTable due GetExpiringMembers(30); if (due.Rows.Count 0) { string names string.Join(、, due.AsEnumerable() .Select(r r[Name].ToString() ( r[CardNo].ToString() ))); MessageBox.Show(以下 due.Rows.Count 个会员将在30天内到期 names, 到期提醒, MessageBoxButtons.OK, MessageBoxIcon.Information); MarkRemindSent(due); } } private DataTable GetExpiringMembers(int days) { string sql SELECT MemberId, CardNo, Name, Phone, ExpireDate FROM Member WHERE Status 1 AND ExpireDate IS NOT NULL AND ExpireDate BETWEEN GETDATE() AND DATEADD(DAY, days, GETDATE()) AND RemindSent 0 ORDER BY ExpireDate; SqlParameter p new SqlParameter(days, SqlDbType.Int) { Value days }; return SqlHelper.ExecuteTable(sql, p); }这段代码里的 days 参数值得多说一句把 30 天写成参数而不是直接拼进 SQL意义在于以后你把这个提醒功能扩展成设置项时只需要在界面上改天数不用改代码。弹出提示后要做的事是把这些会员的 RemindSent 置为 1否则每次启动都弹。如果你不想用 MessageBox把这个查询结果直接绑定到一个 DataGridView 做成提醒列表页逻辑完全一致把弹窗换成一点击查一次就行。我自己做这类改造时养成的一个习惯是所有涉及日期和金额的查询先想好“服务端默认值会不会管住历史数据”再想 UI 怎么调。这个习惯保我少翻了不少车。这里提醒的时间范围、是否重复提醒、过期后是否自动冻结账户都是可以继续深挖的小功能点也都是数据库设计文档里值得补进去的细节。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?