ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于SSM和Flask的工作日志系统开发实战与调试指南

基于SSM和Flask的工作日志系统开发实战与调试指南 先说我看到这个标题的第一反应一个员工工作日志系统技术栈却横跨了Java的SSM和Python的Flask这种组合在学校做课设的学生里不算多见。但为啥我说它挺有代表性因为工作日志这东西本质上就是一家公司“过程管理”的最小闭环——它有用户、有角色、有业务流程、有审批、有统计做一遍下来SSM那套三层架构、拦截器、动态SQL、事务控制基本全练到了挂一个Flask上去做统计和可视化接口又顺手把跨语言协作的坑踩了一遍。这篇文章我就以这套系统为样本把这套组合拳怎么打、里面哪些点最容易翻车、以及调试文档到底该怎么写一次说透。1. 先把需求盘清楚工作日志系统到底在解决什么问题1.1 一个“被低估”的业务场景很多人第一眼看到“工作日志”四个字觉得太平凡了不就是员工每天写几百字汇报吗但你真去一家有几十个人的公司里待过就明白这个功能是企业管理的刚需。领导关心的是“人到底在不在干活”“活推进成什么样了”——日报、周报、月报就是回答这两个问题最直接的载体员工需要的则是一个能沉淀工作记录、被看到、被反馈的入口再往上人事和老板还会想看到“这个月哪个部门提交率高”“哪个项目占了多少人力”这类的统计结果。把这个场景抽象成系统需求就会发现它一点都不简单它需要用户管理、部门管理、角色权限、日志的增删改查、提交与审批状态流转、批阅评论、统计报表、消息提醒。这是典型的小型办公自动化OA系统的核心子集。项目标题里堆了“办公自动化、协同办公、工作流程、团队管理”这些词本质上是在描述同一件事——用系统把“员工—领导—管理动作”之间这条信息链路数字化。拿来做毕业设计也好拿来练手也好这类系统的最大优点是麻雀虽小五脏俱全。它不会像电商系统那样有一堆并发、秒杀、分布式的问题让人望而生畏但该有的工程结构、数据库设计、权限校验、前后端交互一样不落。做完一遍你对“一个Web业务系统是怎么跑起来的”会有一个完整的概念。1.2 需求拆解三类用户与核心用例在动代码之前先把角色定清楚。典型的员工日志系统至少要拆出三类角色他们的关注点完全不同普通员工每天/每周填写日志可以保存草稿最终提交提交后可以查看领导的批阅意见被退回时可以修改重新提交。部门经理查看本部门员工的日志列表对下属日志进行批阅通过/退回查看部门的提交统计。系统管理员管理员工账号、部门信息可以查看全公司日志能做数据导出通常还负责重置密码这类后台操作。这个角色划分直接决定了后面所有的数据库设计、Session管理、拦截器放行策略。这里最容易犯的错是“只做一个登录登进去以后所有人权限一样”——后面你会发现一旦需要“经理只能看本部门”权限判断会牵扯到接口、页面、按钮多处提前把角色模型画出来能省掉至少一半返工。1.3 用例落表功能清单与优先级结合上述用户把功能清单和优先级排成下面这样做项目的时候按这个顺序推进就不会乱。优先级功能模块用户核心动作P0登录认证与会话管理所有用户登录、登出、Session校验P0日志填报与状态流转员工新增、编辑、草稿、提交P0日志审批经理/管理员查看待审日志、通过、退回、填写批语P1部门与员工管理管理员CRUD、分配角色、维护部门P1日志查看与检索员工/经理/管理员按人、按日期、按类型检索P2数据统计与图表展示经理/管理员提交率、日志数量趋势、部门对比P2批语通知与消息中心员工查看被退回原因、收到新批语提醒P0先做P2后做这条线不能乱。很多人一开始就跑去研究ECharts画多高级的图结果把日志的增删改查都写得磕磕绊绊这是典型的次序错乱。2. 技术选型深挖为什么会是SSM Flask这种“混搭”2.1 SSM为什么还是中坚力量标题里同时出现了SSM和Flask很多人第一反应是“这不就是两个项目硬拼吗”其实不然。SSMSpring SpringMVC MyBatis在Java Web领域依然是极其成熟的主力框架组尤其适合以传统分层架构为主的中小团队。Spring负责依赖注入和事务管理SpringMVC负责请求调度和参数绑定MyBatis负责SQL与对象的映射三者配合结构清晰得有点像搭积木。为什么不去追逐Spring Boot Cloud那套对这个体量的系统来说复杂的东西反而会成为负担。SSM的优势是“裸奔可控”——配置规则是显式的请求从Controller到Service到Mapper的每一环你都能看透。对于一个需要写论文也就是交付说明、需要讲清楚设计思路的技术项目来说SSM是一套非常适合“解剖教学”的骨架底层原理都在配置和代码里摆着而不是被框架自动装配和外化配置给吞掉了。这也是大量高校课设、毕设仍然延续SSM的原因之一。2.2 Flask的角色定位不抢主业务专攻辅助那Flask在这里面干吗我见过不少同学把Flask和SSM做成一套系统里互相调用的“双服务”——SSM管登录、菜单、日志CRUDFlask只负责统计数据接口和返回图表需要的数据。这个定位非常合理。原因很简单Python生态在处理数据分析、聚合统计、生成图表JSON这些事情上比Java写起来顺手得多。一个部门提交率统计在Java里要写一长串业务代码加MyBatis动态SQL在Flask里可能几十行就能把数据从MySQL里查出来拼好JSON返回。Flask又是个极轻的框架没有ORM强绑定、没有模块约束适合做“微服务中的微服务”——你甚至可以把它理解成SSM旁边一个独立的计算引擎被Java通过HTTP调用来完成统计职能。所以正确的心态是不要觉得Flask是来抢SSM饭碗的它是来做SSM不擅长、也不愿意做的那部分辅助工作的。一个管业务事务一个管数据计算分工明确。2.3 架构形态从单体到“双引擎”的协作方式从实际交付的架构来看这套系统的典型形态是这样的浏览器(用户页面) ↓ 请求 Java SSM服务端口8080 ├── Controller接收请求 ├── Service处理业务事务 ├── Mapper/MyBatis 访问MySQL ↓ 需要统计数据时通过HTTP调用 Flask服务端口5000 ├── /api/stats/xxx 接口 ├── pymysql连接同一个MySQL └── 返回聚合后的JSON数据这里有一个特别重要的设计细节不要让浏览器直接去请求Flask的接口。你想象一下前端页面在8080端口Flask服务在5000端口浏览器直接跨端口发Ajax请求瞬间就会撞上CORS跨域问题。虽然可以通过flask-cors解决但在生产上其实不推荐。更稳的做法是SSM作为统一对外入口Java后端通过RestTemplate或HttpClient向Flask的5000端口发起一个服务端到服务端的内部请求拿到JSON后再原样返回给前端。这样做有三个好处前端永远只跟8080一个源打交道不会出现跨域。Flask服务对浏览器不可见网络边界更干净也更安全。以后如果要把Flask换成别的服务前端代码一行都不用改。3. 数据库设计与核心模块从建表到权限控制3.1 数据模型设计日志、员工、部门、角色的表关系数据库建设我建议遵循“先建主数据再建业务表”的顺序。员工、部门、角色属于主数据日志和批语属于业务数据。下面这套是我觉得最贴合需求、也适合写进论文的表结构部门表departmentCREATE TABLE department ( id INT PRIMARY KEY AUTO_INCREMENT, dept_name VARCHAR(50) NOT NULL UNIQUE, manager_id INT COMMENT 部门经理的用户ID, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );员工表sys_userCREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL COMMENT MD5/BCrypt加密存储, real_name VARCHAR(50) NOT NULL, dept_id INT, role_id INT COMMENT 关联角色表, status TINYINT DEFAULT 1 COMMENT 1启用 0禁用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );角色表roleCREATE TABLE role ( id INT PRIMARY KEY AUTO_INCREMENT, role_name VARCHAR(50) NOT NULL, role_code VARCHAR(20) NOT NULL UNIQUE COMMENT ADMIN/MANAGER/STAFF );工作日志表work_logCREATE TABLE work_log ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, log_type TINYINT COMMENT 1日报 2周报 3月报, log_date DATE COMMENT 日志所属日期, content TEXT COMMENT 今日完成内容, next_plan TEXT COMMENT 明日/下周计划, status TINYINT DEFAULT 0 COMMENT 0草稿 1待审核 2已通过 3已退回, reviewer_id INT, review_comment VARCHAR(255), review_time DATETIME, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_user_date (user_id, log_type, log_date) );第 一条业务经验是UNIQUE KEY uk_user_date非常关键。一个员工一天最多只能有一条日报这条唯一索引能从数据库层面挡住重复提交不然业务代码里写十遍判重都可能漏。批阅记录表log_reviewCREATE TABLE log_review ( id INT PRIMARY KEY AUTO_INCREMENT, log_id INT NOT NULL, reviewer_id INT NOT NULL, review_result TINYINT COMMENT 1通过 2退回, comment_content VARCHAR(500), review_time DATETIME DEFAULT CURRENT_TIMESTAMP );把批阅历史单独拆一张表而不是只在work_log上放一个状态字段好处是以后可以追溯“退回了几次”“为什么退回”还可以做“被退回率”这类质量统计。另外强调一点密码不能明文存。即便只是课设项目也建议至少用MD5加盐或者BCrypt。这已经是行业底线不是加分项了。3.2 登录会话与基于角色的鉴权实现登录模块看起来简单但它是整个安全模型的地基。我常用的实现方式是登录成功后将用户对象、角色编码写入Session同时提供工具方法从Session取值在这个基础上用拦截器HandlerInterceptor统一处理“未登录”和“无权限”两种情况。先看登录Controller的核心逻辑Controller RequestMapping(/user) public class UserController { Autowired private IUserService userService; PostMapping(/login) public String login(String username, String password, HttpSession session, Model model) { User user userService.login(username, password); if (user null) { model.addAttribute(error, 用户名或密码错误); return login; } if (user.getStatus() 0) { model.addAttribute(error, 账号已被禁用请联系管理员); return login; } // 把用户对象和角色编码存进session session.setAttribute(loginUser, user); session.setAttribute(roleCode, user.getRole().getRoleCode()); return redirect:/index; } }真正容易出错的是拦截器。建议拆成两个一个LoginInterceptor管会话校验一个AuthInterceptor管角色权限校验。前者检查Session里有没有loginUser没有就重定向到login页面并带上“请先登录”后者根据URL或请求参数判断当前操作要求什么角色再跟Session里的roleCode比对。public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); if (session.getAttribute(loginUser) null) { // 判断是否Ajax请求 String xrw request.getHeader(X-Requested-With); if (XMLHttpRequest.equals(xrw)) { response.setContentType(application/json;charsetutf-8); response.getWriter().write({\code\:401,\msg\:\登录已失效请重新登录\}); return false; } response.sendRedirect(request.getContextPath() /login); return false; } return true; } }在spring-mvc.xml里配置拦截路径时要注意三种例外登录接口、静态资源js/css/image、注册接口如果有。否则静态资源全被拦死页面会丑到你怀疑人生。3.3 日志填报主流程从前端表单到MyBatis落库日志填报是整个系统的核心动作流程上要保证操作闭环新建日志 → 选择类型和日期 → 填写内容 → 保存草稿 → 提交审核 → 经理批阅 → 员工查看结果。如果被退回员工看到批语后可以再编辑并二次提交此时状态重新变为待审核。Service层处理提交时建议把“保存”和“提交”两个动作分开。保存只是写草稿状态是0提交则要同时校验内容非空、校验是否重复靠表唯一索引兜底、更新状态为1。这里有一段MyBatis动态SQL值得参考它解决了多条件组合查询的核心场景select idselectLogList resultTypeMap SELECT l.id, u.real_name, d.dept_name, l.log_date, l.log_type, l.status, l.content, l.review_comment FROM work_log l LEFT JOIN sys_user u ON l.user_id u.id LEFT JOIN department d ON u.dept_id d.id where if testdeptId ! null and deptId ! AND u.dept_id #{deptId} /if if testlogType ! null and logType ! AND l.log_type #{logType} /if if teststartDate ! null and startDate ! AND l.log_date #{startDate} /if if testendDate ! null and endDate ! AND l.log_date lt; #{endDate} /if if teststatus ! null AND l.status #{status} /if if testuserId ! null AND l.user_id #{userId} /if /where ORDER BY l.log_date DESC, l.create_time DESC /select这个where标签配合if是做条件查询最经典的方式直接把“经理只看本部门”和“管理员看所有”两种场景统一到了一套SQL里。技能扎实的同学还可以在这里声明where标签会自动去掉第一个多余的AND/OR这是很多面试官爱问的点。4. Flask辅助模块实战数据统计与可视化接口4.1 Flask服务搭建与配置要点Flask这块我按“轻量实用”来搭建它只需要四个文件app.py、config.py、stats_util.py以及一个依赖清单requirements.txt。数据库连接我直接用的是PyMySQL不引入SQLAlchemy。原因很简单Flask在这里只是做聚合统计又不需要建表没必要给项目挂上一个完整的ORM那样反而会让新手看得一头雾水。# app.py from flask import Flask, jsonify, request from flask_cors import CORS import pymysql app Flask(__name__) CORS(app) # 建议保留防止开发阶段前端直接访问时报跨域错 def get_db(): conn pymysql.connect( host127.0.0.1, userroot, password123456, databasework_log_db, charsetutf8mb4, cursorclasspymysql.cursors.DictCursor ) return conn这里有一个新手最容易踩的坑pymysql连接MySQL如果漏了charsetutf8mb4一旦日志内容里有中文或者表情符号聚合查询返回的JSON到前端就是乱码。还有cursorclass必须设成DictCursor否则默认返回元组你还要自己下标取值非常痛苦。4.2 统计接口设计与SQL聚合示例统计接口的设计思路是“为前端准备好字典数组而不是让前端自己拼数据”。举个最常做的接口例子按部门统计近30天的日志提交率。后端思路先查部门列表再查每个部门应提交的天数、实际提交的总数然后计算提交率。为了讲解方便这里用两个SQL就够。app.route(/api/stats/dept_submit_rate, methods[GET]) def dept_submit_rate(): days request.args.get(days, 30, typeint) conn get_db() cur conn.cursor() # 查询各部门员工数 cur.execute( SELECT d.id, d.dept_name, COUNT(u.id) AS emp_count FROM department d LEFT JOIN sys_user u ON d.id u.dept_id WHERE u.status 1 GROUP BY d.id, d.dept_name ) departments cur.fetchall() result [] for dept in departments: dept_id dept[id] cur.execute( SELECT COUNT(DISTINCT l.user_id, l.log_date) AS submit_cnt FROM work_log l JOIN sys_user u ON l.user_id u.id WHERE u.dept_id %s AND l.log_type 1 AND l.log_date DATE_SUB(CURDATE(), INTERVAL %s DAY) AND l.status IN (2, 3) , (dept_id, days)) row cur.fetchone() expect_cnt dept[emp_count] * days rate round(row[submit_cnt] / expect_cnt * 100, 1) if expect_cnt 0 else 0 result.append({ dept_name: dept[dept_name], submit_rate: rate, total: row[submit_cnt] }) cur.close() conn.close() return jsonify({code: 0, data: result})注意SQL里用了%(参数)s或者说%s以后端传参的方式这是参数化查询的标准姿势。我从一开始就强调Python拼接SQL就是给自己埋雷字符串拼接一旦混入用户输入等于把SQL注入漏洞送到别人嘴边。参数化以后这问题就没了。4.3 与Java后端的联动RestTemplate反向代理前面说了前端不要直接访问Flask。在Java后端定义一个RemoteStatsClient把Flask接口包一层这样对调用方来说跟调用本地Service没有区别。Component public class FlaskStatsClient { private RestTemplate restTemplate new RestTemplate(); public String getDepartSubmitRate(int days) { String url http://127.0.0.1:5000/api/stats/dept_submit_rate?days days; ResponseEntityString resp restTemplate.getForEntity(url, String.class); return resp.getBody(); } }然后Controller里调用这个Client把JSON原样返回给前端RestController RequestMapping(/api/stats) public class StatsController { Autowired private FlaskStatsClient flaskStatsClient; GetMapping(/deptSubmitRate) public String deptSubmitRate(RequestParam(defaultValue 30) int days) { return flaskStatsClient.getDepartSubmitRate(days); } }这个模式我把每一层的目的都用一句话写清楚Controller只管接收前端参数不做任何业务计算。FlaskStatsClient负责跨语言通信拼URL、发HTTP请求、拿结果。Flask接口负责查库聚合、返回Jsonify结果。前端只用ECharts等库消费JSON展示图表。4.4 图表展示与数据联动Flask返回的数据交给前端ECharts时尽量不要让前端去处理null或undefined。在Flask端把文案、颜色等展示字段都组装好前端直接setOption省事也少报错。比如这样rate_data.append({ value: rate, name: dept[dept_name], itemStyle: {color: #5470c6 if rate 60 else #ee6666} })实际效果是阈值以上蓝色、以下红色。这不是什么高深逻辑但对“领导一眼看出哪个部门提交率低”非常有帮助。5. 调试文档的价值别把“排障记录”当废纸5.1 一份合格调试文档应该包含的内容项目标题里带着“调试文档”四个字很多人其实是把它当形式主义来看的。但到了最后答辩或公司内部交接时调试文档才是救命的那一叠纸。我建议按下面四个部分来写每个部分是递进关系环境清单JDK版本、Tomcat版本、MySQL版本5.7和8.0差异巨大、Maven版本、Python版本每个都标注“已验证可用版本”。部署步骤从克隆源码到启动成功的顺序每一步配截图尤其是数据库导入脚本和IDEA导入Maven项目的步骤。常见问题对照表把下面的“踩坑实录”直接整理成表格每个问题写明“现象—原因—解决方案”。测试用例至少列出10个冒烟测试用例比如“员工登录→填写日报→提交→经理登录→查看待审→批阅通过→员工看到已通过状态”这个主链路必须完整跑通。5.2 我踩过的5个经典坑与排查思路接下来把这套系统从零跑通时最容易摔跤的坑挨个过一遍。这些是我自己帮人做调试时反复遇到的每个都对应真实会话。坑一启动后访问登录页报HTTP 404Tomcat控制台却不报错排查思路先看spring-mvc.xml里的视图解析器配置。如果是InternalResourceViewResolver前缀/WEB-INF/jsp/那么JSP必须放在src/main/webapp/WEB-INF/jsp/下。很多人把JSP放错目录比如放到了src/main/resources里编译后根本不会被复制到WEB-INF下自然404。另外检查一下web.xml是否配置了DispatcherServlet的url-pattern//url-pattern如果配成*.do所有非.do请求都会走默认Servlet也会引发404。坑二登录查询报ClassNotFoundException: com.mysql.jdbc.Driver原因几乎可以断定是MySQL驱动版本与连接串不匹配。如果用的是MySQL 8.0驱动类应该是com.mysql.cj.jdbc.DriverURL需要加serverTimezoneAsia/Shanghai如果用的是MySQL 5.7那可以用com.mysql.jdbc.Driver但强烈建议统一升级到MySQL 8.0并写成新版驱动类免得到处找老jar包。对应的jdbc.properties参考jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://127.0.0.1:3306/work_log_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse jdbc.usernameroot jdbc.password123456坑三中文乱码登录后页面所有中文全是问号分三个层面排查第一web.xml里有没有CharacterEncodingFilter强制UTF-8第二MySQL表结构默认字符集是不是utf8mb4第三Tomcat的server.xml里Connector是否设置了URIEncodingUTF-8。通常第一处漏了最致命因为GET请求和POST请求都会经过过滤器。建议三处全部统一不要只改其中一处。坑四MyBatis的Mapper接口找不到Spring启动直接报Invalid bound statement (not found)这是Maven配置文件过滤问题。默认Maven在打包时只拷贝src/main/resources下的资源如果mapper/*.xml放在了src/main/java里编译后classes目录下根本没有XML。解决办法有两个一是把Mapper XML统一放到src/main/resources/mapper/二是在pom.xml里显式配置资源resources resource directorysrc/main/java/directory includes include**/*.xml/include /includes /resource resource directorysrc/main/resources/directory /resource /resources坑五前端页面调/api/stats/deptSubmitRate报405 Method Not Allowed多半是Controller里用了GetMapping但请求是用RequestBody那种POST方式发出的也可能是Tomcat的POST表单参数解析需要HttpServletRequest但你用了RequestParam在GET请求上。先确认前端Ajax的方式和后端注解是否一一对应再看Tomcat日志里有没有Parameters is null这样的底层报错。这个坑一般五分钟能定位但描述不出来会让人白熬一夜。5.3 调试文档的“增量维护”习惯最后聊一个习惯问题。我发现很多同学是“项目做完了才想起来写调试文档”写的时候全靠回忆效果大打折扣。我的建议是从拿到项目第一天就在项目根目录建一个DEBUG.md文件每遇到一个坑解决之后立刻记三行现象是什么、原因是什么、怎么解决的。等到项目做完这份文件自然就是最完整的调试文档。你看它根本不用“写”只是“记”。6. 源码阅读与二次开发建议6.1 源码结构梳理从包名看懂项目脉络如果你拿到的是一份完整的源码包第一步不要急着点开Controller先看包结构。一个规范的SSM项目包名已经能透露给你很多东西com.company.worklog ├── controller # 控制器用户、日志、部门、统计、批阅 ├── service # 业务接口 │ └── impl # 业务实现 ├── dao # MyBatis Mapper接口 ├── entity # 实体类User、Department、WorkLog、Role ├── common # 通用类Result、PageBean、常量 ├── interceptor # 登录拦截器、权限拦截器 └── utils # 工具类日期处理、MD5加密看一个项目先搞清楚controller和service的分工Controller里如果出现了超过十行的业务代码这个项目的基本功就有问题。一个健康的项目Controller只负责参数接收、调用Service、返回视图或JSON事务、判重、状态流转全部下沉到Service。前面“日志填报”那一节的逻辑就应该完整写在Service层而不散落在Controller里。6.2 可以扩展的3个方向这套系统做完以后想继续升级可以在三个方向里选一个难度和收益都不同方向一增加消息通知。领导批阅通过或退回时往消息表插一条记录员工登录后在页面右上角看到未读数字。改动量不大但对“协同办公”的体验提升非常明显。方向二整合Redis缓存登录状态。把Session替换成Redis Token登录后生成一个token返回给前端前端请求头里带着token后端用拦截器验证。这一步做完了就离真正的无状态会话不远了。方向三引入日志全文检索。日志内容存久了以后用LIKE查会越来越吃力可以把数据同步到ElasticSearch或者至少用MySQL的全文索引过渡一下。这条路线偏重但对理解搜索原理帮助很大。我个人的建议是优先做方向一因为它在现有数据模型上只增加一张表和两个接口对刚入门的人来说不会造成太大认知负担又能让整个系统功能上显得完整很多。7. 关于LW说明文档的一点提醒我看标题里带了一个“LW”我猜这是论文/说明文档的简称。很多人把它当成最后的苦差事但我干活的经验恰恰相反给这类系统写设计说明应该跟写代码同步进行。你在设计数据库表结构的时候顺手把E-R图画了在做权限设计的时候顺手把用例图补上在调通Flask跨服务调用的时候顺手把系统架构图补上。这样等代码写完论文的第三章“系统设计”和第四章“系统实现”其实已经完成大半了。写的时候还要注意把关键决策写清楚不要只写“我用了SSM”要写“为什么用SSM”——因为项目规模适中、分层清晰、便于教学Flask同理不要只说“我用了Flask”要写“Flask承担了数据聚合职责通过HTTP接口被Java调用从而规避了跨域并保持前后端调用路径单一”。这种“选型理由替代方案对比”的写作方式才是答辩老师最想看到的内容。再补一句论文里所有截图不要直接用浏览器缩放把IDEA、数据库工具、页面都整理干净再截图。一份干净清晰的文档胜过你答辩时多说十句话。8. 尾声一点个人体会式的中肯建议最后讲点我反复跟学生说的经验。如果你从上到下把这篇读完了会发现这套系统的技术难度不在“某个算法、某个框架”而在“把一个多角色业务流程完整落地”的工程能力。你把员工、经理、管理员三个视角分别登录一遍走一遍日志提交、审批退回、统计查看的完整链路这比背诵十个Java面试题都管用。真要去动手做我的建议是顺序绝对不要乱。先把数据库脚本写好灌入至少30个员工和2到3个月的日志假数据然后把登录、日志填报、审批主流程跑通再碰Flask统计。一次只加一个功能模块千万不要同时改权限逻辑和统计接口不然出了Bug你连是哪个环节引入的都不知道。最后分享一个小技巧能帮你省大量时间写一个测试数据生成脚本用Java或Python随机生成上个月的日志数据日期跳过周末每人每天一条内容从预设模板里随机选取。有了这堆假数据调列表、调统计、调饼图都是一瞬间的事没有假数据你排查“为什么图表是空的”这类问题根本无从下手。这个脚本半小时就能写完但它给你省下来的时间是以天为单位的。
返回列表