ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Python Flask校友录管理系统开发:从数据库设计到部署实战

2026/9/28 12:31:23 拓冰建站 浏览量
Python Flask校友录管理系统开发:从数据库设计到部署实战 1. 项目背景与需求拆解1.1 为什么是“校友录管理系统”“基于Python的大学校友录信息管理系统”这个题目在高校的课程设计、毕业设计题库里出现频率非常高。我最早接触这类项目是在帮学弟学妹看代码的时候发现大多数人在拿到类似题目的第一时间就会犯一个选择困难症到底是做Web版还是桌面版是用Django这种大而全的框架还是用Flask这种轻量级选手先说结论如果你现在正在为课程设计或毕业设计选型我强烈建议走Python Flask SQLite/MySQL Jinja2模板这条路线。原因很实在Flask对上手的友好程度远超Django没有那么多“约定俗成”的目录结构和强制规范你可以根据自己的思路写代码。更关键的是对于“校友录管理系统”这个小体量业务场景Django自带的那套Admin后台、ORM、中间件体系用不太上反而显得臃肿。那么“hx2021hx2022”这个后缀是什么意思我看到的这类项目通常会在标题里加一段版本号或作者标识比如“hx2021hx2022”大概率是“HX”这个作者在2021年开始做2022年迭代了一版。这类标识属于项目管理习惯不影响系统本身的功能定义但你拿到题目后最好先确认一下老师或需求方到底要求的是哪些功能模块这比纠结标题后缀重要得多。1.2 核心需求清单毕业设计/课程设计的标准功能矩阵无论是哪个大学、哪个老师出的“校友录管理信息系统”题目功能需求大体上都离不开以下这几块。我把最常见的需求组合成一个表格方便你对着检查自己的系统“还缺什么”模块核心功能点紧急程度校友信息管理新增、编辑、删除、查询校友基本信息姓名、性别、入学年份、毕业年份、学院、专业、现工作单位、联系方式必做校友动态/校友活动发布活动公告、活动报名、活动记录高频出现加分项校友捐赠记录记录捐赠人、捐赠金额、捐赠时间、捐赠用途常见加分项用户登录/权限管理员角色、普通校友角色未登录不可操作系统必做统计分析按毕业年份、学院、行业进行校友数量统计加分项但很多学校要求有图表展示留言/校友互动在校友之间发送站内信或留言少部分学校会要求你需要先把自己的需求范围定下来再动手写代码。我见过太多人一上来就开始建表写到一半发现缺了“活动报名”模块又回头改数据库。改数据库是最折磨人的事情因为数据表关联、前端表单、后端路由全都耦合在一起牵一发而动全身。1.3 技术选型背后的“为什么”我在给别人讲这个项目时被问得最多的问题是“既然是Python项目为什么不用Tkinter做桌面版”我的回答一般是看你的场景。如果这是一个“给校友办老师用的内部管理系统”桌面版的Tkinter确实省事不用配环境、不用考虑部署双击就能跑。但问题是大学校友录的核心使用场景是“分布在不同城市甚至不同国家的校友随时访问”这就意味着它天然倾向于B/S架构。更何况Web版的界面展示能力数据可视化、列表分页、图片上传远超桌面版答辩演示的时候也更漂亮。再来说说为什么是SQLite而不是MySQL。SQLite就是一个文件型数据库不需要单独装数据库服务端学生机或笔记本上直接就跑了课程设计评审专家来验收时也不用现场配置数据库环境。MySQL的优势在于并发处理和数据量级但一个校友录系统撑死几千条数据SQLite绰绰有余。如果你实在想展示一下自己的水平可以在项目里用SQLAlchemy ORM把数据库连接字符串做成可配置的演示的时候说“我可以随时切换到MySQL”这个策略在答辩时非常加分。2. 数据库设计与系统架构2.1 关键数据表结构拆解数据库设计是整个系统的地基。这个阶段偷懒了后面写业务逻辑的时候到处都是坑。我先给你看我比较推荐的一套表结构设计这个设计既覆盖课程设计常见功能又不会过度设计表一users用户表字段名类型说明idINTEGER 主键自增用户IDusernameVARCHAR(50) 唯一登录名password_hashVARCHAR(128)密码哈希值roleVARCHAR(20)管理员/普通校友real_nameVARCHAR(50)真实姓名created_atDATETIME注册时间表二alumni校友信息表字段名类型说明idINTEGER 主键自增校友IDnameVARCHAR(50)姓名genderVARCHAR(10)性别student_idVARCHAR(20)学号enrollment_yearVARCHAR(10)入学年份graduation_yearVARCHAR(10)毕业年份collegeVARCHAR(100)学院majorVARCHAR(100)专业companyVARCHAR(200)现工作单位positionVARCHAR(100)现任职务emailVARCHAR(100)邮箱phoneVARCHAR(20)手机号addressVARCHAR(255)通讯地址photo_urlVARCHAR(255)照片路径created_timeDATETIME录入时间表三activities校友活动表字段名类型说明idINTEGER 主键自增活动IDtitleVARCHAR(200)活动标题contentTEXT活动详情locationVARCHAR(200)活动地点start_timeDATETIME开始时间end_timeDATETIME结束时间max_peopleINTEGER人数上限created_byINTEGER 外键关联users发布人表四donations捐赠记录表字段名类型说明idINTEGER 主键自增捐赠IDalumni_idINTEGER 外键关联alumni校友IDamountDECIMAL(10,2)捐赠金额donation_timeDATETIME捐赠时间purposeVARCHAR(200)捐赠用途remarkVARCHAR(255)备注我的建议是先把这四张表建好再去想业务代码。为什么把用户表和校友信息表分开因为不是所有登录用户都是校友系统里可能还有管理员的角色。有些同学喜欢把用户信息和校友信息塞在同一张表里虽然短期内看起来简化了但后续要加“管理员”角色时就要改表结构。分开设计逻辑清晰扩展性也好。2.2 ORM还是裸SQL——我选了这样一条路实现方式上我推荐使用SQLAlchemy ORM。原因有两条第一它能让你用Python类的方式来描述数据库表模型避免了写大量重复的SQL语句第二SQLAlchemy的query API在查询过滤、分页等操作上非常简洁对新手来说上手难度也不高。我当时给一个学弟改代码时发现他用的是裸写SQL的方式拼接查询条件大概长这样sql SELECT * FROM alumni WHERE name name 这就是典型的SQL注入漏洞。万一有人在查询框里输入恶意字符串整个数据库就没了。用ORM的方式写不仅没有这个问题代码还更短更健壮alumni Alumni.query.filter(Alumni.name name).all()有些人觉得学习ORM还要学一套新语法不如直接写SQL来得痛快。我的观点是ORM的学习成本大概就是几个小时但它的收益是持续性的。答辩的时候如果你能跟评委解释清楚“ORM有效避免了SQL注入风险”这个加分项比写了三百行裸SQL要好得多。2.3 为什么建议你从“Flask工厂模式”起步很多新手拿到Flask项目都是所有代码堆到一个app.py里。初学阶段这无可厚非毕竟代码量也不大。但“校友录管理系统”这种项目虽然体量不大但功能模块互相独立用户、校友、活动、捐赠放在同一个文件里后期维护会非常痛苦。我建议你在项目一开始就按下面这个结构组织目录alumni_system/ ├── app.py # 应用入口初始化Flask应用 ├── config.py # 配置文件数据库连接、密钥等 ├── models.py # 数据库模型文件 ├── forms.py # 表单验证 ├── views/ │ ├── __init__.py │ ├── auth.py # 登录/注册相关路由 │ ├── alumni.py # 校友信息管理路由 │ ├── activity.py # 活动管理路由 │ └── donation.py # 捐赠管理路由 ├── templates/ # HTML模板文件 │ ├── base.html │ ├── auth/ │ ├── alumni/ │ └── activity/ ├── static/ │ ├── css/ │ ├── js/ │ └── images/ └── requirements.txt这个结构的好处是三层的入口文件只负责创建app和注册蓝图models独立成一个文件所有数据模型一目了然views按模块拆开改活动模块的代码时不用去动校友模块的文件。演示和答辩的时候你可以骄傲地说“我的项目使用了Flask蓝图模块化设计实现了关注点分离”这种专业表述对评价有显著加分。3. 后端核心功能实现与实操3.1 用户登录、权限校验与状态管理登录功能是所有管理系统的第一道门。我见过太多人设计注册登录时出现这种问题密码竟然是明文存到数据库里的这要是期末答辩或者毕业设计送审被评委看到很难有好的评价。应用什么方式加密密码答案是用Werkzeug自带的密码哈希函数。Flask项目里基本上都自带这个库你不用额外引包。核心代码非常简单from werkzeug.security import generate_password_hash, check_password_hash # 注册时生成密码哈希 password_hash generate_password_hash(form.password.data) # 登录时验证密码哈希 is_valid check_password_hash(user.password_hash, form.password.data)为什么不用MD5MD5已经被证明可以通过彩虹表快速破解而且同样的密码生成的是相同的摘要。而generate_password_hash采用的是加盐哈希即使用户密码都是“123456”生成的哈希值也都各不相同这能有效对抗密码字典攻击。登录状态的保存我建议采用Flask自带的session机制。用户登录成功后往session里写入一个标记session[user_id] user.id session[role] user.role然后在需要保护的视图函数上做装饰器校验from functools import wraps def login_required(f): wraps(f) def decorated_function(*args, **kwargs): if user_id not in session: flash(请先登录后再操作, warning) return redirect(url_for(auth.login)) return f(*args, **kwargs) return decorated_function以“登录后才能查看校友详情”“非管理员不能删除活动”这类需求配合login_required和一个admin_required装饰器就能覆盖绝大多数权限控制场景。这里有个小细节提醒一下redirect之后要记得url_for(auth.login)要对应你的蓝图名称不然会出现端点找不到的报错——这种新手常见错误往往排查半天才发现是路由名称写错了。3.2 校友信息CRUD列表查询、分页与搜索校友信息管理的核心操作就是增删改查。先来说说查询。一张几百条数据的校友表如果在页面里一次性全列出来既不美观也影响加载速度。我推荐用Flask-SQLAlchemy的分页方法page request.args.get(page, 1, typeint) per_page request.args.get(per_page, 10, typeint) paginate Alumni.query.paginate(pagepage, per_pageper_page, error_outFalse)这样一个分页对象就有了paginate.items当前页数据、paginate.pages总页数、paginate.page当前页码等属性。在模板里配合一个简单的分页导航条就能实现翻页功能。模板分页代码我贴一下参考nav aria-labelPage navigation ul classpagination li class{% if not paginate.has_prev %}disabled{% endif %} a href{{ url_for(alumni.index, pagepaginate.prev_num) }}上一页/a /li li classactive a href#第 {{ paginate.page }} / {{ paginate.pages }} 页/a /li li class{% if not paginate.has_next %}disabled{% endif %} a href{{ url_for(alumni.index, pagepaginate.next_num) }}下一页/a /li /ul /nav搜索逻辑有一个常见误区就是多个搜索条件必须同时满足才返回结果。实际使用中用户可能只填了一个“学院”条件或者只填了一个“毕业年份”条件。所以你要做的搜索是动态SQL按需组合条件。我用SQLAlchemy的filter加判断来实现query Alumni.query if search_name: query query.filter(Alumni.name.contains(search_name)) if search_year: query query.filter(Alumni.graduation_year search_year) if search_college: query query.filter(Alumni.college search_college) results query.all()3.3 活动模块与捐赠模块的联动设计活动模块要注意的坑是第一“活动报名”和“活动表”本身的关联。如果需求里有“报名”功能你就需要一个中间表记录用户和活动之间的多对多关系。我常用的建模方式是这样的activity_signup db.Table(activity_signup, db.Column(user_id, db.Integer, db.ForeignKey(users.id), primary_keyTrue), db.Column(activity_id, db.Integer, db.ForeignKey(activities.id), primary_keyTrue), db.Column(signup_time, db.DateTime, defaultdatetime.utcnow) )然后在Activity模型中只是加一个关联属性查询某个活动已报名人数时直接activity.signups.count()。捐赠模块的核心是金额字段的数据类型。一定用DECIMAL而不是FLOAT。有些同学觉得浮点数也能存金额结果可能会出现19.999999这样的数值。DECIMAL是精确十进制数能确保金额永远精确到分。这个如果有人在做汇总统计时踩过浮点数精度问题的坑应该深有体会。4. 前端页面与用户体验设计4.1 模板继承与页面骨架现在的前端开发讲究的是页面复用。Flask的Jinja2模板引擎正好提供了一套继承机制。我可以直接在base.html里定义一块能够被其他页面复用的整体布局!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title{% block title %}校友录管理系统{% endblock %}/title link relstylesheet href{{ url_for(static, filenamecss/bootstrap.min.css) }} link relstylesheet href{{ url_for(static, filenamecss/custom.css) }} /head body {% include navbar.html %} div classcontainer mt-4 {% with messages get_flashed_messages(with_categoriestrue) %} {% if messages %} {% for category, message in messages %} div classalert alert-{{ category }} alert-dismissible rolealert {{ message }} button typebutton classclose>{% extends base.html %} {% block title %}校友列表{% endblock %} {% block content %} div classcard div classcard-header h3校友列表/h3 /div ... /div {% endblock %}这样写90%的场景都不用再去重复导航和样式引入。有些同学喜欢每个页面都写一整份HTML结果改某个公共样式时发现要改动十几个文件效率低且容易出错。4.2 表单设计一个容易被忽略的核心交互校友录系统里的表单是用户交互的重头戏因为校友信息有十几个字段表单这类页面做得好不好直接决定系统给人留下的印象。我用Flask-WTF来定义表单模型好处是通过forms.py就能看到整个系统所有表单的字段定义。某学弟在我的建议下是这么写的class AlumniForm(FlaskForm): name StringField(姓名, validators[DataRequired(message姓名不能为空), Length(max50)]) gender SelectField(性别, choices[(男, 男), (女, 女)], validators[DataRequired()]) student_id StringField(学号, validators[DataRequired(), Length(max20)]) enrollment_year StringField(入学年份, validators[DataRequired()]) graduation_year StringField(毕业年份, validators[DataRequired()]) college StringField(学院, validators[DataRequired(), Length(max100)]) major StringField(专业, validators[DataRequired(), Length(max100)]) company StringField(现工作单位, validators[Length(max200)]) position StringField(现任职务, validators[Length(max100)]) email StringField(邮箱, validators[Email(), Length(max100)]) phone StringField(手机号, validators[Length(max20)]) address StringField(通讯地址, validators[Length(max255)]) submit SubmitField(保存)表单验证前后端都要做。前端是做用户体验校验不通过马上提示后端是做数据安全保证不合法数据进不了数据库。Flask-WTF已经把后端验证做得很顺手了模板里渲染表单的代码如下form methodPOST action{{ url_for(alumni.edit, idalumni.id) }} {{ form.hidden_tag() }} div classrow div classcol-md-6 div classform-group {{ form.name.label }} {{ form.name(classform-control) }} {% for error in form.name.errors %} span classtext-danger{{ error }}/span {% endfor %} /div /div ... /div {{ form.submit(classbtn btn-primary) }} /form表单验证器的DataRequired不仅保证字段非空还能确保用户没有恶意跳过前端校验直接提交请求。有了这一层系统健壮性会上一个台阶。4.3 搜索筛选、统计图表展示的技巧多数课程设计都要求“有统计功能”那我建议你做一个“校友分布统计”页面按学院或毕业年份统计人数。图表展示选什么库我推荐ECharts。它用JavaScript调用无需后端额外处理图表交互体验也比静态图片好。你在base.html里引入ECharts的CDN然后在统计页面通过Ajax从后端请求数据前端拿到JSON数据后渲染柱状图或饼图。后端返回JSON数据的路由可以参考from flask import jsonify app.route(/api/alumni/statistics/college) def get_college_statistics(): stats db.session.query( Alumni.college, db.func.count(Alumni.id) ).group_by(Alumni.college).all() return jsonify([{name: name, value: count} for name, count in stats])前端ECharts配置项里把数据源指向上面接口返回的列表即可。这种做法的事务分离很清晰后端只管给数据前端负责视觉呈现。答辩时演示统计页面往往能给评委留下更完整系统的印象。5. 部署、测试与常见问题排查5.1 如何在本地跑起来环境变量与依赖管理很多同学在自测运行项目时后台是“一段红”原因往往是因为依赖缺失。所以我每次拿到新项目都会先看有没有requirements.txt没有的话先在本地跑一遍看缺哪个库pip install flask pip install flask-sqlalchemy pip install flask-wtf其实更省心的方法是直接把所有依赖写进requirements.txt然后一条命令装完pip install -r requirements.txt安装完之后环境变量的概念可能对刚接触Flask的人有点陌生。比如密钥直接写死在代码里也能跑但不安全。我建议把配置放到config.py里通过环境变量覆盖默认值import os class Config: SECRET_KEY os.environ.get(SECRET_KEY) or dev-secret-key SQLALCHEMY_DATABASE_URI os.environ.get(DATABASE_URL) or sqlite:///alumni.db SQLALCHEMY_TRACK_MODIFICATIONS False然后入口文件里app.config.from_object(Config)加载配置。这样做的另一个好处是从开发切成生产环境时不用改代码只改环境变量就行。5.2 从开发环境到生产部署最容易忽视的问题开发阶段用app.run(debugTrue)没问题但如果照搬到生产环境就完全是场灾难原因有两点一是Werkzeug自带的服务器性能不足并发几十个人就可能卡顿二是debug模式会暴露详细错误信息很容易成为被攻击的缺口。一个稳妥的部署方式是用Gunicorn作为WSGI服务器用Nginx做反向代理。在服务器上安装依赖后启动命令大概是这样gunicorn -w 4 -b 0.0.0.0:8000 app:app-w 4表示开4个worker进程理论上能处理同时进来的一定数量请求。前面再挂一层Nginx把80端口的请求转发给8000端口再配合静态文件的处理这个架构放在校友录项目里算是比较标准的生产方案了。有一点值得在答辩时提出来如果换成MySQL作为生产数据库只需调整DATABASE_URL的值SQLAlchemy的代码不需要改动。这个“可移植性”设计恰好体现了工程思维的成熟度。5.3 常见问题速查表与排查实录这类系统常见的问题我整理了一个速查表基本上能覆盖90%的翻车场景问题现象根因定位解决方案访问页面报500 Internal Server Error模板变量名写错或数据库表不存在开启debug模式看完整报错用flask shell检查数据表是否存在表单提交后URL不变或跳转异常action属性写错或者url_for名称错误检查模板里的url_for参数里是否对应了正确的蓝图视图函数名中文显示成乱码文件编码或数据库编码问题确保所有Python文件保存为UTF-8如果是MySQL数据库建库时指定utf8mb4字符集点击登录没反应表单验证未通过但未显示错误信息检查模板是否正确渲染了form.errors上传图片不显示静态文件路径问题检查上传配置的UPLOAD_FOLDER路径是否存在url_for(static, filename...)路径是否正确时间字段存储为UTC相差8小时时区处理问题可以统一使用本地时间在config.py里设置TIMEZONE Asia/Shanghai并在模型里处理时区转换批量导入校友数据速度慢每插入一条就commit一次改为循环结束后一次性db.session.commit()举一个比较典型的实测排查案例我调试一个版本时发现校友列表页在搜索“计算机学院”后再点“下一页”结果居然没带搜索条件。这个问题的根源是分页链接只保留了page参数没保留search_name和search_college参数。解决办法是把当前查询条件拼到分页链接里a href{{ url_for(alumni.index, pagepaginate.prev_num, s_namesearch_name, s_collegesearch_college) }}上一页/a这类细节问题很容易踩根源是把查询状态“丢了”。所以你设计搜索时最好把搜索关键词存在request.args里然后把它们传入模板的分页链接。类似的还有按年份筛选后翻页筛选条件同样要带上。5.4 性能优化与安全加固的进阶思路普通课程设计做到CRUD登录就够用了但如果你想拿高分或者准备毕业设计答辩可以在安全性和性能优化上多写几笔密码策略要求密码至少8位包含字母和数字。最简做法是在表单验证器里加一个Regexp或自定义验证函数。虽然有些结构简单的用户会觉得麻烦但作为系统设计密码复杂度要求是基础安全习惯。SQL注入防护前面提到的ORM已经帮你挡住了一种最常见的注入路径但如果你在某个模块里确实写了原生SQL务必用参数化查询不要用字符串拼接。CSRF防护Flask-WTF自带CSRF保护只要在表单里加{{ form.hidden_tag() }}并且在创建Flask应用时设置了SECRET_KEY就能有效地防护跨站请求伪造。查询性能优化校友列表如果将来达到几千条你可以在查询时调用Alumni.query.paginate来限制单次数据量。对于筛选条件较多的场景建议给graduation_year和college字段加数据库索引。加索引在SQLAlchemy模型里非常简单graduation_year db.Column(db.String(10), indexTrue) college db.Column(db.String(100), indexTrue)敏感信息脱敏校友联系方式属于个人隐私建议列表页默认只显姓名、学院、毕业年份详细页才展示手机号和邮箱。这个细节属于“数据合规意识”不仅能保护隐私评审时也是一个亮点。6. 项目展示策略与答辩技巧6.1 功能演示顺序先整体后细节我见过很多同学在答辩演示时毫无章法地乱点页面甚至从个人信息页开始讲起讲着讲着自己都不知道下一步该干什么。其实演示顺序是有讲究的先演示“用户登录”证明系统有安全边界再演示“校友信息列表及搜索”这是系统的核心然后是“新增/编辑校友信息”展示系统能处理数据变化最后是“活动发布与报名”或“捐赠记录与统计图表”展示系统的延伸能力。每演示一个环节最好顺手说出一个你为之骄傲的技术点比如“这里的分页我用的是SQLAlchemy自带的分页方法避免了一次性加载大量数据导致页面卡顿”。“这个统计图表是通过前端ECharts调用后端JSON接口实现的前后端数据交互清晰简洁”。6.2 代码讲解的“黄金比例”答辩时评委一般会看代码结构、读关键逻辑。你讲代码时不要从第一行开始念而应按“功能”来讲。比如讲数据库模型时先说明一个表有几个关键字段自己为什么这样设计讲登录路由时说明密码如何哈希存库、会话状态如何维持。另一个比较常用的技巧是把用户权限继承关系捋清楚管理员、普通校友两个角色的行为差异是在哪里通过什么条件实现的。这往往比通篇念代码更能体现你对项目的整体把握程度。6.3 从优秀到高分可扩展性预埋最后建议你在项目README项目说明文档里明确写出“未来的扩展方向”。比如从SQLite平滑迁移到MySQL满足更大并发量。引入Redis做基于Session的分布式登录态适合后续多服务器部署。增加基于Vue.js的前后端分离版本接口层保持不变。加入邮件发送功能用于系统自动发送活动通知。为什么这么做因为评审专家关心的是“这个系统有没有成长空间”而不是眼前这几张表能不能满足作业。你能多走一步把扩展路径设计好在你同龄人项目里已经属于非常成熟的表现了。7. 我踩过的坑与最终心得在做这个项目以及给学弟学妹改代码的过程中我最大的体会是这类管理系统项目拼的不是新技术而是工程细节。你不需要用神经网络模型去预测校友还钱概率也不需要给校友所在地建GIS地图你只要把“增删改查、登录、权限、分页、搜索、统计”这几个基本功做得稳健这个项目就值非常不错的分数。有几个细节值得特别提醒一下第一base.html模板里全局引入的Javascript/CSS库尽量保存到本地static目录而不是依赖CDN。答辩现场很可能没有外网页面全部依靠CDN的话演示时会发现样式全丢了。第二数据库备份要养成习惯。课程设计周期长如果哪一天不小心把alumni.db文件删了或者改了表结构出现异常没有备份就只能从头再来这个痛苦深有体会。第三项目的README文档相当重要。把运行环境、安装步骤、默认管理员账号比如admin / admin123写清楚评委也好其他同学也罢拿到项目后十分钟就能跑起来对你的印象分完全不一样。对了最后再分享一个在实战中好用的调试技巧如果你发现表单提交后每次总是大概率报错但不清楚具体是哪个字段的问题可以在视图里加一个临时的print(form.errors)提交请求后直接在终端看错误字典。这个比在HTML模板里瞎猜校验错误快太多了。等排查完毕再删掉那行调试语句就好。这个项目本身的水准核心在于你最开始的取舍。选定好Flask加SQLite的基础架构做好模块解耦把安全习惯密码哈希、CSRF、参数化查询立在前面这个“大学校友录信息管理系统”就已经超过六成同类作品了。后续再打磨搜索、统计和部署方案就是一个完整度相当高的课程设计甚至可以作为毕业设计底子的项目。