☰
Flask+Vue前后端分离实现高校校长信箱管理系统
2026/10/22 16:23:58 网站建设 项目流程

做这类校园信息化系统,最怕的不是某个接口写不出来,而是“流程说不清楚”。我去年帮人做过一个高校校长信箱管理系统,需求听起来很简单:学生在线写信、部门回复、校长看结果。可真开工之后才发现,牵扯到的角色、状态、权限和数据关系多到需要画好几张图才能理清楚。最后落地的方案是 Python Flask 做后端 API,Vue 做前端页面,开发环境用 PyCharm,整个项目集成了学生举报投诉处理、部门办结、校长统计完整闭环。这篇就把我怎么设计数据表、怎么拆接口、怎么跟前端联调、又踩过哪些坑,全部整理出来,给后面要写类似系统的人做个参考。不管你是要做课程设计,还是接手一个真实校内项目,这套思路都能直接复用。

1. 项目整体设计与技术选型思路

1.1 这套系统到底在解决什么问题

校长信箱在很多高校里早就存在,但大多数只是挂一个公开邮箱,学生写邮件,管理员不定期整理转发。问题非常明显:学生不知道自己的信到底有没有被看到,部门之间互相推诿,校长看到的只是被筛选过的转述,投诉类信件还可能泄露到无关人员手里。所以这个管理系统的核心不是“发信”,而是“流转”。

我要做的系统本质上是给每封信建立一条可追踪、可审计、可超时的处理链路。学生提交之后,进入待分配状态;管理员按信件分类分配给对应部门;部门处理人写回复意见;学生看到回复后可以选择满意并办结,也可以不满意要求重新处理;系统管理员能看到超时未处理的信件并督办。整个过程里,学生只能看到自己的信件,部门只能看到分配给本部门的信件,校长和系统管理员可以看全局统计,但也要遵守隐私隔离。

这个定位想清楚后,所有技术选型都围绕它展开。如果只是做一个提交表单加一个管理员列表,那没必要前后端分离;正因为状态和权限复杂,才需要一套清晰的 API 加组件化的前端界面。

1.2 Flask 与 Django 的选型对比

很多人在项目里同时提到 Flask 和 Django,说明选型阶段纠结过。我也纠结过。最后选了 Flask,理由是这个系统以提供 JSON 接口为主,前端完全交给 Vue,后端需要的是一个轻量、灵活、方便按模块拆分的服务端,而不是全家桶。

Django 的优势在于自带 Admin、ORM、认证系统,如果你要做的是服务端渲染的多页面管理系统,Django 的开发效率确实很高。但它缺点也很明显:项目初始化后自带大量约定和目录结构,很多功能对纯 API 场景来说是多余的。比如 Django Admin 在这个系统里用不上,因为管理界面是 Vue 写的;Django 的 Form 表单系统也用不上,因为前端已经做了校验。

Flask 相比之下更直接。一个 app 文件加几个 Blueprint 就能把认证、信件、统计模块分清楚。数据库层用 Flask-SQLAlchemy,和 SQLAlchemy 原生体验几乎一致;登录鉴权用 Flask-JWT-Extended,几行代码就能签发和校验 Token。另外,PyCharm 对 Flask 的支持相当成熟,新建项目时可以自动识别运行配置,调试接口时能看到完整的请求上下文,排查问题比 Django 更轻快。

如果你硬要让我给建议:如果项目是纯服务端渲染、表单页面多、需要后台管理系统的完整模板,那就用 Django;如果像我这个项目一样,后端只做 API、前端另有实现,选 Flask 或者其他轻量框架都更顺手。标题里既然有 Flask 也有 Django,说明你大概率在做一个偏向前后端分离的系统,那我的建议是别犹豫,用 Flask。

1.3 为什么前端要交给 Vue

