1. 项目概述:企业OA系统的技术选型与价值
最近在帮一家中小型企业升级他们的办公自动化系统,选择了JavaWeb+SSM的技术栈。这种组合在传统企业级开发中依然保持着强大的生命力,特别是对于需要快速交付又要求系统稳定的场景。OA系统作为企业的"数字中枢",需要处理考勤、审批、文档等核心业务流程,SSM框架的轻量级特性正好匹配这类需求。
这个系统采用经典的三层架构:前端用JSP+JavaScript实现动态交互,中间层是Spring+SpringMVC+MyBatis(SSM)框架,数据层采用MySQL关系型数据库。特别要说明的是,我们没有选择SpringBoot这类新框架,是因为客户IT部门已有成熟的Tomcat部署规范和JDK6运行环境,技术决策永远要结合实际运维条件。
2. 技术架构深度解析
2.1 SSM框架协同工作原理
Spring在这个系统中扮演着"胶水"的角色,通过IOC容器统一管理各类Bean。我们在实际配置时特别注重了bean的作用域控制——例如审批流程服务必须配置为prototype,避免多线程并发问题。SpringMVC则采用注解驱动方式,配合拦截器实现了细粒度的权限控制,这里有个细节:我们在HandlerInterceptor里加入了操作日志切面,记录每个HTTP请求的业务含义。
MyBatis的SQL映射文件我们做了特殊优化:所有查询语句都强制要求指定resultMap,避免后期表结构变更导致NPE异常。对于复杂的统计报表查询,采用了动态SQL拼接技术,例如考勤统计模块的这段代码:
xml复制<select id="getAttendanceReport" resultMap="attendanceMap">
SELECT * FROM att_record
<where>
<if test="deptId != null">AND dept_id = #{deptId}</if>
<if test="startDate != null">AND check_date >= #{startDate}</if>
<choose>
<when test="leaveType != null">AND leave_type = #{leaveType}</when>
<otherwise>AND status IN (1,2)</otherwise>
</choose>
</where>
ORDER BY create_time DESC
</select>
2.2 前端技术实现方案
虽然现在流行Vue/React,但我们坚持使用JSP+JSTL+原生JavaScript的组合,主要考虑三点:一是客户环境限制外网资源加载,二是维护团队熟悉传统技术栈,三是系统需要深度集成Office插件。不过我们在实现上做了这些改进:
- 采用模块化JS加载方案,通过自定义标签库实现按需加载
- 使用DataTables插件增强表格交互,同时保持后端分页
- 重要表单实现双重验证:前端JS验证+后端注解验证
文件上传模块是个典型例子,我们既保留了传统form提交方式,又增加了Ajax上传进度显示:
javascript复制function uploadFile() {
var formData = new FormData($('#uploadForm')[0]);
$.ajax({
url: '/document/upload',
type: 'POST',
data: formData,
processData: false,
contentType: false,
xhr: function() {
var xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', function(e) {
if(e.lengthComputable) {
var percent = Math.round((e.loaded/e.total)*100);
$('#progressBar').width(percent+'%');
}
}, false);
return xhr;
},
success: function(data) {
// 处理响应
}
});
}
3. 数据库设计与优化
3.1 核心表结构设计
MySQL数据库设计遵循了这几个原则:所有表必须包含create_time/update_time字段,状态字段使用tinyint而非varchar,外键关系通过应用层维护。以下是几个关键表的设计要点:
- 用户表(sys_user):采用密码加盐存储,预留多因素认证字段
- 审批流程表(oa_approval):使用JSON类型存储动态表单结构
- 文档表(oa_document):实现版本控制,保留历史修改记录
特别分享一个设计技巧:对于频繁查询的审批状态,我们创建了物化视图提高查询效率:
sql复制CREATE VIEW v_approval_stats AS
SELECT
a.process_type,
COUNT(CASE WHEN a.status=0 THEN 1 END) AS pending_count,
COUNT(CASE WHEN a.status=1 THEN 1 END) AS approved_count,
COUNT(CASE WHEN a.status=2 THEN 1 END) AS rejected_count
FROM oa_approval a
GROUP BY a.process_type;
3.2 性能优化实践
在系统上线初期我们遇到了并发审批时的死锁问题,通过以下措施解决:
- 为审批表增加乐观锁版本号字段
- 对批量更新操作采用队列串行化处理
- 关键查询添加复合索引,例如:
sql复制ALTER TABLE oa_leave_apply
ADD INDEX idx_dept_status (apply_dept, status);
针对MySQL的配置优化也很有讲究:我们将innodb_buffer_pool_size设置为物理内存的70%,同时调整了事务隔离级别为READ-COMMITTED,平衡了一致性和性能的需求。
4. 典型功能模块实现
4.1 工作流引擎设计
没有采用Activiti等重型框架,而是基于状态模式自研了轻量级工作流引擎。核心设计包括:
- 流程模板表(oa_process_template)定义节点和流转规则
- 每个审批节点对应一个Handler处理器类
- 使用责任链模式实现自动流转
审批逻辑的代码结构示例:
java复制public class ApprovalHandlerChain {
private List<ApprovalHandler> handlers;
public ApprovalResult process(ApprovalContext context) {
for (ApprovalHandler handler : handlers) {
if(handler.support(context.getCurrentNode())){
return handler.handle(context);
}
}
throw new WorkflowException("No handler found for node: "+context.getCurrentNode());
}
}
@Service
public class DeptManagerHandler implements ApprovalHandler {
@Override
public boolean support(String nodeType) {
return "DEPT_MANAGER".equals(nodeType);
}
@Override
public ApprovalResult handle(ApprovalContext context) {
// 部门经理审批逻辑
if("同意".equals(context.getFormData().get("opinion"))){
return ApprovalResult.next("FINANCE");
}
return ApprovalResult.reject();
}
}
4.2 报表统计模块
使用POI+JFreeChart实现动态报表导出,这里有个实用技巧:对于大数据量导出,我们采用分页查询+临时文件合并的方式避免OOM:
java复制public void exportAttendanceReport(HttpServletResponse response) throws Exception {
String tempDir = System.getProperty("java.io.tmpdir");
File outputFile = new File(tempDir, "att_report_" + System.currentTimeMillis() + ".xlsx");
try (SXSSFWorkbook workbook = new SXSSFWorkbook(100)) {
Sheet sheet = workbook.createSheet("考勤统计");
// 添加表头等操作
int page = 1;
while (true) {
Page<AttendanceRecord> records = attendanceService.getRecords(page, 500);
if (records.isEmpty()) break;
// 填充数据行
for (AttendanceRecord record : records) {
Row row = sheet.createRow(sheet.getLastRowNum() + 1);
// 填充单元格数据
}
page++;
}
workbook.write(new FileOutputStream(outputFile));
}
// 使用FileSystemResource实现大文件下载
FileSystemResource resource = new FileSystemResource(outputFile);
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.addHeader("Content-Disposition", "attachment;filename=att_report.xlsx");
StreamUtils.copy(resource.getInputStream(), response.getOutputStream());
outputFile.delete();
}
5. 部署与运维实战
5.1 多环境配置管理
通过Maven Profile实现环境隔离是必备技能,我们的pom.xml配置如下:
xml复制<profiles>
<profile>
<id>dev</id>
<properties>
<env>dev</env>
</properties>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
</profile>
<profile>
<id>test</id>
<properties>
<env>test</env>
</properties>
</profile>
<profile>
<id>prod</id>
<properties>
<env>prod</env>
</properties>
</profile>
</profiles>
对应的Spring配置通过@Profile注解实现条件加载:
java复制@Configuration
@Profile("prod")
public class ProdDataSourceConfig {
@Bean
public DataSource dataSource() {
// 生产环境数据源配置
}
}
5.2 性能监控方案
除了常规的JVM监控外,我们还实现了以下监控点:
- 关键业务接口的耗时统计:
java复制@Around("execution(* com..service.*.*(..))")
public Object monitorPerformance(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
try {
return pjp.proceed();
} finally {
long cost = System.currentTimeMillis() - start;
if(cost > 500) { // 记录慢方法
PerformanceLog.log(pjp.getSignature(), cost);
}
}
}
- 数据库连接池状态监控
- 定时任务执行历史记录
6. 踩坑经验与解决方案
6.1 中文乱码问题大全
SSM项目中的中文乱码有三大高发区:
- JSP页面乱码:确保<%@ page contentType="text/html;charset=UTF-8"%>
- MySQL乱码:连接串需要加上characterEncoding=utf8
- Tomcat乱码:server.xml的Connector添加URIEncoding="UTF-8"
最隐蔽的是文件下载时的中文名乱码,需要特殊处理:
java复制String encodedFileName = URLEncoder.encode(fileName, "UTF-8").replace("+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + encodedFileName);
6.2 事务管理陷阱
在SSM框架中使用事务要特别注意:
- 避免在Controller层开启事务
- 同类方法自调用不会触发AOP事务
- 多数据源需要配置不同的事务管理器
我们遇到过一个典型问题:审批通过后需要更新多个表,但由于异常处理不当导致数据不一致。最终解决方案是:
java复制@Service
public class ApprovalService {
@Transactional(rollbackFor = Exception.class)
public void completeApproval(Long approvalId) {
// 更新审批状态
approvalDao.updateStatus(approvalId, "APPROVED");
// 生成业务记录
businessDao.createRecord(buildRecord(approvalId));
// 发送通知(异步且不要求事务)
NotificationEvent event = new NotificationEvent(approvalId);
applicationEventPublisher.publishEvent(event);
}
}
@Component
public class NotificationListener {
@Async
@EventListener
public void handleEvent(NotificationEvent event) {
// 发送邮件/短信通知
}
}
7. 安全防护实践
7.1 认证与授权体系
采用改良版的RBAC模型,特点包括:
- 用户-角色-权限三级结构
- 支持数据权限(如部门过滤)
- 操作日志全记录
登录验证的关键代码:
java复制public String login(String username, String password) {
SysUser user = userDao.findByUsername(username);
if(user == null) throw new AuthException("用户不存在");
String encrypted = encrypt(password + user.getSalt());
if(!encrypted.equals(user.getPassword())) {
logService.logLoginFail(username);
throw new AuthException("密码错误");
}
if(user.getStatus() != 1) {
throw new AuthException("账户已禁用");
}
String token = JwtUtil.generateToken(user);
logService.logLoginSuccess(user);
return token;
}
7.2 SQL注入防御
除了使用MyBatis预编译外,我们还做了这些防护:
- 动态表名/列名白名单校验
- 定期SQL审计
- 敏感操作二次验证
例如对于动态排序参数的处理:
java复制public String validateSortField(String input) {
String[] allowedFields = {"create_time", "update_time", "id"};
if(Arrays.asList(allowedFields).contains(input.toLowerCase())) {
return input;
}
throw new IllegalArgumentException("非法的排序字段: "+input);
}
这个OA系统最终上线后稳定运行,日均处理3000+审批流程。技术选型没有绝对的好坏,关键是要适合团队和业务场景。对于需要快速交付的中小型企业项目,SSM依然是非常可靠的选择。