☰
学生学籍管理系统:SSM主业务与Flask辅助服务完整实现复盘
2026/10/14 4:56:28 网站建设 项目流程

学生学籍管理系统:从SSM主业务到Flask辅助服务的完整实现复盘

最近在帮朋友梳理学籍管理系统这类项目时,我发现一个很有意思的现象:凡是叫"学生学籍管理系统"的项目,大多数最终都做成了"学生信息增删改查页面"——数据库里堆了十几张表,界面上有列表、有表单、有删除按钮,但真正问到"学籍异动怎么走审核流程""批量导入一万条新生数据会不会卡死""不同角色的查询权限怎么隔离"这些问题,就明显答不上来了。这个项目名字听起来像经典CRUD,实际做起来却是一套带着业务流程、状态流转、数据治理和权限控制的完整系统。

这篇文章我想完整复盘我用Java + SSM + Flask实现一套学生学籍管理系统的全过程,从需求边界、技术选型、数据库设计,到跨框架联调、测试文档和最终交付。文章里没有绕来绕去的理论,全部是能直接落到代码里的步骤、参数和避坑经验。如果你正在做课设、在做实际的学生档案类项目,或者只是想把这类系统从"能用"做到"耐看、经得起问",这篇文章应该能提供不少参考。

1. 先搞清楚学籍系统到底在管什么:需求边界拆解

1.1 用户角色和业务流:学籍不是一张静态表

很多人做学籍系统,上来就建一张student表,字段塞得满满的,然后就开始写列表页。这是最典型的错误。

学籍管理的本质是"对学生在校身份状态的全生命周期跟踪"。一个学生从入学注册开始,到休学、复学、转专业、退学,再到毕业归档,学籍状态会多次变化,每一次变化都对应一次"学籍异动",而异动是有审核依赖的。所以做这个系统之前,第一件事是先分清角色、理清流程。

我一般把这些角色落在系统里:

  • 系统管理员:维护用户账号、角色权限、基础数据字典,不直接参与学生数据的日常修改。
  • 教务/学院老师:录入学生档案、发起/审核学籍异动、查看所在院系的学籍统计。
  • 辅导员:查看本班或本专业学生信息,发起部分异动申请(比如休学、退学申请)。
  • 学生本人:查看自己的学籍信息、申请(有的学校允许学生提交转专业申请)、查看审核进度。

流程也不复杂,但必须闭环:

新生数据批量导入(Excel)→ 建立学籍档案 → 在校期间状态维护 → 学籍异动(申请 → 审核 → 更新状态)→ 毕业/离校归档 → 注销

我在做设计时会把这一套流转画成一组状态节点:在籍、休学、复学待审、退学、保留学籍、毕业、注销。任何一个状态变更必须产生一条异动记录,不能直接在student表里改状态字段。为什么?因为直接改字段意味着数据不可追溯,一旦审核出问题,根本无从查起。这一点后面在数据库设计部分会详细展开。

1.2 核心功能模块:按场景划分,不按页面划分

需求拆下来,实际要做的东西比"学生信息管理"多得多。我按使用场景分成了四个模块:

  • 学生档案管理:学生基础信息(学号、姓名、性别、身份证号、入学批次、录取专业、班级、宿舍、家庭信息等)的新增、修改、导入导出。注意这里说的"修改"要区分能不能改、改了要不要留痕——身份证号这类关键信息一旦录入,通常只允许管理员在具备审批条件时修改,不能谁都改。
  • 学籍异动管理:休学、复学、转专业、退学、保留学籍的申请、审核、生效、归档。每一次异动都要有申请时间、申请原因、材料附件、审核人、审核意见、生效日期和起止时间。
  • 学籍查询与自助打印:面向学生和老师。学生只能查自己,老师只能查本院系/所带班级。查询通常支持按学号精确查、按姓名模糊查、按专业/班级/状态组合筛选。后期可以对接自助打印终端,生成在读证明PDF。
  • 统计与报表:按学院、专业、年级、学籍状态做人数统计,生成新生报到统计表、在校生分布表、异动月报等。

这里我想多说一句:不要一开始就想着把界面做得多炫。学籍系统的核心价值在数据准确和流程严谨,查询列表够清晰、表单校验够严格、状态流转不丢数据,这套系统就已经合格了。