校长信箱管理系统天然就是多角色、多页面的典型场景:学生要填表单、传附件、查状态;处理人员要看列表、写回复、编辑分类;校长要看图表、看超时统计。如果后端用 Jinja2 模板硬拼页面,每个页面都要重新加载,交互感会很差,而且不同角色之间的页面复用也很困难。

用 Vue 之后,我把页面拆成了三种角色视角:

  • 学生视角:写信页、我的信件列表、信件详情和评价入口。
  • 处理人视角:待办列表、信件详情、回复编辑、状态操作。
  • 管理视角:全局统计面板、分配管理、超时督办清单。

这些页面之间有很多共享逻辑,比如附件上传、状态标签、富文本编辑器。Vue 组件化之后,我只需要把 UploadBox、StatusTag 这类公共组件抽出来,切换角色的时候只要换路由和请求接口就行,页面本身不需要重新开发。

Vue 版本上我建议直接选 Vue 3,配合 Composition API。Vue 2 虽然资料多,但生态已经进入维护期,新项目没必要开局就走旧路。如果团队对 Vue 3 的 setup 语法不熟,前期慢几天,后期改状态处理的时候会舒服很多。

1.4 项目目录结构规划

前后端分离项目最忌讳把代码全部堆在一起。我的规划是同一个仓库下分 backend 和 frontend 两个目录,各自独立依赖、独立启动。

school_mailbox/ ├── backend/ │ ├── app.py # Flask 应用入口 │ ├── config.py # 配置:数据库、JWT、上传路径 │ ├── models.py # SQLAlchemy 数据模型 │ ├── requirements.txt │ └── api/ │ ├── __init__.py │ ├── auth.py # 登录、用户信息 │ ├── letters.py # 信件提交、分配、回复、办结 │ └── stats.py # 统计报表接口 ├── frontend/ │ ├── package.json │ ├── vite.config.js # 开发代理配置 │ └── src/ │ ├── main.js │ ├── router/index.js │ ├── store/index.js │ ├── api/request.js # axios 封装 │ ├── components/ # 公共组件 │ └── views/ # 角色页面 └── README.md

这个结构看起来简单,但已经考虑了三个关键点:一是后端按 API 模块而不是按“页面”拆分,后续加功能不会互相干扰;二是前端通过 Vite 的代理解决本地跨域,联调时不需要后端开 CORS 放行所有域名;三是后端和前端都可以单独启动,部署时也能分开部署,甚至前端可以扔到 CDN。小项目大结构,不丢人。

2. 核心模块拆解与数据建模

2.1 角色权限:从需求到字段设计

这类系统的权限设计如果不在一开始理清楚,后面一定会返工。我的方案是采用基于角色的访问控制模型,表里不直接给用户配“是否可以访问某个 URL”,而是给用户一个角色标识,后端在接口层校验角色。

角色分为五类:学生、部门处理人、部门管理员、校长、系统管理员。它们之间的权限差异用一个表格就能说清楚:

角色提交信件查看信件处理信件分配信件查看全局统计
学生是仅本人否否否
部门处理人否本部门是否否
部门管理员否本部门是是本部门
校长否全局脱敏否否是
系统管理员否全局否是是

这里有一个特别容易踩的坑:匿名举报是谁提交的,到底要不要让所有人看不到?我的设计是数据库里保留真实用户 ID,前端展示时用一个is_anonymous字段控制是否显示姓名。也就是说,匿名信件对校长和管理员可见真实提交人,对部门处理人则隐藏。这样既能保证举报内容不因匿名而失真,也能让真正需要追溯的人拿到必要信息。匿名如果做成真匿名,一旦发生恶意举报就很难处理。

2.2 信件状态机:流转的逻辑核心

管理系统的成败一半靠状态设计。我一开始以为信件只有“待处理”和“已处理”两个状态,后来一细化就膨胀了。最终我用状态机来约束,每个状态可以执行的操作是有限的。

submitted -> pending_assign pending_assign -> processing processing -> pending_reply pending_reply -> completed(学生确认) pending_reply -> reopened(学生不满意) reopened -> processing

