news 2026/11/2 21:32:06

ASP三级菜单源码解析:Access数据库、递归树与缓存权限改造

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ASP三级菜单源码解析:Access数据库、递归树与缓存权限改造

简介:这份ASP三级菜单源码面向需要构建多层级网站导航的Web开发者与ASP学习者,尤其适合电子商务站点展示产品分类、栏目层级等场景。压缩包为rar格式,约1.81MB;文件总数暂未标注,类型明细也暂缺,从内容构成看以HTML示例页、TXT说明文本、URL快捷方式及ASP主源码为主,可用于参考菜单结构、样式布局与服务端动态生成方式。已有193人浏览学习,说明其在同类ASP导航实现中具备一定参考价值。源码围绕无序列表、CSS布局和JavaScript展开折叠效果组织,并结合ASP按数据库数据生成菜单项,读者可据此理解三级菜单的层级组织、交互控制与页面结构对应关系。对于需要改造现有导航、复用多级分类逻辑或研究服务端渲染菜单的开发者,这份包体较小、主题集中的代码资源便于快速查阅和二次定制。

1. 从一张硬编码的侧边栏说起:ASP 三级菜单源码能接住什么活

后台侧边栏那个三级分类,前端同事改了三次都没改对,最后发现是三个页面各自硬编码了一份 HTML,菜单名字一改就漏一处。这套 ASP 三级菜单源码走的是另一条路:菜单全部躺在数据库一张表里,靠pid串成树,页面加载时一个递归函数把平表压成三级ul,前端一段折叠脚本负责展开收起,后台再加两个增删改页面就能自己维护。它是典型的 ASP + Access 老站配置,asp源码 结构简单、无需编译、丢进 IIS 目录配好应用池就能跑,适合还在维护十几年前后台系统、又不想为一个菜单引入整套前端框架的人。

2. 目录结构与数据表:三级菜单的骨架和 pid 约定

拿到源码包先别急着点index.asp,先把目录翻一遍。这类 ASP 项目没有构建步骤,文件放哪、被谁<!--#include-->进去,全靠约定,搞清楚调用关系比读代码本身更重要。

2.1 一份能直接跑的最小目录清单

完整的源码包通常长这样,不同作者命名略有出入,但分层逻辑基本一致:

menu_src/ ├── index.asp 前台演示页,菜单渲染入口 ├── conn.asp 数据库连接,全站唯一入口 ├── inc/ │ ├── fun.asp 公共函数:LoadMenuCache / RenderMenu / HasChild │ └── check.asp 后台登录校验,被 admin 下所有页面 include ├── admin/ │ ├── menu_list.asp 菜单列表(按树形缩进展示) │ ├── menu_add.asp 新增表单,父级用下拉选择 │ ├── menu_save.asp 接收 POST 入库,新增与编辑共用 │ └── menu_del.asp 删除,含子级检查 ├── js/ │ ├── menu.js 展开收起 + 三级联动 │ └── jquery.min.js 老项目常见,1.x 版本 ├── css/ │ └── menu.css 层级缩进、箭头图标、展开状态 ├── images/ │ └── arrow.gif 文件夹/展开箭头小图 └── data/ └── menu.mdb Access 数据库

这里有两个细节值得注意。conn.asp放在根目录而不是inc/下,是因为admin/里的页面用<!--#include virtual="/conn.asp"-->引用时,虚拟路径比相对路径稳,不会因为页面层级不同而找不到文件。data/menu.mdb直接放在站点目录下,意味着任何人猜到路径都能把整个库拖走,这是老项目最典型的翻车点之一,后面避坑章节会展开说。

inc/fun.asp是整个项目的核心,LoadMenuCache、RenderMenu、HasChild三兄弟都在这儿。index.asp干的事其实只有三行:include 连接、include 函数、调用RenderMenu。

2.2 菜单表字段设计:pid 决定一切

整个三级结构压在一张表上,靠自关联实现。在 Access 设计视图里建表,字段大概是这样:

