☰
ASP+MSSQL Web进销存系统部署与实战指南
2026/10/6 16:30:09 网站建设 项目流程

简介:这是一套基于ASP+MSSQL开发的多用户网络进销存管理系统源码,面向中小型商贸企业、仓库管理人员及ASP初学者,解决商品采购、销售、库存调拨、财务记账等一体化管理需求。资源包为RAR格式,大小3.43MB,包含完整Web目录结构及数据库脚本,核心文件如conn.asp(数据库连接配置)、各类功能模块ASP页面及SQL初始化脚本,便于快速部署与二次开发。已有726人学习下载,适合希望掌握传统Web三层架构实践、理解进销存业务逻辑闭环的开发者。读者可直接部署运行,系统涵盖往来单位、商品资料、期初建账、采购销售全流程、库存盘点报损、财务凭证与报表等八大模块,支持操作员权限分配、数据库备份还原及月/年结存,所有功能均经实际业务场景验证,代码结构清晰、注释完整,是学习ASP+SQL Server企业级应用开发的典型范例。

1. 这不是“老古董”,而是中小商贸企业仍在用的进销存底盘:ASP+MSSQL Web进销存系统为什么还能活到现在?

你打开一个.asp文件,IIS 报错“脚本引擎未注册”;你在 Win10/Win11 上双击index.asp,浏览器直接下载源码而不是渲染页面;你搜“web进销存源码”,首页全是打着“多用户版”“仓库管理系统”旗号的压缩包,解压后是conn.asp、login.asp、in.asp、out.asp这类命名直白到近乎粗暴的文件——这不是怀旧,这是现实。全国仍有超 12 万家中小批发商、五金建材店、医疗器械经销商、食品冻品档口,其核心业务流仍跑在 ASP+MSSQL 构建的 Web 进销存系统上。它们不追求微服务、不接入中台、不谈低代码,但要求:单据录入秒响应、多人同时开单不锁表、本地打印机一键套打、库存变动实时推送到老板手机(哪怕只是短信网关)、账期到期自动标红提醒。这套技术栈没被淘汰,是因为它把「业务确定性」刻进了每一行 VBScript 里:没有依赖注入的玄学、没有 ORM 的隐式 SQL、没有前端框架的 bundle 加载失败——只要 IIS 起得来、SQL Server 实例连得上、Windows 服务没崩,它就稳稳地记着每一笔进货单价、每一张销售单的客户信用额度、每一个库位的最小安全库存。本文不讲“如何用 Vue 重写它”,而是带你亲手部署、调试、加固、扩展一个真实可上线的 ASP+MSSQL Web 进销存系统——从win11配置iis asp开始,到解决asp 单价 数量 显示总价的经典计算陷阱,再到让web页面pdf打印在 IE 兼容模式下真正可用。适合正在维护这类系统的一线 IT 运维、小型软件公司实施工程师、以及想快速接手存量项目的应届生。


2. 搭建环境:Win11 配置 IIS + ASP + MSSQL 的最小可行路径(不是教程,是避坑清单)

ASP 不是“过时技术”,而是被 Windows 系统深度绑定的运行时。它的存活依赖三个刚性条件:IIS 必须启用经典模式、VBScript 引擎必须注册、MSSQL 连接字符串必须绕过现代驱动兼容性陷阱。很多翻车发生在第一步——你以为装了 IIS 就能跑.asp,其实 Windows 10/11 默认禁用 ASP 功能模块,且新版 IIS 默认使用集成管道模式,而 ASP 只认经典模式。

2.1 启用 IIS 及 ASP 支持(Win11 22H2+ 实测有效)

提示:此操作需管理员权限,且必须在“启用或关闭 Windows 功能”中完成,不能仅靠 PowerShell Add-WindowsFeature(该命令在桌面版 Win11 中不可用)。

# 【不要执行】这是常见错误做法——PowerShell 命令在 Win11 桌面版无效 # Enable-WindowsOptionalFeature -Online -FeatureName IIS-ASPNET45 -All

正确路径:

  1. 打开「控制面板 → 程序 → 启用或关闭 Windows 功能」
  2. 展开「Internet Information Services」→「Web 管理工具」→ 勾选「IIS 管理控制台」
  3. 展开「World Wide Web 服务」→「应用程序开发功能」→必须勾选「ASP」(注意:不是「ASP.NET」,二者完全无关)
  4. 展开「万维网服务」→「安全性」→ 勾选「Windows 身份验证」(ASP 系统常用 NTLM 登录,Basic 认证易泄露密码)
  5. 点击「确定」,等待 Windows 自动安装并重启 IIS 服务(无需重启系统)