光有这个还不够,每个状态变化都要记录操作人和时间,所以我单独建了一张action_logs表,专门记录“谁在什么时候把信从哪个状态变成了哪个状态”。这个设计在排障时价值极大,比如学生说“我明明投诉了但部门说没收到”,一查日志就能定位卡在哪一步。

状态字段我在代码里不建议用数字,而是用英文标识字符串,例如"pending_assign"。数字容易被后续接手的人猜错,字符串则一目了然,就算前端正好把状态值直接显示出来,也不会让用户看天书。

2.3 数据库表设计要点

我用 Flask-SQLAlchemy 设计的主要表有六张:用户表、信件表、回复表、附件表、分配记录表、操作日志表。字段设计上有几个容易忽略的地方需要特别说明。

信件表里除了标题、内容、分类、状态,还有一个is_anonymous布尔字段和一个deadline时间字段。deadline的用途是超时督办:提交后默认 7 个工作日必须给出有效回复,系统管理员每天定时任务扫描超过deadline还没进入pending_reply的信件,生成督办列表。

回复表里每条回复还要记录is_internal字段,区分“内部处理意见”和“对外回复”。内部意见只有管理员可见,对外回复学生可见。这个设计一开始没做,后来被用户问“怎么部门处理意见直接发给学生了”才发现,对外和内部必须分开。

附件表和信件表是一对多的关系,考虑到服务器存储空间,我限制了单文件大小不超过 20MB,内容类型只允许图片、PDF、Word 文档。存数据库的是服务器文件路径或者对象存储的地址,而不是二进制本体,这样备份数据库的时候不会因为附件爆炸。

2.4 从数据模型推导出的 API 清单

数据表关系理清之后,接口设计基本是顺水推舟的事。整套后端 API 可以归结为下面这些核心端点:

  • POST /api/auth/login:用户名密码登录,返回 JWT Token 和角色信息。
  • GET /api/auth/profile:获取当前登录用户信息。
  • POST /api/letters:学生提交信件,支持匿名。
  • GET /api/letters:按角色返回信件列表,支持分类和状态筛选。
  • GET /api/letters/<id>:获取信件详情、回复列表、附件列表。
  • PUT /api/letters/<id>/assign:管理员分配信件到部门。
  • POST /api/letters/<id>/reply:处理人提交回复。
  • POST /api/letters/<id>/complete:学生确认办结。
  • POST /api/letters/<id>/reopen:学生不满意后重新打开。
  • GET /api/stats/overview:校长管理端查看分类统计和处理时效。

这些接口完全由后端数据模型驱动,前端只需要按角色调用对应接口,不需要关心具体 SQL。接口数量不多,但覆盖了整套系统的核心业务闭环。

3. Flask 后端实现与接口细节

3.1 PyCharm 环境准备与依赖安装

用 PyCharm 开发 Flask 项目确实省心,但要注意几个细节。新建项目时我建议选 “New Virtualenv”,Python 版本选 3.10 或 3.11,不要用系统自带的全局解释器,否则装依赖容易污染环境。然后安装依赖:

pip install flask flask-sqlalchemy flask-cors flask-jwt-extended pip install pymysql python-dotenv

如果开发阶段用 SQLite,不需要装 pymysql;但生产环境我最终换成了 MySQL,所以提前装好。PyCharm 里跑 Flask 时,最方便的不是自己写app.run(),而是配置一个 Flask Server 类型的运行配置,指定 Target 为app.py,勾上FLASK_DEBUG,这样改代码后自动重载,而且可以看到 Flask 的 debug 页面。

有一个坑必须提醒:Flask 2.x 开始推荐用app.py作为入口文件,不要图省事把所有路由写在一个文件里。项目一复杂,单文件模式在 PyCharm 里虽然能跑,但后续维护时你会非常痛苦。我是在写完第一个版本后才拆分的,中间吃了不少亏。

