Flask实现企业员工人事工资管理系统前台:从登录到工资查询的完整实践
前阵子帮一家做内部管理系统的小公司搭了一个员工自助门户跑起来之后身边好几个同事都问我说你这套“前台”到底是怎么拆出来的跟后台管理端混在一起会不会乱。借着这个机会我把PythonFlask实现企业员工人事工资管理系统前台的整个过程整理出来。所谓前台在这个系统里指的是员工登录后直接使用的部分——查工资条、维护个人资料、提交请假申请、看通知公告而管理员维护全量员工数据、调整薪资项、审批流程这些东西是另一端的后台功能。如果你正准备用Flask做类似的人事、OA、工资查询项目或者刚学完Flask基础想找一个完整的练手案例这篇内容应该能省你不少走弯路的时间。文中所有代码结构、路由设计、权限控制都是可以直接套用的套路。1. 为什么要把“前台”单独拆出来人事工资系统的需求边界与模块优先级1.1 前台与后台的分界员工自助门户 vs 管理端很多第一次做管理系统的人容易犯一个毛病拿到需求就开一张大表把所有功能塞进同一个路由文件里。到了后期一个页面里的按钮既被员工点又被管理员点逻辑越堆越乱。实际上“人事工资管理系统”这种项目天然应该在入口处就分成两个不同的操作面。前台是员工视角。员工关心的东西很具体我上个月工资条上的基本工资是多少、绩效扣了多少、公积金比例是多少、我的手机号和紧急联系人能不能自己改、我想请假应该找谁批。这些操作有一个共同点都是围绕“当前登录的这个人”展开。所以前台的数据库查询基本都要限定到employee_id页面数量不需要多但每个页面的交互逻辑必须清晰。后台则是管理员视角。管理员要做的动作是增删改查员工档案、设置工资项、批量导入考勤、审批请假单。这里的操作对象是全体数据而且通常有权限分级比如人事专员和财务主管能看到的菜单不一样。把这两块混合在一起最直接的后果是权限判断散落在各个视图函数里。今天这里漏了判断明天那里越权查了别人工资线上出了问题排查成本极高。所以我在这类项目里一直坚持前台单独拆成一个Blueprint后台也单独拆一个Blueprint两个模块之间不共享视图逻辑只共享数据库模型和工具函数。1.2 功能优先级登录、信息展示、查询、申请前台功能看似多真正硬核的就四块身份认证、个人信息展示、工资查询、申请流程。我第一次做这个项目时和客户摸需求摸了三天最后归纳出来的优先级是这样员工登录与身份校验没有这个后面全白搭。个人资料页员工能查看和修改自己的联系方式、紧急联系人、银行卡号。工资查询页按月罗列工资条明细区分基本工资、绩效、补贴、扣款、实发金额。请假申请与记录查询提交申请后进入待审批状态员工能在列表里看到审批进度。如果还有余力再上通知公告、工资条打印、年度汇总这些增值功能。这个顺序不是拍脑袋定的而是根据使用频率来的工资查询每月一次个人信息修改偶尔一次登录和申请却是高频动作。把高频动作做稳定系统的用户体验就有保障了。1.3 前台模块的完整规划以我这个项目为例最终前台的页面结构是这样的登录页/auth/login系统首页/展示当月工资摘要、待办审批提醒、最近公告个人资料页/profile支持编辑联系方式工资列表/salary支持按年份、月份筛选工资明细弹窗/salary/detail/id请假申请页/leave/apply我的请假记录/leave/list后台那部分我在这里不展开聊但建议你在设计时就约定所有前台路由挂在/下面所有后台路由统一加/admin/前缀。这样nginx反向代理、权限中间件、日志切割都更好做。2. 技术选型与页面骨架为什么Flask适合这类系统Jinja2模板怎么组织2.1 选型对比Flask vs Django vs 前后端分离做内部管理系统选型往往不是技术炫技而是平衡开发效率和维护成本。当时我在Flask、Django和前后端分离三个方案里做过对比。方案优势劣势适用场景Flask Jinja2 SQLite轻量、灵活你完全掌控代码结构官方不提供ORM和管理后台需要自己攒中小型内部系统、快速交付Django Admin自带ORM、Admin后台、认证系统学习曲线陡迁移灵活度低中大型项目、标准CRUD多的场景Flask Vue RESTful API前后端解耦交互体验好开发成本翻倍需要维护两套工程页面交互复杂的项目这是一个典型的小型人事系统页面交互最多也就是工资条弹窗和申请表单根本用不上前后端分离。Vue那套方案光是接口文档、跨域处理、token刷新就要多写不少代码。Django的Admin后台虽然方便但我们要做的是员工自助前台管理员用不用Admin并不是核心诉求。所以我最后选了Flask。还有一个很实际的理由Flask的本地调试极其轻快。装好Python后pip install flask就能跑不需要配数据库服务SQLite一个文件搞定。对要交付给客户自己部署的软件来说这简直太重要了。2.2 目录结构与Blueprint划分项目根目录我习惯这样组织payroll/ ├── app.py # 应用入口注册蓝图和扩展 ├── config.py # 配置文件DEBUG、SECRET_KEY、数据库路径 ├── models.py # 数据库模型定义 ├── forms.py # WTForms表单类可选项目小也可以手写校验 ├── requirements.txt ├── database/ │ └── payroll.db # SQLite数据库文件 ├── views/ │ ├── __init__.py │ ├── auth.py # 登录、退出、注册前台 │ ├── profile.py # 个人资料前台 │ ├── salary.py # 工资查询前台 │ ├── leave.py # 请假申请前台 │ └── admin.py # 后台管理可后续扩展 ├── templates/ │ ├── base.html │ ├── auth/ │ │ └── login.html │ ├── index.html │ ├── profile.html │ ├── salary/ │ │ ├── list.html │ │ └── detail.html │ └── leave/ │ ├── apply.html │ └── list.html └── static/ ├── css/ │ └── style.css └── js/ └── main.jsBlueprint的好处是每个模块的URL规则不再挤压在同一个文件里。比如views/salary.py里定义from flask import Blueprint, render_template, request, session from functools import wraps salary_bp Blueprint(salary, __name__) salary_bp.route(/salary) def salary_list(): ...然后在app.py里注册from views.auth import auth_bp from views.salary import salary_bp from views.leave import leave_bp app.register_blueprint(auth_bp) app.register_blueprint(salary_bp) app.register_blueprint(leave_bp)这样每个文件的职责就非常清楚了。以后哪个功能出问题直接翻对应的模块文件不需要在整个项目里CtrlF找路由。2.3 base.html骨架与静态资源配置前台的UI不需要多花哨但必须让员工用得顺手。我用了Bootstrap 5的CDN加一个自己写的style.css没有上AdminLTE这类后台模板原因是前台页面数量少重模板反而拖慢加载速度。templates/base.html是整个前台UI的关键把导航栏、消息提示、内容区块都定义好!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title{% block title %}员工自助平台{% endblock %}/title link relstylesheet hrefhttps://cdn.jsdelivr.net/npm/bootstrap5.3.0/dist/css/bootstrap.min.css link relstylesheet href{{ url_for(static, filenamecss/style.css) }} /head body nav classnavbar navbar-expand-lg navbar-dark bg-primary div classcontainer-fluid a classnavbar-brand href/员工自助平台/a div classd-flex a classnav-link text-white href{{ url_for(salary.salary_list) }}工资查询/a a classnav-link text-white href{{ url_for(leave.apply_leave) }}请假申请/a a classnav-link text-white href{{ url_for(profile.profile) }}个人资料/a a classnav-link text-white href{{ url_for(auth.logout) }}退出/a /div /div /nav div classcontainer mt-3 {% with messages get_flashed_messages(with_categoriestrue) %} {% for category, message in messages %} div classalert alert-{{ category }} alert-dismissible fade show rolealert {{ message }} button typebutton classbtn-close>CREATE TABLE employees ( id INTEGER PRIMARY KEY AUTOINCREMENT, employee_no TEXT UNIQUE NOT NULL, -- 工号 name TEXT NOT NULL, -- 姓名 department TEXT, -- 部门 position TEXT, -- 岗位 phone TEXT, email TEXT, bank_card TEXT, -- 银行卡号 hire_date TEXT, -- 入职日期 base_salary NUMERIC DEFAULT 0 -- 基本工资 );工资表CREATE TABLE salaries ( id INTEGER PRIMARY KEY AUTOINCREMENT, employee_id INTEGER NOT NULL, salary_year INTEGER NOT NULL, salary_month INTEGER NOT NULL, base_salary NUMERIC DEFAULT 0, -- 基本工资 bonus NUMERIC DEFAULT 0, -- 绩效奖金 allowance NUMERIC DEFAULT 0, -- 补贴 deduction NUMERIC DEFAULT 0, -- 扣款合计 actual_salary NUMERIC DEFAULT 0, -- 实发金额 issue_date TEXT, -- 发放日期 remark TEXT, FOREIGN KEY (employee_id) REFERENCES employees(id) );请假表CREATE TABLE leaves ( id INTEGER PRIMARY KEY AUTOINCREMENT, employee_id INTEGER NOT NULL, leave_type TEXT NOT NULL, -- 事假/病假/年假 start_time TEXT NOT NULL, end_time TEXT NOT NULL, reason TEXT, status TEXT DEFAULT pending, -- pending/approved/rejected create_time TEXT DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (employee_id) REFERENCES employees(id) );工资表一定要加上(employee_id, salary_year, salary_month)的联合唯一索引防止同一个人同一个月插入两条工资数据。这种错误的代价是员工看到两份看起来都对但金额不一样的数据信任感瞬间崩塌。3.2 工资查询的SQL和参数校验工资查询页的核心需求是员工选一个年份和月份系统列出该员工这一年的工资记录默认按月份倒序。这里有一个常见的坑直接拼接SQL字符串。比如sql SELECT * FROM salaries WHERE employee_id str(emp_id) AND salary_year year千万别这么干。哪怕是内部系统也必须用参数化查询SQLite跟其他数据库一样存在注入风险。正确写法def get_salary_list(employee_id, yearNone, monthNone): sql SELECT * FROM salaries WHERE employee_id ? params [employee_id] if year: sql AND salary_year ? params.append(year) if month: sql AND salary_month ? params.append(month) sql ORDER BY salary_year DESC, salary_month DESC conn get_db() cur conn.execute(sql, params) rows cur.fetchall() conn.close() return rows这里还有一个非常容易踩的坑年份参数直接从前端拿到后存入数据库是字符串如果数据库表里salary_year是INTEGER你用字符串去比较有时候也能查出结果SQLite的类型亲和性会比较宽松但一旦遇到复杂条件就会出诡异问题。所以路由里一定要做类型转换try: year int(request.args.get(year, )) except ValueError: year None月份参数同理。用户手输了一个“13月”系统不应该查出一堆数据而是应该提示“参数不合法”。我在表单选择器里限制了月份1到12但后端依然要校验因为你永远不知道别人会不会直接用接口调。3.3 分页和模糊搜索工资记录一年最多12条按理说不分页也没问题。但如果系统要支持管理员帮员工代查或者未来接入外包员工数据量会上去。我更推荐从一开始就写好分页逻辑。Flask直接手写分页其实很简单不需要额外库page request.args.get(page, 1, typeint) per_page 10 offset (page - 1) * per_page sql SELECT * FROM salaries WHERE employee_id ? ORDER BY salary_year DESC, salary_month DESC LIMIT ? OFFSET ? rows conn.execute(sql, [emp_id, per_page, offset]).fetchall() count_sql SELECT COUNT(*) FROM salaries WHERE employee_id ? total conn.execute(count_sql, [emp_id]).fetchone()[0] total_pages (total per_page - 1) // per_page注意LIMIT ? OFFSET ?这两个参数也是用问号占位的SQLite对LIMIT/OFFSET参数化的支持没问题。把page传给模板时模板里这样渲染分页按钮{% if page 1 %} a href{{ url_for(salary.salary_list, pagepage-1, yearyear, monthmonth) }}上一页/a {% endif %} span第 {{ page }} / {{ total_pages }} 页/span {% if page total_pages %} a href{{ url_for(salary.salary_list, pagepage1, yearyear, monthmonth) }}下一页/a {% endif %}模糊搜索在这个系统里主要用于员工编号查询和姓名匹配。SQLite的LIKE匹配对中文是友好的但有一个细节如果你用LIKE %${keyword}%里面的%和_是通配符员工名字里真有这两个字符就会出问题。稳妥做法是对关键词里的通配符做转义keyword keyword.replace(\\, \\\\).replace(%, \\%).replace(_, \\_) sql AND name LIKE ? ESCAPE \\ params.append(f%{keyword}%)这个我是在一次客户录入名字带“_”的数据时发现的当时查了半小时才发现是通配符搞的鬼。3.4 模板展示与金额格式化工资页面模板里最容易被忽略的是金额格式化。数据库里存的1234.5直接渲染到页面上就是1234.5如果有个字段是0那更是光秃秃的。员工看的是工资小数点后面必须两位而且最好带上千分位。Jinja2自带格式化方式td{{ row[base_salary] | round(2) | format({:,.2f}) }}/td但更省事的做法是在后端先把金额处理成字符串再传模板for row in rows: row[base_salary] f{row[base_salary]:,.2f}这样模板就干净了不需要关心格式化逻辑。这两种方式都能用我的习惯是统一在后端处理理由很简单模板里塞太多管道符改起来容易漏。3.5 越权查询防护工资系统最怕的是什么员工A登录后能查到员工B的工资。大多数人写查询时会下意识在SQL里带上employee_id但偶尔为了“让员工工号搜索好实现一点”就把条件写宽了。我在这里用了一个很简单的约束前台工资路由里的employee_id永远从session里拿不允许前端传参覆盖。salary_bp.route(/salary) def salary_list(): emp_id session.get(employee_id) if not emp_id: return redirect(url_for(auth.login)) ...任何“按工号查工资”的需求都只放后台前台永远只查自己。这个约定写进代码注释里后续接手的人也不容易改错。4. 登录与会话控制前台权限的前后端协同方案4.1 用户表与密码hash存储人事系统的登录不能像一般个人博客那样随便。密码绝不能明文存数据库这是底线。我用werkzeug自带的密码哈希Flask的依赖里自带这个包。用户表设计CREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, employee_id INTEGER UNIQUE NOT NULL, username TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, role TEXT DEFAULT employee, -- employee/admin create_time TEXT DEFAULT CURRENT_TIMESTAMP );创建用户时生成密码hashfrom werkzeug.security import generate_password_hash hash generate_password_hash(initial123) conn.execute( INSERT INTO users (employee_id, username, password_hash, role) VALUES (?, ?, ?, ?), [emp_id, emp_no, hash, employee] )很多初学教程里会教用MD5加盐我劝你不要这么干。现代机器跑MD5暴力破解太快了直接用werkzeug的pbkdf2或bcrypt方案零成本且安全。4.2 登录路由与session机制登录逻辑说白了就是三步查用户名、验密码、写session。但这里有几个容易忽略的细节auth_bp.route(/login, methods[GET, POST]) def login(): if request.method POST: username request.form.get(username, ).strip() password request.form.get(password, ) user conn.execute( SELECT * FROM users WHERE username ?, [username] ).fetchone() if user and check_password_hash(user[password_hash], password): session.clear() session[user_id] user[id] session[employee_id] user[employee_id] session[role] user[role] session.permanent True return redirect(url_for(index)) else: flash(用户名或密码错误, danger) return render_template(auth/login.html)session.clear()这个动作我以前常写有一次漏了还出过bug——老员工在公共电脑上登录后退出再登录别人的账号旧的session里残留了employee_id导致新账号显示别人的工资。所以登录成功后先清一次session再写入是一个很好的防御习惯。session.permanent True配合app.config[PERMANENT_SESSION_LIFETIME]可以控制登录有效期。人事系统的会话建议设短一点比如8小时app.config[PERMANENT_SESSION_LIFETIME] timedelta(hours8)企业内部系统的电脑经常不关机员工人走了浏览器还开着session短会话能减少误操作风险。4.3 login_required装饰器前台所有页面都需要登录才能访问总不能每个路由里都写一遍判断。我写了一个装饰器from functools import wraps def login_required(view): wraps(view) def wrapped(*args, **kwargs): if not session.get(user_id): flash(请先登录, warning) return redirect(url_for(auth.login, nextrequest.url)) return view(*args, **kwargs) return wrapped使用的时候salary_bp.route(/salary) login_required def salary_list(): ...这里把next参数带上员工登录后能跳回原本想访问的页面。这个小细节很提升体验不然员工每次要访问工资页都得再点一次导航。4.4 权限粒度和数据隔离前台员工之间权限是垂直隔离的每个员工只能操作绑定自己employee_id的数据。这要求在写业务代码时保持一个习惯——所有查询的WHERE条件必须带上当前登录者的employee_id而不是只依赖前端传参。如果页面要区分“员工”和“管理员”角色装饰器也可以扩展def admin_required(view): wraps(view) def wrapped(*args, **kwargs): if session.get(role) ! admin: flash(没有权限访问, danger) return redirect(url_for(index)) return view(*args, **kwargs) return wrapped不过前台模块里管理员很少直接操作主要还是在后台使用。4.5 退出登录与清理session退出登录不能简单跳个空页面要处理两件事清理session 清理浏览器缓存。清理session是为了防别人按后退键看到刚才的页面数据清理浏览器缓存则是避免“后退后表单被重新提交”的尴尬。auth_bp.route(/logout) def logout(): session.clear() return redirect(url_for(auth.login))同时登录页模板里要加上meta http-equivCache-Control contentno-cache, no-store, must-revalidate避免浏览器缓存登录页。这个坑是我在测试时发现的员工退出登录后按浏览器后退居然还能看到工资页面内容原因是浏览器缓存了带表格的页面。加上no-store之后就正常了。5. 前台交互功能的落地请假申请、通知提醒与表单处理5.1 请假申请流程设计正面看请假申请只是插入一条记录但实际要考虑的细节不少。前端表单至少要有请假类型、起止时间、申请事由三个字段。后端在入库前必须校验时间合理性结束时间不能早于开始时间也不能提交历史日期的假期除非是补录。from datetime import datetime start request.form.get(start_time) end request.form.get(end_time) try: start_dt datetime.strptime(start, %Y-%m-%d) end_dt datetime.strptime(end, %Y-%m-%d) except ValueError: flash(时间格式不正确, danger) return redirect(url_for(leave.apply_leave)) if end_dt start_dt: flash(结束时间不能早于开始时间, danger) return redirect(url_for(leave.apply_leave))入库后的状态默认是pending只有管理员在后台审批后才会变成approved或rejected。前端只负责展示状态不负责变更权限。刚开始的时候有人提需求说“员工提交后自己也能撤回”我做了撤回功能后发现管理员那边已经审批通过的申请被员工撤回了审批记录就对不上了。所以流程上我建议撤回只允许在pending状态时操作审批过后就不能动了。5.2 CSRF防护与flash提示表单提交页面必须防CSRF。Flask生态里有Flask-WTF可以一键开启CSRF保护但哪怕不用它自己写一个也非常简单。我在config.py里配置WTF_CSRF_ENABLED True SECRET_KEY your-secret-key-here使用Flask-WTF的全局CSRF保护from flask_wtf import CSRFProtect csrf CSRFProtect(app)这样所有POST请求都会校验CSRF token。模板里在每个表单内加上input typehidden namecsrf_token value{{ csrf_token() }}有一种情况我会手动关掉CSRF如果前台页面用了大量AJAX GET请求不涉及数据变更那不需要CSRF但POST请求一律保留。提交成功的提示用flash展示配合base.html里的消息循环。这是Flask的习惯用法简单有效。不要自己写一个全局变量去弹提醒那在并发场景下会互相覆盖。5.3 用户通知提醒的简单实现人事系统里有个小场景员工提交请假申请后希望看到“申请已提交等待审批”之类的反馈管理员审批后员工希望收到“你的假期已批准”。这个功能不需要上消息队列不需要WebSocket一张通知表就能搞定。CREATE TABLE notifications ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, content TEXT NOT NULL, is_read INTEGER DEFAULT 0, create_time TEXT DEFAULT CURRENT_TIMESTAMP );员工提交申请时插入一条通知给自己conn.execute( INSERT INTO notifications (user_id, content) VALUES (?, ?), [session[user_id], 你的请假申请已提交请等待审批] )管理员审批时在这个项目里管理员在后台操作向对应员工插入通知conn.execute( INSERT INTO notifications (user_id, content) VALUES (?, ?), [emp_user_id, 你的年假申请已通过审批] )首页导航栏上的“未读通知”数字unread conn.execute( SELECT COUNT(*) FROM notifications WHERE user_id ? AND is_read 0, [session[user_id]] ).fetchone()[0]这样一套下来员工每天早上打开首页就能看到审批结果体验比邮件通知还直接。5.4 AJAX还是表单提交轻量项目中的选择很多初学者一上来就想着用AJAX刷局部数据。我的经验是别急着上AJAX先把原生表单提交跑通。原因很简单内部系统的用户数量有限表单提交配合flash消息足够直观而且每个操作都是一次完整的页面刷新逻辑回显也不会因JS错误而中断。只有两类场景我才会用AJAX工资明细弹窗不希望跳转新页面直接在列表页弹出来显示员工姓名搜索框的防抖自动补全减少页面刷新。工资明细弹窗的实现可以在salary/list.html里放一个Bootstrap modal然后让明细按钮触发AJAX请求/salary/detail/id返回HTML片段填充到modal body里function showDetail(url) { fetch(url) .then(response response.text()) .then(html { document.getElementById(detailBody).innerHTML html; new bootstrap.Modal(document.getElementById(detailModal)).show(); }); }后端/salary/detail/id返回局部模板同时校验这个工资记录确实属于当前登录员工salary_bp.route(/salary/detail/int:salary_id) login_required def salary_detail(salary_id): row conn.execute( SELECT * FROM salaries WHERE id ? AND employee_id ?, [salary_id, session[employee_id]] ).fetchone() if not row: abort(404) return render_template(salary/detail.html, rowrow)注意这个AND employee_id ?条件前面的越权防护在这里一样适用。离开这个条件员工很容易通过遍历URL看到别人的工资单。6. 本地部署与常见问题排查从开发到可访问的完整记录6.1 开发环境与生产部署开发时最爽的方式就是flask run --debug改代码自动重载报错页面直接给Traceback。但部署到真实环境时有三件事一定要做关闭debug模式否则用户能看到详细的堆栈信息等于把系统内部结构暴露给了外人换一个生产级WSGI服务器。Flask自带的服务器只适合开发并发能力弱。Linux上我常用gunicorngunicorn -w 4 -b 0.0.0.0:8000 app:appWindows环境我用waitress因为它不需要Unix支持waitress-serve --port8000 app:app配置SECRET_KEY为环境变量不要硬编码在代码里。曾经有个客户把项目代码放在公司的GitLab上SECRET_KEY写死在config.py里等于session签名可被预测理论上可以伪造登录态。虽然内部系统风险低但这个习惯不好。6.2 模板找不到与静态文件404的排查我几乎每天都能看到类似问题“我明明把HTML放在templates里了为什么报TemplateNotFound”排查这个问题的固定步骤是看render_template()里写的模板名是不是相对templates根目录的正确路径。比如文件在templates/salary/list.html你却写render_template(salary/list.html)这个没问题但如果你在views/salary.py里写成render_template(list.html)Flask会在全局templates目录里找找不到就报错。检查Blueprint注册时有没有template_folder参数。如果你单独给Blueprint指定了模板目录那么render_template会在Blueprint自己的模板目录里优先查找别在这里和全局模板打架。静态文件404通常是路径写错。确认static目录是不是放在项目根目录下且app初始化时传了static_folderstatic。使用url_for(static, filenamecss/style.css)生成路径一般不会错。6.3 SQLite并发与WAL模式内部系统的并发不大SQLite完全够用。但如果多个进程同时写数据库比如gunicorn起了4个workerSQLite默认的删除/更新锁可能导致“database is locked”报错。我的处理方法是开启WAL模式conn sqlite3.connect(db_path) conn.execute(PRAGMA journal_modeWAL)WAL模式下读写互不阻塞并发能力会好很多。再配合PRAGMA busy_timeout 5000即使偶发锁冲突也会等5秒而不是立即报错。连接管理方面每个请求新建连接、用完关闭不要尝试在模块级别复用连接。SQLite连接不是线程安全的跨请求复用大概率会遇到数据错乱。我在get_db函数里用g对象缓存连接请求结束时统一关闭from flask import g def get_db(): if db not in g: g.db sqlite3.connect(DB_PATH) g.db.row_factory sqlite3.Row g.db.execute(PRAGMA journal_modeWAL) return g.db app.teardown_appcontext def close_db(exception): db g.pop(db, None) if db is not None: db.close()6.4 索引优化与查询调优工资查询页最常用的SQL是WHERE employee_id ? AND salary_year ? ORDER BY salary_month DESC。这条SQL在数据量几千条时完全没问题但到几万条时就要加索引CREATE INDEX idx_salaries_emp_year_month ON salaries(employee_id, salary_year, salary_month);复合索引的顺序很重要选择性高的字段放前面。employee_id区分度高、salary_year其次、salary_month最后这个顺序和查询条件的匹配顺序一致SQLite能直接命中索引。还有一个小技巧如果工资表的数据只增不改写查询时尽量只取需要的列不要无脑SELECT *。模板里用到的字段就那么几个减少数据量对渲染速度也有帮助。6.5 进一步扩展导出Excel与可视化前台跑稳之后客户大概率会提新需求。最常出现的是“能不能把这个月的工资条导出成Excel发到我邮箱”或“我想看看这半年工资变化曲线”。这两件事在Flask里都不难做导出Excel用openpyxl生成文件后用send_file返回给浏览器下载工资趋势图用ECharts后端只提供一个返回JSON的接口模板里用fetch拉数据画折线图。这两个功能我再单独开文章细讲。这里只想提醒你做扩展时保持前台的定位不变凡是涉及“编辑全量数据”的能力都推给后台。前台就是查询和自助服务把这条线守住系统再大也不会乱。在实际部署这次人事工资系统的过程中我踩得最深的一个坑是密码哈希和旧数据的迁移客户那边原来的Excel表格里有一列“登录密码”当年那位HR老兄把所有密码都做成了相同的明文迁移进新系统时我不得不为每个用户重置密码并邮件通知。吃一堑长一智现在凡是有密码导入需求我都默认做批量重置绝不把明文密码带入生产库。最后分享一个小技巧开发这种内部系统时先用一小段脚本生成50条Mock员工和半年的模拟工资数据把前台页面全部跑通再去接真实数据。页面逻辑和数据计算分离着调你会少掉很多“页面看起来正常但数字对不上”的麻烦。