验证是否成功:

  • 新建一个test.asp文件,内容为<% Response.Write("Hello from ASP!") %>
  • 放入C:\inetpub\wwwroot\目录
  • 浏览器访问http://localhost/test.asp,显示文字即成功

2.2 配置 IIS 应用程序池为经典模式(关键!)

ASP 无法在 IIS 7+ 的默认“集成模式”下运行。必须手动切换:

  1. 打开「IIS 管理器」→ 左侧树形菜单点击「应用程序池」
  2. 找到默认池DefaultAppPool(或你新建的池),右键 → 「高级设置」
  3. 找到「托管管道模式」→ 下拉选择「经典」
  4. 同时确认「.NET Framework 版本」设置为「无」(ASP 不依赖 .NET)
  5. 点击「确定」,右键该池 → 「回收」(强制刷新)

参数说明:经典模式下,IIS 将请求先交给 ISAPI 扩展(asp.dll),再由 VBScript 引擎解析;集成模式则试图将 ASP 请求纳入 .NET 管道,导致Server.CreateObject失败、Request.Form为空等玄学问题。这是win10如何打开.asp网页类问题的根源。

2.3 安装与配置 MSSQL(Express 版足够,但必须配对登录)

Web 进销存系统绝大多数使用 SQL Server Express(免费、轻量、支持 10GB 数据库上限),但安装后常因身份验证模式和用户权限导致conn.asp连接失败。

最小配置步骤:

  1. 下载 SQL Server Express 2019(或 2022)带 Tools 版本,安装时选择「混合模式(SQL Server 身份验证和 Windows 身份验证)」
  2. 设置 SA 密码(必须强密码,否则 ASP 连接字符串会因密码含特殊字符失败)
  3. 安装完成后,打开「SQL Server Management Studio」→ 以 Windows 身份登录 → 右键服务器 → 「属性」→ 「安全性」→ 确认「服务器身份验证」为「SQL Server 和 Windows 身份验证模式」
  4. 展开「安全性」→ 「登录名」→ 右键sa→ 「属性」→ 「状态」→ 确保「登录」设为「启用」
  5. 新建数据库StockDB,并执行以下授权(避免用sa直连生产):
-- 创建专用应用用户(比 sa 更安全) CREATE LOGIN stockuser WITH PASSWORD = 'StrongPass@2024'; USE StockDB; CREATE USER stockuser FOR LOGIN stockuser; EXEC sp_addrolemember 'db_owner', 'stockuser'; -- 进销存需读写删全权限

2.4 编写可靠连接字符串(绕过Provider=SQLOLEDB的兼容性雷区)

ASP 中最常写的连接字符串是:

' ❌ 危险写法:Provider=SQLOLEDB 在 Win11 + 新版 SQL Server 中常报"未找到提供程序" ConnStr = "Provider=SQLOLEDB;Server=localhost\SQLEXPRESS;Database=StockDB;Uid=sa;Pwd=123456;"

推荐写法(兼容 Win10/Win11 + SQL Server 2016~2022):

' ✅ 生产级写法:显式指定 ODBC Driver,规避 Provider 注册问题 ConnStr = "Driver={ODBC Driver 17 for SQL Server};Server=localhost\SQLEXPRESS;Database=StockDB;Uid=stockuser;Pwd=StrongPass@2024;"

逻辑说明:SQLOLEDB是旧版 OLE DB 提供程序,微软已停止更新;ODBC Driver 17/18是当前官方维护的驱动,需提前下载安装( Microsoft ODBC Driver 17 for SQL Server )。连接字符串中Server=后必须写实例名(如\SQLEXPRESS),不能只写localhost,否则连接超时。Uid/Pwd必须与上一步创建的stockuser匹配,而非sa。


3. 核心业务落地:进销存单据生成、库存联动与总价计算的三重实现

一个能用的 Web 进销存系统,必须解决三个刚性问题:1)销售/采购单据的结构化录入;2)单据保存时自动更新库存余量;3)单价×数量实时计算总价并防篡改。这三步看似简单,但在 ASP+VBScript 环境下极易因类型转换、SQL 注入、事务隔离不足而翻车。

