如何在opdnew<0时退出foreach与while循环并取消所有数据更新
批量扣除OPD余额并确保全量验证后更新的实现方案
需求梳理
- 从
tb_ManageExpenseSheetOne筛选符合条件的记录,提取每个记录的PreparedBy(人员姓名)和TotalAmount(扣除金额) - 对每个人员,查询其
tb_Person表中的RemainingOPD余额,计算扣除后的余额opdnew = RemainingOPD - TotalAmount - 若任意人员的
opdnew < 0,立即显示警告信息,终止所有流程,不更新任何人员的OPD余额 - 只有所有人员的扣除后余额都≥0时,才批量更新对应人员的
RemainingOPD字段
原代码核心问题
- 边检查边更新,一旦后续出现余额不足,已更新的记录无法回滚,不符合需求
- 直接拼接SQL字符串,存在严重SQL注入风险
- 重复创建/关闭数据库连接,资源利用效率低
- 依赖UI控件临时存储计算值,容易引发异常
优化后的实现代码
protected void btnOPDDetect_Click(object sender, EventArgs e) { // 存储待更新的人员及对应扣除金额(同一人多条记录自动累加) Dictionary<string, double> pendingUpdates = new Dictionary<string, double>(); bool validationFailed = false; string errorMessage = string.Empty; string batchNo = lblIdforReport.Text; // 第一步:全量验证所有人员的OPD余额是否充足 using (SqlConnection dbConn = GetConnection()) { string expenseQuery = @" SELECT PreparedBy, TotalAmount FROM tb_ManageExpenseSheetOne WHERE IsCheckedAdvance='true' AND FinanceOfficer=@FinanceUser AND SubmittedStatus='Submitted' AND VoucherStatus='Voucher Created' AND RecommendedStatus='Recommended' AND BatchNo=@BatchNumber"; using (SqlCommand cmd = new SqlCommand(expenseQuery, dbConn)) { // 参数化查询避免SQL注入 cmd.Parameters.AddWithValue("@FinanceUser", User.Identity.Name); cmd.Parameters.AddWithValue("@BatchNumber", Convert.ToInt64(batchNo)); dbConn.Open(); using (SqlDataReader reader = cmd.ExecuteReader()) { while (reader.Read() && !validationFailed) { string personName = reader["PreparedBy"].ToString(); double deductAmount = Convert.ToDouble(reader["TotalAmount"]); // 获取当前人员的剩余OPD double currentOPD = GetPersonOPD(personName, dbConn); double newOPD = currentOPD - deductAmount; if (newOPD < 0) { validationFailed = true; errorMessage = $"'{personName}' OPD is not enough"; break; } // 累加同一人员的扣除金额 if (pendingUpdates.ContainsKey(personName)) { pendingUpdates[personName] += deductAmount; } else { pendingUpdates.Add(personName, deductAmount); } } } dbConn.Close(); } } // 验证不通过,显示警告并终止 if (validationFailed) { lblWarning.Text = errorMessage; return; } // 第二步:所有验证通过,批量更新OPD余额(事务确保原子性) using (SqlConnection dbConn = GetConnection()) { dbConn.Open(); using (SqlTransaction transaction = dbConn.BeginTransaction()) { try { string updateQuery = @" UPDATE tb_Person SET RemainingOPD = RemainingOPD - @DeductValue WHERE Name=@PersonName"; using (SqlCommand cmd = new SqlCommand(updateQuery, dbConn, transaction)) { // 复用参数对象提升效率 cmd.Parameters.Add("@DeductValue", SqlDbType.Float); cmd.Parameters.Add("@PersonName", SqlDbType.VarChar); foreach (var item in pendingUpdates) { cmd.Parameters["@DeductValue"].Value = item.Value; cmd.Parameters["@PersonName"].Value = item.Key; cmd.ExecuteNonQuery(); } } // 提交事务,确认所有更新生效 transaction.Commit(); btnArchive.Visible = true; lblWarning.Text = string.Empty; } catch (Exception ex) { // 出错回滚,确保数据一致性 transaction.Rollback(); lblWarning.Text = $"Update failed: {ex.Message}"; } finally { dbConn.Close(); } } } } // 辅助方法:查询指定人员的剩余OPD private double GetPersonOPD(string personName, SqlConnection conn) { string query = "SELECT RemainingOPD FROM tb_Person WHERE Name=@Name"; using (SqlCommand cmd = new SqlCommand(query, conn)) { cmd.Parameters.AddWithValue("@Name", personName); object result = cmd.ExecuteScalar(); return result != DBNull.Value ? Convert.ToDouble(result) : 0; } }
关键改进说明
- 两步式流程:先完成全量验证,再执行更新,彻底避免部分更新的问题
- 参数化查询:完全消除SQL注入风险
- 事务控制:所有更新操作原子化,要么全部成功,要么全部回滚
- 资源复用:复用数据库连接和命令对象,提升操作效率
- 字典累加:自动处理同一人员多条记录的扣除金额累加
- 解耦UI依赖:移除对TextBox的依赖,避免UI状态干扰业务逻辑
内容的提问来源于stack exchange,提问作者Shahid
相关产品推荐
相关产品推荐