字段名类型说明常见取值
id自动编号主键1、2、3…
pid数字(长整型)父级 id,顶级固定为 00 表示一级菜单
menu_name文本(50)显示名称订单中心
menu_url文本(255)链接地址,有子级时留空/order/list.asp
menu_target文本(10)打开方式_self/_blank
sort_id数字(长整型)同级排序,小的在前10、20、30
is_show数字(字节)是否显示1 显示,0 隐藏
role_flag文本(50)预留的权限标记空 /admin

pid = 0表示顶级,这是最常见的约定,但也是最容易出问题的地方。有些老库把顶级写成NULL,CStr(Null)在 VBScript 里得到空字符串,跟"0"比较永远不相等,结果就是一级菜单一个都不显示。接手别人写的库,第一件事永远是SELECT pid, COUNT(*) FROM t_menu GROUP BY pid看一眼顶级到底存的是什么。

sort_id用 10、20、30 这种步长而不是 1、2、3,是给自己留后悔药:以后想在两个菜单中间插一个,不用把后面全部重排。这个习惯我从这套源码里学来,之后所有排序字段都这么留。

示例数据大概是这样,注意一级菜单的menu_url是空的:

INSERT INTO t_menu (pid, menu_name, menu_url, menu_target, sort_id, is_show) VALUES (0, '订单中心', '', '_self', 10, 1); INSERT INTO t_menu (pid, menu_name, menu_url, menu_target, sort_id, is_show) VALUES (1, '订单管理', '', '_self', 10, 1); INSERT INTO t_menu (pid, menu_name, menu_url, menu_target, sort_id, is_show) VALUES (11, '待发货订单', '/order/list.asp?state=1', '_self', 10, 1);

三级关系就是0 → 1 → 11,靠两条记录把三层串起来。想加第四级?表结构完全支持,但前端 CSS 缩进和递归上限得跟着改,源码默认按三级写死。

2.3 连接字符串与编码约定

conn.asp是每个页面都要 include 的文件,它同时承担两件事:开数据库连接、定页面编码。

<% ' conn.asp —— 全站唯一的数据库连接入口 Dim conn, dbPath dbPath = Server.MapPath("data/menu.mdb") Set conn = Server.CreateObject("ADODB.Connection") ' 32 位应用池配 Jet 4.0;如果站点跑 64 位且装了 ACE 驱动,换成 ACE 串 conn.ConnectionString = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" & dbPath & ";Persist Security Info=False" conn.Open ' 页面编码必须和 .asp 文件本身保存的编码一致 Response.Charset = "GB2312" Session.CodePage = 936 %>

这里面每个参数都不是随便写的。Server.MapPath把相对路径转成物理路径,好处是站点整体搬家时不用改配置。Persist Security Info=False是 Access 连接字符串的常规写法,意义不大但保留着无害。真正要命的是最后两行编码设置:Response.Charset决定响应头里那个Content-Type后面跟什么字符集,Session.CodePage决定服务端处理字符串时按什么代码页解释。两个都设成 GB 系(936),文件本身也必须存成 ANSI,三者不一致就会出现菜单名全是问号或者乱码的经典画面。

如果你想把项目迁到 UTF-8,那就要三处一起改:文件另存为 UTF-8 无 BOM、Response.Charset = "utf-8"、Session.CodePage = 65001。少改一处都白搭。

3. 核心链路拆解:GetRows 缓存 + 递归函数把平表压成三级树

菜单渲染这条链路,从数据库到浏览器要经过三个环节:取数据、建树、输出 HTML。这套源码最值得看的就是它把「建树」拆成了数组遍历,绕开了 ASP 里最容易出问题的嵌套记录集。

3.1 把整表读成数组:GetRows 替掉嵌套记录集

新手写递归菜单,第一反应是在递归函数里SELECT ... WHERE pid=查一次,递归到哪层查到哪层。思路没错,但在 ASP + Access 下很容易遇到游标问题:同一个Connection上同时开着多个Recordset,Access 的默认游标不支持,页面直接抛出「记录集已关闭」或者「当前记录集不支持该操作」。

这套源码的处理方式是把整张表一次性读成二维数组,递归时只遍历内存:

<% ' inc/fun.asp —— 一次查询,把整张菜单表读进数组 Function LoadMenuCache() Dim rs, arr Set rs = Server.CreateObject("ADODB.Recordset") ' 1,1 = adOpenKeyset, adLockReadOnly,只读场景最省资源 rs.Open "SELECT id,pid,menu_name,menu_url,menu_target,sort_id " & _ "FROM t_menu WHERE is_show=1 ORDER BY sort_id ASC, id ASC", _ conn, 1, 1 If rs.EOF Then LoadMenuCache = Empty ' 空表返回 Empty,交给调用方判断 Else LoadMenuCache = rs.GetRows() ' 二维数组,arr(字段序号, 行号) End If rs.Close Set rs = Nothing End Function %>

GetRows()返回的是个二维数组,第一维是字段在SELECT里的顺序,第二维是行号。所以arr(0, i)是id,arr(1, i)是pid,arr(2, i)是menu_name,以此类推。这个顺序必须和 SQL 里的字段顺序严格对齐,中间插一个字段,后面所有下标全部错位,这是改这类代码最容易犯的错。

WHERE is_show=1把隐藏菜单过滤在数据层,比在递归里判断要清爽。ORDER BY sort_id ASC, id ASC保证同级按手工排序走,sort_id相同的时候用id兜底,避免排序不稳定导致每次刷新菜单顺序变来变去。

空表返回Empty而不是空数组,是因为 VBScript 里空数组的UBound会直接报错,用IsEmpty判断更安全。

3.2 递归函数 RenderMenu:level 上限与排序

拿到数组之后,递归逻辑就变成了纯粹的遍历比对:找pid等于当前节点的行,输出<li>,再拿这行的id当新的pid往下走。