3.2 用 SQLAlchemy 定义数据模型

数据模型是后端的地基。我抽取了最核心的信件表和回复表,代码大概是这样:

from flask_sqlalchemy import SQLAlchemy from datetime import datetime db = SQLAlchemy() class User(db.Model): __tablename__ = 'users' id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(50), unique=True, nullable=False) password_hash = db.Column(db.String(255), nullable=False) role = db.Column(db.String(20), nullable=False) # student/staff/admin/principal/sysadmin real_name = db.Column(db.String(50), nullable=False) department_id = db.Column(db.Integer, db.ForeignKey('departments.id')) class Letter(db.Model): __tablename__ = 'letters' id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) title = db.Column(db.String(100), nullable=False) content = db.Column(db.Text, nullable=False) category = db.Column(db.String(30), nullable=False) status = db.Column(db.String(30), nullable=False, default='submitted') is_anonymous = db.Column(db.Boolean, default=False) deadline = db.Column(db.DateTime, nullable=False) created_at = db.Column(db.DateTime, default=datetime.utcnow) class Reply(db.Model): __tablename__ = 'replies' id = db.Column(db.Integer, primary_key=True) letter_id = db.Column(db.Integer, db.ForeignKey('letters.id'), nullable=False) user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) content = db.Column(db.Text, nullable=False) is_internal = db.Column(db.Boolean, default=False) created_at = db.Column(db.DateTime, default=datetime.utcnow)

外键关联这里不写太多级联关系,避免误删。比如用户注销这类功能在这个系统里根本不存在,所以Letter.user_id正常关联就行,不要顺手写ondelete='CASCADE',否则误操作会把学生所有历史信件全删掉。

3.3 登录鉴权和角色权限装饰器

登录接口我用 Flask-JWT-Extended 实现。密码不是明文存储,而是用 Werkzeug 自带的哈希函数处理。

from werkzeug.security import generate_password_hash, check_password_hash from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity @app.post('/api/auth/login') def login(): data = request.get_json() user = User.query.filter_by(username=data.get('username')).first() if not user or not check_password_hash(user.password_hash, data.get('password')): return {'msg': '用户名或密码错误'}, 401 token = create_access_token(identity=str(user.id), additional_claims={'role': user.role}) return {'token': token, 'role': user.role, 'real_name': user.real_name}

角色校验我封装了一个装饰器,在需要限制角色的接口上直接用:

from functools import wraps from flask_jwt_extended import verify_jwt_in_request, get_jwt def role_required(*roles): def wrapper(fn): @wraps(fn) def decorator(*args, **kwargs): verify_jwt_in_request() claims = get_jwt() if claims.get('role') not in roles: return {'msg': '无权限访问'}, 403 return fn(*args, **kwargs) return decorator return wrapper

这个装饰器比在每个接口里手写角色判断强多了,后续新增接口只要加上@role_required('sysadmin', 'principal')就行。前端做路由守卫只是为了让界面好看,真正的安全边界还是后端这一层。

3.4 信件提交与回复接口实现

提交信件是学生最核心的操作,接口要处理的内容包括:标题、内容、分类、是否匿名、附件列表。我特别强调一点:附件 ID 列表是前端上传完成后返回的,所以后端收到的是一个数组,而不是文件本体。

@app.post('/api/letters') @jwt_required() def create_letter(): user_id = get_jwt_identity() data = request.get_json() letter = Letter( user_id=user_id, title=data.get('title'), content=data.get('content'), category=data.get('category'), is_anonymous=data.get('is_anonymous', False), status='submitted', deadline=datetime.utcnow() + timedelta(days=7) ) db.session.add(letter) db.session.flush() for attach_id in data.get('attachment_ids', []): attach = Attachment.query.get(attach_id) if attach: attach.letter_id = letter.id db.session.commit() return {'id': letter.id, 'msg': '提交成功'}, 201