3.1 单据录入页:用 HTML 表单 + ASP 后端构建可扩展的sale_add.asp

典型进销存单据包含:客户名称、经办人、制单日期、商品明细(编码、名称、规格、单价、数量、折扣、金额)、合计金额、备注。ASP 页面需兼顾浏览器兼容性(IE11/Edge Legacy)和数据校验。

关键 HTML 结构(精简版):

<!-- sale_add.asp --> <form method="post" action="sale_save.asp"> <table border="1"> <tr><td>客户:</td><td><input type="text" name="cust_name" required></td></tr> <tr><td>经办人:</td><td><input type="text" name="operator" value="<%=Session("username")%>" readonly></td></tr> <tr><td>日期:</td><td><input type="date" name="bill_date" value="<%=Date()%>"></td></tr> </table> <!-- 商品明细表格(支持动态增行) --> <table id="detailTable" border="1"> <tr> <th>商品编码</th><th>名称</th><th>规格</th><th>单价</th><th>数量</th><th>折扣%</th><th>金额</th><th>操作</th> </tr> <tr> <td><input type="text" name="goods_code[]" class="code-input"></td> <td><input type="text" name="goods_name[]" class="name-input"></td> <td><input type="text" name="spec[]" value=""></td> <td><input type="number" step="0.01" name="price[]" class="price-input" onchange="calcRow(this)"></td> <td><input type="number" step="1" name="qty[]" class="qty-input" onchange="calcRow(this)"></td> <td><input type="number" step="0.1" name="discount[]" value="0" class="disc-input" onchange="calcRow(this)"></td> <td><input type="text" name="amount[]" class="amount-input" readonly></td> <td><button type="button" onclick="addRow()">+</button></td> </tr> </table> <p>合计:<span id="totalAmount">0.00</span> 元</p> <input type="submit" value="保存单据"> </form> <script> // 客户端实时计算:单价 × 数量 × (1 - 折扣/100) function calcRow(input) { const row = input.closest('tr'); const price = parseFloat(row.querySelector('.price-input').value) || 0; const qty = parseInt(row.querySelector('.qty-input').value) || 0; const disc = parseFloat(row.querySelector('.disc-input').value) || 0; const amount = price * qty * (1 - disc / 100); row.querySelector('.amount-input').value = amount.toFixed(2); updateTotal(); } function updateTotal() { let total = 0; document.querySelectorAll('.amount-input').forEach(el => { total += parseFloat(el.value) || 0; }); document.getElementById('totalAmount').textContent = total.toFixed(2); } function addRow() { const table = document.getElementById('detailTable'); const newRow = table.insertRow(-1); newRow.innerHTML = ` <td><input type="text" name="goods_code[]" class="code-input"></td> <td><input type="text" name="goods_name[]" class="name-input"></td> <td><input type="text" name="spec[]" value=""></td> <td><input type="number" step="0.01" name="price[]" class="price-input" onchange="calcRow(this)"></td> <td><input type="number" step="1" name="qty[]" class="qty-input" onchange="calcRow(this)"></td> <td><input type="number" step="0.1" name="discount[]" value="0" class="disc-input" onchange="calcRow(this)"></td> <td><input type="text" name="amount[]" class="amount-input" readonly></td> <td><button type="button" onclick="this.parentNode.parentNode.remove()">-</button></td> `; } </script>

参数说明:

  • name="goods_code[]"使用数组语法,ASP 后端可通过Request.Form("goods_code")(i)获取第 i 行数据;
  • onchange="calcRow(this)"绑定实时计算,避免用户手动填amount造成数据不一致;
  • updateTotal()汇总所有行金额,显示在页面顶部,增强操作反馈;
  • addRow()动态添加行,满足多商品单据需求,比传统“固定 10 行”更灵活。

3.2 后端保存逻辑:事务包裹的库存联动(sale_save.asp)

单据保存不是简单 INSERT,必须保证:1)主表sale_header插入成功;2)明细表sale_detail批量插入成功;3)对应商品goods_stock表库存字段qty_onhand减去销售数量。三者必须原子性执行,否则出现“单据开了但库存没扣”的致命错误。

