# 企业级能力 Phase 1 实现计划 > **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。 **目标:** 为OKR绩效考核系统补齐3项企业上线必须的基础能力:消息通知系统、RBAC权限体系、多层级OKR拆解与对齐。 **架构:** Spring Boot 事件驱动通知管道(ApplicationEventPublisher + @EventListener),声明式角色权限(@RequireRole注解 + AOP),四层级OKR对齐全树(COMPANY/DEPARTMENT/TEAM/INDIVIDUAL + parent_objective_id)。 **技术栈:** Spring Boot 3.2 + MyBatis-Plus + SQLite + Vue 3 + Element Plus + Vite --- ## 模块一:消息通知系统 ### 任务 1:Notification 实体与 DDL **文件:** - 创建:`backend/src/main/java/com/yimeng/okr/entity/Notification.java` - 创建:`backend/src/main/java/com/yimeng/okr/mapper/NotificationMapper.java` - 创建:`backend/src/main/java/com/yimeng/okr/dto/NotificationDto.java` - 修改:`backend/src/main/resources/schema.sql` - [ ] **步骤 1:创建 Notification 实体** ```java package com.yimeng.okr.entity; import com.baomidou.mybatisplus.annotation.*; import lombok.Data; import java.time.LocalDateTime; @Data @TableName("notification") public class Notification { @TableId(type = IdType.AUTO) private Long id; private Long userId; private String type; private String title; private String content; private String refType; private Long refId; private Integer isRead; private LocalDateTime readAt; @TableField(fill = FieldFill.INSERT) private LocalDateTime createdAt; } ``` - [ ] **步骤 2:创建 NotificationMapper** ```java package com.yimeng.okr.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.yimeng.okr.entity.Notification; import org.apache.ibatis.annotations.Mapper; @Mapper public interface NotificationMapper extends BaseMapper { } ``` - [ ] **步骤 3:创建 NotificationDto** ```java package com.yimeng.okr.dto; import lombok.Data; import java.time.LocalDateTime; @Data public class NotificationDto { private Long id; private String type; private String title; private String content; private String refType; private Long refId; private Integer isRead; private LocalDateTime createdAt; private String createdAtFriendly; } ``` - [ ] **步骤 4:schema.sql 追加建表 DDL** 在 `schema.sql` 末尾追加: ```sql CREATE TABLE IF NOT EXISTS notification ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, type VARCHAR(32) NOT NULL, title VARCHAR(256) NOT NULL, content TEXT, ref_type VARCHAR(32), ref_id INTEGER, is_read INTEGER DEFAULT 0, read_at TEXT, created_at TEXT NOT NULL ); ``` - [ ] **步骤 5:Commit** ```bash git add backend/src/main/java/com/yimeng/okr/entity/Notification.java backend/src/main/java/com/yimeng/okr/mapper/NotificationMapper.java backend/src/main/java/com/yimeng/okr/dto/NotificationDto.java backend/src/main/resources/schema.sql git commit -m "feat: add Notification entity, mapper, dto and schema" ``` --- ### 任务 2:通知事件模型与监听器 **文件:** - 创建:`backend/src/main/java/com/yimeng/okr/event/NotificationEvent.java` - 创建:`backend/src/main/java/com/yimeng/okr/event/NotificationEventListener.java` - [ ] **步骤 1:创建 NotificationEvent** ```java package com.yimeng.okr.event; import lombok.Getter; import org.springframework.context.ApplicationEvent; @Getter public class NotificationEvent extends ApplicationEvent { private final Long userId; private final String type; private final String title; private final String content; private final String refType; private final Long refId; public NotificationEvent(Object source, Long userId, String type, String title, String content, String refType, Long refId) { super(source); this.userId = userId; this.type = type; this.title = title; this.content = content; this.refType = refType; this.refId = refId; } } ``` - [ ] **步骤 2:创建 NotificationEventListener** ```java package com.yimeng.okr.event; import com.yimeng.okr.entity.Notification; import com.yimeng.okr.mapper.NotificationMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.event.EventListener; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; @Slf4j @Component @RequiredArgsConstructor public class NotificationEventListener { private final NotificationMapper notificationMapper; @Async @EventListener public void handleNotification(NotificationEvent event) { Notification notification = new Notification(); notification.setUserId(event.getUserId()); notification.setType(event.getType()); notification.setTitle(event.getTitle()); notification.setContent(event.getContent()); notification.setRefType(event.getRefType()); notification.setRefId(event.getRefId()); notification.setIsRead(0); notificationMapper.insert(notification); } } ``` - [ ] **步骤 3:启用异步支持** 修改 `OkrApplication.java`,添加 `@EnableAsync`: ```java package com.yimeng.okr; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication @EnableScheduling @EnableAsync public class OkrApplication { public static void main(String[] args) { SpringApplication.run(OkrApplication.class, args); } } ``` - [ ] **步骤 4:Commit** ```bash git add backend/src/main/java/com/yimeng/okr/event/NotificationEvent.java backend/src/main/java/com/yimeng/okr/event/NotificationEventListener.java backend/src/main/java/com/yimeng/okr/OkrApplication.java git commit -m "feat: add notification event model and async listener" ``` --- ### 任务 3:通知 REST 端点 **文件:** - 创建:`backend/src/main/java/com/yimeng/okr/controller/NotificationController.java` - 创建:`backend/src/main/java/com/yimeng/okr/service/NotificationService.java` - [ ] **步骤 1:创建 NotificationService** ```java package com.yimeng.okr.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.yimeng.okr.dto.NotificationDto; import com.yimeng.okr.entity.Notification; import com.yimeng.okr.mapper.NotificationMapper; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; @Service @RequiredArgsConstructor public class NotificationService { private final NotificationMapper notificationMapper; public long getUnreadCount(Long userId) { return notificationMapper.selectCount( new LambdaQueryWrapper() .eq(Notification::getUserId, userId) .eq(Notification::getIsRead, 0)); } public IPage getPage(Long userId, int page, int size, String type) { LambdaQueryWrapper qw = new LambdaQueryWrapper() .eq(Notification::getUserId, userId) .orderByDesc(Notification::getCreatedAt); if (type != null && !type.isEmpty()) { qw.eq(Notification::getType, type); } IPage result = notificationMapper.selectPage(new Page<>(page, size), qw); return result.convert(this::toDto); } public void markRead(Long id, Long userId) { Notification n = notificationMapper.selectById(id); if (n != null && n.getUserId().equals(userId)) { n.setIsRead(1); n.setReadAt(LocalDateTime.now()); notificationMapper.updateById(n); } } public void markAllRead(Long userId) { notificationMapper.update(null, new LambdaUpdateWrapper() .eq(Notification::getUserId, userId) .eq(Notification::getIsRead, 0) .set(Notification::getIsRead, 1) .set(Notification::getReadAt, LocalDateTime.now())); } private NotificationDto toDto(Notification n) { NotificationDto dto = new NotificationDto(); dto.setId(n.getId()); dto.setType(n.getType()); dto.setTitle(n.getTitle()); dto.setContent(n.getContent()); dto.setRefType(n.getRefType()); dto.setRefId(n.getRefId()); dto.setIsRead(n.getIsRead()); dto.setCreatedAt(n.getCreatedAt()); long seconds = n.getCreatedAt() != null ? ChronoUnit.SECONDS.between(n.getCreatedAt(), LocalDateTime.now()) : 0; if (seconds < 60) dto.setCreatedAtFriendly("刚刚"); else if (seconds < 3600) dto.setCreatedAtFriendly((seconds / 60) + "分钟前"); else if (seconds < 86400) dto.setCreatedAtFriendly((seconds / 3600) + "小时前"); else dto.setCreatedAtFriendly((seconds / 86400) + "天前"); return dto; } } ``` - [ ] **步骤 2:创建 NotificationController** ```java package com.yimeng.okr.controller; import com.yimeng.okr.dto.ApiResult; import com.yimeng.okr.security.SecurityUtils; import com.yimeng.okr.service.NotificationService; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; import java.util.Map; @RestController @RequestMapping("/api/notifications") @RequiredArgsConstructor public class NotificationController { private final NotificationService notificationService; @GetMapping("/unread-count") public ApiResult> getUnreadCount() { Long userId = SecurityUtils.getCurrentUserId(); return ApiResult.success(Map.of("count", notificationService.getUnreadCount(userId))); } @GetMapping public ApiResult getPage(@RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "20") int size, @RequestParam(required = false) String type) { Long userId = SecurityUtils.getCurrentUserId(); return ApiResult.success(notificationService.getPage(userId, page, size, type)); } @PutMapping("/{id}/read") public ApiResult markRead(@PathVariable Long id) { notificationService.markRead(id, SecurityUtils.getCurrentUserId()); return ApiResult.success(); } @PutMapping("/read-all") public ApiResult markAllRead() { notificationService.markAllRead(SecurityUtils.getCurrentUserId()); return ApiResult.success(); } } ``` - [ ] **步骤 3:Commit** ```bash git add backend/src/main/java/com/yimeng/okr/service/NotificationService.java backend/src/main/java/com/yimeng/okr/controller/NotificationController.java git commit -m "feat: add notification REST endpoints" ``` --- ### 任务 4:业务事件接入 **文件:** - 修改:`backend/src/main/java/com/yimeng/okr/service/OkrService.java` - 修改:`backend/src/main/java/com/yimeng/okr/service/PerformanceService.java` - 修改:`backend/src/main/java/com/yimeng/okr/service/FeedbackService.java` - 修改:`backend/src/main/java/com/yimeng/okr/schedule/PeriodScheduler.java` - [ ] **步骤 1:OkrService — 注入 ApplicationEventPublisher 并在 submitOkr 尾部发布事件** 在 `OkrService` 构造函数中注入 `ApplicationEventPublisher`,在 `submitOkr` 方法 `return obj;` 之前添加(仅非顶层用户提交时通知上级): ```java import com.yimeng.okr.event.NotificationEvent; import org.springframework.context.ApplicationEventPublisher; // 在 submitOkr 方法中,insert krs 之后、return 之前: if (!isTopLevel && user.getSuperiorId() != null) { publisher.publishEvent(new NotificationEvent( this, user.getSuperiorId(), "OKR_REVIEW", "新的OKR待审核", user.getRealName() + " 提交了OKR,请审核", "objective", obj.getId())); } ``` 在 `reviewOkr` 方法 `objectiveMapper.updateById(obj);` 之后添加: ```java String result = approved ? "已通过" : "已驳回"; publisher.publishEvent(new NotificationEvent( this, obj.getUserId(), "OKR_REVIEWED", "OKR审核" + result, "你的OKR已被" + result + (remark != null && !remark.isEmpty() ? "。备注:" + remark : ""), "objective", obj.getId())); ``` - [ ] **步骤 2:PerformanceService — 评分完成后通知** 在 `PerformanceService` 的 `submitSuperiorScore` 方法(或等效评分方法)成功后,发布通知给被评分人: ```java import com.yimeng.okr.event.NotificationEvent; // .. 评分保存成功后: publisher.publishEvent(new NotificationEvent( this, targetUserId, "SCORE_CONFIRM", "绩效评分已完成", "你的上级已完成本周期绩效评分,请确认", "score", savedScore.getId())); ``` 在确认评分方法成功后,通知上级: ```java publisher.publishEvent(new NotificationEvent( this, superiorId, "SCORE_CONFIRMED", "评分已确认", userName + " 已确认绩效评分", "score", scoreId)); ``` - [ ] **步骤 3:FeedbackService — 面谈发起后通知** ```java // 在保存面谈记录成功后: publisher.publishEvent(new NotificationEvent( this, employeeId, "FEEDBACK_NOTIFY", "绩效面谈通知", "你的上级发起了绩效面谈,请查看并回复", "feedback", feedback.getId())); ``` - [ ] **步骤 4:PeriodScheduler — 周期截止提醒 + KR 滞后检测** 在 `PeriodScheduler` 的定时任务中追加: ```java import com.yimeng.okr.event.NotificationEvent; // 周期截止前3天提醒 private void checkPeriodDeadlines() { List periods = periodMapper.selectList( new LambdaQueryWrapper() .eq(AssessmentPeriod::getStatus, PeriodStatus.EXECUTING)); LocalDate today = LocalDate.now(); for (AssessmentPeriod p : periods) { LocalDate end = LocalDate.parse(p.getEndDate().substring(0, 10)); if (ChronoUnit.DAYS.between(today, end) == 3) { // 通知周期内所有用户 List users = userMapper.selectList(null); for (SysUser u : users) { publisher.publishEvent(new NotificationEvent( this, u.getId(), "PERIOD_DEADLINE", "周期即将结束", "考核周期「" + p.getName() + "」将于3天后结束", "period", p.getId())); } } } } ``` - [ ] **步骤 5:Commit** ```bash git add backend/src/main/java/com/yimeng/okr/service/OkrService.java backend/src/main/java/com/yimeng/okr/service/PerformanceService.java backend/src/main/java/com/yimeng/okr/service/FeedbackService.java backend/src/main/java/com/yimeng/okr/schedule/PeriodScheduler.java git commit -m "feat: wire notification events into business services" ``` --- ### 任务 5:前端通知面板与列表 **文件:** - 创建:`frontend/src/views/notification/NotificationView.vue` - 修改:`frontend/src/components/MainLayout.vue` - 修改:`frontend/src/api/index.js` - 修改:`frontend/src/router/index.js` - [ ] **步骤 1:api/index.js 追加通知 API** ```javascript // Notification API (追加到文件末尾) export const notificationApi = { getUnreadCount: () => api.get('/notifications/unread-count').then(r => r.data), getPage: (params) => api.get('/notifications', { params }).then(r => r.data), markRead: (id) => api.put(`/notifications/${id}/read`), markAllRead: () => api.put('/notifications/read-all') } ``` - [ ] **步骤 2:MainLayout.vue — 顶栏添加通知铃铛** 修改 `