回复接口的逻辑稍微复杂一点,因为要判断当前信件状态是否允许回复。比如信件已经在pending_reply之后又被学生reopened,那处理人需要追加回复,而不是新开一条回复草稿。

@app.post('/api/letters/<int:letter_id>/reply') @role_required('staff', 'dept_admin') def reply_letter(letter_id): letter = Letter.query.get_or_404(letter_id) if letter.status not in ('processing', 'reopened'): return {'msg': '当前状态不允许回复'}, 400 data = request.get_json() reply = Reply( letter_id=letter.id, user_id=get_jwt_identity(), content=data.get('content'), is_internal=data.get('is_internal', False) ) db.session.add(reply) letter.status = 'pending_reply' db.session.commit() return {'msg': '回复成功'}

这里的核心心得是:接口不能只做“插入数据”的活,还要负责维护状态机和业务规则。把状态校验放在接口层,前端怎么做都不会绕过流程,安全性才有保障。

3.5 文件上传与内容安全处理

文件上传我用 Flask 的原生request.files配合 Werkzeug 的secure_filename处理。文件名不能直接用用户上传的原始名,因为可能包含路径穿越字符,比如../../xxx.py。我改成的方式是:用 UUID 生成新文件名,把扩展名保留下来,再按日期存放到指定目录。

import uuid from werkzeug.utils import secure_filename @app.post('/api/upload') @jwt_required() def upload_file(): file = request.files.get('file') if not file: return {'msg': '缺少文件'}, 400 ext = file.filename.rsplit('.', 1)[-1].lower() if ext not in {'png', 'jpg', 'jpeg', 'pdf', 'doc', 'docx'}: return {'msg': '不支持的格式'}, 400 filename = f"{uuid.uuid4().hex}.{ext}" file.save(os.path.join(UPLOAD_FOLDER, filename)) return {'url': f'/uploads/{filename}'}

内容安全方面,举报投诉类信件容易夹带个人隐私,我建议在提交时做一次“手机号、邮箱、身份证号”的自动识别,提示学生确认是否真的要公开这些信息。前端做一层提示,后端做一层过滤,避免信件内容被无关角色查看后造成隐私扩散。技术上不是很难,正则表达式就能筛掉大部分,但不要指望它能替代人工审核。

4. Vue 前端联调与关键页面

4.1 初始化 Vue 项目与请求封装

前端我使用 Vite 创建 Vue 3 项目,命令很简单:

npm create vite@latest frontend -- --template vue cd frontend npm install axios vue-router pinia

项目创建后,很多人第一件事就是直接写页面,但我会先封装 Axios 请求实例。这个步骤不能省,否则每个页面都要处理 Token、错误码,重复代码会非常多。