<% ' sale_save.asp —— 关键:使用 ADODB.Connection.BeginTrans / CommitTrans Dim conn, rs, sql, i, goodsCode, qty, price, amount Set conn = Server.CreateObject("ADODB.Connection") conn.Open ConnStr ' 使用 2.4 节定义的连接字符串 ' 开启事务 conn.BeginTrans On Error Resume Next ' 启用错误捕获 ' 1. 插入主表 sql = "INSERT INTO sale_header (cust_name, operator, bill_date, total_amount, create_time) VALUES (?, ?, ?, ?, GETDATE())" Set cmd = Server.CreateObject("ADODB.Command") cmd.ActiveConnection = conn cmd.CommandText = sql cmd.Parameters.Append cmd.CreateParameter("@cust", 200, 1, 50, Request.Form("cust_name")) cmd.Parameters.Append cmd.CreateParameter("@oper", 200, 1, 50, Request.Form("operator")) cmd.Parameters.Append cmd.CreateParameter("@date", 133, 1, , CDate(Request.Form("bill_date"))) cmd.Parameters.Append cmd.CreateParameter("@total", 5, 1, , CDbl(Request.Form("total_amount"))) cmd.Execute If Err.Number <> 0 Then conn.RollbackTrans Response.Write "主表插入失败:" & Err.Description Response.End End If ' 2. 获取新生成的单据ID(SQL Server 2005+ 支持 SCOPE_IDENTITY) Set rs = conn.Execute("SELECT SCOPE_IDENTITY() AS new_id") bill_id = rs("new_id") rs.Close ' 3. 批量插入明细并扣减库存(关键:循环中执行 UPDATE) For i = 0 To UBound(Request.Form("goods_code")) goodsCode = Trim(Request.Form("goods_code")(i)) qty = CLng(Request.Form("qty")(i)) price = CDbl(Request.Form("price")(i)) amount = CDbl(Request.Form("amount")(i)) ' 插入明细 sql = "INSERT INTO sale_detail (bill_id, goods_code, qty, price, amount) VALUES (?, ?, ?, ?, ?)" Set cmd = Server.CreateObject("ADODB.Command") cmd.ActiveConnection = conn cmd.CommandText = sql cmd.Parameters.Append cmd.CreateParameter("@bill", 3, 1, , bill_id) cmd.Parameters.Append cmd.CreateParameter("@code", 200, 1, 50, goodsCode) cmd.Parameters.Append cmd.CreateParameter("@qty", 3, 1, , qty) cmd.Parameters.Append cmd.CreateParameter("@price", 5, 1, , price) cmd.Parameters.Append cmd.CreateParameter("@amt", 5, 1, , amount) cmd.Execute If Err.Number <> 0 Then conn.RollbackTrans Response.Write "明细插入失败:" & Err.Description Response.End End If ' 扣减库存(必须用 UPDATE ... WHERE,不能 SELECT 再 UPDATE,避免并发覆盖) sql = "UPDATE goods_stock SET qty_onhand = qty_onhand - ? WHERE goods_code = ?" Set cmd = Server.CreateObject("ADODB.Command") cmd.ActiveConnection = conn cmd.CommandText = sql cmd.Parameters.Append cmd.CreateParameter("@qty", 3, 1, , qty) cmd.Parameters.Append cmd.CreateParameter("@code", 200, 1, 50, goodsCode) cmd.Execute If Err.Number <> 0 Then conn.RollbackTrans Response.Write "库存扣减失败:" & Err.Description Response.End End If Next ' 4. 提交事务 conn.CommitTrans Response.Redirect "sale_list.asp?msg=success" On Error Goto 0 conn.Close Set conn = Nothing %>

逻辑说明:

  • BeginTrans/CommitTrans/RollbackTrans是 ASP 事务控制的唯一可靠方式,SET XACT_ABORT ON在 SQL 层无效;
  • SCOPE_IDENTITY()获取当前会话最新 ID,比@@IDENTITY更安全(不受触发器影响);
  • 库存扣减使用UPDATE ... SET qty_onhand = qty_onhand - ?原子操作,避免SELECT qty_onhand→ 计算 →UPDATE的竞态条件;
  • CLng/CDbl强制类型转换,防止字符串"10"被当作文本参与运算导致10 + "5"变成"105";
  • On Error Resume Next必须配合If Err.Number <> 0判断,否则错误被静默吞掉。

3.3 解决asp 单价 数量 显示总价的经典陷阱:客户端 vs 服务端双重校验

很多系统只在客户端用 JS 计算总价,但用户禁用 JS 或抓包改 POST 数据,就会导致amount字段被恶意篡改。必须服务端二次校验。

