diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..6a46b5f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+target/
+.idea/
+*.iml
+*.log
+.DS_Store
diff --git a/docker/application-prod.yml b/docker/application-prod.yml
new file mode 100644
index 0000000..f0c88b0
--- /dev/null
+++ b/docker/application-prod.yml
@@ -0,0 +1,52 @@
+# WorkWeekPlan 生产配置(线上以 nacos 的 work-week-plan.yaml 为准,本文件是 nacos 不可达时的兜底)
+# 真正上线请在 Nacos 新建 dataId=work-week-plan.yaml、group=OA_APP 的配置覆盖以下项
+# (尤其 datasource 的库名/账号、yh.sso.client-host-url)。
+server:
+ port: 8070
+ servlet:
+ # 对外经 caddy 暴露为 http://base.yunhaixingtong.com/work-week-plan
+ context-path: /work-week-plan
+
+spring:
+ mvc:
+ date-format: yyyy-MM-dd HH:mm:ss
+ jackson:
+ time-zone: GMT+8
+ date-format: yyyy-MM-dd HH:mm:ss
+ datasource:
+ # 与 UserBase 共用同一台 mysql8;库名为 bsch_work_week_plan(需先在 mysql8 上建库)
+ url: jdbc:mysql://mysql8:3306/bsch_work_week_plan?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
+ username: root
+ password: 'CVf^x$sW*b6CGHxh'
+ driver-class-name: com.mysql.cj.jdbc.Driver
+ min-idle: 10
+ max-active: 30
+ jpa:
+ database: MySQL
+ show-sql: false
+ hibernate:
+ ddl-auto: update
+ database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
+ properties:
+ hibernate.order_by.default_null_ordering: last
+ open-in-view: true
+
+# 单点登录 / 应用间签名鉴权(基础平台 yh-auth / yh-app-auth)
+yh:
+ sso:
+ server-url-prefix: http://sso.yunhaixingtong.com
+ client-host-url: http://base.yunhaixingtong.com/work-week-plan
+ allow-headers: all
+ allow-origins: all
+ url-pattern: /api/*
+ app-auth:
+ enabled: true
+ # 本应用自身身份(客户端签名用)
+ app-key: 100617
+ app-secret: 2a3b4c5d6e7f8091a2b3c4d5e6f70819
+ # 受信应用(服务端验签用):appKey -> appSecret
+ trusted-apps:
+ 100617: 2a3b4c5d6e7f8091a2b3c4d5e6f70819
+
+# 上传文件落容器卷,compose 里挂到宿主机 ./upload-data
+FILE_PATH: /data/upload
diff --git a/docker/run-app.sh b/docker/run-app.sh
new file mode 100644
index 0000000..575b57d
--- /dev/null
+++ b/docker/run-app.sh
@@ -0,0 +1,9 @@
+#!/bin/sh
+# WorkWeekPlan 启动入口:配置统一由 Nacos 配置中心在 bootstrap 阶段下发(见 bootstrap.yml)
+# 不再需要手动 curl 拉取;若 nacos 不可达,bootstrap 中 fail-fast=false 会自动回退到
+# 镜像内烤好的 application-prod.yml 兜底默认。
+set -e
+
+echo "[run-app] starting WorkWeekPlan (profile=prod, nacos=${NACOS_ADDR:-nacos.yunhaixingtong.com:8848}) ..."
+exec java ${JAVA_OPTS:--Xms512m -Xmx1024m} -Dfile.encoding=UTF-8 -jar /app/app.jar \
+ --spring.profiles.active=prod
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..67293d9
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,204 @@
+
+
+ 4.0.0
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.7.18
+
+
+
+ com.yunhai
+ work-week-plan
+ 0.0.1
+ WorkWeekPlan
+ 一周安排(周次工作安排)子应用
+
+ 1.8
+
+
+
+
+
+
+ com.yunhai.platform
+ yh-platform-dependencies
+ 1.0.0-SNAPSHOT
+ pom
+ import
+
+
+
+ com.alibaba.cloud
+ spring-cloud-alibaba-dependencies
+ 2021.0.5.0
+ pom
+ import
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+
+
+ org.springframework.security
+ spring-security-crypto
+
+
+
+ org.springframework.boot
+ spring-boot-devtools
+ runtime
+ true
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-aop
+
+
+
+
+ com.alibaba.cloud
+ spring-cloud-starter-alibaba-nacos-config
+
+
+
+ org.springframework.cloud
+ spring-cloud-starter-bootstrap
+ 3.1.8
+
+
+
+ com.mysql
+ mysql-connector-j
+ runtime
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+
+ com.alibaba
+ fastjson
+ 1.2.80
+
+
+
+
+ org.springdoc
+ springdoc-openapi-ui
+ 1.7.0
+
+
+
+
+ com.yunhai.platform
+ yh-spring-boot-starter
+
+
+
+
+ org.jasig.cas.client
+ cas-client-core
+ 3.5.1
+
+
+
+
+ com.alibaba
+ easyexcel
+ 3.3.2
+
+
+
+
+
+
+
+ work-week-plan
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+ false
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+ com.yunhai.workweekplan.WorkWeekPlanApplication
+
+
+
+
+ repackage
+
+
+
+
+
+
+
+
+
+
+ nexus-public
+ bsch/nexus public
+ http://nexus.yunhaixingtong.com/repository/maven-public/
+ true
+ true
+
+
+
+
+
+ ci
+
+ C:/aca-build/work-week-plan
+
+
+
+
+
diff --git a/src/main/java/com/yunhai/workweekplan/WorkWeekPlanApplication.java b/src/main/java/com/yunhai/workweekplan/WorkWeekPlanApplication.java
new file mode 100644
index 0000000..8fa16ff
--- /dev/null
+++ b/src/main/java/com/yunhai/workweekplan/WorkWeekPlanApplication.java
@@ -0,0 +1,20 @@
+package com.yunhai.workweekplan;
+
+import com.alibaba.fastjson.parser.ParserConfig;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+
+@SpringBootApplication(scanBasePackages = "com.yunhai.workweekplan")
+@EnableTransactionManagement
+@EnableAsync
+public class WorkWeekPlanApplication {
+
+ public static void main(String[] args) {
+ // 开启 fastjson 安全模式
+ ParserConfig.getGlobalInstance().setSafeMode(true);
+ SpringApplication.run(WorkWeekPlanApplication.class, args);
+ }
+
+}
diff --git a/src/main/java/com/yunhai/workweekplan/aspect/ControllerUserContextAspect.java b/src/main/java/com/yunhai/workweekplan/aspect/ControllerUserContextAspect.java
new file mode 100644
index 0000000..3033534
--- /dev/null
+++ b/src/main/java/com/yunhai/workweekplan/aspect/ControllerUserContextAspect.java
@@ -0,0 +1,158 @@
+package com.yunhai.workweekplan.aspect;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.yunhai.workweekplan.enums.UserType;
+import com.yunhai.workweekplan.model.UserSysRole;
+import com.yunhai.workweekplan.utils.IpUtils;
+import com.yunhai.workweekplan.utils.UserContext;
+import com.yunhai.platform.auth.BoaUserContext;
+import com.yunhai.platform.utils.model.BoaUser;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import javax.servlet.http.HttpServletRequest;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * 当前用户上下文填充切面(移植自 UserclassService ControllerUserContextAspect)。
+ *
@Order(1) 先于审计日志切面(Order 3)执行,保证日志能取到 UserContext。
+ * 取用户优先级:方法参数 BoaUser → BoaUserContext 静态持有者 → request attribute;
+ * 角色切换信息从 currentrole(Header/Query,URL 编码 JSON)解析。
+ */
+@Aspect
+@Component
+@Order(1)
+public class ControllerUserContextAspect {
+
+ private static final Logger log = LoggerFactory.getLogger(ControllerUserContextAspect.class);
+
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+
+ /** 外部认证框架把解析好的 BoaUser 放进 request attribute 的候选 key */
+ private static final List BOA_USER_ATTR_KEYS = Arrays.asList(
+ "boaUser", "boauser", "currentUser", "user", "authUser", "loginUser"
+ );
+
+ @Around("execution(* com.yunhai.workweekplan.controller..*.*(..))")
+ public Object setUserContext(ProceedingJoinPoint joinPoint) throws Throwable {
+ Object[] args = joinPoint.getArgs();
+ BoaUser boaUser = null;
+ UserSysRole userSysRole = null;
+
+ for (Object arg : args) {
+ if (arg instanceof BoaUser) {
+ boaUser = (BoaUser) arg;
+ } else if (arg instanceof UserSysRole) {
+ userSysRole = (UserSysRole) arg;
+ }
+ }
+
+ if (boaUser == null) {
+ try {
+ boaUser = BoaUserContext.get();
+ } catch (Throwable ignore) {
+ boaUser = null;
+ }
+ }
+ HttpServletRequest request = getCurrentHttpRequest();
+ if (request != null) {
+ if (boaUser == null) {
+ boaUser = resolveBoaUserFromRequest(request);
+ }
+ if (userSysRole == null) {
+ userSysRole = resolveUserSysRoleFromRequest(request);
+ }
+ }
+
+ populateUserContext(boaUser, userSysRole);
+
+ try {
+ return joinPoint.proceed();
+ } finally {
+ UserContext.clear();
+ log.debug("UserContext cleared for {}", joinPoint.getSignature().toShortString());
+ }
+ }
+
+ /** 从 currentrole(URL 编码,Header 或 Query 参数)解析 UserSysRole */
+ private UserSysRole resolveUserSysRoleFromRequest(HttpServletRequest request) {
+ String raw = request.getHeader("currentrole");
+ boolean fromQuery = false;
+ if (raw == null) {
+ raw = request.getParameter("currentrole");
+ fromQuery = true;
+ }
+ if (raw == null) {
+ return null;
+ }
+ try {
+ String decoded = fromQuery ? raw : URLDecoder.decode(raw, StandardCharsets.UTF_8.name());
+ return MAPPER.readValue(decoded, UserSysRole.class);
+ } catch (Exception e) {
+ log.warn("解析 currentrole 失败: {}", e.getMessage());
+ return null;
+ }
+ }
+
+ /** 从 request attribute(外部框架注入的 BoaUser)回退解析 */
+ private BoaUser resolveBoaUserFromRequest(HttpServletRequest request) {
+ for (String key : BOA_USER_ATTR_KEYS) {
+ Object attr = request.getAttribute(key);
+ if (attr instanceof BoaUser) {
+ return (BoaUser) attr;
+ }
+ }
+ return null;
+ }
+
+ private HttpServletRequest getCurrentHttpRequest() {
+ ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+ return attributes != null ? attributes.getRequest() : null;
+ }
+
+ private void populateUserContext(BoaUser boaUser, UserSysRole userSysRole) {
+ if (boaUser != null) {
+ UserContext.setUserId(boaUser.getId());
+ UserContext.setUserName(boaUser.getName() != null ? boaUser.getName() : "unknown");
+ Integer schId = boaUser.getSchool() == null ? null : boaUser.getSchool().getId();
+ if (schId == null && userSysRole != null) {
+ schId = userSysRole.getSchId();
+ }
+ UserContext.setSchId(schId);
+ } else {
+ UserContext.setUserName("anonymous");
+ if (userSysRole != null && userSysRole.getSchId() != null) {
+ UserContext.setSchId(userSysRole.getSchId());
+ }
+ log.warn("No BoaUser found, using anonymous");
+ }
+
+ if (userSysRole != null) {
+ UserContext.setRoleId(userSysRole.getRoleId());
+ UserContext.setChildId(userSysRole.getChildId());
+ // currentrole 携带 userId 时兜底填充(无 BoaUser 的场景:内部通道/调试直调)
+ if (boaUser == null && userSysRole.getUserId() != null) {
+ UserContext.setUserId(userSysRole.getUserId());
+ }
+ }
+
+ UserContext.setType(boaUser != null ? boaUser.getType() : null);
+
+ // 超级管理员(TYPE=1)不绑定具体学校,schId 统一置 0
+ if (UserType.SUPER_ADMINISTRATOR.getId().equals(UserContext.getType())) {
+ UserContext.setSchId(0);
+ }
+
+ UserContext.setIp(IpUtils.getClientIp());
+ }
+}
diff --git a/src/main/java/com/yunhai/workweekplan/aspect/SysOperationLogAspect.java b/src/main/java/com/yunhai/workweekplan/aspect/SysOperationLogAspect.java
new file mode 100644
index 0000000..d004d50
--- /dev/null
+++ b/src/main/java/com/yunhai/workweekplan/aspect/SysOperationLogAspect.java
@@ -0,0 +1,223 @@
+package com.yunhai.workweekplan.aspect;
+
+import com.alibaba.fastjson.JSON;
+import com.yunhai.workweekplan.entity.SysOperationLog;
+import com.yunhai.workweekplan.service.ISysOperationLogService;
+import com.yunhai.workweekplan.utils.GzipUtil;
+import com.yunhai.workweekplan.utils.IpUtils;
+import com.yunhai.workweekplan.utils.UserContext;
+import com.yunhai.platform.apistd.ApiResponse;
+import com.yunhai.platform.utils.exception.DException;
+import com.yunhai.platform.utils.exception.ErrorCode;
+import com.yunhai.platform.utils.exception.IErrorCode;
+import com.yunhai.platform.utils.http.ApiResult;
+import org.apache.commons.lang3.exception.ExceptionUtils;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 系统审计日志切面(移植自 UserclassService SysOperationLogAspect)。
+ * 全量拦截 controller 包所有方法,不依赖注解;排除日志查询控制器自身(防递归);
+ * 用户信息来自 {@link ControllerUserContextAspect}(Order 1)填充的 UserContext;
+ * 响应体 GZIP 压缩落库,落库走 @Async 不阻塞请求。
+ */
+@Aspect
+@Component
+@Order(3)
+public class SysOperationLogAspect {
+
+ private static final Logger log = LoggerFactory.getLogger(SysOperationLogAspect.class);
+
+ /** 排除的控制器(日志查询自身,防递归刷屏) */
+ private static final List EXCLUDE_CONTROLLERS = java.util.Arrays.asList(
+ "com.yunhai.workweekplan.controller.api.LogController"
+ );
+ private static final int DEFAULT_MAX_RESPONSE_SIZE = 10240;
+ private static final int MAX_ERROR_MSG = 8000;
+ private static final int MAX_PARAMS = 2000;
+
+ @Autowired
+ private ISysOperationLogService sysOperationLogService;
+
+ @Around("execution(* com.yunhai.workweekplan.controller..*.*(..))")
+ public Object around(ProceedingJoinPoint point) throws Throwable {
+ String declType = point.getSignature().getDeclaringTypeName();
+ if (EXCLUDE_CONTROLLERS.contains(declType)) {
+ return point.proceed();
+ }
+
+ HttpServletRequest request = getRequest();
+ String httpMethod = request != null ? request.getMethod() : "";
+
+ long start = System.currentTimeMillis();
+ SysOperationLog entity = buildEntity(point, request, httpMethod);
+
+ Object result;
+ try {
+ result = point.proceed();
+ entity.setStatusCode(resolveSuccessCode(result));
+ fillResponse(entity, result, DEFAULT_MAX_RESPONSE_SIZE);
+ return result;
+ } catch (Throwable e) {
+ entity.setStatusCode(resolveErrorCode(e));
+ entity.setErrorMsg(buildErrorMsg(e));
+ throw e;
+ } finally {
+ entity.setCostTime(System.currentTimeMillis() - start);
+ sysOperationLogService.saveAsync(entity);
+ }
+ }
+
+ private SysOperationLog buildEntity(ProceedingJoinPoint point, HttpServletRequest request, String httpMethod) {
+ SysOperationLog e = new SysOperationLog();
+ e.setCreateTime(LocalDateTime.now());
+
+ // 用户信息(ControllerUserContextAspect Order 1 已填充)
+ e.setUserId(UserContext.getUserId());
+ e.setUserName(StringUtils.hasText(UserContext.getUserName()) ? UserContext.getUserName() : "anonymous");
+ e.setSchId(UserContext.getSchId());
+ e.setRequestIp(StringUtils.hasText(UserContext.getIp()) ? UserContext.getIp() : IpUtils.getClientIp());
+
+ String uri = request != null ? request.getRequestURI() : "";
+ String methodName = point.getSignature().getName();
+ e.setRequestUri(uri);
+ e.setRequestMethod(httpMethod);
+ e.setClassMethod(point.getTarget().getClass().getName() + "." + methodName);
+ e.setLogLevel(resolveLevel(httpMethod));
+ e.setOperationType(resolveOperationType(httpMethod, methodName));
+ if (request != null) {
+ e.setUserAgent(request.getHeader("User-Agent"));
+ }
+ e.setRequestParams(truncate(buildRequestParams(point, request), MAX_PARAMS));
+ return e;
+ }
+
+ private String buildRequestParams(ProceedingJoinPoint point, HttpServletRequest request) {
+ try {
+ if (request != null && "GET".equalsIgnoreCase(request.getMethod())) {
+ String qs = request.getQueryString();
+ if (StringUtils.hasText(qs)) {
+ return qs;
+ }
+ }
+ Object[] args = point.getArgs();
+ if (args == null || args.length == 0) {
+ return "";
+ }
+ List