import axios from 'axios' const request = axios.create({ baseURL: '/api', timeout: 15000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) request.interceptors.response.use( response => response.data, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') window.location.href = '/login' } return Promise.reject(error) } ) export default request

这里有一个经验:baseURL 设成/api,而不是http://localhost:5000/api。在开发阶段让 Vite 代理转发到后端,在部署阶段让 Nginx 把/api反向代理到 Flask 服务。这样前端代码里不需要区分环境,也不会因为后端地址变了而改前端。

4.2 登录态与路由守卫

登录页拿到 Token 后,存到 localStorage,同时用 Pinia 保存用户角色信息。路由守卫的作用是拦截未登录或者越权的访问。

router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') const role = localStorage.getItem('role') if (!token && to.path !== '/login') { next('/login') } else if (token && to.meta.roles && !to.meta.roles.includes(role)) { next('/403') } else { next() } })

但我不建议在路由表里把所有权限写死。角色一旦变多,硬编码会出现漏改。我的做法是在路由 meta 里只标“这个页面需要哪些角色”,后端接口再做二次校验。前端守卫最大的价值是提升体验,而不是作为安全边界。

4.3 学生写信页面与处理人待办列表

学生写信页面是整个系统里交互最复杂的页面。除了标题、分类、内容,还要有富文本编辑器和附件上传。富文本我用的是轻量方案,直接引入一个开源的 Markdown 编辑器组件。为什么不用大型富文本编辑器?因为这个场景里学生写的投诉内容基本是文字描述,Markdown 足够,而且生成的内容是纯文本,存数据库不会带来脚本注入风险。

表单提交前要做两次校验:第一次是前端必填项检查,第二次是确认匿名状态。我特意加了一个“我确认以上内容属实”的复选框,不能勾选就不让提交。这个设计看起来多余,但在举报投诉场景里,它能挡掉不少随手乱写的内容,也从流程上给提交人一个心理确认。

处理人的待办列表用的是卡片加表格的混合布局。列表默认显示所有属于本部门且状态不是completed的信件,每个卡片上放状态标签、分类标签、超时倒计时。处理人点进详情后,能看到信件主体和内部处理意见,外部回复单独放在一个“对外回复”区域,写完后提交。

4.4 联调中的三个经典问题

前后端联调最常遇到的就是跨域。开发阶段我用 Vite 代理解决,配置如下:

// vite.config.js export default defineConfig({ server: { proxy: { '/api': { target: 'http://localhost:5000', changeOrigin: true } } } })

这个配置不需要后端开启 CORS,也不会出现生产环境跨域问题。但如果后端真要开 CORS,记得限定来源,不要用allow_origins=["*"],否则任何第三方网站都能向你的接口发请求。

第二个问题是 Token 过期。JWT 默认过期时间我设成 2 小时,过期的请求会返回 401。前端拦截器统一跳转登录页,但是学生填写到一半的信件内容就丢了。我的做法是:在跳转登录之前,先把当前表单状态暂存到 sessionStorage,重新登录后恢复,至少不让学生白写一大段内容。

第三个问题是富文本内容提交。如果用户粘贴了带样式的文字,里面可能夹带危险的 HTML 标签。后端必须要做内容过滤,最简单的方式是把小白用户输入的<script>等标签直接剔掉。不要指望前端转义就安全,所有从后端渲染用户内容的地方,都要当作不可信数据来对待。

5. 部署与常见问题排查实录

5.1 PyCharm 开发阶段的几个配置细节

用 PyCharm 跑 Flask 项目,有几个配置容易让新手卡住。第一是环境变量,我习惯把数据库地址、Secret Key、上传路径写到.env文件,PyCharm 的 Run Configuration 里要勾选 “Load environment variables from .env”,否则本地能跑、部署后一换机器就起不来。

第二是数据库路径,SQLite 开发时用的是相对路径,比如sqlite:///db.sqlite3。这个路径会随着你启动 Flask 时所在的目录不同而变化,我踩过“明明连上了数据库但表不存在”的坑。解决办法是写成绝对路径:

import os BASE_DIR = os.path.abspath(os.path.dirname(__file__)) SQLALCHEMY_DATABASE_URI = f"sqlite:///{os.path.join(BASE_DIR, 'db.sqlite3')}"

第三是调试模式。PyCharm 里如果手动运行app.run(debug=True),会导致调试工具重新加载两次,有时候会看到打印了两次日志。配置成 Flask Server 类型的运行方式后,这个现象会消失。

5.2 部署到真实服务器时必调的五个地方

开发完成不代表项目结束,部署环节才是最容易翻车的。我总结出五个必须调整的位置:

第一,关闭 Flask 的调试模式。debug=True在生产环境绝对不能开,不仅泄露详细异常信息,还会让攻击者有机会查看 Werkzeug 调试器交互界面。正式部署用waitress或者gunicorn启动,不要让 Flask 自带的服务器直接面向公网。

第二,数据库要从 SQLite 切到 MySQL。SQLite 在并发写入场景下会锁库,校长信箱在开学或者月底投诉高峰期,多人同时提交很容易出现database is locked。切换时注意编码用utf8mb4,否则中文 emoji 可能存不进。

第三,上传文件目录要指定到独立磁盘位置,不要放在项目代码目录下。否则每次更新代码、重新部署的时候,上传的图片和附件会被一起清掉。我用的是独立数据盘下的/data/school_mailbox/uploads,代码更新完全不影响这个目录。

第四,Secret Key 和环境变量要通过.env文件或者服务器环境注入,绝对不能写死在代码里,更不能提交到代码仓库。这个 Key 一旦泄露,任何人都可以伪造 JWT Token。

第五,CORS 要限定具体域名。如果前端部署在https://mail.example.edu.cn,后端就不能对所有来源开放跨域。我用flask-cors做了白名单限制,只允许指定域名访问接口。

5.3 高频问题速查表

实际开发中遇到的问题五花八门,我把最常见的几个整理成了速查表,给后面动手的人参考:

现象原因解决方案
前端登录后请求接口返回 404Vite 代理没生效或后端路由前缀不一致确认/api路径命中了 Flask Blueprint 的 url_prefix
上传文件后返回 413请求体大小超过默认限制在 Flask 里设置MAX_CONTENT_LENGTH,并在 Nginx 里同步调整client_max_body_size
学生能看到其他部门的信件列表查询没有按部门过滤后端查询必须加上department_id条件,不能只在前端隐藏
本地 sqlite 数据库打开是空表相对路径拼到了错误目录用绝对路径拼接BASE_DIR,重新创建数据库
处理人回复成功后学生看不到外层状态没有更新成pending_reply保存回复后必须同步修改信件状态并提交事务
跨域请求出现 OPTIONS 预检失败自定义请求头触发预检但后端没处理使用 Vite 代理,或正确配置Access-Control-Allow-Headers
部署后前端刷新变成 404前端路由用了 history 模式,但服务器没做重写Nginxtry_files $uri $uri/ /index.html;

5.4 真实项目里总结的避坑经验

做这套系统之前,我把注意力都放在“学生投诉能不能提交成功”,后来被用户打了一次脸:系统上线后真正被学生投诉最多的反而是很多部门管理员不知道怎么流转。这让我意识到,这类系统最重要的信息提示一定要做足。状态标签不止是显示一个字符串,最好带上颜色和操作按钮,比如“已超时”就用醒目的红色标签,点击后直接跳转到对应信件详情。

还有一点,举报投诉类数据一定要保留完整历史。我不允许任何角色在界面上直接删除一条信件记录,只能通过状态流转变成cancelled。如果真要删除,那只能是系统管理员在数据库层面操作,并且必须在操作日志里留痕。这个原则不是技术问题,而是系统可信度问题,学生敢不敢真正用起来,就看你的系统能不能让他们相信“内容不会莫名其妙消失”。

更要提醒的是权限越权测试。我第一次写完列表接口时,自以为“只要前端不显示学生入口就没事”,结果我用学生的 Token 直接调部门的接口,发现返回了全部信件,心态直接崩了。后来我把每个接口都过了一遍角色校验,凡是需要限定部门范围的接口,都在 SQL 查询里加上了当前用户的部门条件。前端隐藏按钮只是体验,后端过滤数据才是安全。


我在实际开发中体会最深的一件事:校长信箱这类系统,功能代码其实不难写,难的是状态流转和权限边界必须一开始就清晰。我在第一版里把“已回复”和“已办结”混成了一个状态,结果学生看到部门回复之后根本没有确认入口,所有信件最终都堆在“已回复”,部门以为处理完了,学生以为没下文。后来重新设计状态机、把pending_reply和completed分开,整个流程才真正跑通。所以如果你也要动手做类似系统,建议先别急着写代码,找张白纸把状态和角色权限画清楚,这一步省下来的返工时间,比你想的多得多。

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

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

立即咨询