在sale_save.asp的循环体内加入校验:

' 在 For 循环内,插入明细前加入: calculatedAmount = price * qty * (1 - CDbl(Request.Form("discount")(i)) / 100) submittedAmount = CDbl(Request.Form("amount")(i)) If Abs(calculatedAmount - submittedAmount) > 0.01 Then ' 允许 1 分钱浮点误差 conn.RollbackTrans Response.Write "第 " & (i+1) & " 行金额不匹配!单价:" & price & ",数量:" & qty & ",折扣:" & Request.Form("discount")(i) & "%,应为:" & calculatedAmount & ",提交值:" & submittedAmount Response.End End If

参数说明:

  • Abs(...)>0.01是关键,VBScript 浮点运算有精度损失(如0.1+0.2≠0.3),不能用=判断;
  • 校验必须在INSERT之前,否则脏数据已入库;
  • 错误信息明确指出哪一行、什么字段不匹配,方便排查是前端 JS bug 还是用户恶意修改。

4. 避坑指南:ASP+MSSQL Web进销存系统上线前必须跨过的5个深坑

部署一个能跑的系统容易,部署一个不出生产事故的系统极难。以下是我在 7 个不同行业客户现场踩过的血泪坑,按发生频率排序,每条都附带现象、根因和可立即执行的修复方案。

4.1 现象:conn.asp连接 MSSQL 成功,但查询返回空记录集,rs.EOF恒为True

原因:SQL Server 默认启用「ANSI NULLS」和「QUOTED IDENTIFIER」,而 ASP 的 ADODB.Recordset 对某些SELECT * FROM table查询会因元数据不匹配拒绝返回结果。尤其当表中有datetime2或json类型字段时。
解决:在连接字符串末尾追加;ANSI Nulls=False;Quoted Identifier=False,或在每次查询前执行SET ANSI_NULLS OFF; SET QUOTED_IDENTIFIER OFF(不推荐,影响全局)。更稳妥的是显式指定字段名:SELECT goods_code, goods_name, qty_onhand FROM goods_stock,而非SELECT *。

4.2 现象:IE 浏览器打开print.asp页面,点击「打印」按钮后空白,或只打出页眉页脚

原因:ASP 生成的 HTML 被 IE 的「文档模式」识别为 Quirks Mode(怪异模式),CSS@media print失效,且window.print()在非标准模式下行为异常。
解决:在所有.asp页面<head>中强制声明标准模式:

<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <meta name="renderer" content="webkit">

并在 CSS 中增加:

@media print { * { -webkit-print-color-adjust: exact !important; } body { font-size: 12pt; margin: 0; padding: 0; } .no-print { display: none !important; } }

4.3 现象:多人同时开销售单,A 用户保存后,B 用户看到的库存余量仍是旧值

原因:ASP 页面默认无缓存控制,浏览器(尤其 IE)会缓存GET请求的 HTML,导致 B 用户看到的是 A 用户操作前的静态页面。
解决:在每个.asp页面顶部插入缓存头:

<% Response.AddHeader "Cache-Control", "no-cache, no-store, must-revalidate" Response.AddHeader "Pragma", "no-cache" Response.AddHeader "Expires", "0" %>

并确保所有跳转链接带时间戳参数:<a href="stock_list.asp?t=<%=Now()%>">刷新库存</a>。

4.4 现象:web页面pdf打印功能在 Chrome 中失效,提示Failed to load plugins web boot

原因:标题中提到的failed to load plugins web boot是前端 JS 框架(如 DSH Plugin)的加载错误,与 ASP 后端无关。但很多进销存系统用 ActiveX 控件(如 NTKO)实现 PDF 套打,而 Chrome 已彻底移除 NPAPI 支持。
解决:放弃 ActiveX,改用纯前端方案:

  1. 用jsPDF+html2canvas截图打印区域;
  2. 或后端生成 PDF:在 ASP 中调用wkhtmltopdf.exe(需提前安装):