相关新闻

WOFOST与AquaCrop作物模型对比:机理、参数与应用选择

WOFOST与AquaCrop作物模型对比:机理、参数与应用选择

做作物模型的人,迟早会在某个项目里同时撞见WOFOST和AquaCrop这两个名字。一个来自欧洲,一个出自联合国粮农组织,都是全球应用最广的作物生长模型,但如果你只把它们当作“模拟产量的工具”来用,那从一开始就搞错了方向…

2026/10/11 7:53:04 阅读更多 →
VLX-Seek实时视觉理解模型:架构设计与工程优化实战

VLX-Seek实时视觉理解模型:架构设计与工程优化实战

1. 从标题拆解 VLX-Seek 的真实定位1.1 这个标题到底在说什么第一次看到“VLX-Seek 实时视觉理解模型”这个标题,我脑子里跳出来的第一个判断是:这不是一个单纯的图像分类模型,也不是一个只做目标检测的模型,而是一个把“看”和“…

2026/10/11 7:53:04 阅读更多 →
告别语义漂移:用 BM25 与向量检索打造个人日记的混合召回(Hybrid Search)

告别语义漂移:用 BM25 与向量检索打造个人日记的混合召回(Hybrid Search)

秋雨淅淅沥沥下了一整天,空气里泛着潮湿的泥土香气。我坐在书房里,试图从这几年积攒的上千篇日记与手账随笔中,找出一年前在某家咖啡馆尝过的“桂花乌龙生椰拿铁”。 我在私有 RAG 助手里键入了这几个字,屏幕很快吐出了一堆相关记…