<% ' inc/fun.asp —— 按 pid 递归输出三级 ul Sub RenderMenu(arr, pid, level) Dim i, id, nm, url, tgt, cls, hasChild If IsEmpty(arr) Then Exit Sub If level > 3 Then Exit Sub ' 硬性限制三层,脏数据撑不爆页面 Response.Write "<ul class=""menu lv" & level & """>" For i = 0 To UBound(arr, 2) If Trim(CStr(arr(1, i))) = CStr(pid) Then id = arr(0, i) nm = arr(2, i) url = arr(3, i) tgt = arr(4, i) hasChild = HasChild(arr, id) If hasChild Then cls = "item has-child" Else cls = "item" End If Response.Write "<li class=""" & cls & """>" If hasChild Then ' 有子级的不跳转,交给 JS 展开 Response.Write "<a href=""javascript:;"" class=""parent"">" & nm & "</a>" Else Response.Write "<a href=""" & url & """ target=""" & tgt & """>" & nm & "</a>" End If If hasChild Then RenderMenu arr, id, level + 1 Response.Write "</li>" End If Next Response.Write "</ul>" End Sub ' 判断 id 下面还有没有显示中的子菜单 Function HasChild(arr, pid) Dim i HasChild = False For i = 0 To UBound(arr, 2) If Trim(CStr(arr(1, i))) = CStr(pid) Then HasChild = True Exit Function End If Next End Function

几个参数和判断值得单独说。

pid传进来的是当前层要找的父 id,第一次调用传0,之后每次递归传当前节点的id。level从 1 开始,每下一层加 1,超过 3 直接Exit Sub。这一行看着像偷懒,其实是保命的:数据库里一旦出现pid指回自己、或者 A 是 B 的父级而 B 又是 A 的父级这种循环引用,没有这行限制,递归会一直跑下去直到 IIS 报脚本超时。

Trim(CStr(...))的双重处理是为了兼容脏数据。有的老库pid字段建成了文本型,存进去的值带着空格;有的字段允许空值,CStr(Null)得到空串。统一转成字符串再去空格比较,能挡掉一大半「明明有数据但子菜单不显示」的问题。

HasChild单独抽成函数,是为了让每个节点知道自己该不该渲染成可展开的父节点。这个判断在RenderMenu里被调用了两次(一次算 class,一次决定要不要递归),数据量大时会有重复遍历。菜单通常就几十上百条,两次遍历完全无所谓;真到了上千条,把HasChild的结果预先算成字典缓存会更快,但那是另一个量级的需求了。

页面上调用只有一句:

<% Dim menuArr menuArr = LoadMenuCache() RenderMenu menuArr, 0, 1 %>

3.3 用 Application 缓存菜单,减少每次查询

菜单数据和订单数据不一样,它一年也改不了几次,但每个后台页面都要渲染。每次请求都开一次数据库连接查一遍全表,纯属浪费。

源码里用Application做了一层缓存:

<% ' inc/fun.asp —— 菜单数据的读缓存 Function GetMenuData() Dim arr arr = Application("MENU_DATA") If IsEmpty(arr) Then arr = LoadMenuCache() Application.Lock Application("MENU_DATA") = arr Application.Unlock End If GetMenuData = arr End Function ' 后台增删改菜单之后必须调一次,否则前台看到的还是旧数据 Sub ClearMenuCache() Application.Lock Application.Contents.Remove "MENU_DATA" Application.Unlock End Sub %>

Application是整站共享的,进程重启才清空。第一次访问时Application("MENU_DATA")是Empty,走进LoadMenuCache()查库,然后加锁写进去。后面所有请求直接命中缓存,一次数据库都不碰。

加锁这件事不能省。Application.Lock/Unlock之间只做一次赋值,临界区极小,但少了这两行,两个请求同时判断IsEmpty都为真,就会重复查库并互相覆盖。

要提醒的是,Application存数组在 ASP 里每次读取都会做一次复制,菜单几十条的时候完全无感,上千条就得掂量一下内存开销。另外这套缓存是「读的时候才建」,如果访问量极低,缓存可能还没热起来就已经过期了,不过对后台系统来说这不是问题。

真正容易忘的是ClearMenuCache。后台menu_save.asp和menu_del.asp执行完 SQL 之后,一定要补一句调用,否则管理员改完菜单刷新前台发现没变化,会以为保存失败,反复操作。这种「改了没生效」的问题排查起来最费时间,因为代码逻辑明明是对的。

4. 前端交互层:展开收起、三级联动与当前页高亮

后端负责把结构吐出来,交互全在js/menu.js和css/menu.css里。这部分代码量不大,但有几个老项目特有的写法值得单独拎出来。

4.1 展开收起:事件委托与 return false

菜单节点是服务端渲染出来的,数量不固定,给每个<a>单独绑事件既啰嗦又容易漏。源码用的是事件委托,绑在根容器上:

// js/menu.js —— 事件委托,新增菜单不用重新绑定 (function () { var root = document.getElementById('menuTree'); if (!root) return; root.onclick = function (e) { var el = (e || window.event).target || (e || window.event).srcElement; // 往上找最近的 li,找到 root 就停,防止越界 while (el && el !== root && el.nodeName.toUpperCase() !== 'LI') { el = el.parentNode; } if (!el || el === root) return; if (!/(^|\s)has-child(\s|$)/.test(el.className)) return; // 切换 open 类,CSS 负责显示隐藏 if (/(^|\s)open(\s|$)/.test(el.className)) { el.className = el.className.replace(/\s*\bopen\b/g, ''); } else { el.className += ' open'; } return false; // 关键:阻止 a 标签默认跳转 }; })();

root.onclick而不是addEventListener,是因为这套源码要兼容早期浏览器,onclick属性在所有环境都能用。(e || window.event).target || srcElement是同一件事的两种取值方式,一套代码通吃新旧浏览器。

while循环往上找li,是因为点击的可能是<a>,也可能是里面的文字节点或者图标。找到li之后再用正则判断 class,别用indexOf('has-child'),因为has-child-xxx这种命名会误判。

return false那一行是血泪经验。父级菜单的href写的是javascript:;,看着不会跳转,但在某些浏览器里依然会触发滚动到顶部,加了return false才彻底干净。有的项目写成href="#",那就必须return false,否则地址栏后面会挂个#,用户刷新一下状态全丢。

配套的 CSS 很短:

/* css/menu.css —— 折叠靠 class 切换,不做动画,老浏览器也稳 */ .menu .item > ul { display: none; } .menu .item.open > ul { display: block; } .menu .lv2 { padding-left: 16px; } .menu .lv3 { padding-left: 32px; } .menu .item.has-child > a.parent::before { content: "▸ "; } .menu .item.open > a.parent::before { content: "▾ "; }

>子选择器很重要,只影响直接子级,不会把孙子级的ul一起显示出来。缩进用padding-left递增,比嵌套margin好控制,也不会因为父级宽度变化而错位。

4.2 三级联动 select 版

除了树形展开,这套源码还带了一个select三级联动版本,常见于「选分类」的表单场景。做法是把菜单数据输出成一段 JS 数组,前端根据上一级的值过滤下一级。

服务端输出部分:

<% ' menu_data.asp —— 把菜单表输出成 JS 数组,供联动使用 Response.ContentType = "text/javascript" Response.Charset = "utf-8" Dim arr, i, s arr = GetMenuData() s = "var MENU_DATA=[" & vbCrLf For i = 0 To UBound(arr, 2) If i > 0 Then s = s & "," & vbCrLf ' 单引号必须转义,否则名字里带 ' 直接把 JS 语法打断 s = s & "{id:" & arr(0, i) & ",pid:" & arr(1, i) & _ ",name:'" & Replace(Replace(arr(2, i), "\", "\\"), "'", "\'") & "'}" Next s = s & "];" Response.Write s %>

Replace套两层是必须的:先处理反斜杠再处理单引号,顺序反了会把转义出来的\'里的\又转一遍。菜单名里出现'的概率不低,比如「某某's 专区」,不转义直接白屏。

前端填充逻辑:

// js/menu.js —— 根据父 id 填充某个 select function fillSelect(sel, pid) { sel.options.length = 1; // 只保留第一个“请选择” for (var i = 0; i < MENU_DATA.length; i++) { if (MENU_DATA[i].pid === pid) { sel.options[sel.options.length] = new Option(MENU_DATA[i].name, MENU_DATA[i].id); } } sel.disabled = sel.options.length <= 1; // 没有子项就禁用 }

sel.options.length = 1是清空技巧,比循环remove快得多,前提是第一个 option 是「请选择」。new Option(text, value)是老 API,所有浏览器都认。

三个 select 的联动就是串起来调用:一级onchange时用选中的id填二级,二级onchange时填三级,同时把下级重置。

4.3 当前页高亮与面包屑

菜单点进去之后,得让用户知道自己在哪里。源码里的做法是拿当前脚本路径和menu_url比对:

<% ' inc/fun.asp —— 判断某个菜单是不是当前页 Function IsCurrent(url) Dim cur IsCurrent = False If url = "" Then Exit Function ' 父级菜单没有 url,直接返回 cur = LCase(Request.ServerVariables("SCRIPT_NAME")) If InStr(cur, LCase(url)) > 0 Then IsCurrent = True End Function %>

SCRIPT_NAME拿到的是/order/list.asp这种不含域名和参数的路径,InStr做包含判断。用包含而不是等于,是为了兼容menu_url里带参数的情况。副作用是/order/list.asp会同时匹配到/order/list_export.asp,菜单量不大时可以接受,真要做到精确匹配就给menu_url加个?分隔,先截断再比较。

渲染时把IsCurrent的结果拼进 class,CSS 里给.current加个背景色就行。面包屑则反过来做:从当前节点往上找pid,一路找到0为止,路径反转输出。

5. 避坑与排查:编码、游标、部署这三类坑最容易翻车

这套代码逻辑不复杂,但 ASP 是那种「配置差一点就跑不起来」的环境,翻车点集中在三个地方。下面几条都是实际调试中反复出现的,按现象、原因、解决记下来,下次遇到直接对号入座。

5.1 编码与文件保存类

现象:打开页面顶部多出一行空白,或者直接报「Microsoft VBScript 编译器错误 800a0409 未结束的字符串常量」,但代码肉眼看着完全正常。

原因:.asp文件被编辑器存成了 UTF-8 带 BOM。ASP 解析器会把 BOM 当成页面内容直接输出,于是<%@LANGUAGE...%>前面多了三个不可见字节,服务端认为脚本块从未开始;如果 BOM 出现在 include 文件里,还会导致字符串常量的结束引号被吞。

解决:用编辑器另存为「ANSI」或者「UTF-8 无 BOM」,改完清一次浏览器缓存。VS Code 右下角能看到当前编码,点一下选「通过编码保存」,选GB2312最稳。

现象:菜单名全是问号???,但数据库里明明是中文。

原因:conn.asp里Response.Charset设的是utf-8,而文件本身存成了 ANSI;或者反过来,文件是 UTF-8,Session.CodePage还是 936。

解决:三处必须一致——文件保存编码、Response.Charset、Session.CodePage。要么全 GB 系,要么全 UTF-8,别混着来。

5.2 数据库与游标类

现象:菜单只显示第一级,子级一个都不出来,但数据库里数据明明是对的。

原因:顶级记录的pid存的是NULL而不是0。RenderMenu第一次调用传的是0,Trim(CStr(Null))得到空串,跟"0"比较不成立,整棵树从根上就断了。

解决:先跑一句SELECT id, pid FROM t_menu WHERE pid IS NULL确认,然后把pid字段的「允许空字符串」关掉,历史数据UPDATE t_menu SET pid=0 WHERE pid IS NULL。建表时给pid设默认值 0,从源头堵住。

现象:递归到第二层就报「记录集已关闭」或「当前记录集不支持该操作」。

原因:在递归函数内部又rs.Open了一个新的数据集,而用的是同一个Connection。Access 的默认游标同一连接只允许一个活动记录集。

解决:改成本文第 3 章那种GetRows一次性读出数组的做法,这是最干净的;实在要保留嵌套查询,就每次Server.CreateObject("ADODB.Recordset")新建对象,并把游标类型改成rs.Open sql, conn, 3, 1(客户端游标)。

现象:连接数据库时报0x80004005,提示「找不到可安装的 ISAM」或「未找到提供程序」。

原因:应用池跑在 64 位模式下,而Microsoft.Jet.OLEDB.4.0只有 32 位版本。

解决:IIS 里把该站点的应用池「启用 32 位应用程序」设为True,这是最快的办法。或者装 Access 数据库引擎并改用Microsoft.ACE.OLEDB.12.0,同时勾选 32 位安装。

5.3 部署与权限类

现象:浏览器只显示「HTTP 错误 500 - 内部服务器错误」,看不到具体哪一行出错。

原因:IIS 默认不把详细错误发到浏览器,这是安全默认值,但调试期极其难受。

解决:IIS 管理器里进「错误页」→「编辑功能设置」→ 选「详细错误」;同时在 ASP 的「编译」设置里把「将错误发送到浏览器」设为True。调完记得改回来。

现象:后台改完菜单,前台刷新没变化,得重启 IIS 才生效。

原因:Application缓存没清。menu_save.asp里只执行了UPDATE,忘了调ClearMenuCache()。

解决:在所有写操作(新增、编辑、删除、排序)的末尾统一补一句ClearMenuCache。我一般会把它写进一个AfterMenuChange()里,四个页面都调同一个函数,避免漏。

现象:站点部署到公网没多久,数据库文件就被人下载走了。

原因:data/menu.mdb放在网站根目录下,/data/menu.mdb直接可访问。IIS 默认只拦.asp、.aspx这类脚本扩展名,.mdb不在黑名单里。

解决:把数据库移到站点目录之外,Server.MapPath改成绝对路径或者../data/menu.mdb;实在要放站内,在 IIS 的「请求筛选」→「文件扩展名」里禁掉.mdb。

6. 二次开发:把三级菜单改成带权限的动态菜单

这套源码真正好用的地方,是它的结构留了扩展口子。表里有role_flag,递归函数是按pid通用遍历的,只要稍微改一下取数逻辑,就能从「全站统一菜单」变成「按角色出菜单」。

最常见的改造是加权限过滤。在LoadMenuCache里把当前用户的角色拼进WHERE,父级和子级用同一个条件,保证不会出现「有子级没父级」的断链:

<% ' 按角色取菜单:role_flag 为空表示所有人可见 Function LoadMenuByRole(roleName) Dim rs, sql sql = "SELECT id,pid,menu_name,menu_url,menu_target,sort_id FROM t_menu " & _ "WHERE is_show=1 AND (role_flag IS NULL OR role_flag='' OR role_flag='" & roleName & "') " & _ "ORDER BY sort_id ASC, id ASC" Set rs = Server.CreateObject("ADODB.Recordset") rs.Open sql, conn, 1, 1 If rs.EOF Then LoadMenuByRole = Empty Else LoadMenuByRole = rs.GetRows() End If rs.Close : Set rs = Nothing End Function %>

roleName从Session("role")取,登录时写进去。缓存也要跟着改,Application的 key 换成"MENU_" & roleName,每个角色一份,互不干扰。

改造过程中,有几个点容易踩:

改造点直接改的后果建议做法
权限过滤加在递归里每层都要判断,父级被过滤掉子级成孤儿在 SQL 层统一过滤,父子同条件
缓存 key 不区分角色A 角色看到 B 角色的菜单key 带上角色名,或改用Cache按用户隔离
直接删level > 3限制脏数据导致无限递归、脚本超时保留上限,改成可配置常量
后台删除父级不检查子级子菜单变成找不到父级的孤儿记录删除前先SELECT COUNT(*) WHERE pid=

第一步永远是先备份menu.mdb,然后再动 SQL。Access 没有事务回滚这一说,一条DELETE发出去就没了。

验证改造是否成功,我一般走三步:用两个不同角色分别登录,对比菜单条数;在后台给某个二级菜单加个子级,刷新前台看是否即时出现(顺便验证缓存清理);最后把数据库里某条记录的pid手动改成自身 id,看页面是正常截断还是直接超时。第三步能一次验证递归上限、异常数据处理和缓存三件事。

这套代码我后来又用在两个后台项目上,最大感受是:老技术的坑不在语法,而在那些「默认配置」上。从那以后我每次拿到一份 ASP 源码,第一件事都是把文件编码、应用池位数、数据库路径这三项先确认一遍,再谈改功能,省下来的时间比读代码多得多。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/2 21:32:06

JavaWeb一对一聊天系统:WebSocket实时通信与消息持久化实战

简介&#xff1a;这是一套面向JavaWeb初学者与课程设计学习者的网页一对一聊天系统源码&#xff0c;基于JSP、JavaScript、Ajax、Servlet、Tomcat与MySQL实现&#xff0c;重点解决实时消息收发与页面自动刷新问题。聊天页由JSP负责消息显示框、输入框及参数获取&#xff0c;JS与…

作者头像 李华
网站建设 2026/11/2 21:31:51

OPNET OSPF仿真:协议状态机与收敛过程可视化分析

简介&#xff1a;本资源是基于OPNET 14.5平台的计算机网络仿真实验项目&#xff0c;面向高校网络工程、通信工程等专业本科生及仿真初学者&#xff0c;聚焦OSPF动态路由协议的核心机制验证与实践。内容严格对应《计算机网络仿真OPNET实用指南》&#xff08;机械工业出版社2014年…

作者头像 李华
网站建设 2026/11/2 21:31:06

WPF TreeView拖拽落点判定与跨层级重排实战

简介&#xff1a;面向WPF桌面开发者的TreeView拖放功能示例&#xff0c;聚焦节点顺序调整与数据移动、复制等常见交互需求&#xff0c;适合已具备一定WPF基础、希望深入理解拖放事件与数据绑定的开发者参考。压缩包共12个文件、约11KB&#xff0c;主要由6个cs源码、2个xaml界面…

作者头像 李华
网站建设 2026/11/2 21:27:10

算法分析与设计期末复习参考

启发式搜索 一、概念&#xff08;极简背诵版&#xff09; 状态空间&#xff1a;问题所有合法状态 状态转移规则构成的集合&#xff0c;搜索就是在状态空间里找路径。图灵机&#xff1a;抽象计算模型&#xff0c;由读写头、纸带、状态集组成&#xff1b;能刻画所有可计算问题。…

作者头像 李华
网站建设 2026/11/2 21:09:52

TLC5615三平台驱动详解:51、Arduino与STM32的10位DAC移植指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/11/2 21:05:48

知识图谱在推荐系统中的实践与优化

1. 项目概述&#xff1a;当知识图谱遇上推荐系统去年接手一个电商平台的推荐系统改造项目时&#xff0c;我首次尝试将知识图谱技术引入推荐流程。原本的协同过滤算法在遇到新商品冷启动问题时表现乏力&#xff0c;直到我们为商品库构建了包含200实体类型的关系网络&#xff0c;…

作者头像 李华