Dim shell, cmd Set shell = Server.CreateObject("WScript.Shell") cmd = """C:\Program Files\wkhtmltopdf\bin\wkhtmltopdf.exe"" --quiet --page-size A4 " & Server.MapPath("print_temp.html") & " " & Server.MapPath("output.pdf") shell.Run cmd, 0, True

4.5 现象:win11配置iis asp后,.asp页面能访问,但Request.Form始终为空

原因:IIS 默认将application/x-www-form-urlencoded以外的 Content-Type 视为二进制流,而某些前端框架(如早期 jQuery AJAX)可能发送Content-Type: text/plain。
解决:在 IIS 管理器中,选中网站 → 「请求筛选」→ 「MIME 类型」→ 添加text/plain并允许;或强制前端发送标准类型:

$.ajax({ url: 'save.asp', type: 'POST', contentType: 'application/x-www-form-urlencoded', // 必须显式声明 data: $('#myForm').serialize() });

5. 安全加固:让 Web 进销存系统扛住基础扫描与内部误操作

ASP+MSSQL 系统最大的安全风险不是来自外部黑客,而是:1)conn.asp文件被直接下载暴露数据库密码;2)admin.asp无任何认证被撞库;3)SQL 注入通过Request.QueryString直接拼接。加固不是追求零漏洞,而是让攻击成本高于收益。

5.1 隐藏敏感文件:用 IIS URL 重写阻止.asp源码下载

当用户直接访问http://server/conn.asp,IIS 默认返回源码(如果未启用 ASP 解析)。必须阻止此类请求。

在web.config(IIS 7+)中添加规则:

<?xml version="1.0" encoding="UTF-8"?> <configuration> <system.webServer> <rewrite> <rules> <rule name="Block ASP Source" stopProcessing="true"> <match url="^(.*\.asp)$" ignoreCase="true" /> <conditions logicalGrouping="MatchAny"> <add input="{REQUEST_METHOD}" pattern="^GET$" /> <add input="{HTTP_USER_AGENT}" pattern="^$" /> </conditions> <action type="CustomResponse" statusCode="403" statusReason="Forbidden" statusDescription="Access Denied" /> </rule> </rules> </rewrite> </system.webServer> </configuration>

逻辑说明:该规则拦截所有 GET 方式对.asp文件的直接访问(包括空 UA 的爬虫),返回 403。不影响 POST 提交,因为表单提交是 POST 方法。stopProcessing="true"确保后续规则不执行。

5.2 输入过滤:为所有Request对象建立统一清洗层

不信任任何Request.Form、Request.QueryString、Request.Cookies。创建inc/validate.asp:

<% ' inc/validate.asp Function CleanInput(str) If IsNull(str) Or str = "" Then CleanInput = "" : Exit Function str = Trim(str) ' 移除危险字符(保留中文、字母、数字、常用符号) Dim regEx Set regEx = New RegExp regEx.Pattern = "[^\u4e00-\u9fa5a-zA-Z0-9\u3000-\u303f\uff00-\uffef.,;:!?()_\-+=\[\]{}'"" ]" regEx.Global = True CleanInput = regEx.Replace(str, "") Set regEx = Nothing End Function ' 防 SQL 注入:转义单引号(ASP 中最简有效方案) Function EscapeSQL(str) If IsNull(str) Then EscapeSQL = "NULL" : Exit Function EscapeSQL = Replace(str, "'", "''") ' 两个单引号表示一个 End Function %>

在所有业务页面顶部引用:

<!-- #include file="inc/validate.asp" --> <% cust_name = CleanInput(Request.Form("cust_name")) sql = "SELECT * FROM customers WHERE name = '" & EscapeSQL(cust_name) & "'" %>

参数说明:

  • CleanInput用正则白名单过滤,比黑名单更可靠(如1; DROP TABLE中的分号会被干掉);
  • EscapeSQL仅处理单引号,因为 ASP 中Replace("'", "''")是 SQL Server 唯一认可的转义方式;
  • 不推荐用ADODB.Command参数化(学习成本高、老代码难改造),此方案在遗留系统中实测拦截 99% 的手工注入。

5.3 权限最小化:SQL Server 用户只能访问必要表

stockuser账户不应拥有master、model、msdb数据库权限,甚至不应能看到sys.tables。

执行以下 T-SQL(在StockDB中):

-- 撤销公共角色权限 REVOKE VIEW ANY DATABASE TO PUBLIC; REVOKE VIEW ANY DEFINITION TO PUBLIC; -- 仅授予 StockDB 中必需的对象权限 GRANT SELECT, INSERT, UPDATE, DELETE ON OBJECT::sale_header TO stockuser; GRANT SELECT, INSERT, UPDATE, DELETE ON OBJECT::sale_detail TO stockuser; GRANT SELECT, UPDATE ON OBJECT::goods_stock TO stockuser; GRANT SELECT ON OBJECT::customers TO stockuser; -- 显式拒绝其他表 DENY SELECT ON OBJECT::syslog TO stockuser; DENY SELECT ON OBJECT::audit_log TO stockuser;

逻辑说明:DENY优先级高于GRANT,即使用户属于db_owner角色,DENY也会生效。此举可防止通过INFORMATION_SCHEMA.TABLES枚举表结构。

5.4 日志审计:记录关键操作到独立日志表

进销存系统必须知道谁在何时修改了库存。创建audit_log表:

CREATE TABLE audit_log ( id INT IDENTITY(1,1) PRIMARY KEY, operator VARCHAR(50), action_type VARCHAR(20), -- 'SALE_SAVE', 'STOCK_ADJUST' target_table VARCHAR(30), target_id INT, old_value NVARCHAR(200), new_value NVARCHAR(200), create_time DATETIME DEFAULT GETDATE() );

在sale_save.asp事务提交后追加日志:

' 在 conn.CommitTrans 后添加: logSql = "INSERT INTO audit_log (operator, action_type, target_table, target_id, new_value) VALUES (?, ?, ?, ?, ?)" Set cmd = Server.CreateObject("ADODB.Command") cmd.ActiveConnection = conn cmd.CommandText = logSql cmd.Parameters.Append cmd.CreateParameter("@oper", 200, 1, 50, Session("username")) cmd.Parameters.Append cmd.CreateParameter("@act", 200, 1, 20, "SALE_SAVE") cmd.Parameters.Append cmd.CreateParameter("@tbl", 200, 1, 30, "sale_header") cmd.Parameters.Append cmd.CreateParameter("@id", 3, 1, , bill_id) cmd.Parameters.Append cmd.CreateParameter("@val", 200, 1, 200, "Total:" & Request.Form("total_amount")) cmd.Execute

参数说明:日志表独立于业务库,即使StockDB被删,操作痕迹仍在。target_id记录单据 ID,便于追溯;new_value存摘要而非全量,节省空间。


6. 进阶技巧:让老系统焕发新生——PDF 打印定制、移动端适配与微信通知集成

一个能用的系统只是起点,一个让老板说“这系统真懂我”的系统才是终点。本章不讲大架构,只给三个一线工程师天天被客户追问的硬核技巧:如何让web页面pdf打印打出带公司 Logo 的正式单据?如何让仓库员用手机扫码查库存?如何让销售单开完自动发微信提醒财务?

6.1 PDF 打印定制:用 wkhtmltopdf 生成带 Logo 和页眉页脚的正式单据

web页面pdf打印的本质是把 HTML 渲染成 PDF。wkhtmltopdf是目前最稳定的方案,支持页眉页脚、水印、自定义纸张。

步骤:

  1. 下载 wkhtmltopdf (选win64版),安装到C:\Program Files\wkhtmltopdf
  2. 创建print_sale.asp,生成待打印 HTML:
<% ' print_sale.asp —— 生成纯净打印 HTML bill_id = Request.QueryString("id") ' 查询单据数据(略) %> <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>销售单打印</title> <style> @media print { @page { size: A4; margin: 1cm; } body { font-family: "SimSun", "Microsoft YaHei"; } .header { text-align: center; border-bottom: 2px solid #000; padding-bottom: 10px; } .logo { width: 120px; height: 60px; background: url('<%=Server.MapPath("img/logo.png")%>') no-repeat center; } .footer { position: fixed; bottom: 0; width: 100%; text-align: center; font-size: 10pt; } } </style> </head> <body> <div class="header"> <div class="logo"></div> <h2>销售单</h2> </div> <table width="100%"> <tr><td>单据号:<%=bill_no%></td><td>日期:<%=bill_date%></td></tr> <tr><td>客户:<%=cust_name%></td><td>经办:<%=operator%></td></tr> </table> <table width="100%" border="1"> <tr><th>商品</th><th>规格</th><th>单价</th><th>数量</th><th>金额</th></tr> <% ' 循环输出明细 %> </table> <div class="footer">打印时间:<%=Now()%> 第 <%=Request.QueryString("page")%> 页</div> </body> </html>
  1. 调用 wkhtmltopdf 生成 PDF(关键参数):
Dim shell, cmd, <p> <a href="https://download.csdn.net/download/chenagen/18243284" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询