1. 项目整体设计与思路拆解
1.1 项目到底在解决什么问题
先说结论:这个项目表面上是“活动报名 + 素拓分管理”,实际上解决的是高校第二课堂里两个最让人头疼的痛点——活动信息太散和分数统计太乱。
活动信息太散,指的是很多高校的社团活动、讲座、志愿服务还靠QQ群接龙、Excel表登记、甚至纸质签到表。学生要报名得翻聊天记录,管理员要汇总得一个个手动敲Excel,活动一旦多了,光对名单就能对到怀疑人生。素拓分就更不用说了,很多学校的素拓分是毕业的硬性门槛,但同学们对自己的分数往往一无所知,等到大四要毕业了才发现分数不够,那时候已经来不及补。
这个项目就是把“报名”和“记分”两条线打通:学生在小程序里浏览活动、在线报名、签到完成后系统自动给素拓分;管理员在Web端发布活动、审核参与情况、查看全院学生的素拓分明细。我实际做完以后最大的体会是:它真正省掉的不是报名那一环,而是活动结束后统计签到名单、手动给人加分的那一整晚。
1.2 为什么不直接买现成的系统
有人可能会问,市面上不是有很多第二课堂系统吗?确实有,而且有的功能很强。但现实情况是:学校采购系统要走流程,周期长,预算也紧张;现成系统往往是全校统一部署的,一个学院、一个社团想要临时增加一个活动类型,得层层审批,等你批下来活动都结束了。
所以这个项目最实际的定位是院系级轻量替代方案——不需要采购流程,一个学生会的技术部同学花一两个晚上就能部署起来,数据自己掌控,想加什么功能随时加。我在开发的时候,后端直接用了Flask,原因很简单:它足够轻,一个Python文件就能跑起来,适合这种中小规模的Web服务;同时它又有完善的扩展生态,SQLAlchemy管数据库、Flask-Login管登录,都是现成的,不需要像Django那样一上来就是全家桶,对新手也友好得多。
1.3 技术选型:Flask + 微信小程序
后端的选型经历了Python Flask、Node.js Express、PHP的原生环境三选一,最终选了Flask,理由有三个:Python写起来快,学院里会Python的人多,后续维护不会出现没人接手的情况;Flask的ORM和序列化工具成熟,配合SQLite就能承载几千人的使用量;模板引擎Jinja2渲染管理后台的HTML页面非常顺手,不需要额外搭前端工程。
小程序端选择了微信小程序而不是H5或App,这个决策背后也有考虑。活动报名这个场景里,学生几乎都用微信,“搜一搜小程序”这件事几乎零学习成本;相比H5页面,小程序可以用微信原生能力拿到用户头像昵称、通过wx.login换取openid;同一套代码还可以在Android和iOS上跑,不用各自开发。项目标题里写的是“小程序”,我看热词里还有“uniapp开发微信小程序 vs android/ios/鸿蒙”的讨论,如果未来有跨端需求可以迁移到uni-app,但前期完全不需要,微信渠道已经覆盖了绝大部分目标用户。
1.4 系统模块划分
对接需求和功能拆解之后,我把整个系统分成了五个模块:
| 模块 | 职责 | 用户角色 |
|---|---|---|
| 活动管理 | 活动的发布、编辑、下架、列表展示 | 管理员 |
| 报名中心 | 学生查看活动、提交报名、取消报名 | 学生 |
| 签到核销 | 现场扫码或输入签到码,标记出席 | 管理员/学生 |
| 素拓分管理 | 按规则自动计算并累加分数,支持手动调整 | 管理员 |
| 用户中心 | 登录、身份绑定、个人积分明细 | 学生/管理员 |
五个模块之间通过一个统一的RESTful API接口层连接,小程序端调接口,管理后台也调接口,数据模型是同一套,避免出现“小程序里报名成功了,后台看不到记录”这类因为两套逻辑不同步导致的坑。
2. 核心细节解析与实操要点
2.1 数据库表设计
这是整个项目中最核心的部分。我第一版图省事,只设计了用户表和活动表,报名信息存在一个逗号分隔的字段里,结果活动一多马上出问题:无法查询每个人的报名历史,无法统计某活动的报名人数,取消报名还要改字符串。后来老老实实拆成四张表,问题全部解决。
四张核心表分别是:
用户表(users)
CREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, openid VARCHAR(64) UNIQUE NOT NULL, student_no VARCHAR(20) UNIQUE, name VARCHAR(30), role VARCHAR(10) DEFAULT 'student', total_score FLOAT DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );这里有个设计细节:openid必须唯一,因为微信小程序里openid是用户身份的稳定标识,但openid只在用户第一次通过微信授权登录时写入。student_no是学号,需要在首次登录后引导用户绑定,绑定完成后学号也做唯一约束,防止同一个人注册多个账号。
活动表(activities)
CREATE TABLE activities ( id INTEGER PRIMARY KEY AUTOINCREMENT, title VARCHAR(100) NOT NULL, description TEXT, start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, location VARCHAR(100), capacity INTEGER DEFAULT 100, score FLOAT DEFAULT 0.5, status VARCHAR(10) DEFAULT 'open', created_by INTEGER, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (created_by) REFERENCES users(id) );score字段设计成浮点数而不是整数,是因为很多高校的素拓分是0.5分、0.3分这样的小数,如果一开始用整数,后面所有计算都要做类型转换,非常别扭。status字段是字符串而不是布尔值,因为活动状态有三档:open(报名中)、finished(已结束待归档)、closed(已下架),布尔值表达不了三态。
报名表(enrollments)
CREATE TABLE enrollments ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, activity_id INTEGER NOT NULL, status VARCHAR(10) DEFAULT 'enrolled', signin_time DATETIME, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(user_id, activity_id), FOREIGN KEY (user_id) REFERENCES users(id), FOREIGN KEY (activity_id) REFERENCES activities(id) );UNIQUE(user_id, activity_id)是必须加的,这一步就是为了防止同一个学生对同一个活动重复报名。没有这个约束前,小程序端虽然做了用户点击按钮后置灰,但恶意绕过前端直接调接口,还是能重复创建报名记录。
素拓分记录表(score_records)
CREATE TABLE score_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, activity_id INTEGER NOT NULL, score FLOAT NOT NULL, reason VARCHAR(255), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id), FOREIGN KEY (activity_id) REFERENCES activities(id) );这张表设计的价值在于:素拓分的每一次变动都有据可查。用户在“我的积分”页面看到的不是users.total_score这一个数字,而是score_records的明细列表和汇总。如果学生对分数有疑问,管理员可以去查是哪个活动加的,什么时候加的操作,这样处理申诉就有底气。
2.2 Flask 项目结构
很多新手把Flask项目写成一个app.py里塞几百行路由,数据库也直接用全局变量模拟,这在demo阶段没问题,但一旦要部署上线就寸步难行。我的建议是把这个最小但完整的结构抄下来:
activity_system/ ├── app.py # 应用入口 ├── config.py # 配置文件 ├── models.py # ORM模型 ├── requirements.txt ├── api/ │ ├── __init__.py │ ├── activities.py # 活动相关接口 │ ├── enroll.py # 报名相关接口 │ ├── user.py # 登录/用户信息 │ └── score.py # 素拓分相关接口 ├── admin/ │ ├── __init__.py │ ├── views.py # Web管理后台视图 │ └── templates/ # Jinja2模板 └── static/config.py单独放的好处是环境配置能随时切换,例如开发环境用SQLite,生产环境换成MySQL,只需要改一行DATABASE_URI。小程序端密钥、Session密钥这些敏感信息也放在config里,而不是散落在各个路由函数中。
2.3 小程序端页面设计
小程序端我规划了四个Tab页面:首页(活动列表)、活动详情、我的报名、个人中心。关键页面与数据交互如下:
首页用swiper做轮播图,下面是活动列表,调用/api/activities?status=open接口。活动卡片上要同时展示活动名称、时间地点和还能报名的剩余名额,这个“剩余名额”一定要由后端动态计算,不能只存一个静态值。我在开发时发现,如果前端只读取activities.capacity,会出现用户看到“还剩50个名额”但点击报名后提示“已满”的情况,因为活动表里根本没有记录实时报名数。
活动详情页展示完整描述,报名按钮根据用户状态和活动状态动态显示三种文案:“立即报名”“已报名”“活动已结束”。这里不要相信前端判断,按钮能不能点只是交互体验,真正的判断必须放到后端接口里做。
个人中心页展示用户头像、学号绑定状态、素拓分总数和明细。如果用户尚未绑定学号,引导到绑定页面,这一步建议做成强制——很多学校素拓分是按学号归档的,不绑学号就无法正确关联成绩。
2.4 权限与状态流转
权限模型要从小程序端和Web管理后台两端分别考虑。
小程序端,普通用户登录后只允许操作自己的数据:查看活动、报名、取消报名、查看自己的素拓分明细。管理员要操作的是所有活动记录、给异常用户调整分数。这里不能只在前端把管理员入口隐藏,那只是“看不见”,后端接口必须校验用户角色。
活动状态流转是有固定顺序的:open -> finished -> closed。管理员发布活动时状态默认open,活动实际结束时间到达后,系统会把状态自动改为finished,此时不能再报名,但管理员仍然可以执行签到确认操作;确认完所有签到记录后,管理员手动把状态改为closed,同时系统批量生成素拓分记录。这个流程的好处是把“举办活动”和“给分确认”两个动作分开,避免活动还没办完分数就已经加到学生头上。
3. 实操过程与核心环节实现
3.1 环境准备与Flask后端搭建
在写任何代码之前,先明确依赖版本。我开发时用的是Python 3.10,Flask 2.3,SQLAlchemy 2.0,PyMySQL用于MySQL连接(开发时用SQLite,生产切换MySQL)。requirements.txt建议锁定大版本,但不锁死小版本,避免将来安全补丁无法升级。
pip install flask flask-sqlalchemy flask-cors flask-wtf pymysql安装完后开始搭建应用入口:
# app.py from flask import Flask from flask_sqlalchemy import SQLAlchemy from flask_cors import CORS import config db = SQLAlchemy() def create_app(): app = Flask(__name__) app.config.from_object(config.Config) db.init_app(app) CORS(app, supports_credentials=True) from api.activities import activity_bp from api.enroll import enroll_bp from api.user import user_bp from api.score import score_bp app.register_blueprint(activity_bp, url_prefix='/api/activities') app.register_blueprint(enroll_bp, url_prefix='/api/enroll') app.register_blueprint(user_bp, url_prefix='/api/user') app.register_blueprint(score_bp, url_prefix='/api/score') return app app = create_app() if __name__ == '__main__': app.run(host='0.0.0.0', port=5000)用蓝图(Blueprint)而不是把所有路由堆在app.py里,这一点很关键。路由多了以后,如果不按模块拆分,项目后期找某个接口的实现要去几百行代码里翻,极其痛苦。同时注册蓝图时统一加了url_prefix,后面写具体路由时只写子路径,接口前缀一目了然。
3.2 活动发布与列表接口
活动发布接口属于管理员操作,需要校验role == 'admin'。我用一个简单的装饰器来实现权限校验:
# api/activities.py from flask import Blueprint, request, jsonify from functools import wraps from models import Activity, User, db activity_bp = Blueprint('activities', __name__) def admin_required(f): @wraps(f) def wrapper(*args, **kwargs): # 实际项目中从session或token中取当前用户 openid = request.headers.get('X-Openid') if not openid: return jsonify({'code': 401, 'msg': '未登录'}), 401 user = User.query.filter_by(openid=openid).first() if not user or user.role != 'admin': return jsonify({'code': 403, 'msg': '无权限'}), 403 return f(*args, **kwargs) return wrapper @activity_bp.route('', methods=['POST']) @admin_required def create_activity(): data = request.get_json() if not all([data.get('title'), data.get('start_time'), data.get('end_time')]): return jsonify({'code': 400, 'msg': '缺少必填字段'}), 400 activity = Activity( title=data['title'], description=data.get('description', ''), start_time=datetime.fromisoformat(data['start_time']), end_time=datetime.fromisoformat(data['end_time']), location=data.get('location', ''), capacity=data.get('capacity', 100), score=data.get('score', 0.5), created_by=current_user.id ) db.session.add(activity) db.session.commit() return jsonify({'code': 0, 'data': {'id': activity.id}}), 201这里有个实操细节:时间字段如果前端小程序传过来的是ISO 8601字符串(形如"2025-09-20T14:00:00"),后端可以直接用datetime.fromisoformat解析。小程序端datetime picker返回的格式通常是"2025-09-20 14:00",中间是空格,这样会导致解析报错。我当时的处理方式是在前端先做一次字符串替换,把空格换成T,或者在后端用datetime.strptime(data['start_time'], '%Y-%m-%d %H:%M')来解析,两种方案选一个,前后端约定好就行。
活动列表接口要考虑分页,因为一个校园里一个学期的活动可能有上百个,一次性返回全部数据网络包很大,小程序端渲染也会卡顿。
@activity_bp.route('', methods=['GET']) def get_activities(): page = request.args.get('page', 1, type=int) per_page = request.args.get('per_page', 10, type=int) status = request.args.get('status', 'open') query = Activity.query.filter_by(status=status) activities = query.order_by(Activity.start_time.desc()).paginate( page=page, per_page=per_page, error_out=False ) result = { 'items': [format_activity(a) for a in activities.items], 'total': activities.total, 'page': page, 'pages': activities.pages } return jsonify({'code': 0, 'data': result})error_out=False很重要,它保证当访问的页码超出范围时不会抛出404异常,而是返回一个空列表,小程序端处理空数据时只是展示“暂无更多活动”,不会白屏。
3.3 报名与素拓分自动计算
报名的核心逻辑是事务。记住一句话:涉及写入操作且需要保持数据一致的,一律开事务。报名和扣减名额是两步操作,如果不用事务,就有可能在极端情况下出现报名成功但名额没减的脏数据。
from sqlalchemy.exc import IntegrityError @enroll_bp.route('', methods=['POST']) def enroll(): data = request.get_json() activity_id = data.get('activity_id') user = get_current_user() activity = db.session.execute( db.select(Activity).where(Activity.id == activity_id) ).scalar() if not activity: return jsonify({'code': 404, 'msg': '活动不存在'}), 404 if activity.status != 'open': return jsonify({'code': 400, 'msg': '活动不在报名期'}), 400 enrolled_count = Enrollment.query.filter_by( activity_id=activity_id, status='enrolled' ).count() # 这里用 count 先判断,实际上存在并发窗口,需要数据库唯一约束兜底 if enrolled_count >= activity.capacity: return jsonify({'code': 400, 'msg': '名额已满'}), 400 try: enroll_record = Enrollment(user_id=user.id, activity_id=activity_id) db.session.add(enroll_record) db.session.commit() except IntegrityError: db.session.rollback() return jsonify({'code': 400, 'msg': '您已报名该活动'}), 400 return jsonify({'code': 0, 'msg': '报名成功'}), 201素拓分的自动计算逻辑放在活动归档时执行。活动管理员点击“归档”按钮,后端做这几件事:
- 检查活动状态,
open状态不能归档,必须先改成finished。 - 查出所有
status='enrolled'的报名记录。 - 对每一条记录,检查
score_records表中是否已经存在activity_id对应的加分记录(防止重复加分)。 - 如果不存在,插入一条
score_records记录,同时更新users.total_score += activity.score。 - 全部处理成功后,把活动状态改成
closed。
这个流程比“每当活动结束就自动加分”更安全,因为自动执行往往没时间检查异常,而手动归档给了管理员一个人工审核确认的时机。
3.4 小程序端调用后端接口的核心代码
小程序端发送请求前必须在app.js的globalData里配置后端地址。开发时可以填局域网IP加端口,但在真机预览时要保证手机和电脑在同一WiFi下。
// 小程序请求封装 /utils/request.js const BASE_URL = 'http://你的服务器IP:5000' function request(path, method = 'GET', data = {}) { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + path, method: method, data: data, header: { 'content-type': 'application/json', 'X-Openid': wx.getStorageSync('openid') || '' }, success: (res) => { if (res.data.code === 0) { resolve(res.data.data) } else { if (res.data.code === 401) { // 未登录,跳转到登录页 wx.navigateTo({ url: '/pages/login/login' }) } else { wx.showToast({ title: res.data.msg, icon: 'none' }) } reject(res.data) } }, fail: (err) => reject(err) }) }) }小程序端的登录逻辑要注意顺序:先wx.login()拿到临时code,把这个code发送到后端/api/user/login,后端去微信服务器换openid,然后再返回自定义登录态。这里有个新手经常踩的坑:以为wx.getUserProfile返回的openid就是用户唯一标识,直接存前端,其实是错的。getUserProfile返回的只有昵称和头像,openid根本不会暴露给前端,只有后端通过code2Session才能拿到。
3.5 管理后台的Jinja2页面
管理后台我直接用Flask的Jinja2模板实现,没有再引入Vue或React。因为后台使用人群是学生干部,功能以表格操作为主,模板直接渲染比前端框架更轻更直观。
表格页核心是一个循环:
<table> <thead> <tr> <th>活动标题</th> <th>报名人数</th> <th>已签到</th> <th>素拓分</th> <th>状态</th> <th>操作</th> </tr> </thead> <tbody> {% for item in activities %} <tr> <td>{{ item.title }}</td> <td>{{ item.enrollment_count }}</td> <td>{{ item.signin_count }}</td> <td>{{ item.score }}</td> <td>{{ item.status_label }}</td> <td> <a href="/admin/activity/{{ item.id }}/archive">归档并加分</a> <a href="/admin/activity/{{ item.id }}/export">导出名单</a> </td> </tr> {% endfor %} </tbody> </table>导出名单功能用csv标准库实现,直接生成CSV文件并让浏览器下载,方便存档。班主任问你要活动参与名单时,这个功能能救命。
4. 常见问题与排查技巧实录
4.1 小程序正版域名和HTTPS的问题
这个小程序方向的卡脖子问题必须提前说。微信小程序正式上线要求所有请求域名必须是HTTPS的备案域名,并且在“小程序后台-开发管理-服务器域名”里配置白名单。如果你只是做校内测试,可以在小程序开发工具的“详情-本地设置”里勾选“不校验合法域名”。但如果要发布上线,就必须购买服务器、绑定已备案域名并配置SSL证书。
如果你暂时没有域名,开发阶段的替代方案是:使用内网穿透工具将本机Flask服务映射到一个临时公网HTTPS地址,这样真机预览时不用勾选“不校验合法域名”就能请求后端。但这类工具免费版不稳定,而且映射出去的IP是动态的,每次变了都要重新配置小程序合法域名,所以只建议用于临时演示。
4.2 OpenID会话管理问题
第一次写这套系统时,我把openid直接放在小程序的storage里,每次请求带上,后端凭这个字段识别用户。后来发现隐患:storage是明文存储的,如果用户手机丢了或数据被读取,相当于账号被盗。正规做法是后端自己维护一套登录态:
- 用户登录成功后,后端生成一个
session_token(可以用UUID字符串),在服务端存一份映射表,键是token,值是user_id和过期时间。 - 小程序端只保存
session_token。 - 每次请求时header里带上
Authorization: Bearer <session_token>。 - 后端用装饰器解析token并获取当前用户。
这样openid永远不会出现在前端存储里,安全性提升一个等级。我在热词里看到“微信小程序抓包”和“反编译小程序”这类词,更要提醒一句:永远不要在前端代码里暴露敏感密钥或直接存储openid,抓包工具可以轻松看到HTTP请求明文,如果请求里带着openid,别人改个openid就能冒充他人身份,这个漏洞极其危险。
4.3 CORS跨域请求与Cookie携带问题
Flask后端一般跑在5000端口,管理后台前端可能跑在8080端口,如果不处理CORS(跨域资源共享),浏览器会拦截请求。我之前的做法是直接安装flask-cors并全部放行:
CORS(app, supports_credentials=True)但要注意:如果启用了supports_credentials=True,前端的withCredentials也必须设置为true,否则Cookie带不上。如果你用Session管理登录态,这一步很容易被忽略,表现为“接口返回401未登录”但登录接口明明调用成功了,排查到最后发现就是Cookie没有跨域携带。
4.4 素拓分精度与录入错误
浮点数累加会出现精度问题,比如0.1 + 0.2得到0.30000000000000004,在数据库里存久了会出现分数莫名多了一丁点的怪事。素拓分的值通常只有一位小数,所以最稳妥的做法是:数据库存浮点、Python计算时分乘以10转成整数再相加,显示时再除以10,或者在ORM模型里用Float存储、展示时用round(score, 1)。我最终选择了后者,因为素拓分的数值本身不会超过两位数,round方法的精度已经满足需求,而且加代码的可读性比用Decimal更高。但是,如果用Decimal运算,一定要至少保留两位小数。
4.5 并发报名的“超卖”问题
活动名额为100个,某个瞬间同时有120个人发起报名请求,如果只靠先count()再插入,可能最后报名记录超过名额限制。解决这个问题的关键不在“count检查”,而在于数据库的约束和事务隔离级别。
最直接的方案是限制数据库插入条件——在enrollments表建立UNIQUE(user_id, activity_id)约束,这让同一用户重复报名必然失败。对于容量控制,我采用的方案是:报名接口里先用SELECT ... FOR UPDATE给活动记录加锁,再统计已报名人数,判断是否超限,然后插入报名记录,最后提交事务解锁。
from sqlalchemy import text # 查询时加行级锁,防止并发超卖 activity = db.session.execute( text('SELECT * FROM activities WHERE id = :id FOR UPDATE'), {'id': activity_id} ).first()这样多个并发请求同时到达时,只有一个能拿到锁,其余请求会等待这个事务提交后再执行,从而保证人数不超过容量。这个细节在小规模使用中未必会暴露问题,但一旦公布到全院使用,并发量上来时就会遇到。
4.6 小程序“开发版已过期”问题
热词里有一条很常见:“开发版小程序已过期,请在开发者工具重新扫码”。这指的是开发版和体验版二维码的有效期只有24小时或30分钟,过期后需要重新在微信开发者工具里点击“预览”按钮生成新二维码。这不是项目代码的问题,但很多第一次做小程序的同学在这个坑上卡了很久,以为自己代码崩了。解决办法就是重新预览扫码,如果是开发调试阶段,直接用“真机调试”功能,它的时效比预览更长,而且能直接看到控制台日志。
5. 一些实际验证过的经验与操作技巧
这一节写几条代码之外、但在真实场景里帮了大忙的经验。
第一,活动时间段重叠检测一定要做。我第一版发布活动时没做时间重叠校验,结果出现了同一间教室同一时间被两个部门申请的情况,等到活动当天才发现冲突,场面非常尴尬。后来在创建活动接口里加了一个查询:检查该场地在目标时间段是否已有被占用状态的活动记录,如果有则直接拒绝发布。这个逻辑虽然简单,但它的价值在于把冲突前置到发布环节,而不是事后补救。
第二,后端给前端返回数据时,不要直接返回ORM模型对象。新手经常会写jsonify(activity.__dict__),但SQLAlchemy对象里往往包含一些不应暴露的字段或循环引用,直接序列化会报错。正确做法是定义一个format_activity函数,把需要的字段手动组装成一个字典:
def format_activity(activity): return { 'id': activity.id, 'title': activity.title, 'start_time': activity.start_time.strftime('%Y-%m-%d %H:%M'), 'location': activity.location, 'capacity': activity.capacity, 'score': activity.score, 'status': activity.status, 'enrolled_count': activity.enrollments.filter_by(status='enrolled').count() }这样数据格式可控,接口文档也能写清楚,小程序端拿到数据后不用再猜字段含义。
第三,管理后台加一个原始数据导出功能,比界面做得多好看都管用。实际上,老师最关心的是能不能拿到一份Excel或CSV名单,用来上报或存档。我在后台加了“导出报名名单”和“导出素拓分明细”两个按钮,生成CSV文件直接下载。上线后,同学问“我参加上周那个活动加分了吗”时,直接查系统;老师问“这个活动哪些人参加了”时,导出一份CSV两分钟搞定。这个功能可以说帮我在项目管理上省了一半的沟通精力。
第四,活动结束后,设置一个“归档缓冲期”再真正加分。我最初的设计是管理员点击归档后立即加分,结果有一次活动里有人填了错误学号,管理员还没发现就已经把分数加进去了,改分数还得手动删记录,很麻烦。后来改为“点击归档后先把状态改为‘待归档’,进入确认名单页”,管理员可以在此页面剔除异常记录,点击“确认加分”后才真正执行批量加分。多了一步人工确认,但出错概率大大降低。管理后台的用户是学生干部,他们的操作往往比较快,多一步确认反而能减少返工。
这个项目做到后面,你会发现技术栈本身并没有多高深——Python Flask写接口、小程序做前端、SQLite存储数据,每一个环节单独拎出来都有大量文档和教程,难的是把校园场景里的业务逻辑梳理清楚。活动的生命周期、素拓分的规则、角色权限边界、报名的并发控制,这些才是这个系统真正“含金量”所在。如果你也要做类似的项目,我的建议是:编码前先花一天把你的业务场景拆透,表格设计好,角色明确好,再来写代码。这一步做得越扎实,后面的返工就越少。