1.3 非功能性需求:权限、日志、性能底限

这是很多项目被忽略的部分,却恰恰是答辩和验收时最容易被打分的地方。

第一是权限控制。必须做到方法级权限管理,而不是页面级。我用的是SpringMVC拦截器配合自定义注解,在Controller方法上标@RequiresRole("admin"),拦截器解析当前用户的角色集合,没有权限直接返回403。学生接口和教师接口从接口层就分开,表结构上通过user_role和student_user的关联关系做归属判断。

第二是操作日志。谁在什么时间改了什么数据,之前是什么值,之后变成什么值,写进operation_log表。数据回滚需要时可查。

第三是性能底线。我给自己定的标准是:Excel批量导入5000条学生数据不超过30秒;多条件分页查询在10万条数据量下响应时间小于500毫秒。这决定了后面数据库索引怎么建、批量插入怎么写、分页查询怎么优化。

本节小结一下:开发前花两天时间把上面的流程、角色、模块边界理清楚,远比动手写代码重要。很多项目做到一半推倒重来,都是因为没想清楚学籍异动是有状态的。

2. 技术选型复盘:SSM承担什么,Flask又解决什么问题

2.1 为什么不直接选更"新"的框架组合

先回答一个很多人会问的问题:都什么年代了,为什么还在用 SSM(Spring + SpringMVC + MyBatis),而不是 Spring Boot?

我的看法是:SSM 在课程设计和中小型传统企业系统里依然有非常扎实的群众基础。Spring 负责依赖注入和事务管理,SpringMVC 负责接口路由和参数绑定,MyBatis 负责 SQL 与结果集映射。项目结构清晰,配置虽然比 Boot 繁琐,但每一份配置都让人更理解"底层机制"——比如DispatcherServlet怎么映射、SqlSessionFactory怎么管理数据源。对很多做毕设的同学来说,手动写一遍 SSM 配置本身就是学习收益。

Flask 的角色是我这次比较得意的设计:主业务系统用 SSM,辅助服务用 Flask,两个服务并行部署、职责分离。Flask 部分主要承担三件事:

  • Excel 批量导入:处理新生名册、成绩单导入。用 Python 的openpyxl解析 xlsx 非常顺手,单单元格类型、日期格式、合并单元格都能精细处理,比在 Java 里写 POI 再调整样式方便得多。
  • PDF 生成:实现在读证明、学籍卡、成绩单导出。Python 的reportlab生成头部固定、数据变化的 PDF 比 Java 生成 PDF 省事,字体和排版也可控。
  • 数据统计与可视化接口:给前端大屏/报表页提供聚合JSON。用pandas直接对查询结果做分组聚合,少写很多 Java 循环。

2.2 两个框架的分工边界:避免重复造轮子

我做架构拆分时定了一个非常严格的规矩,这也是整个项目能顺利跑通的根本原因:

SSM 是数据主人,Flask 永远不写数据。Flask 只以只读方式连接数据库,或者调用 SSM 的 HTTP 接口来拿数据,任何导入、审核、状态变更操作都必须走 SSM。

为什么这样定?核心原因是事务一致性。学籍系统中的写操作几乎都是"先查再改"的有状态操作,例如审核一个休学申请,需要同时更新student表状态、插入mutation记录、写操作日志。如果在两套代码里各写一半,事务边界根本没法统一,一旦中途异常,数据可能处于"状态改了但记录没写"的中间状态。让 SSM 统一负责写路径,Flask 只做读和文件生成,就绕开了分布式事务这个坑。

具体的协作方式:

项目SSM 主系统Flask 辅助服务
定位核心业务与数据管理数据导入、导出、统计
端口80805001
数据库读写只读
对外接口/api/**/import、/export/**、/stats/**
数据变更唯一入口不提供变更能力
部署方式Tomcatgunicorn / 开发模式的 dev server

整个系统跑起来后,前后端分离的实际结构是:浏览器访问前端页面,前端调用 SSM 的 RESTful 接口完成业务操作;浏览器需要导出或上传时,再调用 Flask 的辅助接口。而 Flask 内部如果需要拿"最新学籍快照",要么直连数据库查询(只读),要么向 SSM 拉一个GET /api/student/snapshot的内部接口,两个服务之间的调用走局域网内部 token 鉴权。

2.3 这一套技术栈的代价:需要想清楚

任何选型都是收益和代价并存的,我也踩过亏。首先是部署复杂度变高了,原来一个 WAR 包丢进 Tomcat 就行,现在要额外维护一个 Python 进程。其次是两套开发语言带来的上下文切换,我有时候上午写 Java 的 MyBatis XML,下午写 Flask 路由,脑子确实需要几分钟切换。但换来的收益也很直接:导入导出流程的代码量大约减少了三分之一,数据清洗逻辑表达起来也更自然。

如果项目规模真的很小,比如只需要管理几百个学生、没有批量导入和PDF生成需求,我建议只用 SSM 就足够了,别把 Flask 引进来制造额外复杂度。加一个辅助服务的前提是它的优势能明确覆盖它带来的运维成本。

3. 数据库设计与接口约定:把学籍状态管成闭环

3.1 核心表结构:不只一张 student 表

数据库是整个学籍系统的地基。我最终稳定下来的核心表有 7 张,这里挑最重要的 5 张列出设计思路。

student(学生档案表)

这张表只放学生相对稳定的基本信息,注意把"可变属性"和"流程属性"分开。

CREATE TABLE student ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL COMMENT '学号', name VARCHAR(50) NOT NULL, gender TINYINT COMMENT '0-女 1-男', id_card VARCHAR(18) COMMENT '身份证号', birth_date DATE, political VARCHAR(20), major_id BIGINT COMMENT '关联专业表', class_id BIGINT COMMENT '关联班级表', enroll_year VARCHAR(10) COMMENT '入学年份', education VARCHAR(20) COMMENT '培养层次:本科/专科/硕士', photo_path VARCHAR(255), status TINYINT DEFAULT 0 COMMENT '学籍状态 0在籍 1休学 2退学 3保留学籍 4毕业 5注销', created_time DATETIME, updated_time DATETIME, UNIQUE KEY uk_student_no (student_no) ) COMMENT '学生学籍基本信息表';

student_no必须唯一,这是业务主键。status只存当前状态,历史状态全部交给mutation_record表去追溯。

major / class(专业表、班级表)

因为学籍系统几乎每一个查询都离不开学院、专业、班级维度,我把这两张表独立出来,避免直接在student表里写冗余字符串。

mutation_record(学籍异动记录表)

这张表是整个系统的灵魂,也是答辩时最能体现你理解深度的地方。

CREATE TABLE mutation_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id BIGINT NOT NULL, mutation_type TINYINT COMMENT '1休学 2复学 3转专业 4退学 5保留学籍 6恢复学籍 7毕业', from_status TINYINT COMMENT '变更前状态', to_status TINYINT COMMENT '变更后状态', reason VARCHAR(500), attachment VARCHAR(255) COMMENT '材料附件路径', apply_user_id BIGINT, apply_time DATETIME, audit_user_id BIGINT, audit_time DATETIME, audit_opinion VARCHAR(500), status TINYINT DEFAULT 0 COMMENT '0待审 1通过 2驳回', effective_date DATE COMMENT '生效日期', expire_date DATE COMMENT '结束日期(休学到期日)' ) COMMENT '学籍异动记录表';

每次状态变更,都插入一条mutation_record,同时更新student.status。from_status和to_status记录变更前后,人、时间、原因、审核意见全都有,数据完整可追溯。

operation_log(操作日志表)

记录谁改了哪个字段、改之前值是什么、改之后值是什么。我会在 Service 层做统一的日志切面,避免业务代码里到处写日志。

3.2 学籍状态机:状态流转必须有规则

如果允许用户随便把"在籍"改成"毕业",那数据很快就烂了。我的做法是在 Service 层维护一张状态机规则表:

public class MutationRule { // key: fromStatus + "->" + toStatus // value: 是否允许 }

比如:

  • 0(在籍) -> 1(休学)允许
  • 0(在籍) -> 2(退学)允许
  • 1(休学) -> 0(在籍)允许(复学)
  • 1(休学) -> 2(退学)允许
  • 2(退学) -> 0(在籍)不允许
  • 4(毕业) -> 0(在籍)不允许

在MutationService里做状态变更前,先检查这条边是否允许,不允许就直接抛出异常。这个设计看似简单,但能防止绝大多数的人为错误。实测中,最容易出错的是"退学"和"注销"的边界——退学是学生主动/被动离开学校,但档案还在;注销是档案已经处理完毕。规则没定义清楚,操作员就会乱选。

3.3 MyBatis 多条件组合查询与分页

学籍列表页最核心的需求是:按学号、姓名、专业、班级、状态多条件组合查询,并且要支持分页。我用 MyBatis 动态 SQL 实现,<where>标签自动去掉多余的 AND。

<select id="selectStudentPage" resultType="map"> SELECT s.student_no, s.name, s.gender, m.major_name, c.class_name, s.status, s.enroll_year FROM student s LEFT JOIN major m ON s.major_id = m.id LEFT JOIN class c ON s.class_id = c.id <where> <if test="studentNo != null and studentNo != ''"> AND s.student_no = #{studentNo} </if> <if test="name != null and name != ''"> AND s.name LIKE CONCAT('%', #{name}, '%') </if> <if test="majorId != null"> AND s.major_id = #{majorId} </if> <if test="status != null"> AND s.status = #{status} </if> </where> ORDER BY s.enroll_year DESC, s.student_no ASC LIMIT #{offset}, #{pageSize} </select>

关于分页,我实测下来的建议是:数据量在十万条以内时,手写LIMIT ? , ?完全够用,不用特意引入分页插件;查询量比较大时,再考虑 PageHelper。另外,组合查询必须在student_no上建唯一索引,在major_id、class_id、status上建普通索引,否则多条件一组合,慢查询很快就出来了。

3.4 SSM 接口与 Flask 服务的协议约定

接口协议统一用 RESTful + JSON:

  • GET /api/student/page分页查询
  • GET /api/student/{id}学生详情
  • POST /api/student新增
  • PUT /api/student/{id}修改基础信息
  • POST /api/student/{id}/mutation发起异动
  • POST /api/mutation/{id}/audit审核异动

Flask 辅助服务通过requests调用 SSM 内部接口时,在 Header 里带上X-Internal-Token,SSM 拦截器识别内部 token 后跳过登录校验。这个 token 我放在配置中心,两边的配置保持一致。实测中要注意:token 必须是随机字符串,不要用固定的"abc123"这类弱口令,否则内网里也容易被扫到。

4. 联调与调试:跨框架协作中最容易踩的坑

4.1 Excel 批量导入:编码、前导零和超时

新生入学时,教务通常会发一份几百到几千行的 Excel 名册。Flask 端负责解析,SSM 端负责入库。这里我踩过的第一个坑是学号前导零丢失。

比如学号20230101001,前面有一两个 0,如果 Excel 单元格是"常规"格式,openpyxl读出来会变成数字20230101001.0,再一转换就成了科学计数法字符串。我的解决方案是:读取时先检查单元格number_format,如果列的格式是"文本",直接读字符串;如果是常规/数字格式,则用str(cell.value).split('.')[0]手动清理尾部的.0。同时在导入预处理阶段,对学号做正则校验^\d{8,12}$,不符合规则的直接进错误清单,生成"导入失败报告.xlsx"返回给操作员。

第二个坑是大批量导入的事务边界和超时。最初我是把 5000 条数据放在一个事务里循环单条INSERT,结果到 3000 条左右页面直接超时。排查后发现两个问题:一是INSERT次数太多,网络往返开销大;二是 Tomcat 默认的请求超时设置并不适用于长耗时接口。最后改成:MyBatis 批量插入(<foreach>拼INSERT INTO ... VALUES ...,每批 500 条),同时在 Controller 层把导入接口的读取超时调大。实际效果:5000 条数据入库稳定在 8 秒左右,完全达标。

第三,导入时还要注意日期格式不一致。Excel 里可能是2023/9/1、20230901、2023-09-01三种格式混在一起。我在 Flask 端写了一个normalize_date函数,先把值转成字符串,再去匹配常见格式。解析失败的行不直接丢弃,记录错误原因返回给用户。这是最容易被低估的一环,实际操作中名册里永远有脏数据。

4.2 日期时间格式与 JSON 序列化不一致

SSM 端和 Flask 端各自返回 JSON 给前端时,日期格式如果不统一,前端解析就会出问题。

Java 端我最开始返回的是java.util.Date,Jackson 默认序列化成时间戳数字,也就是1693353600000这种。Flask 端返回的则是2023-08-30或2023-08-30 16:00:00。同一个学籍信息,不同接口返回的日期格式完全不同,前端要写两套解析逻辑,非常痛苦。

我最终的统一方案是:所有接口返回的日期一律采用yyyy-MM-dd HH:mm:ss字符串格式。Java 端配置 Jackson 的ObjectMapper,注册JavaTimeModule,关闭WRITE_DATES_AS_TIMESTAMPS,并在applicationContext.xml里指定全局日期格式。Flask 端在返回 JSON 前统一datetime.strftime。从这之后,联调过程中基本没再出过日期问题。

顺便提一个容易忽略的细节:MyBatis 从 MySQL 读DATETIME类型,如果 Java 实体用LocalDateTime,需要在映射 XML 里确认jdbcType=TIMESTAMP和 JavaType 匹配正确,否则可能出现LocalDateTime 无法序列化的报错。

4.3 跨域、内部鉴权和 HTTP 状态码语义

项目前端跑在localhost:8081(Vue/HTML 静态资源),SSM 接口跑在8080,Flask 跑在5001,三个端口一开,跨域问题立刻出现。SSM 端我用一个简单的 CORS 过滤器允许指定来源;Flask 端用flask-cors插件配置CORS(app, origins=["http://localhost:8081"]),只允许自己的前端,不要直接*全开。

内部调用的坑主要出在状态码语义上。比如 Flask 请求 SSM 的POST /api/mutation/{id}/audit,如果审核失败,理论上应该返回400或者500,但我在早期开发时直接返回200加{success: false},导致 Flask 端无法通过response.raise_for_status()快速判断失败,只能跑到业务代码里再去读 body,调试效率很低。后来我统一约定:HTTP 状态码表达请求处理结果,2xx 表示成功、4xx 表示参数/权限错误、5xx 表示服务端异常,业务成功与否再看响应体里的code字段。这样两边联调时,一眼就能从状态码定位问题归属,效率提升非常明显。

4.4 一套有效的联调排查链路

跨框架联调最怕的就是"问题不知道在哪一端"。我的调试方法论很简单:

  1. 先看日志链路。给 SSM 配置 Logback 日志文件,给 Flask 配置 logging 文件,两边每条请求都记录request_id。发生错误时,先用这个request_id串起两边的日志,很快就能定位是 SSM 报错还是 Flask 报错。
  2. Postman 分端测试。任何联合接口,都先把调用链拆成三段分别测试:前端调 SSM 是否正常、前端调 Flask 是否正常、Flask 调 SSM 是否正常。三段都通过,再组合测试。
  3. MySQL 慢查询日志。一旦页面卡顿,先打开慢查询日志,看有没有全表扫描。学籍项目里最典型的问题就是查询条件没走索引,多条件一 OR 就全表扫了。
  4. 配置统一 dump。联调前先打印一份两边的配置关键项,数据源地址、端口、token、文件上传路径这四项最容易不一致,提前对齐能省一半问题。

5. 测试、调试文档与交付:项目能不能交付,看这些细节

5.1 功能测试用例设计:不要只测主流程

测试部分很多项目都露怯了,只测了"新增学生成功""查询列表出现数据"就结束。我这次把测试分成四类:

  • 正常流程:新增学生成功、发起休学申请成功、审核通过后状态变化正确、复学后状态恢复在籍。
  • 异常流程:学号重复时提示明确、未登录访问/api/student/page返回 401、无权限角色访问管理员接口返回 403、休学状态下直接发起毕业(状态机不允许)时被拒绝。
  • 边界值:分页查询第 0 页/超大页码、姓名模糊搜索包含特殊字符%、Excel 导入空行/重复学号/非法日期。
  • 数据一致性:模拟审核休学申请时事务回滚,验证student.status没有变成休学、mutation_record中没有残留半条记录。

其中有一个非常隐蔽的 bug 是在边界测试里发现的:LIKE CONCAT('%', #{name}, '%')如果传入的name本身包含%或_,会把它们当成通配符,导致查询结果范围变大。解决方案是在 SQL 里显式转义:LIKE CONCAT('%', REPLACE(REPLACE(#{name}, '\\', '\\\\'), '%', '\\%'), '%') ESCAPE '\\'。这种问题不测试根本发现不了。

5.2 调试文档怎么组织:让接手的人少走弯路

调试文档不是把代码注释抄一遍,而是回答三个问题:怎么启动、怎么复现问题、出了问题去看哪里。我的调试文档结构是这样:

  1. 环境清单:JDK 版本、Tomcat 版本、MySQL 版本、Python 版本、第三方依赖列表。
  2. 启动顺序:先启动 MySQL,改好两个服务的数据源配置;再启动 SSM(Tomcat),确认日志打印出监听 8080 端口、无 Spring 容器加载异常;最后启动 Flask 服务,确认 5001 端口可访问。文档里写清楚每一步的验证命令,比如 Linux 下用curl -X GET http://localhost:8080/api/health检查 SSM 健康状态。
  3. 数据库初始化脚本:init.sql、data.sql顺序执行说明。
  4. 常见问题 FAQ:比如"Flask 连接 MySQL 出现Access denied怎么办""导入 Excel 报ValueError可能是因为某行数据为空""前端页面中文乱码,先检查两边返回的Content-Type是否带charset=UTF-8"。
  5. 演示数据注意事项:演示阶段不要把真实学生身份证号放进去,用测试数据生成脚本批量造假数据,格式要规范。

5.3 项目文档的组织:从需求到测试的闭环

这里的"LW"指项目的匹配文档/论文,很多第一次做这类项目的同学不知道写什么。我的建议是严格按工程顺序来,每一章内容一定要有系统截图和代码片段支撑,千万别写成空谈:

  • 第一章 需求分析:把我在第一节梳理的用户角色、业务流程、功能模块边界写清楚。用表格列出每个角色的权限。
  • 第二章 技术选型与总体设计:写清楚为什么选 SSM、为什么引入 Flask。这里建议画一张架构部署图,文字描述也可以,但要说明服务端口、模块依赖关系。我前面讲的"SSM 负责写、Flask 负责读和文件"这个设计决策,一定要在这里展开论述。
  • 第三章 数据库设计:核心表的字段说明、表关系说明;状态机规则表值得专门列一节讲清楚。
  • 第四章 功能实现:按模块写关键逻辑,尤其是学籍异动的状态流转和 MyBatis 多条件查询这两块,要贴核心代码并解释。
  • 第五章 测试:功能测试用例表、测试结论、发现的问题和解决过程。
  • 第六章 总结:遇到的问题、收获、后续改进方向。这里不要空喊口号,如实写"哪些功能还没来得及做"反而更真诚。

5.4 演示与答辩的注意事项

项目做完之后如何展示,很多人没有专门准备。我总结了几条实际经验:

  • 提前准备一份干净的演示环境。不要在演示现场临时启动服务,提前把 Tomcat 和 Flask 进程拉起,数据库数据准备好。现场最大的风险往往是端口被占用、数据库没启动、Excel 文件路径不对。
  • 演示数据不要用真实的身份证号和学生数据。用脚本生成 100 条测试数据,学号规律、姓名用"张三1、张三2"这类假名,既能演示分页和搜索,又不涉及隐私。
  • 准备一个"问题清单"。答辩或验收时,老师最爱问的问题通常是:状态流转是怎么控制的?权限怎么实现的?批量导入一万人怎么处理?事务怎么保证?把这些问题想清楚。
  • 演示时先走一个完整流程。比如:登录 → 批量导入 → 查看列表 → 发起一个休学申请 → 审核 → 学生登录查询状态。一镜到底,比零散点几个页面有说服力得多。

这个项目和纯管理系统最大的不同,在于学籍数据不允许错,状态流转不允许乱。开发时最要紧的把"数据完整性"四个字刻在脑子里:该加事务的地方绝不省,该留痕的地方绝不只改一个字段,该校验的格式绝不用异常代替。等你把蒋这套流程走顺了,再回头看普通的业务管理系统,会发现它们的很多思路其实是相通的。我个人的体会是,把状态机、权限、日志这"三件套"想透,比多写一百行 CRUD 代码更能把一个项目从及格提到优秀。

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

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

立即咨询