简介:面向个人站长与Web开发者的ASP源码方案,核心解决手机端拍照上传需反复确认的痛点,适用于移动巡检、现场取证、随手记等场景。系统仅需两个ASP页面:index.asp负责调起摄像头拍照并自动上传,无需二次确认即可连续拍传;view.asp用于后台管理,源码第7行预留了密码修改位。同时借助JS二维码库自动生成访问入口,方便手机扫码或收藏直达。压缩包共5个文件,包括2个ASP页面、1个JS二维码库、1张示例图片与1份说明文档,整体体积仅1.09MB,结构精简、易于部署调试。由于需调用手机摄像头,部署要求开启SSL并具备公网访问能力,适合有一定IIS基础的中高级开发者。已有28人学习,包含可直接运行的源码与关键逻辑注释,可快速掌握二维码生成、拍照无确认上传、后台管理等完整实现思路,并根据自身项目扩展功能。
1. 手机拍照无确认连续上传系统:解决的不是“拍照”,而是“传得快”
做外勤巡检、保险定损、工地验收、门店拜访这类场景的同学,多半遇到过同一个痛点:现场人员用手机拍了一堆照片,回到有网的地方再一张张点“上传”,经常漏传、传错、传一半。你催他,他还不耐烦。所谓的“手机拍照无确认连续上传系统ASP源码”,本质就是把这套“先拍后传”改成“拍完即传、连拍连传”——前端H5页面调用手机摄像头,拍完一张自动上传,不需要再点“确认上传”按钮,拍下一张的时候上一张可能已经在服务器落盘了。服务端用ASP技术栈(经典ASP或ASP.NET均可)接收、命名、落库,统一塞进一套存量管理系统里。适合谁?适合已经在用Windows服务器、不想为一个小功能引入Java/PHP新服务的团队,也适合专门研究ASP源码来做二次开发的技术型读者。这篇文章会把服务端接收、前端连续上传队列、IIS配置和真正上线时最容易翻车的坑,按我实际做过的方式给你拆开。
2. 无确认连续上传的底层逻辑:从“选文件”到“自动入列”
2.1 拍照无确认是怎么实现的:capture 属性与微信 JS-SDK 的取舍
先说“无确认”到底指什么。浏览器出于权限和安全考虑,不可能让网页在用户完全不知情的情况下调起相机咔咔连拍——那个叫间谍软件,不是正常业务系统。实际业务里说的“无确认”,是指用户只需要拍一次照或者选一次照片,系统自动开始上传,不再弹一个“确认上传”的二次确认框。这是两个不同的确认层级:拍照动作本身需要用户参与,但“要不要发送”这个确认被去掉了。
HTML5 的<input type="file">加上 capture 属性,是普通浏览器里的标准做法:
<input type="file" id="cameraInput" accept="image/*" capture="environment" />capture="environment" 在 Android 上会直接调起后置摄像头,iOS 的 Safari 表现略有差异,它可能仍然先弹照片选择器。想要更可控的拍照体验,常见做法是接入微信 JS-SDK 的 chooseImage,拍照后拿到的 localIds 可以直接作为 img 预览,再通过 uploadImage 接口传到微信临时素材,最后推给自己的服务器。但微信通道会多一跳,且只能存三天,长期保存还是得靠自己的接收接口。我给现场团队做的时候,默认给普通浏览器 Web 页走input capture + 隐藏表单,给企业微信内嵌页面走 JS-SDK,两套方案的上传提交逻辑共用同一个队列。
这里有个容易想偏的地方:无确认并不等于无进度。用户拍完照,页面要立刻给出“上传中”的视觉反馈,否则用户以为没拍上,又连拍好几张,造成重复数据。所以前端队列的每个条目都要有状态:拍摄完成、上传中、已上传、上传失败可重试。状态机越简单,现场越不容易乱。
2.2 连续上传的队列模型:串行队列比并行更稳
连续上传,最容易踩的坑是一上来就写“循环上传”:
// 错误示范:把选中文件全部并行发出去 for (let file of fileList) { upload(file); }手机浏览器尤其是安卓 WebView,并发发三五个大图请求,轻则排队阻塞,重则直接把 WebView 撑挂。单张动辄 3MB 到 8MB 的现场照片,并行上传会迅速打满带宽,还会让服务端 ASP 脚本在写磁盘时发生并发争用。我做这类系统,前端永远是一个串行队列:同一时刻只传一张,上一张完成或失败后,才从队列里取出下一张。
class UploadQueue { constructor() { this.list = []; this.busy = false; this.maxRetry = 2; } push(file, onProgress, onDone, onFail) { this.list.push({ file, onProgress, onDone, onFail, retry: 0 }); this.pump(); } pump() { if (this.busy || this.list.length === 0) return; const item = this.list.shift(); this.busy = true; this.uploadOne(item) .then((resp) => { item.onDone(resp); this.busy = false; this.pump(); }) .catch((err) => { if (item.retry < this.maxRetry) { item.retry += 1; this.list.unshift(item); // 放回队头重试 } else { item.onFail(err); } this.busy = false; this.pump(); }); } uploadOne(item) { const formData = new FormData(); formData.append('file', item.file); return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('POST', '/upload/upload.asp', true); xhr.upload.onprogress = (e) => { if (e.lengthComputable) item.onProgress(Math.round((e.loaded / e.total) * 100)); }; xhr.onload = () => { if (xhr.status === 200) resolve(xhr.responseText); else reject(new Error('HTTP ' + xhr.status)); }; xhr.onerror = () => reject(new Error('network error')); xhr.send(formData); }); } }这段代码里三个参数决定连续上传的体感:
- maxRetry 设为 2,是让弱网下有挽回余地,但不至于无限重试拖住后面队列。
- 失败任务
unshift回队头而不是push到队尾,因为越早重试越容易成功,丢到最后反而忘了这张没传上去。 - 串行
pump()每次只推一个任务,避免并发把服务端 ASP 同时处理多个二进制流的局面搞乱。
现场验证时,我会在 DevTools 的 Network 面板里把网络模拟成 Fast 3G,连续拍五张,看请求是不是一张接一张跑的。如果你发现浏览器同时发起了多个请求,说明队列没锁住。
2.3 上传协议与后端语言的适配:multipart 是唯一现实选择
无确认连续上传,本质上就是持续往服务器推 multipart/form-data 数据包。每条上传请求里包含一张图片的二进制内容,附带一个文件名、拍摄时间、经纬度(如果前端拿得到)、设备号。ASP 端接到的就是一个标准的 HTTP POST 请求。
有人问:“asp 能否搭配 mysql?”能,完全能。经典 ASP 通过 ADODB 连接 MySQL 的 ODBC 驱动即可,ASP.NET 则用 MySqlConnector 或 Oracle 官方驱动。图片本体不要进数据库,存磁盘,库里只落文件的相对路径、拍摄时间、上传时间、设备编号和上传状态。这样连续上传时数据库的压力只是多条 INSERT,而不是把大二进制塞进 BLOB 字段来回搬运,后者在 MySQL 的 packet 限制下很容易在连续上传中途翻车。
设计协议的时候,前后端约定一个轻量 JSON 返回结构。上传成功返回{"code":0,"path":"/uploads/2025/06/xxx.jpg"},失败返回{"code":1,"msg":"磁盘写入失败"}。前端 onProgress 只管进度条,onDone 里把后端返回的 path 填进当前照片的预览角标上。这些约定的字段,后端 ASP 接收完文件后一帧一帧拼出来就行。
3. ASP 源码的服务端骨架:IIS、接收函数与存储选型
3.1 win11 配置 IIS ASP 的最小操作清单
“win11配置iis asp”是现在捯饬 ASP 源码的人高频遇到的第一个门槛。Win11 默认没装 IIS,更没开 ASP 功能,得手动启用。图形界面装或者命令行装都行,命令行更快:
# 以管理员身份运行 PowerShell dism /online /enable-feature /featurename:IIS-WebServerRole /all dism /online /enable-feature /featurename:IIS-ASP /all装完 IIS 后,打开“Internet Information Services (IIS) 管理器”,在左侧“连接”里选中站点,双击中间“ASP”图标,把“启用父路径”设为 True。很多老的 ASP 代码里用了../相对路径,不开启这一项直接 500。然后在“处理程序映射”里确认 aspClassic 被启用。
最后在 C 盘留一个可写目录作为上传落点,比如C:\uploads,并给IIS_IUSRS加读写权限。右键目录 -> 属性 -> 安全 -> 编辑 -> 添加 -> 输入IIS_IUSRS,赋予“修改”权限。这个权限没给对的症状是:前端上传报 200,但 ASP 内部写入失败,返回的 JSON 里 code 是 1。
3.2 ASP 接收上传文件的经典代码:BinaryRead 与组件方案
经典 ASP 接收 multipart 图片,官方没有直接的 Request.Form 读取二进制的办法,因为Request.Form只解析文本字段,图片的二进制部分会被截断。最常用的两条路:第一,装第三方上传组件比如 AspUpload、SA-FileUp;第二,用Request.BinaryRead手动解析整个请求体。组件省事但要在服务器装 dll、注册组件;纯 ASP 解析不依赖环境,但代码长、边界情况多。我一般建议:生产上用组件,学习和调试用纯 ASP 解析足矣。
下面是纯 ASP 接收单张小图片的最小实现:
<% Option Explicit Dim RequestBin, i, boundary, pos, contentDisposition, filename, fileData, finalPath ' 1. 读入请求体全部字节 RequestBin = Request.BinaryRead(Request.TotalBytes) ' 2. 从 Content-Type 里取 boundary 分隔串 boundary = Mid(Request.ServerVariables("CONTENT_TYPE"), _ InStr(Request.ServerVariables("CONTENT_TYPE"), "boundary=") + 9) ' 3. 把字节流转成文本只是为了定位字段边界(对图片本体用字节操作) Dim binStr binStr = BinaryToString(RequestBin) pos = InStr(binStr, "filename=""") if pos > 0 Then Dim startFile startFile = InStr(pos, binStr, vbCrLf & vbCrLf) + 4 Dim endMarker endMarker = InStr(startFile, binStr, "--" & boundary & "--") if endMarker = 0 Then endMarker = InStr(startFile, binStr, "--" & boundary) End If If endMarker = 0 Then Response.Write "非法请求" Response.End End If ' 4. 截取二进制图片数据 Dim lenData lenData = endMarker - startFile fileData = MidB(RequestBin, startFile, lenData) ' 5. 生成唯一文件名,落盘 Dim fso, savePath savePath = Server.MapPath("/uploads/") & GenerateFileName() & ".jpg" Set fso = CreateObject("Scripting.FileSystemObject") Dim fs Set fs = fso.CreateTextFile(savePath, True) fs.Write fileData fs.Close Response.ContentType = "application/json" Response.Write "{""code"":0,""path"":""" & savePath & """}" Else Response.Write "{""code"":1,""msg"":""未发现文件字段""}" End If Function GenerateFileName() Dim r r = Year(Now()) & Right("" & Month(Now()), 2) & Right("" & Day(Now()), 2) _ & "_" & Right("" & Hour(Now()), 2) & Right("" & Minute(Now()), 2) _ & Right("" & Second(Now()), 2) & "_" & Int(Rnd * 100000) GenerateFileName = r End Function Function BinaryToString(binValue) Dim cl1, cl2, cl3, pl1, pl2, pl3 Dim result result = "" Dim k For k = 1 To LenB(binValue) Step 3 cl1 = AscB(MidB(binValue, k, 1)) cl2 = AscB(MidB(binValue, k + 1, 1)) cl3 = AscB(MidB(binValue, k + 2, 1)) result = result & Chr(cl1) If cl2 <> 0 Then result = result & Chr(cl2) If cl3 <> 0 Then result = result & Chr(cl3) Next BinaryToString = result End Function %>逻辑说明:这个脚本先取Request.TotalBytes拿到请求体完整长度,用BinaryRead全部读入内存。找 boundary 的目的是在二进制流里定位文件内容的起止位置。filename="出现的位置之后,后面隔一个空行就是文件二进制正文的开始,再往后到下一个 boundary 就是结束。中间截出来的MidB(RequestBin, startFile, lenData)就是完整图片数据。
参数说明:Request.ServerVariables("CONTENT_TYPE")拿到的头信息形如multipart/form-data; boundary=----WebKitFormBoundaryxxx,从中切 boundary 串时注意索引别差一个字符,切错会导致整个解析失败。startFile和endMarker这两个位置的定位是血泪经验,少算一个回车换行,写出来的文件会多一个字节或少几个字节,图片就可能打不开。如果你不想折腾解析,直接装 AspUpload 组件,代码缩成五行:
Dim upload, file Set upload = Server.CreateObject("Persits.Upload") upload.OverwriteFiles = False Set file = upload.Files("file") file.SaveAs Server.MapPath("/uploads/") & GenerateFileName() & ".jpg"3.3 存储落点:MySQL 存路径、磁盘存图片的常见做法
连续上传系统里,数据库表结构要简单到不能再简单。我一般建一个upload_log表:
CREATE TABLE upload_log ( id INT AUTO_INCREMENT PRIMARY KEY, device_no VARCHAR(64) DEFAULT '', latitude DECIMAL(10,6) DEFAULT 0, longitude DECIMAL(10,6) DEFAULT 0, file_path VARCHAR(255) NOT NULL, file_size INT DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;字段解释:device_no 记录是哪台手机传的,latitude/longitude 记录拍照位置,file_size 用于回传后的核对,create_time 按数据库时间,而不是相信手机本地时间——现场手机时间经常不准,统一用服务器时间做排序和审计。
ASP 连 MySQL 的连接串,经典写法是:
Dim conn Set conn = Server.CreateObject("ADODB.Connection") conn.Open "Driver={MySQL ODBC 8.0 Unicode Driver};Server=127.0.0.1;Database=upload_db;User=root;Password=yourpass;Option=3"Option=3是 MyODBC 里的常用配置,表示启用客户端多语句等行为,不加有时会出现中文乱码或者连接不稳定。这里也说明一个点:如果客户只有 ASP 环境没有 PHP/Java,MySQL 完全可以用,前提是你把 MySQL 的 ODBC 驱动装好,64 位 IIS 就要装 64 位驱动,别装岔了。
4. 可复用的前端源码:拍照到连续上传的完整闭环
4.1 HTML 与摄像头交互:input capture 的权限边界
完整的页面只有三个区域:拍摄按钮、预览列表、上传进度条区域。核心 HTML 部分如下:
<div id="cameraArea"> <button id="btnShoot">拍摄照片</button> <input type="file" id="cameraInput" accept="image/*" capture="environment" style="display:none" /> </div> <ul id="previewList"></ul> <div id="uploadStatus" style="display:none;">正在上传 0 张,成功 0 张</div>按钮点击触发隐藏的 file input 打开相机:
document.getElementById('btnShoot').addEventListener('click', function() { document.getElementById('cameraInput').click(); });这里注意一点:accept="image/*"和capture属性同时写,iOS 上有些版本会退化为照片选择器而不是相机,想要强制相机,试capture="camera";但新版本 Safari 对camera的支持反而收紧了,所以兼容写法还是environment为主,业务上接受用户从相册选历史照片也算合理行为。关键是不能在用户没点按钮时主动input.click(),浏览器会直接拦掉。
4.2 JavaScript 上传队列源码:连拍连传的前端闭环
拍照完成触发 change 事件后,立即把 File 对象加入上传队列,同时渲染预览。
document.getElementById('cameraInput').addEventListener('change', function(e) { const files = e.target.files; for (let i = 0; i < files.length; i++) { const file = files[i]; // 文件类型只收图片,大小限制 10MB if (!file.type.startsWith('image/')) continue; if (file.size > 10 * 1024 * 1024) { alert('单张超过10MB,请压缩后再传'); continue; } // 先展示本地预览 const previewItem = document.createElement('li'); const img = document.createElement('img'); img.src = URL.createObjectURL(file); img.width = 100; const statusSpan = document.createElement('span'); statusSpan.className = 'status'; statusSpan.innerText = '等待上传'; previewItem.appendChild(img); previewItem.appendChild(statusSpan); document.getElementById('previewList').appendChild(previewItem); // 入队 queue.push(file, function(pct) { statusSpan.innerText = '上传中 ' + pct + '%'; }, function(resp) { statusSpan.innerText = '已上传 ' + resp; updateGlobalStatus(); }, function(err) { statusSpan.innerText = '失败,点击重试'; // 给该条记录绑定重试点击 previewItem.onclick = function() { queue.push(file, statusSpan, arguments.callee.caller, window.console.error); }; } ); } // 清空 input 值,保证同一张照片连续选择多次时 change 事件不失效 e.target.value = ''; });第二等说明:e.target.value = ''这行很容易漏。如果不重置 input 的 value,用户连拍两张同样文件名的照片(很多安卓相册会生成相同文件名),第二次触发不了 change,照片就不会进入上传队列。重置 value 是最简单也最关键的一行。在这个循环里,预览图用URL.createObjectURL生成临时地址,不上传完成前不要 revoke,否则预览图会裂。
4.3 与 ASP 后端的约定:文件名与字段名的对账
前端 FormData 里 append 的字段名,后端 ASP 组件里读的 Files("file") 或者纯解析里找的 filename= 必须对得上。我统一约定:字段名就叫file,后端返回 JSON 里的 path 是相对路径。前端拿到 path 后,在全部上传完成时,把这次任务的所有 path 集合 POST 给一个finish.asp,由它把整批上传任务标记为“已提交”。这样连续上传哪怕中途断掉,服务端也知道哪些图片孤零零落盘了没有归队。
async function finishBatch(paths) { const resp = await fetch('/upload/finish.asp', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ taskId: currentTaskId, paths: paths }) }); const json = await resp.json(); if (json.code === 0) { alert('本次共上传 ' + paths.length + ' 张,全部入库'); } }这个 finish 接口存在的意义是:上传是逐张瞬时动作,业务上的“一批照片”需要有收口动作。没有它,你永远不知道对方到底传完没传完。
5. 连续上传上线避坑:六个我在现场踩过的真实大坑
5.1 IIS 默认请求长度限制:照片超过 2MB 被静默拦截
现象:前端传小图一切正常,传超过 2MB 的手机原图,请求发出去,服务端返回 404 或 400,ASP 代码根本没执行。
原因:IIS 的maxAllowedContentLength默认是 30000000 字节,看起来不小;但 ASP 的Request.BinaryRead还会受 ASP 脚本自身的AspMaxRequestEntityAllowed限制,经典 ASP 默认值是 204800 字节,也就是 200KB。超过 200KB,请求直接被 IIS 掐断。
解决:在 IIS 管理器里选择站点,双击“ASP”,在“限制属性”里把“最大请求实体主体限制”调大,或者直接改 C:\Windows\System32\inetsrv\config\applicationHost.config:
<configuration> <system.webServer> <asp> <limits maxRequestEntityAllowed="209715200" /> </asp> <security> <requestFiltering> <requestLimits maxAllowedContentLength="209715200" /> </requestFiltering> </security> </system.webServer> </configuration>两个节点都得改,上面的管 ASP 解析的请求体上限,下面的管 IIS 请求筛选器。改完重启站点,单张 200MB 以内都能进 ASP。
5.2 写入目录权限不足:IIS_IUSRS 写不进去
现象:前端上传请求返回 200,但 ASP 返回 JSON 里是 code=1,msg=磁盘写入失败。去服务器看 IIS 日志也没有 500。
原因:上传目录没给 IIS 工作进程账户写权限。IIS 经典模式跑在 IUSR 账户下,IIS 6.0/7.5 之后是 IIS_IUSRS 组。
解决:
# 管理员 PowerShell 给上传目录授权 icacls C:\uploads /grant "IIS_IUSRS:(OI)(CI)(M)" /T icacls C:\uploads /grant "IUSR:(OI)(CI)(M)" /T参数说明:(OI)表示文件对象继承,(CI)表示目录继承,(M)是修改权限。不加/T只改顶层目录,子目录不会继承。改完最好把 IIS 进程回收一次:iisreset /restart。
5.3 文件重名与后传覆盖先传
现象:现场两个人同时用同一台手机或同一批次设备上传,后传的照片把先传的同名照片覆盖了。或者 WebView 在弱网下重试时,同名的旧文件覆盖了新文件。
原因:文件名用了时间戳到秒,同一秒内两张照片同名,或者前端重试时带了相同文件名。
解决:后端命名必须加随机成分和请求序号。
Function GenerateFileName() Randomize Dim t t = Year(Now()) & Right("0" & Month(Now()), 2) & Right("0" & Day(Now()), 2) _ & "_" & Right("0" & Hour(Now()), 2) & Right("0" & Minute(Now()), 2) _ & Right("0" & Second(Now()), 2) & "_" & Int(Rnd * 89999) + 10000 GenerateFileName = t End FunctionInt(Rnd * 89999) + 10000生成 10000~99999 的五位随机数。同一个秒内最多产生约 9 万个不同文件名,对于现场采集足够。另外在保存前加一层文件存在性检查:fso.FileExists(savePath),若存在则再补一个_dup_后缀,绝不覆盖。
5.4 ASP 执行超时与假失败
现象:一张 5MB 的照片上传,前端等了 40 秒后报失败,但后端文件已经写完、数据库也插入了。前端重试一次,产生重复记录。
原因:ASP 脚本默认Server.ScriptTimeout是 90 秒,但如果处理函数里有大文件二进制转换、磁盘写入慢,或者当时 IIS 繁忙,脚本可能在写完文件但还没返回 JSON 响应时被强制终止。前端拿不到响应,认为失败。
解决:一是页头显式把超时调长:
<% Server.ScriptTimeout = 300 %>二是在文件写完、响应未返回之前,把状态先写进一个内存标记表。更稳妥的办法是前端重试前先调一个query.asp?path=xx检查文件是否已存在,存在就不重发,只补写数据库记录。这个“后悔药”机制在线下弱网场景帮了我很多次,重传风暴被直接掐断。
5.5 弱网下的重试风暴:手机 4G 断点后连续重试
现象:信号不好的写字楼地库,用户连拍五张,每张都失败重试,重试又失败,整个上传队列被卡死,用户开骂。
原因:前面队列里我设置了 maxRetry=2,如果现场一直弱网,第一张连续失败两次后直接 onFail,队列会继续处理第二张,但用户看到的全是失败列表。而且失败后手动点击重试,会导致同一张照片在前面队列里被多次排队。
解决:把重试策略改成退避重试,而不是立即重试。第一次失败等 3 秒,第二次失败等 10 秒,第三次失败把照片标记为“待上传”,并允许用户手动单独重传该张。代码里用一个 setTimeout 包住 unshift 操作。
catch (err) { if (item.retry < this.maxRetry) { item.retry += 1; const delay = item.retry === 1 ? 3000 : 10000; setTimeout(() => { this.list.unshift(item); this.busy = false; this.pump(); }, delay); } else { item.onFail(err); this.busy = false; this.pump(); } }这个改动让现场用户感受到的是“上传慢”,而不是“上传挂了”。慢可以接受,挂了就没人愿意用了。
5.6 连续上传时 Session 并发异常
现象:连续上传过程中,用户发现部分图片入库了,但登录状态掉了,被迫重新登录,后续图片传到了匿名目录。
原因:ASP 默认 Session 是进程内保存,并发请求多了之后 Session 锁冲突,或者 IIS 进程回收导致 Session 丢失。经典 ASP 的 Session 并发写支持很差,手机上 WebView 并行上传会碰上。
解决:上传接口改为不依赖 Session,改用一个短期的 upload token,在页面打开时通过token.asp换取,上传时把 token 放进自定义 header 或表单字段。这样即便 Session 回收,只要 token 没过期,上传流程不受影响。
' token.asp 生成令牌 Dim token token = GenerateToken(deviceNo, Now()) Session("UploadToken") = token Response.Write token Function GenerateToken(deviceNo, nowTime) Dim base base = deviceNo & "_" & Hour(nowTime) & Minute(nowTime) & Second(nowTime) & "_" & Int(Rnd * 9999) GenerateToken = base End Function前端每次上传把 token 放进 FormData,服务端在上传入口只核对 token 是否和 Session 里的值一致,不一致就拒绝。注意这个方案只防接口被乱调,不防高并发爆破,因为 token 还在 Session 里,Session 本身是瓶颈,但已经足够覆盖现场身份校验的场景。
6. 从“能传”到“传得稳”:文件名、校验与压缩三板斧
三斧子下去,连续上传系统才算敢放开给人用。
第一斧,统一走服务端生成的唯一文件名,绝不信任前端传的文件名。前端的 File.name 在不同品牌手机上五花八门,有的叫 IMG_20250617_123456.jpg,有的直接叫 1.jpg,甚至重名。我在上一章里给出的 GenerateFileName,建议把它抽成一个公共函数放到inc_save.asp,所有上传入口共用。命名规则里带上日期目录:/uploads/2025/06/17/xxx.jpg,这样磁盘文件按天归档,后面做冷备和清理都方便。
第二斧,服务端要校验文件是不是真图片。只看扩展名没用,把 jpg 改成 png 的脚本文件也是图片后缀。实际的校验在 ASP 里读文件头几个字节,JPEG 的开头是 FF D8 FF,PNG 是 89 50 4E 47。
Function IsValidImage(fileData) Dim header header = MidB(fileData, 1, 4) If AscB(MidB(header,1,1)) = &HFF And AscB(MidB(header,2,1)) = &HD8 _ And AscB(MidB(header,3,1)) = &HFF Then IsValidImage = True ' JPEG ElseIf AscB(MidB(header,1,1)) = &H89 And AscB(MidB(header,2,1)) = &H50 _ And AscB(MidB(header,3,1)) = &H4E And AscB(MidB(header,4,1)) = &H47 Then IsValidImage = True ' PNG Else IsValidImage = False End If End Function参数说明:JPEG 只要校验前三个字节,PNG 要校验前四个字节。文件头检查通过后,再校验 file_size,单张控制在 10MB,超了就返回错误码,让前端提示用户压缩。这个是防御不仅来自正常图片,也来自无效的垃圾数据。
第三斧,图片压缩。手机原图随手一拍就是 4~8MB,连续拍 50 张就是 300MB,服务器磁盘吃紧、上传时间翻倍。常见做法是服务端用 ASP.NET 的 System.Drawing 缩成最长边 2000px 的 JPEG,质量 80。经典 ASP 里没有内置图像库,要么用第三方组件,要么在手机上处理。我的做法是优先前端压缩:canvas 把图片最长边缩到 2000px,toDataURL('image/jpeg', 0.8),输出 Blob 再上传。这样服务端磁盘写的是压缩后的 200~500KB 文件,IIS 也不需要把请求体上限调到很大。
function compressImage(file, maxSize, quality) { return new Promise((resolve, reject) => { const img = new Image(); const url = URL.createObjectURL(file); img.onload = function() { const scale = Math.min(1, maxSize / Math.max(img.width, img.height)); const canvas = document.createElement('canvas'); canvas.width = Math.round(img.width * scale); canvas.height = Math.round(img.height * scale); const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0, canvas.width, canvas.height); canvas.toBlob(function(blob) { URL.revokeObjectURL(url); resolve(blob); }, 'image/jpeg', quality); }; img.onerror = reject; img.src = url; }); }maxSize填 2000,quality填 0.8。这个参数组合在手机上基本看不出画质损失,但体积能压掉 80% 以上。如果你对清晰度敏感,比如定损拍照要看裂纹细节,maxSize 提到 3000,但要注意单张体积可能涨到 1MB 以上,IIS 和带宽都得更宽裕。
这三板斧做完,这套“手机拍照无确认连续上传系统”基本可以从开发机搬到生产了。我自己做这套东西的习惯是:上线头两周,每天看一眼 upload_log 表里是否有空 file_path 记录、磁盘是否无故多出孤儿文件。如果发现孤儿文件,说明前端 finish 批次接口和后端落盘之间还有断点,抓紧在对应环节加补偿。这一路调试下来,最大的教训就是“无确认”三个字骗不了人,现场用户只会管你传上去没有、快不快——你前端优化做得再好,服务端落盘不稳,一切等于零。希望帮到你。
本文还有配套的精品资源,点击获取