简介:面向课程设计、毕业设计及K12网络教学平台建设场景的ASP+ACCESS网上作业提交与课程学习支持系统资料,适合Web开发初学者、教育平台开发者及需要完成数据库课程项目的学生参考。内容围绕作业提交、模块导航、知识点检索导航与知识点关联导航展开,涵盖系统分析、数据库设计、功能实现及C语言实例说明,可帮助读者理解基于Web的作业提交与自适应学习导航如何落地。压缩包约732KB,受上游统计限制,文件总数显示为0、类型明细暂未提供,建议解压后按论文文档、数据库文件与ASP页面等方向自行梳理。已有74人学习,适合用来参考ASP访问ACCESS数据库的建站思路、模块划分和知识点关联导航设计,也可作为完善课程作业提交流程与后台数据组织的借鉴材料。
1. 一个教学点的真实麻烦:ASP 网上作业提交系统到底解决谁的问题
去年帮某教学点收拾一套老系统,起因特别具体:带三个班的老师用群收作业,文件名千奇百怪,谁交了谁没交全靠翻聊天记录,期末要把平时成绩导出来,硬是手工对了两个晚上。后来从旧服务器里翻出一套 ASP 网上作业提交系统,代码不算漂亮,但它把「布置作业—学生上传—教师批阅—成绩统计」这条链路完整串起来了,背后就是一个 Access 或 SQL Server 数据库在支撑。
这套东西适合的人群其实很明确:内网环境的中小教学点、实验课、需要快速上线又不想折腾新框架的 K12 课程资源管理场景。Classic ASP 加 IIS,几乎所有 Windows 机器都能直接跑,不需要额外装运行时。如果你手里正好有这么一份源码,想把它跑起来、看懂、再改成自己能用,后面的拆解按目录、数据库、核心链路、排错、二次开发一路展开。
2. 技术栈与目录结构:Classic ASP + Access 这套老组合怎么在 IIS 上站稳
先把技术底子交代清楚,不然后面看代码容易懵。这套系统是典型的 2000 年代中后期写法:服务端用 VBScript 写 ASP 页面,数据库访问一律走 ADODB,文件上传靠无组件上传类自己解析multipart/form-data,前端就是 HTML 表格加一点点 JavaScript 校验,没有框架,没有构建,改完文件刷新浏览器就生效。听起来原始,但对一个只有十几张页面、日活几百人的作业系统来说,这套组合的部署成本和维护成本低到几乎可以忽略。
2.1 运行环境与依赖清单:为什么必须开 32 位应用池
跑这套系统,环境准备是三件事:IIS 打开 ASP 功能、应用池设成 32 位、数据库驱动装对。这三件事任何一件没做,页面都会以各种莫名其妙的报错糊在你脸上。
第一件事,Windows 默认是不装 ASP 的,得手动开。图形界面走「启用或关闭 Windows 功能 → Internet 信息服务 → World Wide Web 服务 → 应用程序开发功能 → ASP」,命令行更省事:
# 以管理员身份运行,一次性把 IIS 和 ASP 都装上 Enable-WindowsOptionalFeature -Online -FeatureName IIS-WebServerRole -All Enable-WindowsOptionalFeature -Online -FeatureName IIS-ASP -AllIIS-ASP这个特性名就是 Classic ASP 支持,装完 IIS 管理器里站点才会出现「ASP」图标,没装的话你连调试开关都找不到。
第二件事,应用池必须启用 32 位。原因是 Access 的 Jet OLEDB 4.0 驱动只有 32 位版本,如果站点跑在 64 位工作进程里,ADODB.Connection.Open会直接抛「未找到提供程序。该程序可能未正确安装」。这不是驱动没装,是位数对不上。我一般新建一个独立应用池,.NET CLR 版本选「无托管代码」,启用 32 位应用程序设为 True,然后把站点绑到这个池上,不去动 DefaultAppPool,免得影响机器上别的站点。
第三件事,数据库驱动。用.mdb文件的话系统自带 Jet 4.0 就够;用.accdb就得装 Access Database Engine,并且连接字符串换成Microsoft.ACE.OLEDB.12.0。这两者不要混用,混用的结果同样是那句「未找到提供程序」。
目录权限容易被忽略。上传目录必须给IIS_IUSRS或应用池虚拟账户写权限,否则学生点上传,页面转半天然后告诉你保存失败。数据库文件所在的data目录则相反,只需要读权限,最好连写权限都不给,避免被直接下载——IIS 默认会拒绝.mdb的静态请求,但这条规则在换服务器、换 IIS 版本时不一定还在,稳妥做法是把 data 目录放到站点根目录之外。
2.2 目录分层:从 login.asp 到 admin 后台的文件地图
老 ASP 项目的目录结构通常很朴素,按角色分文件夹,公共逻辑塞进inc。我拿到的这套大致长这样:
/webroot ├─ default.asp 入口,跳登录或首页 ├─ login.asp 登录页,含验证码 ├─ logout.asp 清 Session 跳回登录 ├─ global.asa Session_OnStart,存站点配置 ├─ /inc │ ├─ conn.asp 数据库连接,被所有页面 include │ ├─ check.asp 登录态与角色校验 │ ├─ func.asp 公共函数:转义、格式化、分页 │ └─ upload_5xsoft.inc 无组件上传类 ├─ /student │ ├─ hw_list.asp 作业列表(待交 / 已交 / 已批) │ ├─ hw_detail.asp 作业要求与附件下载 │ └─ hw_submit.asp 上传提交 ├─ /teacher │ ├─ hw_publish.asp 布置作业 │ ├─ submit_list.asp 某次作业的提交清单 │ └─ check_save.asp 打分与评语回写 ├─ /admin │ ├─ user_list.asp 学生 / 教师账号管理 │ ├─ class_list.asp 班级与课程 │ └─ score_export.asp 成绩导出 CSV ├─ /upload 学生提交的文件,按作业 ID 分目录 ├─ /data home.mdb 数据库文件 └─ /images这个分层有一个关键约定:所有业务页面第一行都是 includeconn.asp,第二行 includecheck.asp。前者建立数据库连接,后者判断 Session 里有没有UserID,没有就踢回登录页。这套约定简单粗暴,但也意味着任何新加的页面忘了 include,就是一个未授权访问的口子。我在二次开发时第一步永远是全站搜一遍check.asp的 include 数量,对一下页面总数。
global.asa是个容易被遗忘的文件,它放在站点根目录,IIS 会自动加载。里面通常写Session_OnStart,把站点名称、上传大小上限这类配置塞进 Session 或 Application,页面里直接读,省得每个文件都写死一遍常量。注意.asa文件改完不用重启 IIS,但浏览器要新开一个会话才生效,因为老的 Session 还在。
2.3 IIS 配置与跑通:从空白页到能登录的完整步骤
环境装完,落地步骤我按顺序走一遍,这套顺序能避免 90% 的「页面打不开」。
第一步,把站点目录指到/webroot,绑定端口。第二步,应用池按 2.1 说的设成无托管代码 + 32 位。第三步,进「ASP」设置页,把两个开关打开:启用父路径设为 True,将错误发送到浏览器设为 True。父路径这个开关很关键,老代码里大量<!--#include file="../inc/conn.asp"-->这种写法,父路径一关,全部报「Active Server Pages 错误 'ASP 0131'」。调试完上线前再把错误发送关掉,不然数据库连接字符串会直接显示给用户看。第四步,默认文档里加上default.asp和login.asp。第五步,给/upload加写权限,给/data保持只读。
改 IIS 的 ASP 配置也可以走命令行,批量部署时更方便:
# 开启父路径,命令行方式(需管理员权限) %windir%\system32\inetsrv\appcmd.exe set config "Default Web Site" ^ -section:system.webServer/asp /enableParentPaths:"True" /commit:apphostDefault Web Site换成你自己的站点名,/commit:apphost表示写到 applicationHost.config 而不是站点的 web.config,这样对该站点下所有应用生效。改完iisreset一下最保险,虽然多数时候回收应用池就够。
跑通之后第一件要做的事不是点功能,而是确认conn.asp里的Server.MapPath指向真的存在。很多人接手项目后页面一片空白,查半天是数据库路径写成了别人机器的绝对路径,Open失败又没有On Error Resume Next,页面就静默死了。我习惯在conn.asp末尾加一行If Err.Number <> 0 Then Response.Write "DB Error: " & Err.Description,只在调试期留,上线删掉。
3. 数据库设计:六张表把「布置—提交—批阅—统计」串成闭环
系统的所有业务逻辑,最后都落到数据库上。这套 ASP 网上作业提交系统的表不算多,但设计上有几个点值得说:角色用单表加字段区分、作业和提交分表、成绩挂在提交记录上而不是学生上。理解了这个模型,后面看任何一段 SQL 都不会迷路。
3.1 表结构与字段含义:角色、作业、提交三者的关系
核心六张表:用户表、班级表、课程表、作业表、提交表、日志表。关系是:一个老师教多门课,一门课属于一个班级,一门课下有多次作业,一次作业对应多条提交记录,每条提交记录挂一个学生和一份文件。
| 表名 | 主要字段 | 说明 |
|---|---|---|
| t_user | UserID, UserName, Password, RealName, Role, ClassID | Role 用 1/2/3 区分学生、教师、管理员 |
| t_class | ClassID, ClassName, Grade | Grade 存年级,方便按 K12 学段筛选 |
| t_course | CourseID, CourseName, TeacherID, ClassID | 一门课绑定一个授课教师和一个班 |
| t_homework | HwID, CourseID, Title, Content, Deadline, FullScore, Status | Status 控制是否开放提交 |
| t_submit | SubmitID, HwID, UserID, FileName, FilePath, FileSize, SubmitTime, Score, Comment, CheckTime, CheckUser | 一条记录对应一个学生的一次提交 |
| t_log | LogID, UserID, Action, TargetID, LogTime | 记录登录、提交、批阅,排查问题时很有用 |
几个设计细节值得留意。t_submit上没有对(HwID, UserID)建唯一约束,这是老项目常见的偷懒,导致同一学生可能提交多条记录,教师端列表就出现重复行。我一般在二次开发时补一个唯一索引,或者至少在提交前先SELECT COUNT(*)判断一次。t_homework.Deadline是datetime类型,判断是否逾期直接用Now() > Deadline,注意服务器时区和学生本地时区可能不一致,内网项目通常无所谓,跨区域部署就要统一。
Password字段老项目里多半是明文或者简单 MD5。拿到源码第一件事建议先看一眼存储方式,如果是明文,改造时至少换成「MD5 加盐」——ASP 里没有现成的bcrypt,用MD5(Password & Salt)也比明文强得多,盐可以存在用户表的另一个字段里。
3.2 建表脚本与初始化数据:能直接跑的 SQL
Access 的 DDL 语法和 SQL Server 有差异,下面这版是 Access 兼容写法,可以直接在 Access 查询设计器里执行,也可以在 ASP 里用conn.Execute跑一遍做初始化:
-- 用户表:Role 1=学生 2=教师 3=管理员 CREATE TABLE t_user ( UserID AUTOINCREMENT PRIMARY KEY, UserName TEXT(50) NOT NULL, Password TEXT(64) NOT NULL, RealName TEXT(50), Role INTEGER DEFAULT 1, ClassID INTEGER, Email TEXT(100), AddTime DATETIME ); -- 作业表:Deadline 之后学生端不再显示提交按钮 CREATE TABLE t_homework ( HwID AUTOINCREMENT PRIMARY KEY, CourseID INTEGER NOT NULL, Title TEXT(100) NOT NULL, Content MEMO, PublishTime DATETIME, Deadline DATETIME, FullScore INTEGER DEFAULT 100, Status INTEGER DEFAULT 1 ); -- 提交表:FilePath 存相对路径,Score 为空表示未批阅 CREATE TABLE t_submit ( SubmitID AUTOINCREMENT PRIMARY KEY, HwID INTEGER NOT NULL, UserID INTEGER NOT NULL, FileName TEXT(200), FilePath TEXT(255), FileSize LONG, SubmitTime DATETIME, Score DOUBLE, Comment MEMO, CheckTime DATETIME, CheckUser INTEGER ); -- 关键索引:按作业查提交、按学生查历史 CREATE INDEX idx_submit_hw ON t_submit (HwID); CREATE INDEX idx_submit_user ON t_submit (UserID);几点说明。AUTOINCREMENT是 Access 的自增写法,SQL Server 里对应IDENTITY(1,1),迁移时这一句必须改。MEMO对应长文本,Access 里单条记录有 4000 字符左右的限制,评语够用,作业要求内容如果很长要注意截断。LONG是长整型,存文件字节数,单个文件超过 2GB 才会有问题,作业场景不用担心。
FilePath我建议存相对路径比如/upload/2024/0601/1001_20240601.zip,不要存绝对路径。老代码里存Server.MapPath结果的情况很常见,一换服务器整个下载链接全废,这是迁移时最典型的翻车点。相对路径加Server.MapPath拼回来,换机器、换盘符都不受影响。
3.3 连接封装与公共函数:所有页面共用的那一层
inc/conn.asp是全站最核心的一个文件,它决定了系统连哪个库、用什么驱动。典型写法:
<% ' inc/conn.asp —— 全站共用的数据库连接 Dim conn, connStr, dbPath dbPath = Server.MapPath("/data/homework.mdb") ' Jet 4.0 只支持 32 位,应用池必须开 32 位 connStr = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" & dbPath & ";" On Error Resume Next Set conn = Server.CreateObject("ADODB.Connection") conn.Open connStr If Err.Number <> 0 Then Response.Write "数据库连接失败:" & Err.Description Response.End End If On Error GoTo 0 %>On Error Resume Next在这个位置是必要的,因为连接失败是全局性问题,必须给个明确提示而不是白屏。但注意Response.End之后页面就断了,所以这段代码要放在每个业务页面的最顶部,在任何 HTML 输出之前,否则会报「HTTP 头已发送」。
如果换成 SQL Server,连接串改成:
connStr = "Provider=SQLOLEDB;Data Source=127.0.0.1;Initial Catalog=HomeworkDB;User ID=sa;Password=xxx;"生产环境不要用sa,建一个只有db_datareader和db_datawriter权限的账号就够。老项目里连接串硬编码在conn.asp是常态,改造时可以把这段抽到一个不在站点目录下的配置文件,用FileSystemObject读进来,避免源码泄露时连数据库一起丢。
func.asp里通常放几个被反复调用的函数,最典型的是 HTML 转义和 SQL 单引号转义:
<% ' 防注入的最低成本做法:单引号翻倍 Function SafeStr(s) If IsNull(s) Then SafeStr = "" : Exit Function SafeStr = Replace(Trim(s & ""), "'", "''") End Function ' 输出到 HTML 时转义,防止 XSS Function HtmlEnc(s) If IsNull(s) Then HtmlEnc = "" : Exit Function HtmlEnc = Server.HTMLEncode(s & "") End Function %>SafeStr只能防住单引号注入,数字型参数、LIKE查询、ORDER BY拼接它都管不住,真正稳妥的是ADODB.Command参数化,4.3 节会写。但老代码里这套Replace满地都是,接手上先别急着全改,先把用户输入入口处——登录、搜索、提交评语——这三处改成参数化,性价比最高。
4. 核心模块拆解:一次作业提交从点击到入库的完整链路
前面铺垫了环境和数据,这一章看真正的业务代码。一个学生从打开页面到文件落库,中间要穿过登录态校验、表单解析、文件保存、记录写入四道关,任何一道出问题,表现出来都是「提交失败」四个字,看不到具体原因。把这条链路拆开看,排错时才知道该在哪一环下断点。
4.1 登录态与角色鉴权:Session 是唯一凭据
这套系统没有 token,没有 JWT,全靠Session。login.asp校验账号密码成功后,把UserID、Role、RealName塞进 Session,后续每个页面靠check.asp判断。
<% ' inc/check.asp —— 放在每个需要登录的页面顶部 Dim curUserID, curRole, curName curUserID = Session("UserID") curRole = Session("Role") curName = Session("RealName") ' Session 过期或未登录,带上原地址跳登录页 If IsEmpty(curUserID) Or curUserID = "" Then Response.Redirect "/login.asp?back=" & Server.URLEncode(Request.ServerVariables("URL")) Response.End End If %>Response.End不能省。Response.Redirect只是往响应头写一个 302,后面的代码在服务端照样会执行完,如果页面下半部分有数据库写操作,跳转和写入会同时发生。这个坑我在一个老项目上踩过,用户在未登录状态下刷新页面,居然往日志表里插了一条记录。
角色控制一般写在业务页面里,用If curRole <> 2 Then Response.End这种硬判断。教师页面要求Role=2或3,学生页面要求Role=1,管理员页面要求Role=3。粗糙但有效。注意curRole从 Session 里取出来是 Variant 类型,和数字比较时最好CInt(curRole)一次,否则在某些区域设置下"2" <> 2会成立,导致教师进不去自己的页面。
登录页通常还带一个验证码,老实现是用Server.CreateObject("Scripting.FileSystemObject")配合 GDI+ 组件生成图片,或者干脆用纯数字文本验证码存 Session 比对。纯文本那种安全性约等于零,内网项目常见,改成图片也不难,用System.Drawing的 COM 封装或者第三方 ASP 组件都行。真正要紧的是登录失败次数限制,不然密码可以无限撞。
4.2 作业上传:无组件上传到底在做什么
Classic ASP 没有内置的文件上传能力,Request.Form读不到enctype="multipart/form-data"的二进制内容。所以老项目全靠无组件上传类,原理是用Request.BinaryRead(Request.TotalBytes)把整个请求体读成字节流,然后手工按boundary分隔符切开,找出文件段和普通字段段。这套解析在 VBScript 里写起来极其痛苦,所以基本都是引入现成的类,upload_5xsoft.inc是最常见的一个。
页面端写法:
<!--#include file="../inc/conn.asp"--> <!--#include file="../inc/check.asp"--> <!--#include file="../inc/upload_5xsoft.inc"--> <% ' student/hw_submit.asp —— 接收学生提交的文件 Dim upload, fileObj, hwID, fileName, saveDir, savePath, fileSize hwID = Request.QueryString("hwid") Set upload = New UpFile_5xSoft ' 实例化上传类,内部已调用 BinaryRead Set fileObj = upload.File("hwfile") ' hwfile 是表单里 file 控件的 name If fileObj.FileSize <= 0 Then Response.Write "<script>alert('没有选择文件');history.back();</script>" Response.End End If ' 大小上限 20MB,超过直接拒绝 If fileObj.FileSize > 20 * 1024 * 1024 Then Response.Write "<script>alert('文件超过 20MB');history.back();</script>" Response.End End If ' 按作业 ID 分目录,避免单目录文件过多 saveDir = Server.MapPath("/upload/") & "\" & hwID & "\" ' 目录不存在则创建,FSO 不支持递归创建,需先建上级 Dim fso Set fso = Server.CreateObject("Scripting.FileSystemObject") If Not fso.FolderExists(saveDir) Then fso.CreateFolder(saveDir) ' 重命名:学号_时间戳_原文件名,避免同名覆盖 fileName = curUserID & "_" & Year(Now()) & Month(Now()) & Day(Now()) & _ Hour(Now()) & Minute(Now()) & Second(Now()) & "_" & fileObj.FileName savePath = saveDir & fileName fileObj.SaveAs savePath ' 写提交记录,有则更新,无则插入 Dim rsCheck Set rsCheck = conn.Execute("SELECT SubmitID FROM t_submit WHERE HwID=" & CLng(hwID) & " AND UserID=" & CLng(curUserID)) If Not rsCheck.EOF Then conn.Execute "UPDATE t_submit SET FileName='" & SafeStr(fileName) & "', FilePath='/upload/" & hwID & "/" & SafeStr(fileName) & "', FileSize=" & fileObj.FileSize & ", SubmitTime=#" & Now() & "# WHERE SubmitID=" & rsCheck("SubmitID") Else conn.Execute "INSERT INTO t_submit (HwID, UserID, FileName, FilePath, FileSize, SubmitTime) VALUES (" & CLng(hwID) & "," & CLng(curUserID) & ",'" & SafeStr(fileName) & "','/upload/" & hwID & "/" & SafeStr(fileName) & "'," & fileObj.FileSize & ",#" & Now() & "#)" End If %>这段代码有几个关键点。upload.File("hwfile")里的参数必须和 HTML 表单里<input type="file" name="hwfile">完全一致,大小写敏感,写错了FileSize就是 0,页面提示「没有选择文件」,你会以为是自己没选。SaveAs的路径必须是绝对物理路径,Server.MapPath转出来的那种,传相对路径会报权限错误,提示信息完全看不出是路径问题。
重命名规则里带上UserID和时间戳,是血泪经验。老代码直接存原文件名,一个班三十个人有三个叫「作业.docx」,后交的把先交的覆盖了,学生来找你说交了,你查记录确实有,但文件是别人的。加时间戳之后同名也不会覆盖,代价是文件名变长,教师端下载时得用FileName字段显示原名、用FilePath去取文件,两个字段分开用。
Now()在 Access SQL 里要用#包起来当日期字面量,SQL Server 里则用单引号。这是迁移时最容易批量出错的地方之一,#2024-06-01#在 SQL Server 里会被当字符串,虽然有时能隐式转换成功,但换个语言区域就报错。稳妥做法是统一用ADODB.Command加日期类型参数,让驱动自己去处理格式。
fso.CreateFolder不支持递归创建,/upload/12/这种一级目录没问题,如果以后改成/upload/2024/06/12/,得先建2024,再建2024/06,依次往上,否则报「路径未找到」。
4.3 教师批阅与成绩回写:参数化更新为什么更稳
教师端的流程是:进作业 → 看到提交清单 → 逐条打分 → 保存。清单页是个典型的分页查询,老写法用Recordset.PageSize加AbsolutePage,简单但不高效;改造成 SQL Server 后可以用ROW_NUMBER()做服务端分页,数据量上万时差别很明显。
打分保存这一步,我强烈建议用参数化而不是拼串,因为评语里出现单引号是家常便饭:
<% ' teacher/check_save.asp —— 教师打分回写 Dim cmd, submitID, score, comment submitID = CLng(Request.Form("submitid")) score = Request.Form("score") comment = Request.Form("comment") ' 基本校验:分数必须在 0 到满分之间 If Not IsNumeric(score) Then Response.Write "<script>alert('分数必须是数字');history.back();</script>" Response.End End If score = CDbl(score) Set cmd = Server.CreateObject("ADODB.Command") Set cmd.ActiveConnection = conn cmd.CommandText = "UPDATE t_submit SET Score=?, Comment=?, CheckTime=?, CheckUser=? WHERE SubmitID=?" ' 参数顺序必须和 ? 一一对应,Jet 不支持命名参数 cmd.Parameters.Append cmd.CreateParameter("Score", 5, 1, , score) ' adDouble cmd.Parameters.Append cmd.CreateParameter("Comment", 202, 1, 500, comment) ' adVarWChar cmd.Parameters.Append cmd.CreateParameter("CheckTime", 7, 1, , Now()) ' adDate cmd.Parameters.Append cmd.CreateParameter("CheckUser", 3, 1, , CInt(curUserID)) ' adInteger cmd.Parameters.Append cmd.CreateParameter("SubmitID", 3, 1, , submitID) cmd.Execute , , 128 ' adExecuteNoRecords Set cmd = Nothing %>CreateParameter的五个参数依次是:参数名、数据类型、方向(1 表示输入)、大小、值。类型常量5是adDouble,202是adVarWChar,7是adDate,3是adInteger。记不住没关系,关键是Access 的 OLEDB 驱动不支持命名参数,CommandText里必须用?占位,Append的顺序必须和?出现的顺序严格一致。写成@Score那种 SQL Server 风格,在 Access 下会直接抛「参数对象定义不正确」。
cmd.Execute , , 128里那个128是adExecuteNoRecords,告诉驱动这次不要返回记录集,能省一点开销。不加也能跑,但会多创建一个空 Recordset 对象。
批阅完成之后,学生端列表里这条记录的状态要跟着变。老代码往往靠Score IS NULL判断是否批阅,而不是用Status字段,两套逻辑并存就容易出现「已经打了分但状态还是待批」的情况。改造时统一到Status上:0 未提交、1 已提交待批、2 已批阅,所有查询都按Status走,别再依赖Score是否为空。这种状态字段统一的事,越早做越省事,等到数据几万条再来清洗,写脚本对账能耗掉一整天。
5. 避坑与排查:老 ASP 项目最容易翻车的五个地方
这套系统跑起来之后,真正花时间的不是写新功能,是处理那些「昨天还好好的,今天突然不行了」的问题。下面五条都是我实际遇到过的,按现象、原因、解决三段的顺序写,遇到类似的直接对号入座。
5.1 上传稍大一点就报 ASP 0104
现象很直接:传几百 KB 的小文件正常,传两三 MB 的 PDF 或者压缩包,页面立刻返回「Request 对象 错误 'ASP 0104 : 80004005'」,连业务代码都没进去。
原因是 IIS 对请求体大小有两道限制。第一道是maxRequestEntityAllowed,默认 200KB,这是 ASP 层面的;第二道是uploadReadAheadSize,默认 48KB 左右,在system.webServer/serverRuntime节点下。两道里任何一道超了都会被拦,报错信息还不一样。很多人只改了第一道,传大文件还是失败,就是卡在第二道。
解决是两道一起放开。图形界面在站点「ASP → 限制属性 → 最大请求实体主体限制」里改,单位是字节,20MB 就是 20971520。命令行更清楚:
# 放开 ASP 请求体上限到 20MB %windir%\system32\inetsrv\appcmd.exe set config "Default Web Site" ^ -section:system.webServer/asp /limits.maxRequestEntityAllowed:"20971520" /commit:apphost # 同时放开 serverRuntime 的预读大小 %windir%\system32\inetsrv\appcmd.exe set config "Default Web Site" ^ -section:system.webServer/serverRuntime /uploadReadAheadSize:"20971520" /commit:apphost改完iisreset。另外注意,应用池回收之后这些配置不会丢,但如果站点是从别的机器拷过来的,applicationHost.config里的站点名对不上,命令会静默失败,改完记得去配置文件里确认一遍。
5.2 中文文件名存进去变乱码,下载还 404
现象是学生上传「第一次作业.docx」,教师端列表里显示成一串问号或者????.docx,点下载提示文件不存在。
原因有两层。第一层是编码:ASP 页面默认按系统 ANSI 代码页解释,fileObj.FileName取出来的是原始字节,如果服务器区域设置和客户端不一致,中文就乱了。第二层更隐蔽——即使显示正常,SaveAs保存时用的文件名可能和数据库里存的对不上,因为浏览器传上来的文件名经过了 URL 编码,某些上传类不会自动解码。
解决分两步。保存时不要依赖原始文件名,用UserID + 时间戳 + 扩展名生成一个纯 ASCII 的安全名字,原始文件名单独存一个字段OriginName只用来显示和下载时重命名。这样磁盘上是1001_20240601153000.docx,界面上显示「第一次作业.docx」,两边都不受影响。下载时用Response.AddHeader "Content-Disposition", "attachment; filename=" & Server.URLEncode(originName),注意中文文件名要 URL 编码,否则某些浏览器会截断。
如果一定要保留中文原名,就得在保存前做一次编码转换,用ADODB.Stream把 UTF-8 转成本地代码页,代码量不小。我的建议是别折腾,统一用安全文件名加独立显示字段,一劳永逸。
5.3 Access 报「操作必须使用一个可更新的查询」
现象是查询页面正常显示数据,但只要点保存、删除、打分,就抛「Microsoft JET Database Engine 错误 '80004005' 操作必须使用一个可更新的查询」。
原因基本只有一个:/data目录没有给 IIS 工作进程写权限。Access 不是纯文件读取,任何写操作都需要在数据库同目录下创建.ldb锁文件,目录不可写就创建不了锁文件,写操作全部失败,但读操作不受影响。所以出现「能看不能改」这个特征,八成是权限。
解决是给IIS_IUSRS或者应用池的实际身份加Modify权限。应用池如果是ApplicationPoolIdentity,实际身份是IIS AppPool\你的池名,直接在文件夹安全里输这个名字加权限,比给IIS_IUSRS更精确。加完权限不用重启 IIS,刷新页面就能写。
还有一种情况是数据库文件被设成了只读属性,从别的机器拷贝过来时常带着只读标记,右键属性里把只读勾掉即可。第三种种情况是 SQL 语句本身有问题,比如对含MEMO字段的表做DISTINCT查询、或者 JOIN 了不可更新的视图,这种报错信息和权限问题一模一样,得看具体 SQL。排查顺序是:先看目录权限,再看文件只读,最后才怀疑 SQL。
顺带提一句.ldb文件。它是 Access 的锁文件,正常关闭连接后会自己消失,如果服务器异常重启后残留,会出现「文件已被占用」导致所有人连不上。做法是停掉 IIS 应用池,手动删掉.ldb,再启动。这也是为什么很多老项目后来都迁到了 SQL Server——并发一上来,Access 的锁竞争就开始玄学了。
5.4 放一会儿不操作就要重新登录
现象是教师正在打分,去接了杯水回来点保存,页面直接跳登录页,填的内容全丢了。
原因是 Session 超时。IIS 里 ASP 的默认会话超时是 20 分钟,从最后一次请求算起,超过就丢弃 Session,Session("UserID")变空,check.asp就把人踢走。老系统因为没有 AJAX 心跳,纯靠页面跳转触发请求,用户停在那儿不动,20 分钟一到就断。
解决有两条路。简单的是把超时调长,站点「ASP → 会话属性 → 会话超时」改成 120 分钟,命令行是:
%windir%\system32\inetsrv\appcmd.exe set config "Default Web Site" ^ -section:system.webServer/asp /session.timeout:"00:02:00" /commit:apphost注意格式是时:分:秒,写120会报错。但调长只是延缓问题,真正的坑在工作进程回收——应用池默认有闲置超时(默认 20 分钟无请求就回收)和定时回收(默认 1740 分钟),回收一次 Session 全清,用户照样被踢。所以还得去应用池的「回收」设置里把「闲置超时」关掉,或者至少调到和工作时间匹配。生产环境我一般直接设成 0(不回收),配合定时的低峰期手动回收。
彻底一点的做法是加一个隐藏 iframe 或者定时请求的接口,每 10 分钟打一次空请求保活。但这会带来无谓的服务器负载,用户量不大时没必要。另外 Session 依赖 Cookie,如果浏览器禁用了 Cookie,每次请求都是新会话,表现是「登录成功但一刷新就掉」,这种情况要在登录页加个提示。
5.5 拼串查询被注入,参数顺序也能踩坑
现象是某天发现成绩表里多出几条Score为 0 的奇怪记录,或者登录页随便输个' or '1'='1就进去了。
原因是全站用字符串拼接 SQL,Request.Form拿到什么直接往 SQL 里塞。SafeStr那种单引号翻倍的写法只能挡住一部分,数字型参数比如HwID=Request.QueryString("hwid")完全没防,?hwid=1 or 1=1就能把所有作业列出来。
解决是把所有涉及用户输入的 SQL 改成ADODB.Command参数化,至少要覆盖登录、查询、保存这三类。改造顺序建议按风险排:登录页最先改,然后是教师打分,最后是各种列表查询。列表查询即使被注入,最坏是数据泄露,登录页被注入就是整个系统失守。
参数化本身也有坑。CreateParameter的顺序必须和?严格对应,中间少写一个或者顺序颠倒,数据会静默写错位置,不报错。我见过一个案例,Score和CheckUser两个参数写反了,结果教师 ID 写进了分数字段,成绩单上一片 3 分 5 分,查了两天才发现。改完一定要拿一条测试数据跑一遍,去数据库里看字段值对不对,别只看页面提示「保存成功」。
还有一个细节:Request.Form取到的值都是字符串,CreateParameter声明成adInteger而传进去的是空字符串,会抛类型不匹配。所以传参前要么CLng一次,要么在页面上做非空校验。宁可多写一行If IsNumeric(x) Then,也别让错误在运行时才爆出来。
6. 二次开发:把它从单班工具改成能扛住整个年级的提交系统
这套系统最典型的改造需求,是原来只服务一两个班,用 Access 跑得好好的,现在要上整个年级,几百人同时交作业。这时候瓶颈会依次出现在数据库、上传、批量操作三个地方,改造顺序也应该按这个来。
6.1 从 Access 迁到 SQL Server 的改动清单
迁移不是改个连接字符串就完事,下面这几处必须一起改,漏一个就跑不起来。
| 改动点 | Access 写法 | SQL Server 写法 |
|---|---|---|
| 自增主键 | AUTOINCREMENT | IDENTITY(1,1) |
| 日期字面量 | #2024-06-01# | '2024-06-01' |
| 长文本类型 | MEMO | NVARCHAR(MAX) |
| 分页 | Recordset.AbsolutePage | ROW_NUMBER() OVER(...) |
| 连接串 | Provider=Microsoft.Jet.OLEDB.4.0 | Provider=SQLOLEDB或MSOLEDBSQL |
连接字符串改完之后,应用池的 32 位设置其实可以关掉了,SQL Server 的驱动有 64 位版本。但老代码里可能还有别的 32 位 COM 组件(比如生成验证码图片的那个),关之前先全站搜一遍Server.CreateObject,确认没有依赖 32 位的东西。
日期处理是迁移中最容易出错的地方。老代码里大量Now()拼进 SQL,Access 认,SQL Server 在默认语言设置下也认,但服务器语言如果是British English,06/01/2024会被解释成 1 月 6 日而不是 6 月 1 日,成绩时间全乱。稳妥做法是统一用参数化传adDate,让驱动处理格式,别在 SQL 字符串里出现日期字面量。
6.2 加作业查重和批量打包下载
年级规模上来之后,两个功能会立刻被提需求:一是查重,防止学生互抄;二是批量下载,教师不想一个个点。
查重最简单的做法是文件 MD5 比对,提交时算一次存进t_submit新加的FileHash字段,同一次作业下GROUP BY FileHash HAVING COUNT(*) > 1就能揪出完全相同的文件。ASP 里算 MD5 可以用System.Security.Cryptography的 COM 封装,或者调用certutil -hashfile命令行,后者简单粗暴:
certutil -hashfile "C:\upload\12\1001_20240601.docx" MD5输出第二行就是哈希值,用WScript.Shell执行并解析。缺点是每个文件起一个进程,几百个文件会慢,但对一次批阅来说可以接受。真正要防的是「改几个字就绕过 MD5」的情况,那就得上文本相似度,ASP 里做这个不现实,通常是另写一个小工具离线跑,把结果写回数据库,ASP 只负责展示。
批量下载建议在服务端打包成 zip 再返回,不要让用户点几十次。ASP 本身没有 zip 能力,常见做法是调用命令行 7z 或者用Shell.Application的 COM 接口。文件多的时候注意超时,IIS 默认的脚本超时是 90 秒,打包几百 MB 会被掐断,得在页面顶部Server.ScriptTimeout = 600临时放宽,并且打包目录用完要清理,不然磁盘很快满。
最后说个验证习惯。每次改完这类老系统,我不会只看页面能不能点,而是固定走一遍:新建一个测试学生账号 → 提交一个中文名文件 → 教师端打分 → 学生端看成绩 → 导出 CSV 打开确认编码。这五步能覆盖编码、权限、状态流转、导出格式四个最容易出问题的地方。从那以后我每次动数据库字段或者上传逻辑,都强制走一遍这条链路,宁可多花十分钟,也不想再经历一次期末前夜被叫起来修系统。希望帮到你。
本文还有配套的精品资源,点击获取