2026/10/11 7:52:03 阅读更多 →

最新新闻

2025年AI IDE实战测评榜:从个人开发到企业部署的完整选型攻略(TaoToken统一API接入篇)

2025年AI IDE实战测评榜:从个人开发到企业部署的完整选型攻略(TaoToken统一API接入篇)

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

2026/10/11 10:24:10 阅读更多 →
付了GPT-5的钱,用的是开源模型?用TaoToken统一Key看清每次调用

付了GPT-5的钱,用的是开源模型?用TaoToken统一Key看清每次调用

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

2026/10/11 10:24:10 阅读更多 →
装完这16个Skills,我的OpenClaw终于会自己查文档了:TaoToken统一Key接入实录

装完这16个Skills,我的OpenClaw终于会自己查文档了:TaoToken统一Key接入实录

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

2026/10/11 10:24:10 阅读更多 →
用 Java 5 分钟写一个 MCP Server:基于开源 MCP Java SDK 接入 TaoToken 统一 Key

用 Java 5 分钟写一个 MCP Server:基于开源 MCP Java SDK 接入 TaoToken 统一 Key

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

2026/10/11 10:24:10 阅读更多 →
免越狱批量控制iPhone:基于Accessibility API的合规自动化方案

免越狱批量控制iPhone:基于Accessibility API的合规自动化方案

1. 为什么“免越狱批量控制iPhone”这件事,过去十年几乎没人真正做成?“不用越狱也能批量控制 iPhone”——这句话放在2024年之前,对绝大多数iOS开发者、自动化测试工程师甚至企业IT管理员来说,都像一句带点讽刺意味的行业黑话。不…

2026/10/11 10:24:10 阅读更多 →
从“impeccable”到工程实践:代码格式化、静态检查与CI流水线

从“impeccable”到工程实践:代码格式化、静态检查与CI流水线

“impeccable”这个词,按读音是 /ɪmˈpɛkəbəl/,意思是“无可挑剔、毫无瑕疵”。我见过不少人把它当成代码注释里的形容词,写“keep the code impeccable”。说实话,第一次看到某公司前端代码仓库的提交规范里,用这…

2026/10/11 10:23:09 阅读更多 →

日新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/11 0:00:27 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/11 0:00:27 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

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

2026/10/10 5:23:50 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

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

2026/10/9 21:32:20 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

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

2026/10/10 10:38:42 阅读更多 →