@@ -0,0 +1,18 @@
|
||||
package com.yunhai.familyactivity;
|
||||
|
||||
import com.alibaba.fastjson.parser.ParserConfig;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
|
||||
@SpringBootApplication(scanBasePackages = "com.yunhai.familyactivity")
|
||||
@EnableTransactionManagement
|
||||
public class FamilyActivityApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
// 开启 fastjson 安全模式
|
||||
ParserConfig.getGlobalInstance().setSafeMode(true);
|
||||
SpringApplication.run(FamilyActivityApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.yunhai.familyactivity.annotation;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 业务操作日志注解:标注在控制器/服务写操作方法上,由
|
||||
* {@link com.yunhai.familyactivity.aspect.OperationLogAspect} 落库到 operation_log。
|
||||
* 参考 UserclassService 的 @OperationLog。
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface OperationLog {
|
||||
|
||||
/** 模块名称,如 "活动管理" */
|
||||
String module() default "";
|
||||
|
||||
/** 操作内容摘要(支持静态文案;动态细节由切面附加请求参数) */
|
||||
String content() default "";
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package com.yunhai.familyactivity.aspect;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.yunhai.familyactivity.utils.UserContext;
|
||||
import com.yunhai.platform.auth.BoaUserContext;
|
||||
import com.yunhai.platform.auth.LoginUser;
|
||||
import com.yunhai.platform.auth.UserContextHolder;
|
||||
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.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 控制器切面:请求进入前把当前登录用户填充到 {@link UserContext},请求结束后清理。
|
||||
* <p>身份来源优先级(参考 UserclassService 的 ControllerUserContextAspect):</p>
|
||||
* <ol>
|
||||
* <li>控制器方法参数中平台参数解析器绑定的 {@link BoaUser}(兼容现有写法);</li>
|
||||
* <li>平台静态持有者 {@link BoaUserContext#get()};</li>
|
||||
* <li>request attribute(外部认证框架注入的 BoaUser);</li>
|
||||
* <li>{@link UserContextHolder} 的 {@link LoginUser}(仅补 account/userName,无 id/schId)。</li>
|
||||
* </ol>
|
||||
* 超管(type=1)不绑定具体学校,schId 统一置 0,保证数据隔离口径一致。
|
||||
*/
|
||||
@Aspect
|
||||
@Component
|
||||
@Order(1)
|
||||
public class ControllerUserContextAspect {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ControllerUserContextAspect.class);
|
||||
|
||||
/** 外部认证框架把解析好的 BoaUser 放进 request attribute 的候选 key。 */
|
||||
private static final List<String> BOA_USER_ATTR_KEYS = Arrays.asList(
|
||||
"boaUser", "boauser", "currentUser", "user", "authUser", "loginUser"
|
||||
);
|
||||
|
||||
/** 平台用户类型:超级管理员 */
|
||||
private static final Integer USER_TYPE_SUPER_ADMIN = 1;
|
||||
|
||||
@Around("execution(* com.yunhai.familyactivity.controller..*.*(..))")
|
||||
public Object setUserContext(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
// 1. 优先从方法参数提取 BoaUser(兼容旧写法)
|
||||
BoaUser boaUser = null;
|
||||
for (Object arg : joinPoint.getArgs()) {
|
||||
if (arg instanceof BoaUser) {
|
||||
boaUser = (BoaUser) arg;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 参数缺失时依次回退:平台静态持有者 → request attribute
|
||||
if (boaUser == null) {
|
||||
try {
|
||||
boaUser = BoaUserContext.get();
|
||||
} catch (Throwable ignore) {
|
||||
boaUser = null;
|
||||
}
|
||||
}
|
||||
HttpServletRequest request = getCurrentHttpRequest();
|
||||
if (request != null && boaUser == null) {
|
||||
boaUser = resolveBoaUserFromRequest(request);
|
||||
}
|
||||
|
||||
// 3. 兜底:UserContextHolder 的 LoginUser(CAS 会话),至少补齐 account/userName
|
||||
LoginUser loginUser = null;
|
||||
if (boaUser == null) {
|
||||
try {
|
||||
loginUser = UserContextHolder.get();
|
||||
} catch (Throwable ignore) {
|
||||
loginUser = null;
|
||||
}
|
||||
}
|
||||
|
||||
populateUserContext(boaUser, loginUser, request);
|
||||
|
||||
try {
|
||||
return joinPoint.proceed();
|
||||
} finally {
|
||||
UserContext.clear();
|
||||
log.debug("UserContext cleared for {}", joinPoint.getSignature().toShortString());
|
||||
}
|
||||
}
|
||||
|
||||
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, LoginUser loginUser, HttpServletRequest request) {
|
||||
if (boaUser != null) {
|
||||
UserContext.setUserId(boaUser.getId());
|
||||
UserContext.setUserName(boaUser.getName() != null ? boaUser.getName() : "unknown");
|
||||
UserContext.setAccount(boaUser.getAccount());
|
||||
UserContext.setType(boaUser.getType());
|
||||
if (boaUser.getRoleIds() != null && !boaUser.getRoleIds().isEmpty()
|
||||
&& boaUser.getRoleIds().get(0) != null) {
|
||||
UserContext.setRoleId(boaUser.getRoleIds().get(0));
|
||||
}
|
||||
Integer schId = boaUser.getSchool() == null ? null : boaUser.getSchool().getId();
|
||||
if (schId == null && boaUser.getCurrentRole() != null) {
|
||||
schId = boaUser.getCurrentRole().getSchId();
|
||||
}
|
||||
UserContext.setSchId(schId);
|
||||
log.debug("UserContext set from BoaUser: id={}, name={}", boaUser.getId(), boaUser.getName());
|
||||
} else if (loginUser != null) {
|
||||
UserContext.setUserName(loginUser.getDisplayName() != null
|
||||
? loginUser.getDisplayName() : loginUser.getLoginName());
|
||||
UserContext.setAccount(loginUser.getLoginName());
|
||||
log.debug("UserContext set from LoginUser: loginName={}", loginUser.getLoginName());
|
||||
} else {
|
||||
UserContext.setUserName("anonymous");
|
||||
log.warn("No BoaUser/LoginUser found, using anonymous");
|
||||
}
|
||||
|
||||
// 超管(TYPE=1)不绑定具体学校,schId 统一置 0
|
||||
if (USER_TYPE_SUPER_ADMIN.equals(UserContext.getType())) {
|
||||
UserContext.setSchId(0);
|
||||
log.debug("Super admin detected(TYPE=1), force schId = 0");
|
||||
}
|
||||
|
||||
if (request != null) {
|
||||
UserContext.setIp(getClientIp(request));
|
||||
}
|
||||
}
|
||||
|
||||
private String getClientIp(HttpServletRequest request) {
|
||||
String ip = request.getHeader("X-Forwarded-For");
|
||||
if (ip != null && !ip.isEmpty() && !"unknown".equalsIgnoreCase(ip)) {
|
||||
int idx = ip.indexOf(',');
|
||||
return idx > 0 ? ip.substring(0, idx).trim() : ip.trim();
|
||||
}
|
||||
ip = request.getHeader("X-Real-IP");
|
||||
if (ip != null && !ip.isEmpty() && !"unknown".equalsIgnoreCase(ip)) {
|
||||
return ip.trim();
|
||||
}
|
||||
return request.getRemoteAddr();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package com.yunhai.familyactivity.aspect;
|
||||
|
||||
import com.yunhai.familyactivity.annotation.OperationLog;
|
||||
import com.yunhai.familyactivity.entity.OperationLogRecord;
|
||||
import com.yunhai.familyactivity.repository.OperationLogRepository;
|
||||
import com.yunhai.familyactivity.utils.UserContext;
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
import org.aspectj.lang.annotation.AfterReturning;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
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.time.LocalDateTime;
|
||||
import java.util.Arrays;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 操作日志切面:拦截标注 {@link OperationLog} 的方法,成功返回后落库 operation_log。
|
||||
* 操作人/学校/IP 取自 {@link UserContext}(由 ControllerUserContextAspect 在同一请求线程填充,
|
||||
* 此处同步落库以保证上下文可见,写操作频率低无性能问题)。
|
||||
*/
|
||||
@Aspect
|
||||
@Component
|
||||
public class OperationLogAspect {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(OperationLogAspect.class);
|
||||
|
||||
/** content 落库最大长度 */
|
||||
private static final int CONTENT_MAX_LEN = 3000;
|
||||
|
||||
@Autowired
|
||||
private OperationLogRepository operationLogRepository;
|
||||
|
||||
@AfterReturning("@annotation(operationLog)")
|
||||
public void recordLog(JoinPoint joinPoint, OperationLog operationLog) {
|
||||
try {
|
||||
OperationLogRecord record = new OperationLogRecord();
|
||||
record.setOperatorId(UserContext.getUserId());
|
||||
record.setOperator(UserContext.getUserName());
|
||||
record.setIp(UserContext.getIp());
|
||||
record.setModule(operationLog.module());
|
||||
record.setContent(truncate(buildContent(operationLog, joinPoint)));
|
||||
record.setSchId(UserContext.getSchId());
|
||||
record.setOperateTime(LocalDateTime.now());
|
||||
|
||||
HttpServletRequest request = getCurrentRequest();
|
||||
if (request != null) {
|
||||
record.setRequestUri(request.getRequestURI());
|
||||
record.setRequestMethod(request.getMethod());
|
||||
}
|
||||
operationLogRepository.save(record);
|
||||
} catch (Exception e) {
|
||||
// 日志失败不影响业务
|
||||
log.warn("[operation-log] 落库失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private String buildContent(OperationLog operationLog, JoinPoint joinPoint) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (!operationLog.content().isEmpty()) {
|
||||
sb.append(operationLog.content());
|
||||
}
|
||||
Object[] args = joinPoint.getArgs();
|
||||
if (args != null && args.length > 0) {
|
||||
String params = Arrays.stream(args)
|
||||
.filter(a -> !(a instanceof javax.servlet.ServletRequest)
|
||||
&& !(a instanceof javax.servlet.ServletResponse))
|
||||
.map(a -> {
|
||||
try {
|
||||
return String.valueOf(a);
|
||||
} catch (Exception e) {
|
||||
return "?";
|
||||
}
|
||||
})
|
||||
.collect(Collectors.joining(", "));
|
||||
if (!params.isEmpty()) {
|
||||
sb.append(sb.length() > 0 ? ";参数: " : "参数: ").append(params);
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private String truncate(String s) {
|
||||
if (s == null) {
|
||||
return "";
|
||||
}
|
||||
return s.length() > CONTENT_MAX_LEN ? s.substring(0, CONTENT_MAX_LEN) : s;
|
||||
}
|
||||
|
||||
private HttpServletRequest getCurrentRequest() {
|
||||
ServletRequestAttributes attrs =
|
||||
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
return attrs != null ? attrs.getRequest() : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.yunhai.familyactivity.common.listener;
|
||||
|
||||
import com.yunhai.familyactivity.entity.BaseEntity;
|
||||
import com.yunhai.platform.auth.LoginUser;
|
||||
import com.yunhai.platform.auth.UserContextHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.persistence.PrePersist;
|
||||
import javax.persistence.PreUpdate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* JPA 审计监听器:在实体持久化/更新前自动填充创建人/更新人(平台当前登录人)与时间戳。
|
||||
* 对应 {@link BaseEntity} 的 createBy / updateBy / createTime / updateTime 字段。
|
||||
* 当前登录人取自基础平台 {@link UserContextHolder}。
|
||||
*/
|
||||
@Component
|
||||
public class NoticeAuditListener {
|
||||
|
||||
@PrePersist
|
||||
public void prePersist(BaseEntity entity) {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
entity.setCreateTime(now);
|
||||
entity.setUpdateTime(now);
|
||||
String account = currentAccount();
|
||||
if (account != null) {
|
||||
if (entity.getCreateBy() == null) {
|
||||
entity.setCreateBy(account);
|
||||
}
|
||||
entity.setUpdateBy(account);
|
||||
}
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
public void preUpdate(BaseEntity entity) {
|
||||
entity.setUpdateTime(LocalDateTime.now());
|
||||
String account = currentAccount();
|
||||
if (account != null) {
|
||||
entity.setUpdateBy(account);
|
||||
}
|
||||
}
|
||||
|
||||
private String currentAccount() {
|
||||
LoginUser user = UserContextHolder.get();
|
||||
return user == null ? null : user.getLoginName();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.yunhai.familyactivity.common.validation;
|
||||
|
||||
/**
|
||||
* JSR-303 分组校验标记接口(对齐 UserBase 规范)。
|
||||
* Create:新建场景必填字段;Update:编辑场景必填字段(通常含 id)。
|
||||
*/
|
||||
public interface ValidationGroups {
|
||||
interface Create {}
|
||||
interface Update {}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.yunhai.familyactivity.config;
|
||||
|
||||
/**
|
||||
* 全局路径常量。与 UserBase 一致:API 统一挂在 /{platform}/ 之下(platform = api / manage / mobiapi)。
|
||||
*/
|
||||
public enum Config {
|
||||
Inst;
|
||||
|
||||
public static final String API_PATH = "/{platform}"; // manage / api / mobiapi,前面有/后面无/,拼接子路径时用"/"分隔
|
||||
public static final String OPEN_API_PATH = "/oapi";
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.yunhai.familyactivity.config;
|
||||
|
||||
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
|
||||
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
/**
|
||||
* Jackson 时间序列化全局约定(对齐旧 .NET 版 "yyyy-MM-dd HH:mm:ss"):
|
||||
* 接口出入参中的 LocalDateTime/LocalDate 统一使用该格式,避免 ISO 的 "T" 分隔符
|
||||
* (如 2026-09-01T16:38:40)直接暴露给前端表格。
|
||||
*/
|
||||
@Configuration
|
||||
public class JacksonConfig {
|
||||
|
||||
private static final DateTimeFormatter DATE_TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||
|
||||
@Bean
|
||||
public Jackson2ObjectMapperBuilderCustomizer jacksonCustomizer() {
|
||||
return builder -> builder
|
||||
.serializers(
|
||||
new LocalDateTimeSerializer(DATE_TIME),
|
||||
new LocalDateSerializer(DATE))
|
||||
.deserializers(
|
||||
new LocalDateTimeDeserializer(DATE_TIME),
|
||||
new LocalDateDeserializer(DATE));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.yunhai.familyactivity.config;
|
||||
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* 接口文档(springdoc-openapi,替代旧 Springfox)。
|
||||
*/
|
||||
@Configuration
|
||||
public class SwaggerConfig {
|
||||
|
||||
@Bean
|
||||
public OpenAPI familyActivityOpenAPI() {
|
||||
return new OpenAPI()
|
||||
.info(new Info()
|
||||
.title("活动信息发布接口文档")
|
||||
.description("武校家校信息发布 · 活动信息发布子应用:活动发布/报名回执/统计/班级树/字典/附件(springdoc-openapi)")
|
||||
.version("1.0"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
package com.yunhai.familyactivity.controller.api;
|
||||
|
||||
import com.yunhai.familyactivity.annotation.OperationLog;
|
||||
import com.yunhai.familyactivity.config.Config;
|
||||
import com.yunhai.familyactivity.model.BasePageResult;
|
||||
import com.yunhai.familyactivity.model.activity.ActivityDetailVo;
|
||||
import com.yunhai.familyactivity.model.activity.ActivityPageResult;
|
||||
import com.yunhai.familyactivity.model.activity.ActivityQuery;
|
||||
import com.yunhai.familyactivity.model.activity.ActivityRequest;
|
||||
import com.yunhai.familyactivity.model.activity.ActivityStatisticsVo;
|
||||
import com.yunhai.familyactivity.model.activity.ActivityVo;
|
||||
import com.yunhai.familyactivity.model.activity.InitializeRedVo;
|
||||
import com.yunhai.familyactivity.model.activity.MyActivitiesVo;
|
||||
import com.yunhai.familyactivity.model.activity.MyActivityVo;
|
||||
import com.yunhai.familyactivity.model.activity.NextPrevVo;
|
||||
import com.yunhai.familyactivity.model.activity.ReadGroup;
|
||||
import com.yunhai.familyactivity.model.activity.ReviewRequest;
|
||||
import com.yunhai.familyactivity.model.activity.SignupDetailVo;
|
||||
import com.yunhai.familyactivity.model.activity.SignupRequest;
|
||||
import com.yunhai.familyactivity.model.activity.SignupsResultVo;
|
||||
import com.yunhai.familyactivity.service.IActivityService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 活动信息发布接口(/api/activity/*,对齐前端 activityApi)。
|
||||
* <p>返回裸对象,由平台 ApiResponseAdvice 自动包装为 {code, msg, data};
|
||||
* 写操作标注 {@link OperationLog} 落库 operation_log。</p>
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping(value = Config.API_PATH + "/activity/")
|
||||
public class ActivityController {
|
||||
|
||||
@Autowired
|
||||
private IActivityService activityService;
|
||||
|
||||
// ============================ 家长端收件箱 ============================
|
||||
|
||||
/** 已发布活动列表(家长/学生视角,含未读总数) */
|
||||
@GetMapping("List")
|
||||
public ActivityPageResult<ActivityVo> list(ActivityQuery query) {
|
||||
return activityService.list(query);
|
||||
}
|
||||
|
||||
/** 清单:一键全部已读 */
|
||||
@PostMapping("InitializeRed")
|
||||
@OperationLog(module = "活动管理", content = "一键全部已读")
|
||||
public InitializeRedVo initializeRed() {
|
||||
return activityService.initializeRed();
|
||||
}
|
||||
|
||||
// ============================ 我发布的 / 全部活动 ============================
|
||||
|
||||
/** 我发布的 / 全部活动列表(scope="user" 只看自己) */
|
||||
@GetMapping("SendList")
|
||||
public BasePageResult<ActivityVo> sendList(ActivityQuery query) {
|
||||
return activityService.sendList(query);
|
||||
}
|
||||
|
||||
// ============================ 详情 ============================
|
||||
|
||||
/** 查看详情(type=1 标读;type=0 编辑/管理视角不标读) */
|
||||
@GetMapping("Detail")
|
||||
public ActivityDetailVo detail(@RequestParam("serid") Integer serid,
|
||||
@RequestParam(value = "type", defaultValue = "0") Integer type) {
|
||||
return activityService.detail(serid, type);
|
||||
}
|
||||
|
||||
/** 编辑回显(不标读) */
|
||||
@GetMapping("GetDetail")
|
||||
public ActivityDetailVo getDetail(@RequestParam("serid") Integer serid) {
|
||||
return activityService.getDetail(serid);
|
||||
}
|
||||
|
||||
/** 上一篇/下一篇 */
|
||||
@GetMapping("GetNextPrev")
|
||||
public NextPrevVo getNextPrev(@RequestParam("serid") Integer serid,
|
||||
@RequestParam(value = "type", defaultValue = "next") String type) {
|
||||
return activityService.getNextPrev(serid, type);
|
||||
}
|
||||
|
||||
/** 阅读情况(按班级分组读/未读名单) */
|
||||
@GetMapping("ReadDetail")
|
||||
public List<ReadGroup> readDetail(@RequestParam("serid") Integer serid) {
|
||||
return activityService.readDetail(serid);
|
||||
}
|
||||
|
||||
/** 追加接收班级(补发,自动补回执记录) */
|
||||
@PostMapping("ReceiveClasses")
|
||||
@OperationLog(module = "活动管理", content = "追加接收班级")
|
||||
public void receiveClasses(@RequestParam("serid") Integer serid,
|
||||
@RequestParam("classes") String classes) {
|
||||
activityService.receiveClasses(serid, classes);
|
||||
}
|
||||
|
||||
// ============================ 发布/编辑/删除 ============================
|
||||
|
||||
/** 新建活动,返回活动 id */
|
||||
@PostMapping("Create")
|
||||
@OperationLog(module = "活动管理", content = "新建活动")
|
||||
public Integer create(@RequestBody ActivityRequest request) {
|
||||
return activityService.create(request);
|
||||
}
|
||||
|
||||
/** 编辑活动(含 sourceId 复用场景) */
|
||||
@PostMapping("Edit")
|
||||
@OperationLog(module = "活动管理", content = "编辑活动")
|
||||
public void edit(@RequestBody ActivityRequest request) {
|
||||
activityService.edit(request);
|
||||
}
|
||||
|
||||
/** 批量删除(级联清回执/已读/附件) */
|
||||
@PostMapping("Delete")
|
||||
@OperationLog(module = "活动管理", content = "删除活动")
|
||||
public void delete(@RequestBody List<Integer> ids) {
|
||||
activityService.delete(ids);
|
||||
}
|
||||
|
||||
/** 撤回(家长端不可见) */
|
||||
@PostMapping("UserPull")
|
||||
@OperationLog(module = "活动管理", content = "撤回活动")
|
||||
public void userPull(@RequestParam("serid") Integer serid) {
|
||||
activityService.userPull(serid);
|
||||
}
|
||||
|
||||
/** 置顶/取消置顶 */
|
||||
@PostMapping("SetTopmost")
|
||||
@OperationLog(module = "活动管理", content = "设置置顶")
|
||||
public void setTopmost(@RequestParam("serid") Integer serid,
|
||||
@RequestParam(value = "topmost", defaultValue = "1") Integer topmost) {
|
||||
activityService.setTopmost(serid, topmost);
|
||||
}
|
||||
|
||||
/** 催办未读 */
|
||||
@PostMapping("Expedite")
|
||||
@OperationLog(module = "活动管理", content = "催办未读")
|
||||
public void expedite(@RequestParam("serid") Integer serid) {
|
||||
activityService.expedite(serid);
|
||||
}
|
||||
|
||||
// ============================ 报名回执 ============================
|
||||
|
||||
/** 某活动的回执/报名名单(按班级分组)+ 汇总 */
|
||||
@GetMapping("Signups")
|
||||
public SignupsResultVo signups(@RequestParam("serid") Integer serid,
|
||||
@RequestParam(value = "signupStatus", defaultValue = "all") String signupStatus) {
|
||||
return activityService.signups(serid, signupStatus);
|
||||
}
|
||||
|
||||
/** 单条回执详情 */
|
||||
@GetMapping("SignupDetail")
|
||||
public SignupDetailVo signupDetail(@RequestParam("signupId") Integer signupId) {
|
||||
return activityService.signupDetail(signupId);
|
||||
}
|
||||
|
||||
/** 处理回执:action=confirm/cancel/waitlist */
|
||||
@PostMapping("Review")
|
||||
@OperationLog(module = "活动管理", content = "处理回执")
|
||||
public void review(@RequestBody ReviewRequest request) {
|
||||
activityService.review(request);
|
||||
}
|
||||
|
||||
/** 催报名,返回 {reminded} */
|
||||
@PostMapping("Remind")
|
||||
@OperationLog(module = "活动管理", content = "催报名")
|
||||
public Map<String, Object> remind(@RequestParam("serid") Integer serid) {
|
||||
return activityService.remind(serid);
|
||||
}
|
||||
|
||||
/** 导出回执名单(JSON 包装 {csv},前端取 csv 字段生成文件下载) */
|
||||
@GetMapping("ExportSignups")
|
||||
public Map<String, Object> exportSignups(@RequestParam("serid") Integer serid) {
|
||||
return activityService.exportSignups(serid);
|
||||
}
|
||||
|
||||
// ============================ 家长端(v2) ============================
|
||||
|
||||
/** 活动广场(含分桶计数) */
|
||||
@GetMapping("MyActivities")
|
||||
public MyActivitiesVo myActivities(ActivityQuery query) {
|
||||
return activityService.myActivities(query);
|
||||
}
|
||||
|
||||
/** 我对某活动的详情(同时标读;confirm 模式自动确认) */
|
||||
@GetMapping("MySignup")
|
||||
public MyActivityVo mySignup(@RequestParam("serid") Integer serid) {
|
||||
return activityService.mySignup(serid);
|
||||
}
|
||||
|
||||
/** 报名 / 修改报名,返回 {signupId, status} */
|
||||
@PostMapping("Signup")
|
||||
@OperationLog(module = "活动管理", content = "家长报名")
|
||||
public Map<String, Object> signup(@RequestBody SignupRequest request) {
|
||||
return activityService.signup(request);
|
||||
}
|
||||
|
||||
/** 取消报名 */
|
||||
@PostMapping("CancelSignup")
|
||||
@OperationLog(module = "活动管理", content = "取消报名")
|
||||
public void cancelSignup(@RequestParam("serid") Integer serid) {
|
||||
activityService.cancelSignup(serid);
|
||||
}
|
||||
|
||||
// ============================ 统计 / 附件 ============================
|
||||
|
||||
/** 活动统计 */
|
||||
@GetMapping("Statis")
|
||||
public ActivityStatisticsVo statistics(@RequestParam(value = "startTime", required = false) String startTime,
|
||||
@RequestParam(value = "endTime", required = false) String endTime) {
|
||||
return activityService.statistics(startTime, endTime);
|
||||
}
|
||||
|
||||
/** 全部附件打包下载 */
|
||||
@GetMapping("AllFileDown")
|
||||
public void allFileDown(@RequestParam("dataid") Integer dataid, HttpServletResponse response) {
|
||||
activityService.allFileDown(dataid, response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
package com.yunhai.familyactivity.controller.api;
|
||||
|
||||
import com.yunhai.familyactivity.config.Config;
|
||||
import com.yunhai.familyactivity.model.UpFileResultVo;
|
||||
import com.yunhai.familyactivity.model.bsch.SchClassDto;
|
||||
import com.yunhai.familyactivity.model.bsch.SchGradeDto;
|
||||
import com.yunhai.familyactivity.model.bsch.SchInfoDto;
|
||||
import com.yunhai.familyactivity.model.bsch.SchStudentDto;
|
||||
import com.yunhai.familyactivity.service.IClassService;
|
||||
import com.yunhai.familyactivity.service.IFileService;
|
||||
import com.yunhai.familyactivity.service.IUserCenterService;
|
||||
import com.yunhai.familyactivity.utils.UserContext;
|
||||
import com.yunhai.platform.apistd.BizException;
|
||||
import com.yunhai.platform.auth.LoginUser;
|
||||
import com.yunhai.platform.auth.UserContextHolder;
|
||||
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||
import com.yunhai.platform.utils.model.BoaModule;
|
||||
import com.yunhai.platform.utils.model.BoaUser;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 平台代理域接口(base/)。
|
||||
* <p>路径前缀:{@code Config.API_PATH + "/base/"}(原硬编码已修正)。
|
||||
* 承载平台代理接口:GetUserInfo / GetUserMod / GetSchoolAll / GetClassTree / SetRole / GetGroupUser / Logout,
|
||||
* 业务逻辑已下沉 {@link IUserCenterService},本控制器仅做"取参 → 调 Service → 返回"。
|
||||
* 返回裸对象,由 ApiResponseAdvice 自动包装为 {code, msg, data}。</p>
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping(value = Config.API_PATH + "/base/")
|
||||
public class BaseApiController {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(BaseApiController.class);
|
||||
|
||||
/** 会话属性:当前视角(teacher 发布端 / parent 家长端,前端 App.vue 按路由前缀切换) */
|
||||
public static final String SESSION_VIEW_ROLE = "activityViewRole";
|
||||
|
||||
@Autowired
|
||||
private IUserCenterService userCenterService;
|
||||
|
||||
@Autowired
|
||||
private IFileService fileService;
|
||||
|
||||
@Autowired
|
||||
private IClassService classService;
|
||||
|
||||
// ============================ FileUpload ============================
|
||||
|
||||
/**
|
||||
* 附件上传(单文件)。
|
||||
* 前端: POST base/FileUpload (form-data: file)
|
||||
* 返回: { fileid, host, fileurl, name, type, size }
|
||||
*/
|
||||
@PostMapping("FileUpload")
|
||||
public UpFileResultVo fileUpload(@RequestParam("file") MultipartFile file) {
|
||||
return fileService.uploadToResult(file, "activity");
|
||||
}
|
||||
|
||||
// ============================ GetUserInfo ============================
|
||||
|
||||
/**
|
||||
* 获取当前登录用户信息(CAS 会话注入)。
|
||||
*/
|
||||
@GetMapping("GetUserInfo")
|
||||
public BoaUser getUserInfo(BoaUser authUserWithDept) {
|
||||
return authUserWithDept;
|
||||
}
|
||||
|
||||
// ============================ GetUserMod ============================
|
||||
|
||||
/**
|
||||
* 获取当前用户已授权的模块列表(左侧菜单树)。
|
||||
* 模块树组装已下沉 IUserCenterService。
|
||||
*/
|
||||
@GetMapping("GetUserMod")
|
||||
public List<BoaModule> getUserMod(BoaUser authUserWithDept) {
|
||||
Integer schId = authUserWithDept.getCurrentRole() != null
|
||||
? authUserWithDept.getCurrentRole().getSchId() : null;
|
||||
return userCenterService.getUserModuleTree(authUserWithDept.getId(), schId);
|
||||
}
|
||||
|
||||
// ============================ GetSchoolAll ============================
|
||||
|
||||
/**
|
||||
* 获取当前用户学校组及用户(树形结构)。
|
||||
* 组树组装/教师挂组/递归建树逻辑已下沉 IUserCenterService。
|
||||
*/
|
||||
@GetMapping("GetSchoolAll")
|
||||
public List<Map<String, Object>> getSchoolAll(BoaUser authUserWithDept) {
|
||||
Integer schId = authUserWithDept.getCurrentRole() != null
|
||||
? authUserWithDept.getCurrentRole().getSchId() : null;
|
||||
return userCenterService.getSchoolAllTree(schId);
|
||||
}
|
||||
|
||||
// ============================ GetGroupUser ============================
|
||||
|
||||
/**
|
||||
* 获取指定组内用户(教师 + 管理员)。
|
||||
* 空桩已改为显式报错(避免前端误以为组内无用户),由 IUserCenterService 统一处理。
|
||||
*/
|
||||
@GetMapping("GetGroupUser")
|
||||
public List<Map<String, Object>> getGroupUser(
|
||||
@RequestParam("gid") Integer gid,
|
||||
@RequestParam(value = "role", defaultValue = "2") Integer role) {
|
||||
return userCenterService.getGroupUser(gid, role);
|
||||
}
|
||||
|
||||
// ============================ GetClassTree ============================
|
||||
|
||||
/**
|
||||
* 年级 → 班级树(活动发布对象:选择到班级一级,叶子带 studentCount)。
|
||||
* 前端: GET base/GetClassTree,返回 ClassTreeNode[]
|
||||
*/
|
||||
@GetMapping("GetClassTree")
|
||||
public List<Map<String, Object>> getClassTree(BoaUser authUserWithDept) {
|
||||
Integer schId = authUserWithDept.getCurrentRole() != null
|
||||
? authUserWithDept.getCurrentRole().getSchId() : null;
|
||||
if (schId == null) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "缺少学校信息");
|
||||
}
|
||||
SchInfoDto sch = classService.getSchFirst(schId);
|
||||
if (sch == null || !StringUtils.hasText(sch.getSchSerId())) {
|
||||
throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "获取学校信息失败");
|
||||
}
|
||||
List<SchGradeDto> grades = classService.getSchGradeList(sch.getSchSerId());
|
||||
List<SchClassDto> classes = classService.getSchClassList(sch.getSchSerId());
|
||||
// 每班学生数(一次取全校学生统计,失败降级为不计学生数)
|
||||
Map<Integer, Long> stuCount = new HashMap<>();
|
||||
try {
|
||||
List<SchStudentDto> students = classService.getSchStuList(sch.getSchSerId());
|
||||
stuCount = students.stream()
|
||||
.filter(s -> s.getClassId() != null)
|
||||
.collect(Collectors.groupingBy(SchStudentDto::getClassId, Collectors.counting()));
|
||||
} catch (Exception e) {
|
||||
log.warn("[GetClassTree] 学生数统计失败,降级为 0: {}", e.getMessage());
|
||||
}
|
||||
Map<Integer, Long> finalStuCount = stuCount;
|
||||
Map<Integer, List<SchClassDto>> byGrade = classes.stream()
|
||||
.filter(c -> c.getGradeId() != null)
|
||||
.collect(Collectors.groupingBy(SchClassDto::getGradeId));
|
||||
|
||||
List<Map<String, Object>> tree = new ArrayList<>();
|
||||
for (SchGradeDto grade : grades) {
|
||||
Map<String, Object> gradeNode = new LinkedHashMap<>();
|
||||
gradeNode.put("id", grade.getId());
|
||||
gradeNode.put("name", grade.getName());
|
||||
gradeNode.put("isParent", true);
|
||||
List<Map<String, Object>> children = new ArrayList<>();
|
||||
long gradeStuCount = 0;
|
||||
for (SchClassDto cls : byGrade.getOrDefault(grade.getId(), Collections.emptyList())) {
|
||||
long count = finalStuCount.getOrDefault(cls.getClassId(), 0L);
|
||||
gradeStuCount += count;
|
||||
Map<String, Object> classNode = new LinkedHashMap<>();
|
||||
classNode.put("id", cls.getClassId());
|
||||
classNode.put("name", StringUtils.hasText(cls.getClassName()) ? cls.getClassName() : String.valueOf(cls.getClassId()));
|
||||
classNode.put("isParent", false);
|
||||
classNode.put("studentCount", count);
|
||||
classNode.put("children", new ArrayList<>());
|
||||
children.add(classNode);
|
||||
}
|
||||
gradeNode.put("studentCount", gradeStuCount);
|
||||
gradeNode.put("children", children);
|
||||
tree.add(gradeNode);
|
||||
}
|
||||
return tree;
|
||||
}
|
||||
|
||||
// ============================ SetRole ============================
|
||||
|
||||
/**
|
||||
* 端分离入口:切换当前视角(teacher 发布端 / parent 家长端)。
|
||||
* 前端: POST base/SetRole (JSON: {as}),需在拉取用户信息前调用。
|
||||
*/
|
||||
@PostMapping("SetRole")
|
||||
public Map<String, Object> setRole(@RequestBody(required = false) Map<String, String> body,
|
||||
HttpServletRequest request) {
|
||||
String as = body != null ? body.get("as") : null;
|
||||
if (!"teacher".equals(as) && !"parent".equals(as)) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "不支持的角色: " + as);
|
||||
}
|
||||
request.getSession(true).setAttribute(SESSION_VIEW_ROLE, as);
|
||||
log.info("[SetRole] 切换视角: {} (account={})", as, UserContext.getAccount());
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("role", as);
|
||||
return result;
|
||||
}
|
||||
|
||||
// ============================ Logout ============================
|
||||
|
||||
/**
|
||||
* 退出登录。
|
||||
* 认证由平台网关处理,此处仅返回成功。
|
||||
*/
|
||||
@PostMapping("Logout")
|
||||
public void logout() {
|
||||
LoginUser user = UserContextHolder.get();
|
||||
log.info("[logout] 用户退出登录: {}", user == null ? null : user.getLoginName());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.yunhai.familyactivity.controller.api;
|
||||
|
||||
import com.yunhai.familyactivity.config.Config;
|
||||
import com.yunhai.familyactivity.service.IContactsService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 联系组接口(contacts/)。
|
||||
* <p>路径前缀:{@code Config.API_PATH + "/contacts/"}(原硬编码已修正为 Config 拼接)。
|
||||
* 方法名已为 PascalCase 且写操作已是 POST,保持不动。
|
||||
* 返回裸对象,由 ApiResponseAdvice 自动包装为 {code, msg, data}。</p>
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping(value = Config.API_PATH + "/contacts/")
|
||||
public class ContactsController {
|
||||
|
||||
@Autowired
|
||||
private IContactsService contactsService;
|
||||
|
||||
/**
|
||||
* 添加联系组。
|
||||
*/
|
||||
@PostMapping("AddGroup")
|
||||
public void addGroup(
|
||||
@RequestParam("name") String name,
|
||||
@RequestParam(value = "type", required = false, defaultValue = "1") String type) {
|
||||
contactsService.addGroup(name, Integer.parseInt(type));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取联系组列表。
|
||||
*/
|
||||
@GetMapping("GetGroup")
|
||||
public Map<String, Object> getGroup(
|
||||
@RequestParam(value = "page", defaultValue = "0") Integer page,
|
||||
@RequestParam(value = "count", defaultValue = "0") Integer count,
|
||||
@RequestParam(value = "type", defaultValue = "1") Integer type) {
|
||||
return contactsService.getGroup(page, count, type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑联系组。
|
||||
*/
|
||||
@PostMapping("EditGroup")
|
||||
public void editGroup(
|
||||
@RequestParam("name") String name,
|
||||
@RequestParam("id") String id,
|
||||
@RequestParam(value = "type", required = false, defaultValue = "1") String type) {
|
||||
contactsService.editGroup(Integer.parseInt(id), name, Integer.parseInt(type));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除联系组(支持批量)。
|
||||
*/
|
||||
@PostMapping("DelGroup")
|
||||
public void delGroup(@RequestParam("ids") String ids) {
|
||||
contactsService.delGroup(ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置联系组教师。
|
||||
*/
|
||||
@PostMapping("SetContactsTea")
|
||||
public void setContactsTea(
|
||||
@RequestParam("gid") String gid,
|
||||
@RequestParam(value = "ids", required = false) String ids) {
|
||||
contactsService.setContactsTea(Integer.parseInt(gid), ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取联系组教师列表。
|
||||
*/
|
||||
@GetMapping("GetContactsTea")
|
||||
public Map<String, Object> getContactsTea(
|
||||
@RequestParam("gid") String gid,
|
||||
@RequestParam(value = "page", defaultValue = "0") Integer page,
|
||||
@RequestParam(value = "count", defaultValue = "0") Integer count) {
|
||||
return contactsService.getContactsTea(gid, page, count);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取联系组树。
|
||||
*/
|
||||
@GetMapping("GetGroupTree")
|
||||
public List<Map<String, Object>> getGroupTree(@RequestParam("type") Integer type) {
|
||||
return contactsService.getGroupTree(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取部门角色组树。
|
||||
*/
|
||||
@GetMapping("GetDeptRoleGroupTree")
|
||||
public List<Map<String, Object>> getDeptRoleGroupTree() {
|
||||
return contactsService.getDeptRoleGroupTree();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.yunhai.familyactivity.controller.api;
|
||||
|
||||
import com.yunhai.familyactivity.config.Config;
|
||||
import com.yunhai.familyactivity.service.IDictService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 系统字典接口(主办部门 / 活动分类域)。
|
||||
* <p>路径前缀:{@code Config.API_PATH + "/dict/"}(原 base/ 前缀已修正)。
|
||||
* 写操作一律 POST(原 DelDept/OrdbyDicIds 为 GET 写操作,已 POST 化)。
|
||||
* <p>type 参数:dept=主办部门(默认,兼容旧前端)、category=活动分类。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping(value = Config.API_PATH + "/dict/")
|
||||
public class DictController {
|
||||
|
||||
@Autowired
|
||||
private IDictService dictService;
|
||||
|
||||
/**
|
||||
* 获取系统字典(下拉框用,type=1 主办部门 / type=2 活动分类)。
|
||||
*/
|
||||
@GetMapping("List")
|
||||
public List<Map<String, Object>> list(@RequestParam(value = "type", defaultValue = "0") Integer type) {
|
||||
return dictService.getSysDic(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统字典管理列表(type=dept 主办部门 / type=category 活动分类)。
|
||||
*/
|
||||
@GetMapping("ManageList")
|
||||
public List<Map<String, Object>> manageList(@RequestParam(value = "type", defaultValue = "dept") String type) {
|
||||
return dictService.getDictMangeList(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加字典项(type 缺省为主办部门)。
|
||||
*/
|
||||
@PostMapping("Add")
|
||||
public void add(@RequestParam("name") String name,
|
||||
@RequestParam(value = "type", defaultValue = "dept") String type) {
|
||||
dictService.addDict(type, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新字典项。
|
||||
*/
|
||||
@PostMapping("Update")
|
||||
public void update(
|
||||
@RequestParam("SerID") Integer serID,
|
||||
@RequestParam("Name") String name,
|
||||
@RequestParam(value = "Enable", required = false, defaultValue = "1") Integer enable,
|
||||
@RequestParam(value = "type", defaultValue = "dept") String type) {
|
||||
dictService.updateDict(type, serID, name, enable);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除字典项(原 GET DelDept,已 POST 化)。
|
||||
*/
|
||||
@PostMapping("Delete")
|
||||
public void delete(@RequestParam("serid") Integer serID,
|
||||
@RequestParam(value = "type", defaultValue = "dept") String type) {
|
||||
dictService.delDict(type, serID);
|
||||
}
|
||||
|
||||
/**
|
||||
* 字典项排序(原 GET OrdbyDicIds,已 POST 化)。
|
||||
*/
|
||||
@PostMapping("Sort")
|
||||
public void sort(@RequestParam("ids") String ids,
|
||||
@RequestParam(value = "type", defaultValue = "dept") String type) {
|
||||
dictService.sortDict(type, ids);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package com.yunhai.familyactivity.controller.api;
|
||||
|
||||
import com.yunhai.familyactivity.config.Config;
|
||||
import com.yunhai.familyactivity.model.UpFileResultVo;
|
||||
import com.yunhai.familyactivity.model.UpFileVo;
|
||||
import com.yunhai.familyactivity.service.IFileService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 附件域接口(file/)。
|
||||
* <p>路径前缀:{@code Config.API_PATH + "/file/"}(原 base/ 前缀已修正为独立文件域)。
|
||||
* 方法名统一 PascalCase;写操作一律 POST。
|
||||
* 返回裸对象,由 ApiResponseAdvice 自动包装为 {code, msg, data}。</p>
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping(value = Config.API_PATH + "/file/")
|
||||
public class FileController {
|
||||
|
||||
@Autowired
|
||||
private IFileService fileService;
|
||||
|
||||
/**
|
||||
* 附件上传(单文件)。
|
||||
* 前端: POST file/Upload (form-data)
|
||||
* 返回: { fileid, host, fileurl, name, type, size }
|
||||
*/
|
||||
@PostMapping("Upload")
|
||||
public UpFileResultVo upload(@RequestParam("file") MultipartFile file) {
|
||||
return fileService.uploadToResult(file, "activity");
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片上传(多文件,返回数组)。
|
||||
* 前端: POST file/UploadImage (form-data, accept=image/*)
|
||||
*/
|
||||
@PostMapping("UploadImage")
|
||||
public List<UpFileVo> uploadImage(@RequestParam("file") MultipartFile[] files) {
|
||||
return fileService.uploadImages(files, "activity");
|
||||
}
|
||||
/**
|
||||
* 文件下载(浏览器直接 location.href 跳转)。
|
||||
*/
|
||||
@GetMapping("Down")
|
||||
public void down(@RequestParam("id") Integer id, HttpServletResponse response) {
|
||||
fileService.download(id, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件在线预览(inline 输出,前端取 Blob 交给 open-file-viewer 渲染)。
|
||||
*/
|
||||
@GetMapping("Preview")
|
||||
public void preview(@RequestParam("id") Integer id,
|
||||
@RequestParam(value = "type", required = false) Integer type,
|
||||
HttpServletResponse response) {
|
||||
fileService.preview(id, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文件。
|
||||
*/
|
||||
@PostMapping("Delete")
|
||||
public void delete(@RequestParam("fileId") Integer fileId) {
|
||||
fileService.delete(fileId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询某通知下的附件列表。
|
||||
*/
|
||||
@GetMapping("List")
|
||||
public List<UpFileVo> list(@RequestParam("topicId") Integer topicId) {
|
||||
return fileService.listByTopic("activity", topicId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量绑定附件到通知。
|
||||
*/
|
||||
@PostMapping("Bind")
|
||||
public void bind(@RequestParam("fileIds") List<Integer> fileIds,
|
||||
@RequestParam("topicId") Integer topicId) {
|
||||
fileService.bindToTopic(fileIds, topicId);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package com.yunhai.familyactivity.entity;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Table;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 活动主表(fa_activity)。对齐前端 ActivityVo / ActivityRequest:
|
||||
* 发布对象到班级一级(receiveList="ALL" 或班级 id 逗号串),回执方式 receiptMode
|
||||
* (none 仅通知 / signup 需报名 / confirm 需确认)。流程状态 actStatus 按时间推导,不落库。
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(name = "fa_activity")
|
||||
public class Activity extends BaseEntity {
|
||||
|
||||
/** 学校ID(数据隔离) */
|
||||
@Column(name = "sch_id")
|
||||
private Integer schId;
|
||||
|
||||
/** 标题 */
|
||||
@Column(name = "title", length = 200)
|
||||
private String title;
|
||||
|
||||
/** 活动内容(富文本) */
|
||||
@Column(name = "content", columnDefinition = "TEXT")
|
||||
private String content;
|
||||
|
||||
/** 活动分类:武术比赛 / 家长会 / 文艺汇演 / 体检 / 放假通知 / 研学实践 / 安全演练 / 其他 */
|
||||
@Column(name = "category", length = 50)
|
||||
private String category;
|
||||
|
||||
/** 活动地点 */
|
||||
@Column(name = "venue", length = 200)
|
||||
private String venue;
|
||||
|
||||
/** 活动开始时间 */
|
||||
@Column(name = "start_time")
|
||||
private LocalDateTime startTime;
|
||||
|
||||
/** 活动结束时间 */
|
||||
@Column(name = "end_time")
|
||||
private LocalDateTime endTime;
|
||||
|
||||
/** 报名截止时间 */
|
||||
@Column(name = "signup_deadline")
|
||||
private LocalDateTime signupDeadline;
|
||||
|
||||
/** 回执方式:none / signup / confirm */
|
||||
@Column(name = "receipt_mode", length = 16)
|
||||
private String receiptMode = "none";
|
||||
|
||||
/** 名额上限(0 不限) */
|
||||
@Column(name = "quota")
|
||||
private Integer quota = 0;
|
||||
|
||||
/** 是否允许家长随行:0 否 / 1 是 */
|
||||
@Column(name = "allow_accompany")
|
||||
private Integer allowAccompany = 0;
|
||||
|
||||
/** 活动费用(元) */
|
||||
@Column(name = "cost", precision = 10, scale = 2)
|
||||
private BigDecimal cost = BigDecimal.ZERO;
|
||||
|
||||
/** 主办部门ID(sys_dict.dict_key) */
|
||||
@Column(name = "dept_id", length = 50)
|
||||
private String deptId;
|
||||
|
||||
/** 主办部门名称(冗余) */
|
||||
@Column(name = "dept_name", length = 100)
|
||||
private String deptName;
|
||||
|
||||
/** 发布人ID(平台用户ID;家长端=学生ID) */
|
||||
@Column(name = "publisher_id")
|
||||
private Integer publisherId;
|
||||
|
||||
/** 发布人姓名 */
|
||||
@Column(name = "publisher_name", length = 50)
|
||||
private String publisherName;
|
||||
|
||||
/** 发送状态:0 草稿 / 1 已发布 */
|
||||
@Column(name = "status")
|
||||
private Integer status = 0;
|
||||
|
||||
/** 置顶:0 否 / 1 是 */
|
||||
@Column(name = "topmost")
|
||||
private Integer topmost = 0;
|
||||
|
||||
/** 撤回:0 否 / 1 是(撤回后家长端不可见) */
|
||||
@Column(name = "is_pulled")
|
||||
private Integer isPulled = 0;
|
||||
|
||||
/** 定时发送时间 */
|
||||
@Column(name = "timing_send_time")
|
||||
private LocalDateTime timingSendTime;
|
||||
|
||||
/** 短信发送标记:0 否 / 1 是(仅存储,催办通道本期占位) */
|
||||
@Column(name = "sms_flag")
|
||||
private Integer smsFlag = 0;
|
||||
|
||||
/** 接收对象类型:class=班级 */
|
||||
@Column(name = "receive_type", length = 16)
|
||||
private String receiveType = "class";
|
||||
|
||||
/** 接收列表:"ALL" 或班级 id 逗号串 */
|
||||
@Column(name = "receive_list", columnDefinition = "TEXT")
|
||||
private String receiveList;
|
||||
|
||||
/** 转发/复用的源活动ID */
|
||||
@Column(name = "source_id")
|
||||
private Integer sourceId;
|
||||
|
||||
public boolean isPublished() {
|
||||
return status != null && status == 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.yunhai.familyactivity.entity;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Table;
|
||||
import javax.persistence.UniqueConstraint;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 活动已读记录(fa_activity_read)。
|
||||
* 已读按「班级」粒度(前端 classRead 约定):应收/已读人数按班级 studentCount 累加;
|
||||
* 同一活动同一班级只有一条记录,由唯一约束兜底。
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(name = "fa_activity_read",
|
||||
uniqueConstraints = @UniqueConstraint(columnNames = {"activity_id", "class_id"}))
|
||||
public class ActivityRead extends BaseEntity {
|
||||
|
||||
/** 活动ID */
|
||||
@Column(name = "activity_id", nullable = false)
|
||||
private Integer activityId;
|
||||
|
||||
/** 班级ID */
|
||||
@Column(name = "class_id", length = 50, nullable = false)
|
||||
private String classId;
|
||||
|
||||
/** 首读学生ID(家长端以学生身份打开,存学生ID) */
|
||||
@Column(name = "student_id")
|
||||
private Integer studentId;
|
||||
|
||||
/** 阅读时间 */
|
||||
@Column(name = "read_time")
|
||||
private LocalDateTime readTime;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.yunhai.familyactivity.entity;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Table;
|
||||
import javax.persistence.UniqueConstraint;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 活动回执/报名记录(fa_activity_signup)。对齐前端 SignupBrief / SignupDetailVo:
|
||||
* 一个学生对一个活动一条回执(可反复修改),唯一约束 (activity_id, student_id) 兜底。
|
||||
* 发布时按接收班级×学生预生成(status=none),保证应收人数、未回执名单可查。
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(name = "fa_activity_signup",
|
||||
uniqueConstraints = @UniqueConstraint(columnNames = {"activity_id", "student_id"}))
|
||||
public class ActivitySignup extends BaseEntity {
|
||||
|
||||
/** 活动ID */
|
||||
@Column(name = "activity_id", nullable = false)
|
||||
private Integer activityId;
|
||||
|
||||
/** 学校ID(数据隔离) */
|
||||
@Column(name = "sch_id")
|
||||
private Integer schId;
|
||||
|
||||
/** 学生ID */
|
||||
@Column(name = "student_id")
|
||||
private Integer studentId;
|
||||
|
||||
/** 学生姓名 */
|
||||
@Column(name = "student_name", length = 50)
|
||||
private String studentName;
|
||||
|
||||
/** 班级ID */
|
||||
@Column(name = "class_id", length = 50)
|
||||
private String classId;
|
||||
|
||||
/** 班级名称 */
|
||||
@Column(name = "class_name", length = 100)
|
||||
private String className;
|
||||
|
||||
/** 家长姓名 */
|
||||
@Column(name = "guardian_name", length = 50)
|
||||
private String guardianName;
|
||||
|
||||
/** 联系电话 */
|
||||
@Column(name = "guardian_phone", length = 20)
|
||||
private String guardianPhone;
|
||||
|
||||
/** 参加人数(含学生本人) */
|
||||
@Column(name = "join_count")
|
||||
private Integer joinCount = 1;
|
||||
|
||||
/** 回执状态:none / signed / confirmed / canceled */
|
||||
@Column(name = "status", length = 16)
|
||||
private String status = "none";
|
||||
|
||||
/** 备注(过敏史、随行人数说明等) */
|
||||
@Column(name = "remark", length = 500)
|
||||
private String remark;
|
||||
|
||||
/** 回执时间 */
|
||||
@Column(name = "signed_at")
|
||||
private LocalDateTime signedAt;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.yunhai.familyactivity.entity;
|
||||
|
||||
import com.yunhai.familyactivity.common.listener.NoticeAuditListener;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 所有实体的公共基类:自增主键 + 创建人/时间 + 更新人/时间(由 {@link NoticeAuditListener} 自动填充)。
|
||||
*/
|
||||
@MappedSuperclass
|
||||
@Getter
|
||||
@Setter
|
||||
@EntityListeners(NoticeAuditListener.class)
|
||||
public abstract class BaseEntity implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Integer id;
|
||||
|
||||
/**
|
||||
* 创建人登录账号
|
||||
*/
|
||||
@Column(name = "create_by", updatable = false)
|
||||
private String createBy;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@Column(name = "create_time", updatable = false)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 更新人登录账号
|
||||
*/
|
||||
@Column(name = "update_by")
|
||||
private String updateBy;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@Column(name = "update_time")
|
||||
private LocalDateTime updateTime;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.yunhai.familyactivity.entity;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Table;
|
||||
|
||||
/**
|
||||
* 联系组。对应原 .NET Contacts_Group 表。
|
||||
*/
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "contacts_group")
|
||||
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
|
||||
public class ContactsGroup extends BaseEntity {
|
||||
|
||||
/** 组名称 */
|
||||
@Column(name = "group_name", length = 200)
|
||||
private String groupName;
|
||||
|
||||
/** 创建人ID(平台用户ID) */
|
||||
@Column(name = "creator_id")
|
||||
private Integer creatorId;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.yunhai.familyactivity.entity;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Table;
|
||||
import javax.persistence.UniqueConstraint;
|
||||
|
||||
/**
|
||||
* 联系组-用户关联。对应原 .NET Contacts_Tea 表。
|
||||
*/
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "contacts_group_user",
|
||||
uniqueConstraints = @UniqueConstraint(name = "uk_group_user",
|
||||
columnNames = {"group_id", "user_id"}))
|
||||
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
|
||||
public class ContactsGroupUser extends BaseEntity {
|
||||
|
||||
/** 联系组ID */
|
||||
@Column(name = "group_id", nullable = false)
|
||||
private Integer groupId;
|
||||
|
||||
/** 用户ID(基础平台用户ID) */
|
||||
@Column(name = "user_id", nullable = false)
|
||||
private Integer userId;
|
||||
|
||||
/** 用户姓名 */
|
||||
@Column(name = "user_name", length = 64)
|
||||
private String userName;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.yunhai.familyactivity.entity;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.GeneratedValue;
|
||||
import javax.persistence.GenerationType;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.Table;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 业务操作日志(表 operation_log)。
|
||||
* 参考 UserclassService {@code com.yunhai.bschclassmanage.entity.OperationLogRecord}:
|
||||
* 由 {@link com.yunhai.familyactivity.aspect.OperationLogAspect} 从 {@link com.yunhai.familyactivity.utils.UserContext}
|
||||
* 取操作人/IP/学校写入,与系统审计日志物理隔离。
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Entity
|
||||
@Table(name = "operation_log")
|
||||
public class OperationLogRecord {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
/** 操作人平台用户ID */
|
||||
private Integer operatorId;
|
||||
|
||||
/** 操作时间 */
|
||||
private LocalDateTime operateTime;
|
||||
|
||||
/** 操作人姓名 */
|
||||
private String operator;
|
||||
|
||||
/** 客户端IP */
|
||||
private String ip;
|
||||
|
||||
/** 模块名称(如 "活动管理") */
|
||||
private String module;
|
||||
|
||||
/** 操作内容(含关键参数摘要,超长截断) */
|
||||
private String content;
|
||||
|
||||
/** 学校ID(数据隔离;超管=0) */
|
||||
private Integer schId;
|
||||
|
||||
/** 请求URI */
|
||||
private String requestUri;
|
||||
|
||||
/** 请求方法 GET/POST */
|
||||
private String requestMethod;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.yunhai.familyactivity.entity;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Table;
|
||||
|
||||
/**
|
||||
* 系统字典(发文部门等)。对应原 .NET Common_DeptDic 表。
|
||||
*/
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "sys_dict")
|
||||
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
|
||||
public class SysDict extends BaseEntity {
|
||||
|
||||
/** 字典类型(如 "dept"=发文部门) */
|
||||
@Column(name = "dict_type", length = 50)
|
||||
private String dictType;
|
||||
|
||||
/** 字典 key(原 CodeID) */
|
||||
@Column(name = "dict_key", length = 100)
|
||||
private String dictKey;
|
||||
|
||||
/** 字典 value(显示名称,原 Name) */
|
||||
@Column(name = "dict_value", length = 200)
|
||||
private String dictValue;
|
||||
|
||||
/** 排序号 */
|
||||
@Column(name = "sort_order")
|
||||
private Integer sortOrder = 0;
|
||||
|
||||
/** 是否启用:0 禁用 / 1 启用(原 Enable) */
|
||||
@Column(name = "enable_flag")
|
||||
private Integer enableFlag = 1;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.yunhai.familyactivity.entity;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Table;
|
||||
|
||||
/**
|
||||
* 附件文件记录。对应原 .NET Common_UpFiles 表。
|
||||
* <p>
|
||||
* 存储文件元信息(真实文件名、存储路径、大小、类型),物理文件存储在
|
||||
* application.yml 的 file.upload-path 目录下。
|
||||
*/
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "up_file")
|
||||
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
|
||||
public class UpFile extends BaseEntity {
|
||||
|
||||
/** 应用类型(如 "notice"),用于区分不同业务的附件 */
|
||||
@Column(name = "topic_type", length = 50)
|
||||
private String topicType;
|
||||
|
||||
/** 关联主题ID(如通知 serid) */
|
||||
@Column(name = "topic_id")
|
||||
private Integer topicId;
|
||||
|
||||
/** 文件真实名称(用户上传时的原始文件名) */
|
||||
@Column(name = "file_true_name", length = 255)
|
||||
private String fileTrueName;
|
||||
|
||||
/** 文件存储路径(相对路径,如 notice/20260823/uuid.pdf) */
|
||||
@Column(name = "file_name", length = 500)
|
||||
private String fileName;
|
||||
|
||||
/** 文件大小(字节) */
|
||||
@Column(name = "file_size")
|
||||
private Integer fileSize;
|
||||
|
||||
/** 文件后缀名(如 pdf, docx, jpg) */
|
||||
@Column(name = "file_type", length = 20)
|
||||
private String fileType;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.yunhai.familyactivity.enums;
|
||||
|
||||
/**
|
||||
* 活动流程状态(对齐前端 ActivityStatus)。
|
||||
* <ul>
|
||||
* <li>draft 草稿 → signup 报名中 → closed 已截止 → finished 已结束</li>
|
||||
* </ul>
|
||||
* closed/finished 按当前时间与报名截止/结束时间动态推导,不落库。
|
||||
*/
|
||||
public enum ActivityStatusEnum {
|
||||
|
||||
DRAFT("draft", "草稿"),
|
||||
SIGNUP("signup", "报名中"),
|
||||
CLOSED("closed", "已截止"),
|
||||
FINISHED("finished", "已结束");
|
||||
|
||||
private final String code;
|
||||
private final String desc;
|
||||
|
||||
ActivityStatusEnum(String code, String desc) {
|
||||
this.code = code;
|
||||
this.desc = desc;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public String getDesc() {
|
||||
return desc;
|
||||
}
|
||||
|
||||
public static ActivityStatusEnum of(String code) {
|
||||
if (code != null) {
|
||||
for (ActivityStatusEnum e : values()) {
|
||||
if (e.code.equalsIgnoreCase(code)) {
|
||||
return e;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 推导流程状态。
|
||||
*
|
||||
* @param published 是否已发布(false=草稿)
|
||||
* @param receiptMode 回执方式(none 仅通知无报名语义)
|
||||
* @param signupDeadline 报名截止时间(可空)
|
||||
* @param endTime 活动结束时间(可空)
|
||||
* @param now 当前时间
|
||||
*/
|
||||
public static ActivityStatusEnum derive(boolean published, String receiptMode,
|
||||
java.time.LocalDateTime signupDeadline,
|
||||
java.time.LocalDateTime endTime,
|
||||
java.time.LocalDateTime now) {
|
||||
if (!published) {
|
||||
return DRAFT;
|
||||
}
|
||||
if (endTime != null && !now.isBefore(endTime)) {
|
||||
return FINISHED;
|
||||
}
|
||||
boolean hasReceipt = receiptMode != null && !"none".equalsIgnoreCase(receiptMode);
|
||||
if (hasReceipt && signupDeadline != null && !now.isBefore(signupDeadline)) {
|
||||
return CLOSED;
|
||||
}
|
||||
return SIGNUP;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.yunhai.familyactivity.enums;
|
||||
|
||||
/**
|
||||
* 回执方式(对齐前端 ReceiptMode)。
|
||||
* <ul>
|
||||
* <li>none 仅通知(阅读即知悉,只统计已读)</li>
|
||||
* <li>signup 需报名(家长填报参加人数,可带随行)</li>
|
||||
* <li>confirm 需确认(家长端打开活动即自动确认)</li>
|
||||
* </ul>
|
||||
*/
|
||||
public enum ReceiptModeEnum {
|
||||
|
||||
NONE("none", "仅通知"),
|
||||
SIGNUP("signup", "需报名"),
|
||||
CONFIRM("confirm", "需确认");
|
||||
|
||||
private final String code;
|
||||
private final String desc;
|
||||
|
||||
ReceiptModeEnum(String code, String desc) {
|
||||
this.code = code;
|
||||
this.desc = desc;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public String getDesc() {
|
||||
return desc;
|
||||
}
|
||||
|
||||
public static ReceiptModeEnum of(String code) {
|
||||
if (code != null) {
|
||||
for (ReceiptModeEnum e : values()) {
|
||||
if (e.code.equalsIgnoreCase(code)) {
|
||||
return e;
|
||||
}
|
||||
}
|
||||
}
|
||||
return NONE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.yunhai.familyactivity.enums;
|
||||
|
||||
/**
|
||||
* 单条回执(报名)状态(对齐前端 SignupStatus)。
|
||||
*/
|
||||
public enum SignupStatusEnum {
|
||||
|
||||
NONE("none", "未回执"),
|
||||
SIGNED("signed", "已报名"),
|
||||
CONFIRMED("confirmed", "已确认"),
|
||||
CANCELED("canceled", "已取消");
|
||||
|
||||
private final String code;
|
||||
private final String desc;
|
||||
|
||||
SignupStatusEnum(String code, String desc) {
|
||||
this.code = code;
|
||||
this.desc = desc;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public String getDesc() {
|
||||
return desc;
|
||||
}
|
||||
|
||||
public static SignupStatusEnum of(String code) {
|
||||
if (code != null) {
|
||||
for (SignupStatusEnum e : values()) {
|
||||
if (e.code.equalsIgnoreCase(code)) {
|
||||
return e;
|
||||
}
|
||||
}
|
||||
}
|
||||
return NONE;
|
||||
}
|
||||
|
||||
/** 严格匹配(区分于 of() 的 NONE 兜底),用于请求参数校验 */
|
||||
public static boolean isValid(String code) {
|
||||
if (code != null) {
|
||||
for (SignupStatusEnum e : values()) {
|
||||
if (e.code.equalsIgnoreCase(code)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.yunhai.familyactivity.model;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 统一分页查询入参。pageIndex 从 1 起。
|
||||
*/
|
||||
@Data
|
||||
public class BasePageQuery {
|
||||
private int pageIndex = 1;
|
||||
private int pageSize = 10;
|
||||
/** 通用模糊搜索关键词 */
|
||||
private String keyword;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.yunhai.familyactivity.model;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 统一分页响应外壳。
|
||||
*/
|
||||
@Data
|
||||
public class BasePageResult<T> {
|
||||
/** 总记录数 */
|
||||
private int total;
|
||||
/** 当前页数据 */
|
||||
private List<T> content;
|
||||
/** 当前页码(从 1 起) */
|
||||
private int pageIndex;
|
||||
/** 每页条数 */
|
||||
private int pageSize;
|
||||
/** 总页数 */
|
||||
private int totalPages;
|
||||
/** 是否还有下一页 */
|
||||
private boolean hasNext;
|
||||
|
||||
public BasePageResult(List<T> content, int total, int pageIndex, int pageSize, int totalPages, boolean hasNext) {
|
||||
this.content = content;
|
||||
this.total = total;
|
||||
this.pageIndex = pageIndex;
|
||||
this.pageSize = pageSize;
|
||||
this.totalPages = totalPages;
|
||||
this.hasNext = hasNext;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package com.yunhai.familyactivity.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 班级管理接口-教师(bschclass {@code /oapi/DbClass/GetSchteaList} 响应项)。
|
||||
* 对应 C# BestSchool.Entity.Model.SchTeaList。
|
||||
*/
|
||||
public class SchTeaList {
|
||||
|
||||
/** 学校 GUID(SchSerID) */
|
||||
@JsonProperty("SchSerID")
|
||||
private String schSerID;
|
||||
|
||||
@JsonProperty("TeaTel")
|
||||
private String teaTel;
|
||||
|
||||
/** 教师姓名 */
|
||||
@JsonProperty("TeaName")
|
||||
private String teaName;
|
||||
|
||||
/** 教师用户ID */
|
||||
@JsonProperty("UserTeaSerID")
|
||||
private Integer userTeaSerID;
|
||||
|
||||
@JsonProperty("TeaIDCard")
|
||||
private String teaIDCard;
|
||||
|
||||
/** 教师工号 */
|
||||
@JsonProperty("TeaNo")
|
||||
private String teaNo;
|
||||
|
||||
/** 教师所属组ID列表(兼容旧字段 GroupIds) */
|
||||
@JsonProperty("GroupIds")
|
||||
private List<Integer> groupIds;
|
||||
|
||||
/** 教师所属组ID(接口现行字段 GroupID,单个) */
|
||||
@JsonProperty("GroupID")
|
||||
private Integer groupId;
|
||||
|
||||
public Integer getGroupId() {
|
||||
return groupId;
|
||||
}
|
||||
|
||||
public void setGroupId(Integer groupId) {
|
||||
this.groupId = groupId;
|
||||
}
|
||||
|
||||
public String getSchSerID() {
|
||||
return schSerID;
|
||||
}
|
||||
|
||||
public void setSchSerID(String schSerID) {
|
||||
this.schSerID = schSerID;
|
||||
}
|
||||
|
||||
public String getTeaTel() {
|
||||
return teaTel;
|
||||
}
|
||||
|
||||
public void setTeaTel(String teaTel) {
|
||||
this.teaTel = teaTel;
|
||||
}
|
||||
|
||||
public String getTeaName() {
|
||||
return teaName;
|
||||
}
|
||||
|
||||
public void setTeaName(String teaName) {
|
||||
this.teaName = teaName;
|
||||
}
|
||||
|
||||
public Integer getUserTeaSerID() {
|
||||
return userTeaSerID;
|
||||
}
|
||||
|
||||
public void setUserTeaSerID(Integer userTeaSerID) {
|
||||
this.userTeaSerID = userTeaSerID;
|
||||
}
|
||||
|
||||
public String getTeaIDCard() {
|
||||
return teaIDCard;
|
||||
}
|
||||
|
||||
public void setTeaIDCard(String teaIDCard) {
|
||||
this.teaIDCard = teaIDCard;
|
||||
}
|
||||
|
||||
public String getTeaNo() {
|
||||
return teaNo;
|
||||
}
|
||||
|
||||
public void setTeaNo(String teaNo) {
|
||||
this.teaNo = teaNo;
|
||||
}
|
||||
|
||||
public List<Integer> getGroupIds() {
|
||||
return groupIds;
|
||||
}
|
||||
|
||||
public void setGroupIds(List<Integer> groupIds) {
|
||||
this.groupIds = groupIds;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.yunhai.familyactivity.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 文件上传结果视图对象(替代原 Map<String,Object> 返回)。
|
||||
* 使用 @JsonProperty 保持旧字段名兼容前端渲染。
|
||||
*/
|
||||
@Data
|
||||
public class UpFileResultVo {
|
||||
|
||||
@JsonProperty("fileid")
|
||||
private Integer fileid;
|
||||
|
||||
@JsonProperty("name")
|
||||
private String name;
|
||||
|
||||
@JsonProperty("fileurl")
|
||||
private String fileurl;
|
||||
|
||||
@JsonProperty("host")
|
||||
private String host;
|
||||
|
||||
@JsonProperty("type")
|
||||
private String type;
|
||||
|
||||
@JsonProperty("size")
|
||||
private Integer size;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.yunhai.familyactivity.model;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 附件视图对象(对应原 .NET FileObj)。
|
||||
*/
|
||||
@Data
|
||||
public class UpFileVo {
|
||||
|
||||
/** 文件记录ID */
|
||||
private Integer id;
|
||||
|
||||
/** 文件真实名称 */
|
||||
private String fileTrueName;
|
||||
|
||||
/** 文件存储路径(相对路径) */
|
||||
private String fileName;
|
||||
|
||||
/** 文件大小(字节) */
|
||||
private Integer fileSize;
|
||||
|
||||
/** 文件后缀名 */
|
||||
private String fileType;
|
||||
|
||||
/** 关联主题ID */
|
||||
private Integer topicId;
|
||||
|
||||
/** 应用类型 */
|
||||
private String topicType;
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package com.yunhai.familyactivity.model;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户可访问模块(左菜单)视图对象。
|
||||
* <p>将基础平台返回的扁平 {@code List<BoaModule>} 转换为前端 layout(参考 UserBase)期望的树结构:
|
||||
* {@code modId}(路由高亮匹配)/{@code page}(跳转路径)/{@code children}(子菜单)。</p>
|
||||
*/
|
||||
public class UserModuleVo {
|
||||
|
||||
/** 模块主键 id(基础平台 module id) */
|
||||
private Integer id;
|
||||
|
||||
/** 菜单高亮匹配用:等价于后端模块 id */
|
||||
private Integer modId;
|
||||
|
||||
/** 模块名称 */
|
||||
private String name;
|
||||
|
||||
/** 父级模块 id(0 表示根节点) */
|
||||
private Integer parentId;
|
||||
|
||||
/** 前端路由路径(由 BoaModule.path 映射而来) */
|
||||
private String page;
|
||||
|
||||
/** 层级(0=根分组) */
|
||||
private Integer level;
|
||||
|
||||
/** 菜单图标 */
|
||||
private String icon;
|
||||
|
||||
/** 排序号 */
|
||||
private Integer orderNumber;
|
||||
|
||||
/** 是否可用 */
|
||||
private Boolean usable;
|
||||
|
||||
/** 子菜单 */
|
||||
private List<UserModuleVo> children = new ArrayList<>();
|
||||
|
||||
public Integer getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Integer id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Integer getModId() {
|
||||
return modId;
|
||||
}
|
||||
|
||||
public void setModId(Integer modId) {
|
||||
this.modId = modId;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Integer getParentId() {
|
||||
return parentId;
|
||||
}
|
||||
|
||||
public void setParentId(Integer parentId) {
|
||||
this.parentId = parentId;
|
||||
}
|
||||
|
||||
public String getPage() {
|
||||
return page;
|
||||
}
|
||||
|
||||
public void setPage(String page) {
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
public Integer getLevel() {
|
||||
return level;
|
||||
}
|
||||
|
||||
public void setLevel(Integer level) {
|
||||
this.level = level;
|
||||
}
|
||||
|
||||
public String getIcon() {
|
||||
return icon;
|
||||
}
|
||||
|
||||
public void setIcon(String icon) {
|
||||
this.icon = icon;
|
||||
}
|
||||
|
||||
public Integer getOrderNumber() {
|
||||
return orderNumber;
|
||||
}
|
||||
|
||||
public void setOrderNumber(Integer orderNumber) {
|
||||
this.orderNumber = orderNumber;
|
||||
}
|
||||
|
||||
public Boolean getUsable() {
|
||||
return usable;
|
||||
}
|
||||
|
||||
public void setUsable(Boolean usable) {
|
||||
this.usable = usable;
|
||||
}
|
||||
|
||||
public List<UserModuleVo> getChildren() {
|
||||
return children;
|
||||
}
|
||||
|
||||
public void setChildren(List<UserModuleVo> children) {
|
||||
this.children = children;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.yunhai.familyactivity.model.activity;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 活动详情附件项(对齐前端 ActivityDetailFileItem)。
|
||||
*/
|
||||
@Data
|
||||
public class ActivityDetailFileItem {
|
||||
|
||||
private Integer id;
|
||||
|
||||
/** 显示名 */
|
||||
private String name;
|
||||
|
||||
/** 文件大小(字节) */
|
||||
private Long size;
|
||||
|
||||
/** 文件记录ID */
|
||||
private Integer fileid;
|
||||
|
||||
/** 存储文件名 */
|
||||
private String filename;
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package com.yunhai.familyactivity.model.activity;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 活动详情视图对象(对齐前端 ActivityDetailVo)。
|
||||
* 保留旧字段名(Contents / DeptID / DeptName / TeaName / EditDate / DsfsTime / ChaoSend),
|
||||
* 便于复用既有详情排版。
|
||||
*/
|
||||
@Data
|
||||
public class ActivityDetailVo {
|
||||
|
||||
/** 活动 id(旧字段名 serid) */
|
||||
@JsonProperty("serid")
|
||||
private Integer serid;
|
||||
|
||||
private String title;
|
||||
|
||||
/** 活动内容(旧字段名 Contents) */
|
||||
@JsonProperty("Contents")
|
||||
private String contents;
|
||||
|
||||
/** 主办部门ID(旧字段名 DeptID) */
|
||||
@JsonProperty("DeptID")
|
||||
private String deptId;
|
||||
|
||||
/** 主办部门名称(旧字段名 DeptName) */
|
||||
@JsonProperty("DeptName")
|
||||
private String deptName;
|
||||
|
||||
/** 发布人姓名(旧字段名 TeaName) */
|
||||
@JsonProperty("TeaName")
|
||||
private String teaName;
|
||||
|
||||
/** 发布时间(旧字段名 EditDate,已格式化) */
|
||||
@JsonProperty("EditDate")
|
||||
private String editDate;
|
||||
|
||||
/** 应读人数 */
|
||||
@JsonProperty("numcount")
|
||||
private long numcount;
|
||||
|
||||
/** 已读人数 */
|
||||
@JsonProperty("readnumcount")
|
||||
private long readnumcount;
|
||||
|
||||
/** 接收列表(旧字段名):"ALL" 或 班级 id 逗号串 */
|
||||
@JsonProperty("ReceiveList")
|
||||
private String receiveList;
|
||||
|
||||
/** 定时发送时间(旧字段名 DsfsTime) */
|
||||
@JsonProperty("DsfsTime")
|
||||
private String dsfsTime;
|
||||
|
||||
/** 是否定时发送 */
|
||||
@JsonProperty("isDsfs")
|
||||
private boolean dsfs;
|
||||
|
||||
/** 活动分类 */
|
||||
private String category;
|
||||
|
||||
/** 活动地点 */
|
||||
private String venue;
|
||||
|
||||
/** 活动开始时间 */
|
||||
private String startTime;
|
||||
|
||||
/** 活动结束时间 */
|
||||
private String endTime;
|
||||
|
||||
/** 报名截止时间 */
|
||||
private String signupDeadline;
|
||||
|
||||
/** 回执方式 */
|
||||
private String receiptMode;
|
||||
|
||||
private String receiptModeDesc;
|
||||
|
||||
/** 名额上限(0 不限) */
|
||||
private Integer quota;
|
||||
|
||||
/** 是否允许家长随行 */
|
||||
private Integer allowAccompany;
|
||||
|
||||
/** 活动费用(元) */
|
||||
private java.math.BigDecimal cost;
|
||||
|
||||
/** 活动流程状态 */
|
||||
private String actStatus;
|
||||
|
||||
private String actStatusDesc;
|
||||
|
||||
/** 回执/报名进度(草稿或仅通知为 null) */
|
||||
private ActivityProgress progress;
|
||||
|
||||
/** 附件列表(旧字段名 ChaoSend) */
|
||||
@JsonProperty("ChaoSend")
|
||||
private List<ActivityDetailFileItem> chaoSend;
|
||||
|
||||
/** 接收对象列表:"all" 或 班级 id 数组 */
|
||||
private Object tlist;
|
||||
|
||||
/** 接收对象类型:class=班级 */
|
||||
private String receiveType;
|
||||
|
||||
/** 接收对象可读文本 */
|
||||
private String receiveText;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.yunhai.familyactivity.model.activity;
|
||||
|
||||
import com.yunhai.familyactivity.model.BasePageResult;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 活动分页结果(对齐前端 ActivityPageResult:分页外壳 + 当前用户未读总数)。
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ActivityPageResult<T> extends BasePageResult<T> {
|
||||
|
||||
/** 当前用户未读活动总数 */
|
||||
private long unreadCount;
|
||||
|
||||
public ActivityPageResult(List<T> content, int total, int pageIndex, int pageSize,
|
||||
int totalPages, boolean hasNext, long unreadCount) {
|
||||
super(content, total, pageIndex, pageSize, totalPages, hasNext);
|
||||
this.unreadCount = unreadCount;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.yunhai.familyactivity.model.activity;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 活动回执/报名进度(对齐前端 ActivityProgress)。
|
||||
* receiptMode=none 或草稿时 progress 为 null(不产生回执记录,只统计已读)。
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ActivityProgress {
|
||||
|
||||
/** 应收人数 */
|
||||
private int should;
|
||||
|
||||
/** 已报名人数 */
|
||||
private int signed;
|
||||
|
||||
/** 已确认人数 */
|
||||
private int confirmed;
|
||||
|
||||
/** 已取消/请假人数 */
|
||||
private int canceled;
|
||||
|
||||
/** 未回执人数 */
|
||||
private int unsigned;
|
||||
|
||||
/** 回执率(%) */
|
||||
private double signupRate;
|
||||
|
||||
/** 名额上限,0 表示不限 */
|
||||
private int quota;
|
||||
|
||||
/** 已用名额(按随行人数合计) */
|
||||
private int usedQuota;
|
||||
|
||||
/** 是否名额已满 */
|
||||
private boolean full;
|
||||
|
||||
/** 待处理回执(名额超额/待定),统计用,列表场景恒为 0 */
|
||||
private int pendingReview;
|
||||
|
||||
public ActivityProgress(int should, int signed, int confirmed, int canceled, int unsigned,
|
||||
double signupRate, int quota, int usedQuota, boolean full) {
|
||||
this(should, signed, confirmed, canceled, unsigned, signupRate, quota, usedQuota, full, 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.yunhai.familyactivity.model.activity;
|
||||
|
||||
import com.yunhai.familyactivity.model.BasePageQuery;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 活动列表查询参数(对齐前端 ActivityQuery,GET query 绑定)。
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ActivityQuery extends BasePageQuery {
|
||||
|
||||
/** 列表范围(管理端):"user" 只看自己发布的,不传=全部 */
|
||||
private String scope;
|
||||
|
||||
/** 主办部门ID */
|
||||
private String deptId;
|
||||
|
||||
/** 活动分类筛选 */
|
||||
private String category;
|
||||
|
||||
/** 流程状态筛选:draft/signup/closed/finished,缺省或 all=全部 */
|
||||
private String actStatus;
|
||||
|
||||
/** 发布人ID */
|
||||
private Integer publisherId;
|
||||
|
||||
/** 开始时间(yyyy-MM-dd 短日期,筛选活动开始时间下界) */
|
||||
private String startTime;
|
||||
|
||||
/** 结束时间(yyyy-MM-dd 短日期,筛选活动开始时间上界) */
|
||||
private String endTime;
|
||||
|
||||
/** 阅读状态过滤(仅家长端):0 全部 / 1 未读 / 2 已读 */
|
||||
private Integer readType;
|
||||
|
||||
/** 排序:1 时间倒序 / 2 时间正序 */
|
||||
private String orderByTime;
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.yunhai.familyactivity.model.activity;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 活动发布/编辑入参(对齐前端 ActivityRequest,@RequestBody JSON)。
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class ActivityRequest {
|
||||
|
||||
/** 标题 */
|
||||
private String title;
|
||||
|
||||
/** 活动内容(富文本) */
|
||||
private String content;
|
||||
|
||||
/** 活动分类 */
|
||||
private String category;
|
||||
|
||||
/** 活动地点 */
|
||||
private String venue;
|
||||
|
||||
/** 活动开始时间 yyyy-MM-dd HH:mm:ss */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime startTime;
|
||||
|
||||
/** 活动结束时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime endTime;
|
||||
|
||||
/** 报名截止时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime signupDeadline;
|
||||
|
||||
/** 回执方式:none / signup / confirm */
|
||||
private String receiptMode;
|
||||
|
||||
/** 名额上限(0 不限) */
|
||||
private Integer quota;
|
||||
|
||||
/** 是否允许家长随行:0 否 / 1 是 */
|
||||
private Integer allowAccompany;
|
||||
|
||||
/** 活动费用(元) */
|
||||
private BigDecimal cost;
|
||||
|
||||
/** 主办部门ID */
|
||||
private String deptId;
|
||||
|
||||
/** 主办部门名称 */
|
||||
private String deptName;
|
||||
|
||||
/** 存草稿:true 则不发布 */
|
||||
private Boolean draft;
|
||||
|
||||
/** 置顶:0 否 / 1 是 */
|
||||
private Integer topmost;
|
||||
|
||||
/** 接收对象:"ALL" 或 班级 id 逗号串(receiveType=class) */
|
||||
private String receiveList;
|
||||
|
||||
/** 接收对象类型:class=班级 */
|
||||
private String receiveType;
|
||||
|
||||
/** 附件文件ID列表(逗号分隔串) */
|
||||
private String fileIds;
|
||||
|
||||
/** 定时发送时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime timingSendTime;
|
||||
|
||||
/** 短信发送标记:0 否 / 1 是 */
|
||||
private Integer smsFlag;
|
||||
|
||||
/** 转发/复用的源活动ID */
|
||||
private Integer sourceId;
|
||||
|
||||
/** 编辑时的活动ID(前端编辑回显后提交带 serid 时使用,可空) */
|
||||
private Integer serid;
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package com.yunhai.familyactivity.model.activity;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 活动统计视图(对齐前端 ActivityStatisticsVo)。
|
||||
*/
|
||||
@Data
|
||||
public class ActivityStatisticsVo {
|
||||
|
||||
/** 活动总数 */
|
||||
private long total;
|
||||
|
||||
/** 已发布(含报名中/已截止/已结束) */
|
||||
private long publishedCount;
|
||||
|
||||
/** 我发布的活动数 */
|
||||
private long myActivityCount;
|
||||
|
||||
/** 累计回执/报名人次 */
|
||||
private long totalSignups;
|
||||
|
||||
/** 平均回执率(%) */
|
||||
private double avgSignupRate;
|
||||
|
||||
/** 待处理回执(名额超额或需人工确认) */
|
||||
private long pendingReview;
|
||||
|
||||
/** 按主办部门 */
|
||||
private List<PublisherCount> byPublisher;
|
||||
|
||||
/** 按月份 */
|
||||
private List<MonthCount> byMonth;
|
||||
|
||||
/** 按活动分类分布 */
|
||||
private List<CategoryCount> byCategory;
|
||||
|
||||
/** 我发布的活动·回执与已读 */
|
||||
private List<MyActivityItem> byActivity;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class PublisherCount {
|
||||
private String publisherName;
|
||||
private long count;
|
||||
}
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class MonthCount {
|
||||
private int year;
|
||||
private int month;
|
||||
private long count;
|
||||
}
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class CategoryCount {
|
||||
/** 显示名(分类文案) */
|
||||
private String label;
|
||||
/** 分类 key */
|
||||
private String key;
|
||||
private long count;
|
||||
}
|
||||
|
||||
/** 我发布的活动明细(基础信息 + ActivityProgress 展开字段) */
|
||||
@Data
|
||||
public static class MyActivityItem {
|
||||
private Integer id;
|
||||
private String title;
|
||||
private String category;
|
||||
private String venue;
|
||||
private String actStatus;
|
||||
private String actStatusDesc;
|
||||
/** 应收人数 */
|
||||
private int should;
|
||||
/** 已报名人数 */
|
||||
private int signed;
|
||||
/** 已确认人数 */
|
||||
private int confirmed;
|
||||
/** 已取消/请假人数 */
|
||||
private int canceled;
|
||||
/** 未回执人数 */
|
||||
private int unsigned;
|
||||
/** 回执率(%) */
|
||||
private double signupRate;
|
||||
/** 名额上限,0 表示不限 */
|
||||
private int quota;
|
||||
/** 已用名额(按随行人数合计) */
|
||||
private int usedQuota;
|
||||
/** 是否名额已满 */
|
||||
private boolean full;
|
||||
/** 已读人数 */
|
||||
private long readCount;
|
||||
/** 应读人数 */
|
||||
private long allowCount;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package com.yunhai.familyactivity.model.activity;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 活动视图对象(列表用,对齐前端 ActivityVo)。
|
||||
* 时间字段为格式化字符串 "yyyy-MM-dd HH:mm:ss" 或 null。
|
||||
*/
|
||||
@Data
|
||||
public class ActivityVo {
|
||||
|
||||
private Integer id;
|
||||
|
||||
private String title;
|
||||
|
||||
private String content;
|
||||
|
||||
/** 活动分类 */
|
||||
private String category;
|
||||
|
||||
/** 活动地点 */
|
||||
private String venue;
|
||||
|
||||
/** 活动开始时间 */
|
||||
private String startTime;
|
||||
|
||||
/** 活动结束时间 */
|
||||
private String endTime;
|
||||
|
||||
/** 报名截止时间 */
|
||||
private String signupDeadline;
|
||||
|
||||
/** 回执方式 */
|
||||
private String receiptMode;
|
||||
|
||||
private String receiptModeDesc;
|
||||
|
||||
/** 名额上限(0 不限) */
|
||||
private Integer quota;
|
||||
|
||||
/** 是否允许家长随行 */
|
||||
private Integer allowAccompany;
|
||||
|
||||
/** 活动费用(元) */
|
||||
private java.math.BigDecimal cost;
|
||||
|
||||
/** 发布人ID(平台用户ID;家长端=学生ID) */
|
||||
private Integer publisherId;
|
||||
|
||||
private String publisherName;
|
||||
|
||||
/** 主办部门 */
|
||||
private String deptId;
|
||||
|
||||
private String deptName;
|
||||
|
||||
/** 发送状态:0 草稿 / 1 已发布 */
|
||||
private Integer status;
|
||||
|
||||
private String statusDesc;
|
||||
|
||||
/** 活动流程状态 */
|
||||
private String actStatus;
|
||||
|
||||
private String actStatusDesc;
|
||||
|
||||
/** 置顶:0 否 / 1 是 */
|
||||
private Integer topmost;
|
||||
|
||||
/** 已读人数(按班级学生数累加) */
|
||||
private long readCount;
|
||||
|
||||
/** 应读人数(按班级学生数累加) */
|
||||
private long allowCount;
|
||||
|
||||
/** 当前用户是否已读 */
|
||||
private boolean read;
|
||||
|
||||
/** 是否撤回:0 否 / 1 是 */
|
||||
private Integer isPulled;
|
||||
|
||||
/** 定时发送时间 */
|
||||
private String timingSendTime;
|
||||
|
||||
/** 短信发送标记 */
|
||||
private Integer smsFlag;
|
||||
|
||||
/** 接收对象类型:class=班级 */
|
||||
private String receiveType;
|
||||
|
||||
/** 接收列表:"all" 或 班级 id 逗号串 */
|
||||
private String receiveList;
|
||||
|
||||
/** 接收对象可读文本(列表为摘要,超长时形如「高一 1班、2班、3班等30个班级」) */
|
||||
private String receiveText;
|
||||
|
||||
/** 接收对象完整可读文本(列表摘要对应的全文,供悬浮提示等场景;家长端列表不填充) */
|
||||
private String receiveTextFull;
|
||||
|
||||
/** 覆盖班级数 */
|
||||
private Integer classCount;
|
||||
|
||||
/** 回执/报名进度(草稿或仅通知为 null) */
|
||||
private ActivityProgress progress;
|
||||
|
||||
private String createBy;
|
||||
|
||||
private String createTime;
|
||||
|
||||
private String updateBy;
|
||||
|
||||
private String updateTime;
|
||||
|
||||
/** 附件ID串(逗号分隔,列表场景不填充) */
|
||||
private List<Integer> fileIds;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.yunhai.familyactivity.model.activity;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 一键全部已读结果(对齐前端 InitializeRedVo)。
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class InitializeRedVo {
|
||||
|
||||
/** 本次标读的活动数 */
|
||||
private int count;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.yunhai.familyactivity.model.activity;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 家长端活动列表(对齐前端 MyActivitiesVo:分桶计数 + 列表)。
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class MyActivitiesVo {
|
||||
|
||||
/** 分桶计数 */
|
||||
private Buckets buckets;
|
||||
|
||||
/** 活动列表 */
|
||||
private List<ParentActivityVo> list;
|
||||
|
||||
/**
|
||||
* 分桶计数(对齐前端 buckets)。
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class Buckets {
|
||||
/** 待回执 */
|
||||
private int todo;
|
||||
/** 已报名 */
|
||||
private int signed;
|
||||
/** 已结束 */
|
||||
private int finished;
|
||||
/** 全部 */
|
||||
private int all;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.yunhai.familyactivity.model.activity;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 家长端某活动详情(对齐前端 MyActivityVo:活动信息 + 我的报名)。
|
||||
*/
|
||||
@Data
|
||||
public class MyActivityVo {
|
||||
|
||||
/** 活动信息 */
|
||||
private ActivityInfo activity;
|
||||
|
||||
/** 我的报名(未回执为 null) */
|
||||
private MySignupInfo my;
|
||||
|
||||
/**
|
||||
* 活动信息(对齐前端 MyActivityVo.activity 内联对象)。
|
||||
*/
|
||||
@Data
|
||||
public static class ActivityInfo {
|
||||
|
||||
private Integer id;
|
||||
|
||||
private String title;
|
||||
|
||||
private String category;
|
||||
|
||||
private String venue;
|
||||
|
||||
private String content;
|
||||
|
||||
private String startTime;
|
||||
|
||||
private String endTime;
|
||||
|
||||
private String signupDeadline;
|
||||
|
||||
private String receiptMode;
|
||||
|
||||
private String receiptModeDesc;
|
||||
|
||||
private Integer quota;
|
||||
|
||||
private Integer allowAccompany;
|
||||
|
||||
private java.math.BigDecimal cost;
|
||||
|
||||
private String publisherName;
|
||||
|
||||
private String deptName;
|
||||
|
||||
/** 活动流程状态 */
|
||||
private String actStatus;
|
||||
|
||||
private String actStatusDesc;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.yunhai.familyactivity.model.activity;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 家长/学生视角的报名态(对齐前端 MySignupInfo)。
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class MySignupInfo {
|
||||
|
||||
private Integer signupId;
|
||||
|
||||
/** 回执状态 */
|
||||
private String status;
|
||||
|
||||
private String statusDesc;
|
||||
|
||||
/** 参加人数(含学生本人) */
|
||||
private Integer joinCount;
|
||||
|
||||
private String guardianName;
|
||||
|
||||
private String guardianPhone;
|
||||
|
||||
private String remark;
|
||||
|
||||
/** 回执时间 */
|
||||
private String signedAt;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.yunhai.familyactivity.model.activity;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 上一篇/下一篇活动ID(对齐前端 NextPrevVo)。
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class NextPrevVo {
|
||||
|
||||
private Integer prevId;
|
||||
|
||||
private Integer nextId;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.yunhai.familyactivity.model.activity;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 家长端活动项(对齐前端 ParentActivityVo)。
|
||||
*/
|
||||
@Data
|
||||
public class ParentActivityVo {
|
||||
|
||||
private Integer id;
|
||||
|
||||
private String title;
|
||||
|
||||
private String category;
|
||||
|
||||
private String venue;
|
||||
|
||||
private String startTime;
|
||||
|
||||
private String endTime;
|
||||
|
||||
private String signupDeadline;
|
||||
|
||||
private String receiptMode;
|
||||
|
||||
private String receiptModeDesc;
|
||||
|
||||
private Integer quota;
|
||||
|
||||
private Integer allowAccompany;
|
||||
|
||||
private java.math.BigDecimal cost;
|
||||
|
||||
private String content;
|
||||
|
||||
private String publisherName;
|
||||
|
||||
private String deptName;
|
||||
|
||||
private String createTime;
|
||||
|
||||
/** 活动流程状态 */
|
||||
private String actStatus;
|
||||
|
||||
private String actStatusDesc;
|
||||
|
||||
/** 无需报名(仅通知) */
|
||||
private boolean viewOnly;
|
||||
|
||||
/** 是否还能报名 */
|
||||
private boolean canSignup;
|
||||
|
||||
/** 我的报名态(未回执为 null) */
|
||||
private MySignupInfo my;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.yunhai.familyactivity.model.activity;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 阅读情况分组项(对齐前端 ReadGroup,字段名固定旧口径)。
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ReadGroup {
|
||||
|
||||
/** 分组名(年级/班级名) */
|
||||
@JsonProperty("GroupName")
|
||||
private String groupName;
|
||||
|
||||
/** 组内用户读/未读明细 */
|
||||
@JsonProperty("teaslist")
|
||||
private List<ReadGroupUser> teaslist;
|
||||
|
||||
/**
|
||||
* 阅读情况用户项(对齐前端 ReadGroupUser;id 为班级ID)。
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class ReadGroupUser {
|
||||
|
||||
/** 班级ID(按班级粒度展示读/未读) */
|
||||
private String id;
|
||||
|
||||
/** 班级名(旧字段名 teaname) */
|
||||
@JsonProperty("teaname")
|
||||
private String teaname;
|
||||
|
||||
/** read / unread */
|
||||
private String states;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.yunhai.familyactivity.model.activity;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 回执审核/处理入参(对齐前端 ReviewRequest,@RequestBody JSON)。
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class ReviewRequest {
|
||||
|
||||
/** 回执记录ID */
|
||||
private Integer signupId;
|
||||
|
||||
/** confirm 确认参加 / cancel 取消或请假 / waitlist 转待定 */
|
||||
private String action;
|
||||
|
||||
/** 备注 */
|
||||
private String remark;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.yunhai.familyactivity.model.activity;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 回执名单项(对齐前端 SignupBrief,精简)。
|
||||
*/
|
||||
@Data
|
||||
public class SignupBrief {
|
||||
|
||||
/** 回执记录ID */
|
||||
private Integer signupId;
|
||||
|
||||
/** 学生ID */
|
||||
private Integer studentId;
|
||||
|
||||
/** 学生姓名 */
|
||||
private String studentName;
|
||||
|
||||
/** 班级名称 */
|
||||
private String className;
|
||||
|
||||
/** 家长姓名 */
|
||||
private String guardianName;
|
||||
|
||||
/** 联系电话 */
|
||||
private String guardianPhone;
|
||||
|
||||
/** 参加人数(含学生本人) */
|
||||
private Integer joinCount;
|
||||
|
||||
/** 回执状态:none/signed/confirmed/canceled */
|
||||
private String status;
|
||||
|
||||
private String statusDesc;
|
||||
|
||||
/** 回执时间 */
|
||||
private String signedAt;
|
||||
|
||||
/** 是否已回执/已报名 */
|
||||
private boolean hasReceipt;
|
||||
|
||||
/** 备注(如过敏史、随行人数说明) */
|
||||
private String remark;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.yunhai.familyactivity.model.activity;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 单条回执详情(对齐前端 SignupDetailVo,回执详情弹窗用)。
|
||||
*/
|
||||
@Data
|
||||
public class SignupDetailVo {
|
||||
|
||||
private Integer signupId;
|
||||
|
||||
private Integer activityId;
|
||||
|
||||
private String activityTitle;
|
||||
|
||||
private String category;
|
||||
|
||||
private Integer studentId;
|
||||
|
||||
private String studentName;
|
||||
|
||||
private String classId;
|
||||
|
||||
private String className;
|
||||
|
||||
private String guardianName;
|
||||
|
||||
private String guardianPhone;
|
||||
|
||||
/** 参加人数(含学生本人) */
|
||||
private Integer joinCount;
|
||||
|
||||
/** 回执状态 */
|
||||
private String status;
|
||||
|
||||
private String statusDesc;
|
||||
|
||||
/** 回执时间 */
|
||||
private String signedAt;
|
||||
|
||||
/** 是否已读 */
|
||||
private boolean read;
|
||||
|
||||
/** 阅读时间 */
|
||||
private String readAt;
|
||||
|
||||
/** 备注 */
|
||||
private String remark;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.yunhai.familyactivity.model.activity;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 回执名单按班级分组(对齐前端 SignupGroup)。
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class SignupGroup {
|
||||
|
||||
/** 班级名称 */
|
||||
private String className;
|
||||
|
||||
/** 班级内回执名单 */
|
||||
private List<SignupBrief> students;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.yunhai.familyactivity.model.activity;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 家长端报名入参(对齐前端 SignupRequest,@RequestBody JSON)。
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class SignupRequest {
|
||||
|
||||
/** 活动ID */
|
||||
private Integer serid;
|
||||
|
||||
/** 参加人数(含学生本人) */
|
||||
private Integer joinCount = 1;
|
||||
|
||||
/** 家长姓名 */
|
||||
private String guardianName;
|
||||
|
||||
/** 联系电话 */
|
||||
private String guardianPhone;
|
||||
|
||||
/** 备注(过敏史、随行人数说明等) */
|
||||
private String remark;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.yunhai.familyactivity.model.activity;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 某活动的回执/报名结果(对齐前端 SignupsResultVo)。
|
||||
*/
|
||||
@Data
|
||||
public class SignupsResultVo {
|
||||
|
||||
private Integer activityId;
|
||||
|
||||
private String title;
|
||||
|
||||
private String category;
|
||||
|
||||
private String venue;
|
||||
|
||||
/** 活动开始时间 */
|
||||
private String startTime;
|
||||
|
||||
/** 报名截止时间 */
|
||||
private String signupDeadline;
|
||||
|
||||
/** 活动流程状态 */
|
||||
private String actStatus;
|
||||
|
||||
private String actStatusDesc;
|
||||
|
||||
/** 回执方式 */
|
||||
private String receiptMode;
|
||||
|
||||
private String receiptModeDesc;
|
||||
|
||||
/** 汇总进度 */
|
||||
private ActivityProgress summary;
|
||||
|
||||
/** 按班级分组名单 */
|
||||
private List<SignupGroup> groups;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.yunhai.familyactivity.model.bsch;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* bschclass 班级 DTO(/oapi/VSchClass/GetVClassList,SchClass 视图)。
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class SchClassDto {
|
||||
|
||||
/** 班级业务ID(sch_class.class_id;学生 ClassID 指向它) */
|
||||
@JsonProperty("ClassID")
|
||||
private Integer classId;
|
||||
|
||||
/** 班级名称 */
|
||||
@JsonProperty("ClassName")
|
||||
private String className;
|
||||
|
||||
/** 年级ID(= gradeId) */
|
||||
@JsonProperty("Levell")
|
||||
private Integer gradeId;
|
||||
|
||||
/** 排序号 */
|
||||
@JsonProperty("sx")
|
||||
private Integer sort;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.yunhai.familyactivity.model.bsch;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* bschclass 年级 DTO(/oapi/Schoolen/GetSchLenLevellist,BaseSchsystemGrade)。
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class SchGradeDto {
|
||||
|
||||
/** 年级ID(=gradeId;班级 Levell 指向它) */
|
||||
@JsonProperty("Id")
|
||||
private Integer id;
|
||||
|
||||
/** 年级名称(gradeNickname,如「初一」) */
|
||||
@JsonProperty("Name")
|
||||
private String name;
|
||||
|
||||
/** 所属学段(学制)ID */
|
||||
@JsonProperty("OwnerId")
|
||||
private Integer ownerId;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.yunhai.familyactivity.model.bsch;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* bschclass 学校信息 DTO(/oapi/SchInfo/GetUserSchFirst)。
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class SchInfoDto {
|
||||
|
||||
/** 学校 GUID(bschclass 侧 SchSerId) */
|
||||
@JsonProperty("SchSerId")
|
||||
private String schSerId;
|
||||
|
||||
/** 用户中心学校组ID(与本项目 schId 同域) */
|
||||
@JsonProperty("UserSchSerId")
|
||||
private Integer userSchSerId;
|
||||
|
||||
/** 学校名称 */
|
||||
@JsonProperty("SchName")
|
||||
private String schName;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.yunhai.familyactivity.model.bsch;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* bschclass 学生 DTO(/oapi/OAStu/*,SchStudent 视图)。
|
||||
* 登录账号口径:家长端账号 = SchStudent.account(家长挂学生档案,学生账号即家长账号)。
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class SchStudentDto {
|
||||
|
||||
/** 学员登录账号 */
|
||||
@JsonProperty("Account")
|
||||
private String account;
|
||||
|
||||
/** 学生ID */
|
||||
@JsonProperty("UserStuSerID")
|
||||
private Integer stuId;
|
||||
|
||||
/** 学生姓名 */
|
||||
@JsonProperty("StuName")
|
||||
private String stuName;
|
||||
|
||||
/** 所在班级ID(sch_class.class_id) */
|
||||
@JsonProperty("ClassID")
|
||||
private Integer classId;
|
||||
|
||||
/** 所在组ID */
|
||||
@JsonProperty("GroupID")
|
||||
private Integer groupId;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.yunhai.familyactivity.model.bsch;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* bschclass 学段(学制)DTO(/oapi/Schoolen/GetSchLenlist,BaseSchsystem)。
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class SchSystemDto {
|
||||
|
||||
/** 学制ID */
|
||||
@JsonProperty("Id")
|
||||
private Integer id;
|
||||
|
||||
/** 学段名称(如「小学部」「初中部」) */
|
||||
@JsonProperty("Name")
|
||||
private String name;
|
||||
|
||||
/** 学制类型 */
|
||||
@JsonProperty("Type")
|
||||
private Integer type;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.yunhai.familyactivity.model.bsch;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* bschclass 组织树节点 DTO(/oapi/SchGroup/GetTeaTree、GetStuTree,TeaTreeVO 视图)。
|
||||
* type=1 组节点(学校/学段/年级/班级、教师组),type=2 人员叶子(学生/教师)。
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class SchTreeNodeDto {
|
||||
|
||||
/** 节点ID(学校/学段/年级/班级为对应业务ID,叶子为学生 stuId 或教师 teaId) */
|
||||
private Integer id;
|
||||
|
||||
/** 节点名称 */
|
||||
private String name;
|
||||
|
||||
/** 父节点ID(根节点为 0) */
|
||||
private Integer pid;
|
||||
|
||||
/** 排序号(组取 sort,叶子固定 0) */
|
||||
private Integer no;
|
||||
|
||||
/** 1=组节点,2=人员叶子 */
|
||||
private Integer type;
|
||||
|
||||
/** 是否父节点(组 true,叶子 false) */
|
||||
@JsonProperty("isParent")
|
||||
private Boolean isParent;
|
||||
|
||||
/** 头像(当前固定空串) */
|
||||
private String photo;
|
||||
|
||||
/** 子节点(叶子为 null,组节点非 null) */
|
||||
private List<SchTreeNodeDto> children;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.yunhai.familyactivity.repository;
|
||||
|
||||
import com.yunhai.familyactivity.entity.ActivityRead;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 活动已读记录仓库(班级粒度)。
|
||||
*/
|
||||
public interface ActivityReadRepository extends JpaRepository<ActivityRead, Integer> {
|
||||
|
||||
Optional<ActivityRead> findByActivityIdAndClassId(Integer activityId, String classId);
|
||||
|
||||
List<ActivityRead> findByActivityId(Integer activityId);
|
||||
|
||||
List<ActivityRead> findByActivityIdIn(Collection<Integer> activityIds);
|
||||
|
||||
long deleteByActivityId(Integer activityId);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.yunhai.familyactivity.repository;
|
||||
|
||||
import com.yunhai.familyactivity.entity.Activity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
import org.springframework.data.jpa.repository.Lock;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import javax.persistence.LockModeType;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 活动仓库。列表筛选走 Specification(动态条件),家长端收件箱在 Service 内做接收班级匹配。
|
||||
*/
|
||||
public interface ActivityRepository extends JpaRepository<Activity, Integer>,
|
||||
JpaSpecificationExecutor<Activity> {
|
||||
|
||||
/** 某学校已发布且未撤回的活动(家长端收件箱候选集,数据量小,内存匹配接收班级) */
|
||||
List<Activity> findBySchIdAndStatusAndIsPulled(Integer schId, Integer status, Integer isPulled);
|
||||
|
||||
/** 某学校全部活动(管理视角详情导航,含草稿/撤回) */
|
||||
List<Activity> findBySchId(Integer schId);
|
||||
|
||||
/** 我发布的活动(发布人ID) */
|
||||
List<Activity> findBySchIdAndPublisherId(Integer schId, Integer publisherId);
|
||||
|
||||
/** 按源活动查复用生成的活动 */
|
||||
List<Activity> findBySourceId(Integer sourceId);
|
||||
|
||||
/** 字典删除保护:主办部门是否被活动引用 */
|
||||
boolean existsByDeptId(String deptId);
|
||||
|
||||
/** 字典删除保护:活动分类是否被活动引用(category 冗余存分类名称) */
|
||||
boolean existsByCategory(String category);
|
||||
|
||||
/** 报名场景悲观锁加载(须在事务内调用),串行化同一活动的名额校验防超卖 */
|
||||
@Lock(LockModeType.PESSIMISTIC_WRITE)
|
||||
@Query("select a from Activity a where a.id = :id")
|
||||
Optional<Activity> findByIdForUpdate(@Param("id") Integer id);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.yunhai.familyactivity.repository;
|
||||
|
||||
import com.yunhai.familyactivity.entity.ActivitySignup;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 活动回执/报名记录仓库。
|
||||
*/
|
||||
public interface ActivitySignupRepository extends JpaRepository<ActivitySignup, Integer> {
|
||||
|
||||
List<ActivitySignup> findByActivityId(Integer activityId);
|
||||
|
||||
List<ActivitySignup> findByActivityIdAndStatusIn(Integer activityId, Collection<String> statuses);
|
||||
|
||||
List<ActivitySignup> findByActivityIdIn(Collection<Integer> activityIds);
|
||||
|
||||
Optional<ActivitySignup> findByActivityIdAndStudentId(Integer activityId, Integer studentId);
|
||||
|
||||
List<ActivitySignup> findByStudentIdAndActivityIdIn(Integer studentId, Collection<Integer> activityIds);
|
||||
|
||||
long deleteByActivityId(Integer activityId);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.yunhai.familyactivity.repository;
|
||||
|
||||
import com.yunhai.familyactivity.entity.ContactsGroup;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface ContactsGroupRepository extends JpaRepository<ContactsGroup, Integer> {
|
||||
Page<ContactsGroup> findAllByOrderByCreateTimeDesc(Pageable pageable);
|
||||
|
||||
/**
|
||||
* 按创建人ID查询联系组列表。
|
||||
*/
|
||||
List<ContactsGroup> findByCreatorId(Integer creatorId);
|
||||
|
||||
/**
|
||||
* 按创建人ID查询联系组列表(分页)。
|
||||
*/
|
||||
Page<ContactsGroup> findByCreatorId(Integer creatorId, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 查询全部联系组(按创建时间降序)。
|
||||
*/
|
||||
List<ContactsGroup> findAllByOrderByCreateTimeDesc();
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.yunhai.familyactivity.repository;
|
||||
|
||||
import com.yunhai.familyactivity.entity.ContactsGroupUser;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface ContactsGroupUserRepository extends JpaRepository<ContactsGroupUser, Integer> {
|
||||
Page<ContactsGroupUser> findByGroupId(Integer groupId, Pageable pageable);
|
||||
List<ContactsGroupUser> findByGroupId(Integer groupId);
|
||||
|
||||
/**
|
||||
* 按组ID批量删除成员(JPQL bulk delete,SQL 立即执行)。
|
||||
* 注意:不能用 derived delete——其 DELETE 延迟到 flush 才执行,
|
||||
* 同事务内先 insert 会触发 uk_group_user 唯一约束冲突。
|
||||
*/
|
||||
@Modifying(clearAutomatically = true)
|
||||
@Query("delete from ContactsGroupUser g where g.groupId = :groupId")
|
||||
void deleteByGroupIdBulk(@Param("groupId") Integer groupId);
|
||||
|
||||
/**
|
||||
* 按多个组ID批量删除成员。
|
||||
*/
|
||||
@Modifying(clearAutomatically = true)
|
||||
@Query("delete from ContactsGroupUser g where g.groupId in :groupIds")
|
||||
void deleteByGroupIdInBulk(@Param("groupIds") List<Integer> groupIds);
|
||||
|
||||
/**
|
||||
* 按多个组ID查询所有成员(用于统计每组人数、组树构建)。
|
||||
*/
|
||||
List<ContactsGroupUser> findByGroupIdIn(List<Integer> groupIds);
|
||||
|
||||
/**
|
||||
* 按组ID和用户ID查询(判断是否已存在)。
|
||||
*/
|
||||
boolean existsByGroupIdAndUserId(Integer groupId, Integer userId);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.yunhai.familyactivity.repository;
|
||||
|
||||
import com.yunhai.familyactivity.entity.OperationLogRecord;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
/**
|
||||
* 业务操作日志仓库。
|
||||
*/
|
||||
public interface OperationLogRepository extends JpaRepository<OperationLogRecord, Long> {
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.yunhai.familyactivity.repository;
|
||||
|
||||
import com.yunhai.familyactivity.entity.SysDict;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface SysDictRepository extends JpaRepository<SysDict, Integer> {
|
||||
List<SysDict> findByDictTypeOrderBySortOrderAsc(String dictType);
|
||||
List<SysDict> findByDictTypeAndEnableFlagOrderBySortOrderAsc(String dictType, Integer enableFlag);
|
||||
|
||||
/** 按类型+key 取单条字典(发文部门名称解析用) */
|
||||
Optional<SysDict> findByDictTypeAndDictKey(String dictType, String dictKey);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.yunhai.familyactivity.repository;
|
||||
|
||||
import com.yunhai.familyactivity.entity.UpFile;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
/**
|
||||
* 附件文件仓储。
|
||||
*/
|
||||
public interface UpFileRepository extends JpaRepository<UpFile, Integer> {
|
||||
|
||||
/**
|
||||
* 按主题类型和主题ID查询附件列表。
|
||||
*/
|
||||
List<UpFile> findByTopicTypeAndTopicId(String topicType, Integer topicId);
|
||||
|
||||
/**
|
||||
* 按主题类型和多个主题ID批量查询附件(消除 N+1)。
|
||||
*/
|
||||
List<UpFile> findByTopicTypeAndTopicIdIn(String topicType, List<Integer> topicIds);
|
||||
|
||||
/**
|
||||
* 按主题类型和主题ID删除附件(通知删除时级联清理)。
|
||||
*/
|
||||
void deleteByTopicTypeAndTopicId(String topicType, Integer topicId);
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package com.yunhai.familyactivity.service;
|
||||
|
||||
import com.yunhai.familyactivity.model.BasePageResult;
|
||||
import com.yunhai.familyactivity.model.activity.ActivityDetailVo;
|
||||
import com.yunhai.familyactivity.model.activity.ActivityPageResult;
|
||||
import com.yunhai.familyactivity.model.activity.ActivityQuery;
|
||||
import com.yunhai.familyactivity.model.activity.ActivityRequest;
|
||||
import com.yunhai.familyactivity.model.activity.ActivityStatisticsVo;
|
||||
import com.yunhai.familyactivity.model.activity.ActivityVo;
|
||||
import com.yunhai.familyactivity.model.activity.InitializeRedVo;
|
||||
import com.yunhai.familyactivity.model.activity.MyActivitiesVo;
|
||||
import com.yunhai.familyactivity.model.activity.MyActivityVo;
|
||||
import com.yunhai.familyactivity.model.activity.NextPrevVo;
|
||||
import com.yunhai.familyactivity.model.activity.ReadGroup;
|
||||
import com.yunhai.familyactivity.model.activity.ReviewRequest;
|
||||
import com.yunhai.familyactivity.model.activity.SignupDetailVo;
|
||||
import com.yunhai.familyactivity.model.activity.SignupRequest;
|
||||
import com.yunhai.familyactivity.model.activity.SignupsResultVo;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 活动信息发布业务接口(对齐前端 activityApi 全部端点)。
|
||||
*/
|
||||
public interface IActivityService {
|
||||
|
||||
// ========== 家长端收件箱 ==========
|
||||
|
||||
/** 已发布活动列表(家长/学生视角,含未读总数) */
|
||||
ActivityPageResult<ActivityVo> list(ActivityQuery query);
|
||||
|
||||
/** 清单:一键全部已读 */
|
||||
InitializeRedVo initializeRed();
|
||||
|
||||
// ========== 我发布的 / 全部活动 ==========
|
||||
|
||||
/** 我发布的 / 全部活动列表(scope="user" 只看自己) */
|
||||
BasePageResult<ActivityVo> sendList(ActivityQuery query);
|
||||
|
||||
// ========== 详情 ==========
|
||||
|
||||
/** 查看详情(type=1 标读;type=0 编辑/管理视角不标读) */
|
||||
ActivityDetailVo detail(Integer serid, Integer type);
|
||||
|
||||
/** 编辑回显(不标读) */
|
||||
ActivityDetailVo getDetail(Integer serid);
|
||||
|
||||
/** 上一篇/下一篇(type=prev/next) */
|
||||
NextPrevVo getNextPrev(Integer serid, String type);
|
||||
|
||||
/** 阅读情况(按班级分组读/未读名单) */
|
||||
List<ReadGroup> readDetail(Integer serid);
|
||||
|
||||
/** 追加接收班级(补发,自动补回执记录) */
|
||||
void receiveClasses(Integer serid, String classes);
|
||||
|
||||
// ========== 发布/编辑/删除 ==========
|
||||
|
||||
/** 新建活动(draft=true 存草稿),返回活动 id */
|
||||
Integer create(ActivityRequest request);
|
||||
|
||||
/** 编辑活动(含 sourceId 复用场景) */
|
||||
void edit(ActivityRequest request);
|
||||
|
||||
/** 批量删除(级联清回执/已读/附件) */
|
||||
void delete(List<Integer> ids);
|
||||
|
||||
/** 撤回(家长端不可见) */
|
||||
void userPull(Integer serid);
|
||||
|
||||
/** 置顶/取消置顶 */
|
||||
void setTopmost(Integer serid, Integer topmost);
|
||||
|
||||
/** 催办未读(本期日志占位) */
|
||||
void expedite(Integer serid);
|
||||
|
||||
// ========== 报名回执(家校核心闭环) ==========
|
||||
|
||||
/** 某活动的回执/报名名单(按班级分组)+ 汇总;signupStatus: all/signed/confirmed/canceled/none */
|
||||
SignupsResultVo signups(Integer serid, String signupStatus);
|
||||
|
||||
/** 单条回执详情 */
|
||||
SignupDetailVo signupDetail(Integer signupId);
|
||||
|
||||
/** 处理回执:action=confirm/cancel/waitlist */
|
||||
void review(ReviewRequest request);
|
||||
|
||||
/** 催报名(本期日志占位),返回 {reminded} */
|
||||
Map<String, Object> remind(Integer serid);
|
||||
|
||||
/** 导出回执名单,返回 {csv: CSV文本(含BOM)}(走 JSON 包装,前端取 csv 字段生成文件) */
|
||||
Map<String, Object> exportSignups(Integer serid);
|
||||
|
||||
// ========== 家长端(v2) ==========
|
||||
|
||||
/** 活动广场(当前家长/学生视角,含我的报名态 + 分桶计数) */
|
||||
MyActivitiesVo myActivities(ActivityQuery query);
|
||||
|
||||
/** 我对某活动的详情(活动信息 + 我的报名,同时标读;confirm 模式自动确认) */
|
||||
MyActivityVo mySignup(Integer serid);
|
||||
|
||||
/** 报名 / 修改报名,返回 {signupId, status} */
|
||||
Map<String, Object> signup(SignupRequest request);
|
||||
|
||||
/** 取消报名 */
|
||||
void cancelSignup(Integer serid);
|
||||
|
||||
// ========== 统计 / 附件 ==========
|
||||
|
||||
/** 活动统计(startTime/endTime 为 yyyy-MM-dd 短日期,可空) */
|
||||
ActivityStatisticsVo statistics(String startTime, String endTime);
|
||||
|
||||
/** 全部附件打包下载 */
|
||||
void allFileDown(Integer dataid, HttpServletResponse response);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.yunhai.familyactivity.service;
|
||||
|
||||
import com.yunhai.familyactivity.model.SchTeaList;
|
||||
import com.yunhai.familyactivity.model.bsch.SchClassDto;
|
||||
import com.yunhai.familyactivity.model.bsch.SchGradeDto;
|
||||
import com.yunhai.familyactivity.model.bsch.SchInfoDto;
|
||||
import com.yunhai.familyactivity.model.bsch.SchStudentDto;
|
||||
import com.yunhai.familyactivity.model.bsch.SchSystemDto;
|
||||
import com.yunhai.familyactivity.model.bsch.SchTreeNodeDto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 班级管理(bschclass)开放接口客户端门面。
|
||||
* <p>基址取配置 {@code yh.open.bschclass}(/oapi/** 无需应用签名,独立 RestTemplate 直连)。
|
||||
* 班级树、圈人(发布对象/回执名单)、学生身份解析统一经本门面取数。
|
||||
* 参考 CampusNotice 同名门面。</p>
|
||||
*/
|
||||
public interface IClassService {
|
||||
|
||||
/**
|
||||
* 获取指定学校全部教师(含所属组 GroupIds)。
|
||||
* 对应 bschclass 开放接口:GET {@code {bschclass}/oapi/DbClass/GetSchteaList?Group={schId}}
|
||||
*
|
||||
* @param schId 学校ID(即用户当前身份的 schId)
|
||||
*/
|
||||
List<SchTeaList> getSchTeaList(int schId);
|
||||
|
||||
/**
|
||||
* 用户中心学校ID → bschclass 学校信息(SchSerId / 学校名)。
|
||||
* 对应:GET {@code {bschclass}/oapi/SchInfo/GetUserSchFirst?UserSchID={schId}}
|
||||
*/
|
||||
SchInfoDto getSchFirst(int schId);
|
||||
|
||||
/**
|
||||
* 学校学段(学制)列表:小学部 / 初中部…。
|
||||
* 对应:GET {@code {bschclass}/oapi/Schoolen/GetSchLenlist?SchSerID=}
|
||||
*/
|
||||
List<SchSystemDto> getSchLenList(String schSerId);
|
||||
|
||||
/**
|
||||
* 学校年级列表(Id=gradeId、Name=gradeNickname、OwnerId=学段id)。
|
||||
* 对应:GET {@code {bschclass}/oapi/Schoolen/GetSchLenLevellist?SchSerID=}
|
||||
*/
|
||||
List<SchGradeDto> getSchGradeList(String schSerId);
|
||||
|
||||
/**
|
||||
* 学校全部在读班级(ClassID/ClassName/Levell=gradeId)。
|
||||
* 对应:GET {@code {bschclass}/oapi/VSchClass/GetVClassList?SchSerID=}
|
||||
*/
|
||||
List<SchClassDto> getSchClassList(String schSerId);
|
||||
|
||||
/**
|
||||
* 按校全部学生(发布对象预生成回执、家长班级身份解析)。
|
||||
* 对应:GET {@code {bschclass}/oapi/OAStu/GetSchStulist?schserid=}
|
||||
*/
|
||||
List<SchStudentDto> getSchStuList(String schSerId);
|
||||
|
||||
/**
|
||||
* 按班级批量查学生(补发班级、回执名单圈人)。
|
||||
* 对应:GET {@code {bschclass}/oapi/OAStu/GetClaArrStulist?ClassIDs=1,2,3}
|
||||
*/
|
||||
List<SchStudentDto> getClaArrStuList(List<Integer> classIds);
|
||||
|
||||
/**
|
||||
* 学校学生树:学校→学段→年级→班级→学生(单元素列表,根为学校节点)。
|
||||
* 对应:GET {@code {bschclass}/oapi/SchGroup/GetStuTree?schId=&toUser=}
|
||||
*/
|
||||
List<SchTreeNodeDto> getStuTree(Integer schId, boolean toUser);
|
||||
|
||||
/**
|
||||
* 学校教师树:学校→部门/组→教师。
|
||||
* 对应:GET {@code {bschclass}/oapi/SchGroup/GetTeaTree?schId=&toUser=}
|
||||
*/
|
||||
List<SchTreeNodeDto> getTeaTree(Integer schId, boolean toUser);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.yunhai.familyactivity.service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 联系组业务接口。
|
||||
* 对应前端 Contacts 组接口:AddGroup/GetGroup/EditGroup/DelGroup/SetContactsTea/GetContactsTea/GetGroupTree/GetDeptRoleGroupTree/Import/Export。
|
||||
*/
|
||||
public interface IContactsService {
|
||||
|
||||
/**
|
||||
* 添加联系组。
|
||||
*
|
||||
* @param name 组名
|
||||
* @param type 类型(1 自定义组, 2 明信片)
|
||||
*/
|
||||
void addGroup(String name, Integer type);
|
||||
|
||||
/**
|
||||
* 获取联系组列表(含每组人数,分页)。
|
||||
*
|
||||
* @param page 页码(从1起)
|
||||
* @param count 每页条数
|
||||
* @param type 类型(1 自定义组, 2 明信片)
|
||||
* @return Map: { list, total }
|
||||
*/
|
||||
Map<String, Object> getGroup(Integer page, Integer count, Integer type);
|
||||
|
||||
/**
|
||||
* 编辑联系组。
|
||||
*
|
||||
* @param id 组ID
|
||||
* @param name 组名
|
||||
* @param type 类型
|
||||
*/
|
||||
void editGroup(Integer id, String name, Integer type);
|
||||
|
||||
/**
|
||||
* 删除联系组(支持批量,同时删除组内成员)。
|
||||
*
|
||||
* @param ids 逗号分隔的组ID
|
||||
*/
|
||||
void delGroup(String ids);
|
||||
|
||||
/**
|
||||
* 设置联系组教师(全量替换:先删后增)。
|
||||
*
|
||||
* @param gid 组ID
|
||||
* @param ids 逗号分隔的用户ID
|
||||
*/
|
||||
void setContactsTea(Integer gid, String ids);
|
||||
|
||||
/**
|
||||
* 获取联系组教师列表(分页)。
|
||||
*
|
||||
* @param gid 组ID
|
||||
* @param page 页码
|
||||
* @param count 每页条数
|
||||
* @return Map: { list, total }
|
||||
*/
|
||||
Map<String, Object> getContactsTea(String gid, Integer page, Integer count);
|
||||
|
||||
/**
|
||||
* 获取联系组树(组节点+用户节点,递归构建)。
|
||||
*
|
||||
* @param type 类型(1 自定义组, 2 明信片)
|
||||
* @return 树形结构 List<Map>
|
||||
*/
|
||||
List<Map<String, Object>> getGroupTree(Integer type);
|
||||
|
||||
/**
|
||||
* 获取部门角色组树。
|
||||
* C# 从外部API获取部门树+用户角色映射。Java版预留。
|
||||
*
|
||||
* @return 树形结构 List<Map>
|
||||
*/
|
||||
List<Map<String, Object>> getDeptRoleGroupTree();
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.yunhai.familyactivity.service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 系统字典业务接口。
|
||||
* 对应前端 Base 组接口:GetSysDic/GetDictMangeList/AddDeptDic/UpdateDeptDic/DelDept/OrdbyDicIds/GetDeptSync/GetSchModuleIsSms。
|
||||
* 字典类型:dept=主办部门(发文部门)、category=活动分类。
|
||||
*/
|
||||
public interface IDictService {
|
||||
|
||||
/**
|
||||
* 获取系统字典(下拉框用)。
|
||||
* Type=1 主办部门(DEPT),Type=2 活动分类(CATEGORY)。
|
||||
*
|
||||
* @param type 字典类型编号(1=主办部门,2=活动分类)
|
||||
* @return List<Map> 每项 {k, v}
|
||||
*/
|
||||
List<Map<String, Object>> getSysDic(Integer type);
|
||||
|
||||
/**
|
||||
* 获取系统字典管理列表(含 SerID/CodeID/Name/Des/Enable)。
|
||||
*
|
||||
* @param type 字典类型("dept"=主办部门,"category"=活动分类)
|
||||
* @return List<Map>
|
||||
*/
|
||||
List<Map<String, Object>> getDictMangeList(String type);
|
||||
|
||||
/**
|
||||
* 添加字典项。
|
||||
*
|
||||
* @param dictType 字典类型(dept/category)
|
||||
* @param name 名称
|
||||
*/
|
||||
void addDict(String dictType, String name);
|
||||
|
||||
/**
|
||||
* 更新字典项(名称、启用状态)。
|
||||
*
|
||||
* @param dictType 字典类型(dept/category)
|
||||
* @param serID 记录ID
|
||||
* @param name 名称
|
||||
* @param enable 启用状态
|
||||
*/
|
||||
void updateDict(String dictType, Integer serID, String name, Integer enable);
|
||||
|
||||
/**
|
||||
* 删除字典项(校验是否被活动引用)。
|
||||
*
|
||||
* @param dictType 字典类型(dept/category)
|
||||
* @param serID 记录ID
|
||||
*/
|
||||
void delDict(String dictType, Integer serID);
|
||||
|
||||
/**
|
||||
* 字典项排序。
|
||||
*
|
||||
* @param dictType 字典类型(dept/category)
|
||||
* @param ids 逗号分隔的 SerID 列表(按前端顺序)
|
||||
*/
|
||||
void sortDict(String dictType, String ids);
|
||||
|
||||
/**
|
||||
* 是否发送短信(查询当前学校模块短信开关)。
|
||||
*
|
||||
* @return Boolean
|
||||
*/
|
||||
Boolean getSchModuleIsSms();
|
||||
|
||||
/**
|
||||
* 按主办部门字典 key(CodeID)解析部门名称;未命中返回 null。
|
||||
* 用于活动列表/详情展示 DeptName(对应 C# GetDeptName)。
|
||||
*
|
||||
* @param deptId 活动 deptId(存的是 sys_dict.dict_key)
|
||||
* @return 部门名称;无匹配时返回 null
|
||||
*/
|
||||
String resolveDeptName(String deptId);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package com.yunhai.familyactivity.service;
|
||||
|
||||
import com.yunhai.familyactivity.entity.UpFile;
|
||||
import com.yunhai.familyactivity.model.UpFileVo;
|
||||
import com.yunhai.familyactivity.model.UpFileResultVo;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 文件上传/下载/删除业务接口。
|
||||
*/
|
||||
public interface IFileService {
|
||||
/**
|
||||
* 上传单个文件。
|
||||
*
|
||||
* @param file 上传的文件
|
||||
* @param appType 应用类型(如 "notice")
|
||||
* @param topicId 关联主题ID(可为空,通知保存后再绑定)
|
||||
* @return 文件视图对象
|
||||
*/
|
||||
UpFileVo upload(MultipartFile file, String appType, Integer topicId);
|
||||
|
||||
/**
|
||||
* 将已上传但未绑定主题的附件绑定到指定通知。
|
||||
*
|
||||
* @param fileIds 文件记录ID列表
|
||||
* @param topicId 通知ID
|
||||
*/
|
||||
void bindToTopic(List<Integer> fileIds, Integer topicId);
|
||||
|
||||
/**
|
||||
* 查询某主题下的附件列表。
|
||||
*
|
||||
* @param appType 应用类型
|
||||
* @param topicId 主题ID
|
||||
* @return 附件列表
|
||||
*/
|
||||
List<UpFileVo> listByTopic(String appType, Integer topicId);
|
||||
|
||||
/**
|
||||
* 下载文件。
|
||||
*
|
||||
* @param fileId 文件记录ID
|
||||
* @param response HTTP 响应
|
||||
*/
|
||||
void download(Integer fileId, HttpServletResponse response);
|
||||
|
||||
/**
|
||||
* 在线预览文件(Content-Disposition: inline,按扩展名输出真实 Content-Type,
|
||||
* 供前端 open-file-viewer 取 Blob / video 播放)。
|
||||
*
|
||||
* @param fileId 文件记录ID
|
||||
* @param response HTTP 响应
|
||||
*/
|
||||
void preview(Integer fileId, HttpServletResponse response);
|
||||
|
||||
/**
|
||||
* 删除文件(同时删除物理文件和数据库记录)。
|
||||
*
|
||||
* @param fileId 文件记录ID
|
||||
*/
|
||||
void delete(Integer fileId);
|
||||
|
||||
/**
|
||||
* 打包下载多个附件为 ZIP。
|
||||
*
|
||||
* @param files 附件记录列表(调用方按 fileIds 查好)
|
||||
* @param zipName 压缩包显示名(自动追加 .zip)
|
||||
* @param response HTTP 响应
|
||||
*/
|
||||
void downloadZip(List<UpFile> files, String zipName, HttpServletResponse response);
|
||||
|
||||
/**
|
||||
* 按主题类型和主题ID打包下载附件为 ZIP(Controller 便捷方法,内部自行查附件列表)。
|
||||
*
|
||||
* @param topicType 主题类型(如 "notice")
|
||||
* @param topicId 主题ID
|
||||
* @param zipName 压缩包显示名
|
||||
* @param response HTTP 响应
|
||||
*/
|
||||
void downloadZipByTopic(String topicType, Integer topicId, String zipName, HttpServletResponse response);
|
||||
|
||||
/**
|
||||
* 批量上传图片(循环上传,返回每个文件的视图对象列表)。
|
||||
*/
|
||||
List<UpFileVo> uploadImages(MultipartFile[] files, String appType);
|
||||
|
||||
/**
|
||||
* 上传单个文件并返回前端兼容的结果 VO(含 fileid/name/fileurl/host/type/size)。
|
||||
*/
|
||||
UpFileResultVo uploadToResult(MultipartFile file, String appType);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.yunhai.familyactivity.service;
|
||||
|
||||
import com.yunhai.platform.utils.model.BoaModule;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 用户中心门面服务(对齐 UserBase {@code UserCenterService} 模式)。
|
||||
* <p>封装 {@code YschBaseOpenApi} / {@code OpenApiUserClient} 静态门面调用,
|
||||
* 把原本散落在 Controller 的组树组装、教师挂组、成员解析等业务逻辑下沉到 Service 层,
|
||||
* Controller 只保留"取参 → 调 Service → 返回"的薄壳。</p>
|
||||
*/
|
||||
public interface IUserCenterService {
|
||||
|
||||
/**
|
||||
* 获取当前用户学校组及用户(树形结构)。
|
||||
* <p>组装规则(从原 BaseApiController.getSchoolAll 下沉):</p>
|
||||
* <ol>
|
||||
* <li>userbase 读取当前学校整棵组树(getGroupById(schId, withSub=true)),剔除学生组(type=8)及其子组;</li>
|
||||
* <li>bschclass 获取该校全部教师(/oapi/DbClass/GetSchteaList?Group=schId),按 GroupIds 挂到对应组下,
|
||||
* 无有效组(如所属组全被剔除)的教师兜底挂学校根节点;</li>
|
||||
* <li>扁平节点按 pid 递归组装为树(空组剔除,同 C# FormatUserTree)。</li>
|
||||
* </ol>
|
||||
*
|
||||
* @param schId 学校ID(用户当前角色所属学校)
|
||||
* @return 用户树形结构(组节点 type=1 / 教师节点 type=2)
|
||||
*/
|
||||
List<Map<String, Object>> getSchoolAllTree(Integer schId);
|
||||
|
||||
/**
|
||||
* 获取指定组内用户(教师 + 管理员)。
|
||||
* <p>对应前端 base/GetGroupUser。原实现为空桩返回空列表,现按 C# 语义
|
||||
* (ITeainfoBLL.GetGroupTeainfo + IAdminfoBLL.GetGroupAdminfo)接入用户中心。
|
||||
* 若用户中心不可用或未配置对应开放接口,则抛出带语义的异常(显式报错而非静默失能)。</p>
|
||||
*
|
||||
* @param gid 组ID
|
||||
* @param role 角色(默认 2=教师;1=管理员等)
|
||||
* @return 组内用户列表
|
||||
*/
|
||||
List<Map<String, Object>> getGroupUser(Integer gid, Integer role);
|
||||
|
||||
/**
|
||||
* 获取当前用户可访问的模块树(左侧菜单)。
|
||||
* <p>从基础平台拉取当前用户模块列表(含学校上下文),组装为前端 layout 期望的树结构
|
||||
* ({@code modId/page/children}),对应前端 GetUserMod 接口。</p>
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param schId 学校ID(可为 null)
|
||||
* @return 模块树(顶层 List,含 children 嵌套)
|
||||
*/
|
||||
List<BoaModule> getUserModuleTree(Integer userId, Integer schId);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,159 @@
|
||||
package com.yunhai.familyactivity.service.impl;
|
||||
|
||||
import com.yunhai.familyactivity.model.SchTeaList;
|
||||
import com.yunhai.familyactivity.model.bsch.SchClassDto;
|
||||
import com.yunhai.familyactivity.model.bsch.SchGradeDto;
|
||||
import com.yunhai.familyactivity.model.bsch.SchInfoDto;
|
||||
import com.yunhai.familyactivity.model.bsch.SchStudentDto;
|
||||
import com.yunhai.familyactivity.model.bsch.SchSystemDto;
|
||||
import com.yunhai.familyactivity.model.bsch.SchTreeNodeDto;
|
||||
import com.yunhai.familyactivity.service.IClassService;
|
||||
import com.yunhai.platform.apistd.BizException;
|
||||
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.client.RestClientException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 班级管理(bschclass)开放接口实现。
|
||||
* bschclass 为开放接口(无需应用签名),使用独立 RestTemplate 直连。
|
||||
* <p>学段 GetSchLenlist、年级 GetSchLenLevellist、班级 GetVClassList、
|
||||
* 学生 GetSchStulist / GetClaArrStulist、组织树 SchGroup/GetStuTree、GetTeaTree。</p>
|
||||
*/
|
||||
@Service
|
||||
public class ClassService implements IClassService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ClassService.class);
|
||||
|
||||
/** 班级管理服务基址,如 http://base.yunhaixingtong.com/bschclass */
|
||||
@Value("${yh.open.bschclass:}")
|
||||
private String bschClassHost;
|
||||
|
||||
private final RestTemplate restTemplate;
|
||||
|
||||
public ClassService() {
|
||||
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
|
||||
factory.setConnectTimeout(5000);
|
||||
factory.setReadTimeout(10000);
|
||||
this.restTemplate = new RestTemplate(factory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SchTeaList> getSchTeaList(int schId) {
|
||||
return exchangeList(bschClassHost + "/oapi/DbClass/GetSchteaList?Group=" + schId,
|
||||
new ParameterizedTypeReference<List<SchTeaList>>() {
|
||||
}, "获取教师数据", "schId=" + schId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SchInfoDto getSchFirst(int schId) {
|
||||
requireHost();
|
||||
String url = bschClassHost + "/oapi/SchInfo/GetUserSchFirst?UserSchID=" + schId;
|
||||
try {
|
||||
ResponseEntity<SchInfoDto> resp = restTemplate.getForEntity(URI.create(url), SchInfoDto.class);
|
||||
return resp.getBody();
|
||||
} catch (RestClientException e) {
|
||||
log.error("[getSchFirst] 请求班级管理接口失败, url={}", url, e);
|
||||
throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "获取学校信息失败");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SchSystemDto> getSchLenList(String schSerId) {
|
||||
return exchangeList(bschClassHost + "/oapi/Schoolen/GetSchLenlist?SchSerID=" + schSerId,
|
||||
new ParameterizedTypeReference<List<SchSystemDto>>() {
|
||||
}, "获取学段列表", "schSerId=" + schSerId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SchGradeDto> getSchGradeList(String schSerId) {
|
||||
return exchangeList(bschClassHost + "/oapi/Schoolen/GetSchLenLevellist?SchSerID=" + schSerId,
|
||||
new ParameterizedTypeReference<List<SchGradeDto>>() {
|
||||
}, "获取年级列表", "schSerId=" + schSerId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SchClassDto> getSchClassList(String schSerId) {
|
||||
return exchangeList(bschClassHost + "/oapi/VSchClass/GetVClassList?SchSerID=" + schSerId,
|
||||
new ParameterizedTypeReference<List<SchClassDto>>() {
|
||||
}, "获取班级列表", "schSerId=" + schSerId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SchStudentDto> getSchStuList(String schSerId) {
|
||||
return exchangeList(bschClassHost + "/oapi/OAStu/GetSchStulist?schserid=" + schSerId,
|
||||
new ParameterizedTypeReference<List<SchStudentDto>>() {
|
||||
}, "获取学校学生", "schSerId=" + schSerId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SchStudentDto> getClaArrStuList(List<Integer> classIds) {
|
||||
String csv = joinIds(classIds);
|
||||
return exchangeList(bschClassHost + "/oapi/OAStu/GetClaArrStulist?ClassIDs=" + csv,
|
||||
new ParameterizedTypeReference<List<SchStudentDto>>() {
|
||||
}, "获取班级学生", "classIds=" + csv);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SchTreeNodeDto> getStuTree(Integer schId, boolean toUser) {
|
||||
return exchangeList(bschClassHost + "/oapi/SchGroup/GetStuTree?schId=" + schId + "&toUser=" + toUser,
|
||||
new ParameterizedTypeReference<List<SchTreeNodeDto>>() {
|
||||
}, "获取学生树", "schId=" + schId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SchTreeNodeDto> getTeaTree(Integer schId, boolean toUser) {
|
||||
return exchangeList(bschClassHost + "/oapi/SchGroup/GetTeaTree?schId=" + schId + "&toUser=" + toUser,
|
||||
new ParameterizedTypeReference<List<SchTreeNodeDto>>() {
|
||||
}, "获取教师树", "schId=" + schId);
|
||||
}
|
||||
|
||||
// ============================ 通用调用 ============================
|
||||
|
||||
private <T> List<T> exchangeList(String url, ParameterizedTypeReference<List<T>> type,
|
||||
String action, String arg) {
|
||||
requireHost();
|
||||
try {
|
||||
// GET + Accept: application/json
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
|
||||
ResponseEntity<List<T>> resp = restTemplate.exchange(
|
||||
url, HttpMethod.GET, new HttpEntity<>(headers), type);
|
||||
List<T> list = resp.getBody();
|
||||
log.info("[bschclass] {} 完成, {} 返回 {} 条", action, arg, list == null ? 0 : list.size());
|
||||
return list != null ? list : Collections.emptyList();
|
||||
} catch (RestClientException e) {
|
||||
log.error("[bschclass] {} 失败, {}, url={}", action, arg, url, e);
|
||||
throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, action + "失败");
|
||||
}
|
||||
}
|
||||
|
||||
private void requireHost() {
|
||||
if (!StringUtils.hasText(bschClassHost)) {
|
||||
throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "班级管理接口未配置(yh.open.bschclass)");
|
||||
}
|
||||
}
|
||||
|
||||
private String joinIds(List<Integer> ids) {
|
||||
if (ids == null || ids.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
return ids.stream().map(String::valueOf).collect(Collectors.joining(","));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,398 @@
|
||||
package com.yunhai.familyactivity.service.impl;
|
||||
|
||||
import com.yunhai.familyactivity.entity.ContactsGroup;
|
||||
import com.yunhai.familyactivity.entity.ContactsGroupUser;
|
||||
import com.yunhai.familyactivity.repository.ContactsGroupRepository;
|
||||
import com.yunhai.familyactivity.repository.ContactsGroupUserRepository;
|
||||
import com.yunhai.familyactivity.service.IContactsService;
|
||||
import com.yunhai.familyactivity.utils.UserContext;
|
||||
import com.yunhai.platform.apistd.BizException;
|
||||
import com.yunhai.platform.openapi.base.YschBaseOpenApi;
|
||||
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||
import com.yunhai.platform.utils.model.BoaUser;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 联系组服务实现。
|
||||
* 对应 C# ContactsController + ManageContacts.cs。
|
||||
*/
|
||||
@Service
|
||||
public class ContactsService implements IContactsService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ContactsService.class);
|
||||
|
||||
@Autowired
|
||||
private ContactsGroupRepository groupRepository;
|
||||
|
||||
@Autowired
|
||||
private ContactsGroupUserRepository groupUserRepository;
|
||||
|
||||
// ============================ AddGroup ============================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void addGroup(String name, Integer type) {
|
||||
if (!StringUtils.hasText(name)) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "请输入自定义组的名字");
|
||||
}
|
||||
if (type == null) {
|
||||
type = 1;
|
||||
}
|
||||
Integer creatorId = currentUserId();
|
||||
|
||||
// 查重:同创建人下同名同类型组不重复
|
||||
List<ContactsGroup> existing = groupRepository.findByCreatorId(creatorId);
|
||||
boolean duplicate = existing.stream().anyMatch(g ->
|
||||
name.equals(g.getGroupName()));
|
||||
if (duplicate) {
|
||||
throw new BizException(ErrorCode.FORBIDDEN, "已存在该联系组!");
|
||||
}
|
||||
|
||||
ContactsGroup group = new ContactsGroup();
|
||||
group.setGroupName(name);
|
||||
group.setCreatorId(creatorId);
|
||||
groupRepository.save(group);
|
||||
}
|
||||
|
||||
// ============================ GetGroup ============================
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getGroup(Integer page, Integer count, Integer type) {
|
||||
if (type == null) {
|
||||
type = 1;
|
||||
}
|
||||
Integer creatorId = currentUserId();
|
||||
|
||||
// 查询当前用户创建的全部联系组
|
||||
List<ContactsGroup> allGroups = groupRepository.findByCreatorId(creatorId);
|
||||
|
||||
// 统计每组人数
|
||||
List<Integer> groupIds = allGroups.stream().map(ContactsGroup::getId).collect(Collectors.toList());
|
||||
List<ContactsGroupUser> allMembers = groupIds.isEmpty() ? Collections.emptyList()
|
||||
: groupUserRepository.findByGroupIdIn(groupIds);
|
||||
Map<Integer, Long> countByGroup = allMembers.stream()
|
||||
.collect(Collectors.groupingBy(ContactsGroupUser::getGroupId, Collectors.counting()));
|
||||
|
||||
// 构建列表数据
|
||||
List<Map<String, Object>> listData = allGroups.stream().map(g -> {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("serid", g.getId());
|
||||
item.put("GroupName", g.getGroupName());
|
||||
item.put("count", countByGroup.getOrDefault(g.getId(), 0L).intValue());
|
||||
return item;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
// 分页
|
||||
int total = listData.size();
|
||||
if (count != null && count > 0 && page != null && page > 0) {
|
||||
int fromIndex = (page - 1) * count;
|
||||
int toIndex = Math.min(fromIndex + count, total);
|
||||
if (fromIndex < total) {
|
||||
listData = listData.subList(fromIndex, toIndex);
|
||||
} else {
|
||||
listData = Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("list", listData);
|
||||
result.put("total", total);
|
||||
return result;
|
||||
}
|
||||
|
||||
// ============================ EditGroup ============================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void editGroup(Integer id, String name, Integer type) {
|
||||
if (!StringUtils.hasText(name)) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "联系组名称不能为空");
|
||||
}
|
||||
if (id == null) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "参数错误!");
|
||||
}
|
||||
Integer creatorId = currentUserId();
|
||||
|
||||
// 查重(排除自身)
|
||||
List<ContactsGroup> existing = groupRepository.findByCreatorId(creatorId);
|
||||
boolean duplicate = existing.stream().anyMatch(g ->
|
||||
!g.getId().equals(id) && name.equals(g.getGroupName()));
|
||||
if (duplicate) {
|
||||
throw new BizException(ErrorCode.FORBIDDEN, "已存在该联系组");
|
||||
}
|
||||
|
||||
ContactsGroup group = groupRepository.findById(id)
|
||||
.orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "联系组不存在"));
|
||||
group.setGroupName(name);
|
||||
groupRepository.save(group);
|
||||
}
|
||||
|
||||
// ============================ DelGroup ============================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void delGroup(String ids) {
|
||||
if (!StringUtils.hasText(ids)) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "参数错误,删除失败");
|
||||
}
|
||||
List<Integer> idList = Arrays.stream(ids.split(","))
|
||||
.map(String::trim)
|
||||
.filter(s -> !s.isEmpty())
|
||||
.map(Integer::parseInt)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 先删成员再删组
|
||||
groupUserRepository.deleteByGroupIdInBulk(idList);
|
||||
groupRepository.findAllById(idList).forEach(groupRepository::delete);
|
||||
}
|
||||
|
||||
// ============================ SetContactsTea ============================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void setContactsTea(Integer gid, String ids) {
|
||||
if (gid == null || gid == 0) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "参数错误");
|
||||
}
|
||||
// 全量替换:bulk delete 立即执行 SQL,避免 derived delete 延迟 flush
|
||||
// 导致同事务 insert 先行触发 uk_group_user(参考 PartyLearn GroupService.memberEdit)
|
||||
groupUserRepository.deleteByGroupIdBulk(gid);
|
||||
|
||||
if (StringUtils.hasText(ids)) {
|
||||
List<Integer> userIds = Arrays.stream(ids.split(","))
|
||||
.map(String::trim)
|
||||
.filter(s -> !s.isEmpty())
|
||||
.map(Integer::parseInt)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
for (Integer userId : userIds) {
|
||||
ContactsGroupUser member = new ContactsGroupUser();
|
||||
member.setGroupId(gid);
|
||||
member.setUserId(userId);
|
||||
// 姓名快照落库(对齐 PartyLearn memberEdit 保存 userName 的做法),
|
||||
// 查询时 fillUserInfo 仅对缺失值兜底,避免组树/分页反复远程调用
|
||||
try {
|
||||
BoaUser user = YschBaseOpenApi.getUserInfo(userId);
|
||||
if (user != null) {
|
||||
member.setUserName(user.getName());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[setContactsTea] 查询用户信息失败, userId={}", userId, e);
|
||||
}
|
||||
groupUserRepository.save(member);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================ GetContactsTea ============================
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getContactsTea(String gid, Integer page, Integer count) {
|
||||
Integer groupId = 0;
|
||||
try {
|
||||
groupId = Integer.parseInt(gid);
|
||||
} catch (NumberFormatException e) {
|
||||
groupId = 0;
|
||||
}
|
||||
if (groupId == 0) {
|
||||
Map<String, Object> empty = new LinkedHashMap<>();
|
||||
empty.put("list", Collections.emptyList());
|
||||
empty.put("total", 0);
|
||||
return empty;
|
||||
}
|
||||
|
||||
List<ContactsGroupUser> allMembers = groupUserRepository.findByGroupId(groupId);
|
||||
int total = allMembers.size();
|
||||
|
||||
// 先分页再补全用户信息(避免为未分页数据调用用户中心)
|
||||
List<ContactsGroupUser> pageMembers = allMembers;
|
||||
if (count != null && count > 0 && page != null && page > 0) {
|
||||
int fromIndex = (page - 1) * count;
|
||||
int toIndex = Math.min(fromIndex + count, total);
|
||||
pageMembers = fromIndex < total ? allMembers.subList(fromIndex, toIndex) : Collections.emptyList();
|
||||
}
|
||||
|
||||
List<Map<String, Object>> listData = pageMembers.stream().map(m -> {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("sid", m.getId());
|
||||
item.put("id", m.getUserId());
|
||||
fillUserInfo(item, m);
|
||||
return item;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("list", listData);
|
||||
result.put("total", total);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 补全成员姓名:落库值为空时从用户中心按 userId 反查。
|
||||
* 查到的值会回写落库自愈,避免历史空数据每次读取都重复远程调用。
|
||||
*/
|
||||
private void fillUserInfo(Map<String, Object> item, ContactsGroupUser m) {
|
||||
String name = m.getUserName();
|
||||
if (m.getUserId() != null && !StringUtils.hasText(name)) {
|
||||
BoaUser user = null;
|
||||
try {
|
||||
user = YschBaseOpenApi.getUserInfo(m.getUserId());
|
||||
} catch (Exception e) {
|
||||
log.warn("[fillUserInfo] 查询用户中心用户失败, userId={}", m.getUserId(), e);
|
||||
}
|
||||
if (user == null) {
|
||||
log.warn("[fillUserInfo] 用户中心未查到用户(历史脏数据?), userId={}", m.getUserId());
|
||||
} else {
|
||||
if (!StringUtils.hasText(name)) {
|
||||
name = user.getName();
|
||||
}
|
||||
// 回写落库:下次读取直接命中快照
|
||||
if (!StringUtils.hasText(m.getUserName()) && StringUtils.hasText(name)) {
|
||||
m.setUserName(name);
|
||||
try {
|
||||
groupUserRepository.save(m);
|
||||
} catch (Exception e) {
|
||||
log.warn("[fillUserInfo] 回写用户信息失败, userId={}", m.getUserId(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
item.put("name", name != null ? name : "");
|
||||
}
|
||||
|
||||
// ============================ GetGroupTree ============================
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> getGroupTree(Integer type) {
|
||||
if (type == null) {
|
||||
type = 1;
|
||||
}
|
||||
Integer creatorId = currentUserId();
|
||||
|
||||
// 查询当前用户的联系组
|
||||
List<ContactsGroup> groups = groupRepository.findByCreatorId(creatorId);
|
||||
List<Integer> groupIds = groups.stream().map(ContactsGroup::getId).collect(Collectors.toList());
|
||||
|
||||
// 查询全部成员
|
||||
List<ContactsGroupUser> allMembers = groupIds.isEmpty() ? Collections.emptyList()
|
||||
: groupUserRepository.findByGroupIdIn(groupIds);
|
||||
|
||||
// 构建树节点
|
||||
List<Map<String, Object>> treeNodes = new ArrayList<>();
|
||||
|
||||
// 组节点 (type=1, pid=0)
|
||||
for (ContactsGroup g : groups) {
|
||||
Map<String, Object> node = new LinkedHashMap<>();
|
||||
node.put("id", g.getId());
|
||||
node.put("name", g.getGroupName());
|
||||
node.put("pid", 0);
|
||||
node.put("no", 0);
|
||||
node.put("type", 1);
|
||||
node.put("isParent", true);
|
||||
node.put("photo", "");
|
||||
node.put("children", new ArrayList<Map<String, Object>>());
|
||||
treeNodes.add(node);
|
||||
}
|
||||
|
||||
// 用户节点 (type=2, pid=groupId),姓名缺失时从用户中心补全
|
||||
for (ContactsGroupUser m : allMembers) {
|
||||
Map<String, Object> node = new LinkedHashMap<>();
|
||||
node.put("id", m.getUserId());
|
||||
node.put("name", "");
|
||||
node.put("pid", m.getGroupId());
|
||||
node.put("no", 0);
|
||||
node.put("type", 2);
|
||||
node.put("isParent", false);
|
||||
node.put("photo", "");
|
||||
fillUserInfo(node, m);
|
||||
treeNodes.add(node);
|
||||
}
|
||||
|
||||
// 递归构建树:各自定义组为顶级节点(空组自动剔除),组合成森林
|
||||
return formatUserTree(treeNodes, 0);
|
||||
}
|
||||
|
||||
// ============================ GetDeptRoleGroupTree ============================
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> getDeptRoleGroupTree() {
|
||||
// C# 从外部API获取部门树+用户角色映射。
|
||||
// Java版暂未接入用户中心OpenAPI,返回空列表。
|
||||
log.info("[getDeptRoleGroupTree] 暂未接入用户中心API,返回空列表");
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// ============================ 辅助方法 ============================
|
||||
|
||||
/**
|
||||
* 递归构建用户树(对应 C# HttpUserApi.FormatUserTree)。
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<Map<String, Object>> formatUserTree(List<Map<String, Object>> treeList, int pid) {
|
||||
List<Map<String, Object>> data;
|
||||
if (pid == 0) {
|
||||
// 根层:取没有父节点引用的节点(pid=0 或 pid不在列表中的)
|
||||
Set<Integer> allIds = treeList.stream()
|
||||
.map(n -> (Integer) n.get("id"))
|
||||
.collect(Collectors.toSet());
|
||||
data = treeList.stream()
|
||||
.filter(n -> {
|
||||
int p = (Integer) n.get("pid");
|
||||
return p == 0 || !allIds.contains(p);
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
} else {
|
||||
data = treeList.stream()
|
||||
.filter(n -> (Integer) n.get("pid") == pid)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
for (Map<String, Object> item : data) {
|
||||
int itemId = (Integer) item.get("id");
|
||||
int itemType = (Integer) item.get("type");
|
||||
|
||||
List<Map<String, Object>> children = formatUserTree(treeList, itemId);
|
||||
|
||||
// type=1(组) 且无子节点 → 不加入(C# 逻辑)
|
||||
if (itemType == 1 && children.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Map<String, Object> node = new LinkedHashMap<>(item);
|
||||
// 仅组节点携带 children;叶子不带该字段,避免 ztree 误判 isParent
|
||||
if (itemType == 1) {
|
||||
node.put("children", children);
|
||||
}
|
||||
result.add(node);
|
||||
}
|
||||
|
||||
// 按 no 排序
|
||||
result.sort(Comparator.comparingInt(n -> {
|
||||
Object no = n.get("no");
|
||||
if (no instanceof Integer) return (Integer) no;
|
||||
if (no instanceof String) {
|
||||
try { return Integer.parseInt((String) no); } catch (NumberFormatException e) { return 9999; }
|
||||
}
|
||||
return 9999;
|
||||
}));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前登录人ID(平台用户ID,由 ControllerUserContextAspect 填充;无登录态时为 null)。
|
||||
*/
|
||||
private Integer currentUserId() {
|
||||
return UserContext.getUserId();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
package com.yunhai.familyactivity.service.impl;
|
||||
|
||||
import com.yunhai.familyactivity.entity.SysDict;
|
||||
import com.yunhai.familyactivity.repository.ActivityRepository;
|
||||
import com.yunhai.familyactivity.repository.SysDictRepository;
|
||||
import com.yunhai.familyactivity.service.IDictService;
|
||||
import com.yunhai.platform.auth.LoginUser;
|
||||
import com.yunhai.platform.auth.UserContextHolder;
|
||||
import com.yunhai.platform.apistd.BizException;
|
||||
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 系统字典服务实现。
|
||||
* 对应 C# CommonController + GetSysDict.cs 中的字典管理逻辑。
|
||||
* 支持两类字典:dept=主办部门、category=活动分类,CRUD 逻辑同构。
|
||||
*/
|
||||
@Service
|
||||
public class DictService implements IDictService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(DictService.class);
|
||||
|
||||
/** 字典类型常量:主办部门 */
|
||||
private static final String DICT_TYPE_DEPT = "dept";
|
||||
/** 字典类型常量:活动分类 */
|
||||
private static final String DICT_TYPE_CATEGORY = "category";
|
||||
|
||||
@Autowired
|
||||
private SysDictRepository sysDictRepository;
|
||||
|
||||
@Autowired
|
||||
private ActivityRepository activityRepository;
|
||||
|
||||
// ============================ GetSysDic ============================
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> getSysDic(Integer type) {
|
||||
String dictType = mapDictType(type);
|
||||
if (dictType == null) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "参数错误");
|
||||
}
|
||||
List<SysDict> list = sysDictRepository.findByDictTypeAndEnableFlagOrderBySortOrderAsc(dictType, 1);
|
||||
if (list.isEmpty()) {
|
||||
throw new BizException(ErrorCode.FORBIDDEN, emptyTip(dictType));
|
||||
}
|
||||
// 按 sortOrder 升序返回 {k, v}
|
||||
return list.stream()
|
||||
.sorted(Comparator.comparingInt(d -> d.getSortOrder() == null ? Integer.MAX_VALUE : d.getSortOrder()))
|
||||
.map(d -> {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("k", d.getDictKey());
|
||||
item.put("v", d.getDictValue());
|
||||
return item;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// ============================ GetDictMangeList ============================
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> getDictMangeList(String type) {
|
||||
String dictType = normalizeManageType(type);
|
||||
if (dictType == null) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "参数错误");
|
||||
}
|
||||
List<SysDict> list = sysDictRepository.findByDictTypeOrderBySortOrderAsc(dictType);
|
||||
return list.stream()
|
||||
.sorted(Comparator.comparingInt(d -> d.getSortOrder() == null ? 999 : d.getSortOrder()))
|
||||
.map(d -> {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("SerID", d.getId());
|
||||
item.put("CodeID", d.getDictKey());
|
||||
item.put("Name", d.getDictValue());
|
||||
item.put("Des", d.getSortOrder() != null ? d.getSortOrder().toString() : "");
|
||||
item.put("Enable", d.getEnableFlag());
|
||||
return item;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// ============================ AddDeptDic ============================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void addDict(String dictType, String name) {
|
||||
String type = normalizeManageType(dictType);
|
||||
if (type == null || !StringUtils.hasText(name)) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "输入字符串格式不正确!");
|
||||
}
|
||||
// 校验重名
|
||||
List<SysDict> existing = sysDictRepository.findByDictTypeOrderBySortOrderAsc(type);
|
||||
boolean duplicate = existing.stream().anyMatch(d ->
|
||||
type.equals(d.getDictType()) && name.trim().equals(d.getDictValue()) && d.getEnableFlag() != 3);
|
||||
if (duplicate) {
|
||||
throw new BizException(ErrorCode.FORBIDDEN, "已存在该分类,请重新填写!");
|
||||
}
|
||||
|
||||
// 计算 sortOrder = max + 1
|
||||
int maxSort = existing.stream()
|
||||
.filter(d -> d.getEnableFlag() != 3)
|
||||
.mapToInt(d -> d.getSortOrder() != null ? d.getSortOrder() : 0)
|
||||
.max().orElse(0);
|
||||
|
||||
SysDict dict = new SysDict();
|
||||
dict.setDictType(type);
|
||||
dict.setDictValue(name.trim());
|
||||
dict.setSortOrder(maxSort + 1);
|
||||
dict.setDictKey(type + (maxSort + 1));
|
||||
dict.setEnableFlag(1);
|
||||
sysDictRepository.save(dict);
|
||||
}
|
||||
|
||||
// ============================ UpdateDeptDic ============================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void updateDict(String dictType, Integer serID, String name, Integer enable) {
|
||||
String type = normalizeManageType(dictType);
|
||||
if (type == null || serID == null) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "参数错误!");
|
||||
}
|
||||
// 校验重名(排除自身)
|
||||
List<SysDict> existing = sysDictRepository.findByDictTypeOrderBySortOrderAsc(type);
|
||||
boolean duplicate = existing.stream().anyMatch(d ->
|
||||
!d.getId().equals(serID) && name != null && name.trim().equals(d.getDictValue()) && d.getEnableFlag() != 3);
|
||||
if (duplicate) {
|
||||
throw new BizException(ErrorCode.FORBIDDEN, "已存在该分类,请重新填写!");
|
||||
}
|
||||
|
||||
SysDict dict = sysDictRepository.findById(serID)
|
||||
.orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "字典记录不存在"));
|
||||
if (StringUtils.hasText(name)) {
|
||||
dict.setDictValue(name.trim());
|
||||
}
|
||||
if (enable != null) {
|
||||
dict.setEnableFlag(enable);
|
||||
}
|
||||
sysDictRepository.save(dict);
|
||||
}
|
||||
|
||||
// ============================ DelDept ============================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void delDict(String dictType, Integer serID) {
|
||||
String type = normalizeManageType(dictType);
|
||||
if (type == null) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "参数错误!");
|
||||
}
|
||||
SysDict dict = sysDictRepository.findById(serID)
|
||||
.orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "字典记录不存在"));
|
||||
if (DICT_TYPE_CATEGORY.equals(type)) {
|
||||
// 活动分类:fa_activity.category 冗余存分类名称,按名称判断引用
|
||||
if (activityRepository.existsByCategory(dict.getDictValue())) {
|
||||
throw new BizException(ErrorCode.FORBIDDEN, "已引用该分类,无法删除!");
|
||||
}
|
||||
} else {
|
||||
// 主办部门:按 deptId = dictKey 匹配
|
||||
if (activityRepository.existsByDeptId(dict.getDictKey())) {
|
||||
throw new BizException(ErrorCode.FORBIDDEN, "已引用该部门,无法删除!");
|
||||
}
|
||||
}
|
||||
sysDictRepository.delete(dict);
|
||||
}
|
||||
|
||||
// ============================ OrdbyDicIds ============================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void sortDict(String dictType, String ids) {
|
||||
String type = normalizeManageType(dictType);
|
||||
if (type == null || !StringUtils.hasText(ids)) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "参数错误");
|
||||
}
|
||||
List<SysDict> list = sysDictRepository.findByDictTypeOrderBySortOrderAsc(type);
|
||||
if (list.isEmpty()) {
|
||||
throw new BizException(ErrorCode.FORBIDDEN, "字典类型错误,未找到该字典类型");
|
||||
}
|
||||
|
||||
// C# 逻辑:取当前最大 Sort 为起始序号,逆序遍历 ids 递减赋值 Des
|
||||
int[] idArr = Arrays.stream(ids.split(","))
|
||||
.map(String::trim)
|
||||
.filter(s -> !s.isEmpty())
|
||||
.mapToInt(Integer::parseInt)
|
||||
.toArray();
|
||||
|
||||
int maxSort = list.stream()
|
||||
.mapToInt(d -> d.getSortOrder() != null ? d.getSortOrder() : idArr.length)
|
||||
.max().orElse(idArr.length);
|
||||
|
||||
// lambda 中不能修改局部变量,用数组包装
|
||||
final int[] indexOrd = { maxSort };
|
||||
|
||||
// 逆序遍历,从最大序号递减
|
||||
for (int i = idArr.length - 1; i >= 0; i--) {
|
||||
int id = idArr[i];
|
||||
sysDictRepository.findById(id).ifPresent(dict -> {
|
||||
dict.setSortOrder(indexOrd[0]);
|
||||
sysDictRepository.save(dict);
|
||||
indexOrd[0]--;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ============================ GetSchModuleIsSms ============================
|
||||
|
||||
@Override
|
||||
public Boolean getSchModuleIsSms() {
|
||||
// C# 通过外部 ISchIsSmsBLL 查询学校短信开关。
|
||||
// Java 版暂不接入外部短信平台,默认返回 false。
|
||||
String account = currentAccount();
|
||||
if (account == null) {
|
||||
return false;
|
||||
}
|
||||
// 后续接入短信平台后补全查询逻辑
|
||||
return false;
|
||||
}
|
||||
|
||||
// ============================ 辅助方法 ============================
|
||||
|
||||
/**
|
||||
* 按主办部门字典 key 解析部门名称(对应 C# GetDeptName)。
|
||||
*/
|
||||
@Override
|
||||
public String resolveDeptName(String deptId) {
|
||||
if (!StringUtils.hasText(deptId)) {
|
||||
return null;
|
||||
}
|
||||
return sysDictRepository.findByDictTypeAndDictKey(DICT_TYPE_DEPT, deptId.trim())
|
||||
.map(SysDict::getDictValue)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将前端 Type 参数映射到字典类型字符串。
|
||||
* C# 中使用枚举:1=DEPT(主办部门),2=CATEGORY(活动分类)。
|
||||
*/
|
||||
private String mapDictType(Integer type) {
|
||||
if (type == null) return null;
|
||||
switch (type) {
|
||||
case 1: return DICT_TYPE_DEPT;
|
||||
case 2: return DICT_TYPE_CATEGORY;
|
||||
// 其余类型(文印/请假/物品等)本系统不涉及
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理端 type 参数归一化:"dept"/"category"(大小写不敏感,容忍首尾空白),
|
||||
* 并兼容旧前端的数字编码:1=dept、2=category(与 GetSysDic 的 type 枚举一致),其余返回 null。
|
||||
*/
|
||||
private String normalizeManageType(String type) {
|
||||
if (type == null) return null;
|
||||
String t = type.trim();
|
||||
if (t.isEmpty()) return null;
|
||||
if (DICT_TYPE_DEPT.equalsIgnoreCase(t) || "1".equals(t)) return DICT_TYPE_DEPT;
|
||||
if (DICT_TYPE_CATEGORY.equalsIgnoreCase(t) || "2".equals(t)) return DICT_TYPE_CATEGORY;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 字典为空时按类型给出对应提示。
|
||||
*/
|
||||
private String emptyTip(String dictType) {
|
||||
return DICT_TYPE_CATEGORY.equals(dictType)
|
||||
? "尚未添加活动分类,请联系管理员在「应用设置」中添加!"
|
||||
: "尚未添加主办部门,请联系管理员添加!";
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前登录人账号(来自平台 UserContextHolder,无登录态时为 null)。
|
||||
*/
|
||||
private String currentAccount() {
|
||||
LoginUser user = UserContextHolder.get();
|
||||
return user == null ? null : user.getLoginName();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
package com.yunhai.familyactivity.service.impl;
|
||||
|
||||
import com.yunhai.familyactivity.entity.UpFile;
|
||||
import com.yunhai.familyactivity.model.UpFileVo;
|
||||
import com.yunhai.familyactivity.model.UpFileResultVo;
|
||||
import com.yunhai.familyactivity.repository.UpFileRepository;
|
||||
import com.yunhai.familyactivity.service.IFileService;
|
||||
import com.yunhai.platform.apistd.BizException;
|
||||
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URLConnection;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.ArrayList;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
/**
|
||||
* 文件上传/下载/删除业务实现。
|
||||
* <p>
|
||||
* 存储路径规则:{uploadPath}/{appType}/{yyyyMMdd}/{uuid}.{ext}
|
||||
* 与 C# UpLoadFile.FileSave 逻辑等价,但去除分片上传和 Office 转 PDF(后续按需补充)。
|
||||
*/
|
||||
@Service
|
||||
public class FileService implements IFileService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(FileService.class);
|
||||
|
||||
@Autowired
|
||||
private UpFileRepository upFileRepository;
|
||||
|
||||
@Value("${file.upload-path}")
|
||||
private String uploadPath;
|
||||
|
||||
private static final String DEFAULT_APP_TYPE = "activity";
|
||||
|
||||
// ============================ 上传 ============================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public UpFileVo upload(MultipartFile file, String appType, Integer topicId) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "上传文件不得为空");
|
||||
}
|
||||
String originalName = file.getOriginalFilename();
|
||||
if (!StringUtils.hasText(originalName)) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "文件名为空");
|
||||
}
|
||||
// 提取后缀
|
||||
String ext = "";
|
||||
int dotIdx = originalName.lastIndexOf('.');
|
||||
if (dotIdx >= 0) {
|
||||
ext = originalName.substring(dotIdx + 1).toLowerCase();
|
||||
}
|
||||
|
||||
// 构造存储子路径:{appType}/{yyyyMMdd}/{uuid}.{ext}
|
||||
String dateDir = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
|
||||
String savedName = UUID.randomUUID().toString().replace("-", "") + (ext.isEmpty() ? "" : "." + ext);
|
||||
String relativePath = (appType == null ? DEFAULT_APP_TYPE : appType) + "/" + dateDir + "/" + savedName;
|
||||
|
||||
// 物理写入
|
||||
Path fullPath = Paths.get(uploadPath, relativePath);
|
||||
try {
|
||||
Files.createDirectories(fullPath.getParent());
|
||||
file.transferTo(fullPath.toFile());
|
||||
} catch (Exception e) {
|
||||
log.error("[upload] 文件保存失败: {}", fullPath, e);
|
||||
throw new BizException(ErrorCode.ERROR, "文件保存失败", e);
|
||||
}
|
||||
|
||||
// 入库
|
||||
UpFile entity = new UpFile();
|
||||
entity.setTopicType(appType == null ? DEFAULT_APP_TYPE : appType);
|
||||
entity.setTopicId(topicId);
|
||||
entity.setFileTrueName(originalName);
|
||||
entity.setFileName(relativePath);
|
||||
entity.setFileSize((int) file.getSize());
|
||||
entity.setFileType(ext);
|
||||
upFileRepository.save(entity);
|
||||
|
||||
return toVo(entity);
|
||||
}
|
||||
|
||||
// ============================ 绑定主题 ============================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void bindToTopic(List<Integer> fileIds, Integer topicId) {
|
||||
if (fileIds == null || fileIds.isEmpty() || topicId == null) {
|
||||
return;
|
||||
}
|
||||
for (Integer fileId : fileIds) {
|
||||
upFileRepository.findById(fileId).ifPresent(f -> {
|
||||
f.setTopicId(topicId);
|
||||
upFileRepository.save(f);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ============================ 查询附件列表 ============================
|
||||
|
||||
@Override
|
||||
public List<UpFileVo> listByTopic(String appType, Integer topicId) {
|
||||
String type = appType == null ? DEFAULT_APP_TYPE : appType;
|
||||
return upFileRepository.findByTopicTypeAndTopicId(type, topicId).stream()
|
||||
.map(this::toVo)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// ============================ 下载 ============================
|
||||
|
||||
@Override
|
||||
public void download(Integer fileId, HttpServletResponse response) {
|
||||
writeFile(fileId, response, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preview(Integer fileId, HttpServletResponse response) {
|
||||
writeFile(fileId, response, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件输出。inline=true 时 Content-Disposition 用 inline 并按扩展名输出真实 Content-Type
|
||||
* (浏览器/前端组件可直接渲染),否则 attachment 强制下载。
|
||||
*/
|
||||
private void writeFile(Integer fileId, HttpServletResponse response, boolean inline) {
|
||||
UpFile file = upFileRepository.findById(fileId)
|
||||
.orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "文件不存在: " + fileId));
|
||||
Path fullPath = Paths.get(uploadPath, file.getFileName());
|
||||
if (!Files.exists(fullPath)) {
|
||||
throw new BizException(ErrorCode.FORBIDDEN, "物理文件不存在: " + file.getFileName());
|
||||
}
|
||||
try (FileInputStream fis = new FileInputStream(fullPath.toFile());
|
||||
OutputStream os = response.getOutputStream()) {
|
||||
String encodedName = URLEncoder.encode(file.getFileTrueName(), "UTF-8")
|
||||
.replaceAll("\\+", "%20");
|
||||
if (inline) {
|
||||
response.setContentType(resolveContentType(file.getFileName()));
|
||||
response.setHeader("Content-Disposition", "inline; filename=\"" + encodedName + "\"");
|
||||
} else {
|
||||
response.setContentType("application/octet-stream");
|
||||
response.setHeader("Content-Disposition", "attachment; filename=\"" + encodedName + "\"");
|
||||
}
|
||||
response.setContentLengthLong(file.getFileSize());
|
||||
byte[] buffer = new byte[4096];
|
||||
int len;
|
||||
while ((len = fis.read(buffer)) != -1) {
|
||||
os.write(buffer, 0, len);
|
||||
}
|
||||
os.flush();
|
||||
} catch (Exception e) {
|
||||
log.error("[writeFile] 文件输出失败: {}", file.getFileName(), e);
|
||||
throw new BizException(ErrorCode.ERROR, "文件下载失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 按扩展名推断 Content-Type(JDK 自带表 + 常见办公/图片格式兜底),未知类型回退流格式。
|
||||
*/
|
||||
private String resolveContentType(String fileName) {
|
||||
String name = String.valueOf(fileName).toLowerCase();
|
||||
Map<String, String> table = new HashMap<>();
|
||||
table.put("pdf", "application/pdf");
|
||||
table.put("png", "image/png");
|
||||
table.put("jpg", "image/jpeg");
|
||||
table.put("jpeg", "image/jpeg");
|
||||
table.put("gif", "image/gif");
|
||||
table.put("bmp", "image/bmp");
|
||||
table.put("webp", "image/webp");
|
||||
table.put("svg", "image/svg+xml");
|
||||
table.put("txt", "text/plain;charset=UTF-8");
|
||||
table.put("mp4", "video/mp4");
|
||||
table.put("webm", "video/webm");
|
||||
table.put("ogg", "video/ogg");
|
||||
table.put("doc", "application/msword");
|
||||
table.put("docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document");
|
||||
table.put("xls", "application/vnd.ms-excel");
|
||||
table.put("xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||
table.put("ppt", "application/vnd.ms-powerpoint");
|
||||
table.put("pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation");
|
||||
String type = table.get(name.substring(name.lastIndexOf('.') + 1));
|
||||
if (type != null) {
|
||||
return type;
|
||||
}
|
||||
String guessed = URLConnection.guessContentTypeFromName(name);
|
||||
return guessed != null ? guessed : "application/octet-stream";
|
||||
}
|
||||
|
||||
// ============================ 删除 ============================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void delete(Integer fileId) {
|
||||
UpFile file = upFileRepository.findById(fileId)
|
||||
.orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "文件不存在: " + fileId));
|
||||
// 删物理文件
|
||||
Path fullPath = Paths.get(uploadPath, file.getFileName());
|
||||
try {
|
||||
Files.deleteIfExists(fullPath);
|
||||
} catch (Exception e) {
|
||||
log.warn("[delete] 物理文件删除失败: {}(数据库记录仍会删除)", fullPath, e);
|
||||
}
|
||||
// 删数据库记录
|
||||
upFileRepository.delete(file);
|
||||
}
|
||||
|
||||
// ============================ 打包下载 ZIP ============================
|
||||
|
||||
@Override
|
||||
public void downloadZip(List<UpFile> files, String zipName, HttpServletResponse response) {
|
||||
if (files == null || files.isEmpty()) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "无附件可下载");
|
||||
}
|
||||
try {
|
||||
response.setContentType("application/zip");
|
||||
String name = URLEncoder.encode(zipName == null ? "附件" : zipName, "UTF-8")
|
||||
.replaceAll("\\+", "%20");
|
||||
response.setHeader("Content-Disposition", "attachment; filename=\"" + name + ".zip\"");
|
||||
try (ZipOutputStream zos = new ZipOutputStream(response.getOutputStream())) {
|
||||
Set<String> used = new HashSet<>();
|
||||
for (UpFile f : files) {
|
||||
Path p = Paths.get(uploadPath, f.getFileName());
|
||||
if (!Files.exists(p)) {
|
||||
log.warn("[downloadZip] 物理文件缺失,跳过: {}", f.getFileName());
|
||||
continue;
|
||||
}
|
||||
String entryName = sanitizeEntryName(f.getFileTrueName());
|
||||
if (used.contains(entryName)) {
|
||||
entryName = f.getId() + "_" + entryName;
|
||||
}
|
||||
used.add(entryName);
|
||||
zos.putNextEntry(new ZipEntry(entryName));
|
||||
Files.copy(p, zos);
|
||||
zos.closeEntry();
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[downloadZip] 打包下载失败", e);
|
||||
throw new BizException(ErrorCode.ERROR, "打包下载失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 净化 ZIP 条目名:去除路径分隔符等非法字符,防止 Zip Slip。
|
||||
*/
|
||||
private String sanitizeEntryName(String name) {
|
||||
if (name == null || name.trim().isEmpty()) {
|
||||
return "file";
|
||||
}
|
||||
return name.replaceAll("[\\\\/:*?\"<>|]", "_");
|
||||
}
|
||||
|
||||
// ============================ 按主题打包下载 ============================
|
||||
|
||||
@Override
|
||||
public void downloadZipByTopic(String topicType, Integer topicId, String zipName,
|
||||
HttpServletResponse response) {
|
||||
String type = topicType == null ? DEFAULT_APP_TYPE : topicType;
|
||||
List<UpFile> files = upFileRepository.findByTopicTypeAndTopicId(type, topicId);
|
||||
downloadZip(files, zipName, response);
|
||||
}
|
||||
|
||||
// ============================ 批量上传图片 ============================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public List<UpFileVo> uploadImages(MultipartFile[] files, String appType) {
|
||||
List<UpFileVo> results = new ArrayList<>();
|
||||
if (files == null || files.length == 0) {
|
||||
return results;
|
||||
}
|
||||
String type = appType != null ? appType : DEFAULT_APP_TYPE;
|
||||
for (MultipartFile file : files) {
|
||||
results.add(upload(file, type, null));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
// ============================ 上传(返回前端兼容 VO) ============================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public UpFileResultVo uploadToResult(MultipartFile file, String appType) {
|
||||
UpFileVo vo = upload(file, appType, null);
|
||||
UpFileResultVo result = new UpFileResultVo();
|
||||
result.setFileid(vo.getId());
|
||||
result.setName(vo.getFileTrueName());
|
||||
result.setFileurl(vo.getFileName());
|
||||
result.setHost("");
|
||||
result.setType(vo.getFileType());
|
||||
result.setSize(vo.getFileSize());
|
||||
return result;
|
||||
}
|
||||
|
||||
// ============================ 转换 ============================
|
||||
|
||||
private UpFileVo toVo(UpFile entity) {
|
||||
UpFileVo vo = new UpFileVo();
|
||||
BeanUtils.copyProperties(entity, vo);
|
||||
return vo;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
package com.yunhai.familyactivity.service.impl;
|
||||
|
||||
import com.yunhai.familyactivity.model.SchTeaList;
|
||||
import com.yunhai.familyactivity.service.IClassService;
|
||||
import com.yunhai.familyactivity.service.IUserCenterService;
|
||||
import com.yunhai.platform.apistd.BizException;
|
||||
import com.yunhai.platform.openapi.client.YschBaseOpenApi;
|
||||
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||
import com.yunhai.platform.utils.model.BoaGroup;
|
||||
import com.yunhai.platform.utils.model.BoaModule;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 用户中心门面服务实现。
|
||||
* <p>原 BaseApiController.getSchoolAll 的组树组装、教师挂组、递归建树、排序逻辑整体下沉至此;
|
||||
* GetGroupUser 空桩改为显式报错(未接入用户中心对应开放接口时提示,而非静默返回空列表)。
|
||||
* 跨仓调用统一收敛到 {@link YschBaseOpenApi} 门面,Controller 不再直接静态调用。</p>
|
||||
*/
|
||||
@Service
|
||||
public class UserCenterService implements IUserCenterService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(UserCenterService.class);
|
||||
|
||||
/** 学生组(UserBase GroupTypeEnum.STUDENT_GROUP),组树中剔除 */
|
||||
private static final int GROUP_TYPE_STUDENT = 8;
|
||||
|
||||
@Autowired
|
||||
private IClassService classService;
|
||||
|
||||
// ============================ 学校组织树 ============================
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> getSchoolAllTree(Integer schId) {
|
||||
if (schId == null) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "获取学校信息失败");
|
||||
}
|
||||
|
||||
// 1. userbase 组树(整棵学校子树),剔除学生组
|
||||
List<Map<String, Object>> flatNodes = new ArrayList<>();
|
||||
BoaGroup root = YschBaseOpenApi.getGroupById(schId, true);
|
||||
if (root == null) {
|
||||
log.warn("[getSchoolAll] 用户中心未找到学校组, schId={}", schId);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
flattenGroup(root, 0, flatNodes);
|
||||
|
||||
// 2. bschclass 全部教师,按 GroupIds 挂到对应组下;无有效组的兜底挂学校根节点
|
||||
Set<Integer> treeGroupIds = new HashSet<>();
|
||||
for (Map<String, Object> node : flatNodes) {
|
||||
treeGroupIds.add((Integer) node.get("id"));
|
||||
}
|
||||
Map<Integer, SchTeaList> teacherMap = new LinkedHashMap<>();
|
||||
Map<Integer, Set<Integer>> teacherPids = new LinkedHashMap<>();
|
||||
List<SchTeaList> teachers = classService.getSchTeaList(schId);
|
||||
for (SchTeaList tea : teachers) {
|
||||
if (tea.getUserTeaSerID() == null) {
|
||||
continue;
|
||||
}
|
||||
// 同一教师多行(多组/接口重复数据)合并,按 (教师, 组) 去重
|
||||
teacherMap.putIfAbsent(tea.getUserTeaSerID(), tea);
|
||||
// 接口现行字段为单个 GroupID,兼容旧字段 GroupIds
|
||||
List<Integer> teaGroupIds = tea.getGroupIds();
|
||||
if ((teaGroupIds == null || teaGroupIds.isEmpty()) && tea.getGroupId() != null) {
|
||||
teaGroupIds = Collections.singletonList(tea.getGroupId());
|
||||
}
|
||||
if (teaGroupIds != null) {
|
||||
for (Integer gid : teaGroupIds) {
|
||||
if (gid != null && treeGroupIds.contains(gid)) {
|
||||
teacherPids.computeIfAbsent(tea.getUserTeaSerID(), k -> new HashSet<>()).add(gid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (Map.Entry<Integer, SchTeaList> entry : teacherMap.entrySet()) {
|
||||
int teaId = entry.getKey();
|
||||
SchTeaList tea = entry.getValue();
|
||||
Set<Integer> pids = teacherPids.get(teaId);
|
||||
if (pids == null || pids.isEmpty()) {
|
||||
// 教师所属组均被剔除(如学生组)或无组信息,挂学校根节点
|
||||
pids = new HashSet<>(Collections.singletonList(treeGroupIds.contains(schId) ? schId : 0));
|
||||
}
|
||||
for (Integer pid : pids) {
|
||||
Map<String, Object> node = new LinkedHashMap<>();
|
||||
node.put("id", teaId);
|
||||
node.put("name", tea.getTeaName() != null ? tea.getTeaName() : "");
|
||||
node.put("pid", pid);
|
||||
node.put("no", tea.getTeaNo() != null ? tea.getTeaNo() : 0);
|
||||
node.put("type", 2);
|
||||
node.put("isParent", false);
|
||||
node.put("photo", "");
|
||||
flatNodes.add(node);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 扁平节点递归组装为树
|
||||
return formatUserTree(flatNodes, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 BoaGroup 子树扁平化为组节点列表(type=1);学生组(type=8)整棵剔除。
|
||||
* isParent=true 标识组节点,前端 ztree 据此渲染为文件夹。
|
||||
*/
|
||||
private void flattenGroup(BoaGroup group, int pid, List<Map<String, Object>> nodes) {
|
||||
if (group == null || group.getId() == null) {
|
||||
return;
|
||||
}
|
||||
if (group.getType() != null && group.getType() == GROUP_TYPE_STUDENT) {
|
||||
return;
|
||||
}
|
||||
Map<String, Object> node = new LinkedHashMap<>();
|
||||
node.put("id", group.getId());
|
||||
node.put("name", group.getName() != null ? group.getName() : "");
|
||||
node.put("pid", pid);
|
||||
node.put("no", group.getSort() != null ? group.getSort() : 0);
|
||||
node.put("type", 1);
|
||||
node.put("isParent", true);
|
||||
node.put("photo", "");
|
||||
nodes.add(node);
|
||||
|
||||
if (group.getChildren() != null) {
|
||||
for (BoaGroup child : group.getChildren()) {
|
||||
flattenGroup(child, group.getId(), nodes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归构建用户树(对应 C# HttpUserApi.FormatUserTree)。
|
||||
* 根层取 pid=0 或父节点不在列表中的节点;type=1 且无子节点的空组剔除。
|
||||
*/
|
||||
private List<Map<String, Object>> formatUserTree(List<Map<String, Object>> treeList, int pid) {
|
||||
List<Map<String, Object>> data;
|
||||
if (pid == 0) {
|
||||
Set<Integer> allIds = new HashSet<>();
|
||||
for (Map<String, Object> n : treeList) {
|
||||
allIds.add((Integer) n.get("id"));
|
||||
}
|
||||
data = new ArrayList<>();
|
||||
for (Map<String, Object> n : treeList) {
|
||||
int p = (Integer) n.get("pid");
|
||||
if (p == 0 || !allIds.contains(p)) {
|
||||
data.add(n);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
data = new ArrayList<>();
|
||||
for (Map<String, Object> n : treeList) {
|
||||
if ((Integer) n.get("pid") == pid) {
|
||||
data.add(n);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
for (Map<String, Object> item : data) {
|
||||
int itemId = (Integer) item.get("id");
|
||||
int itemType = (Integer) item.get("type");
|
||||
|
||||
List<Map<String, Object>> children = formatUserTree(treeList, itemId);
|
||||
|
||||
// type=1(组) 且无子节点 → 不加入(C# 逻辑)
|
||||
if (itemType == 1 && children.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Map<String, Object> node = new LinkedHashMap<>(item);
|
||||
// 仅组节点携带 children;叶子不带该字段,避免 ztree 误判 isParent
|
||||
if (itemType == 1) {
|
||||
node.put("children", children);
|
||||
}
|
||||
result.add(node);
|
||||
}
|
||||
|
||||
// 按 no 排序(数字优先,无法解析的排最后)
|
||||
result.sort(Comparator.comparingInt(n -> {
|
||||
Object no = n.get("no");
|
||||
if (no instanceof Integer) {
|
||||
return (Integer) no;
|
||||
}
|
||||
if (no instanceof String) {
|
||||
try {
|
||||
return Integer.parseInt((String) no);
|
||||
} catch (NumberFormatException e) {
|
||||
return 9999;
|
||||
}
|
||||
}
|
||||
return 9999;
|
||||
}));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ============================ 组内用户 ============================
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> getGroupUser(Integer gid, Integer role) {
|
||||
if (gid == null) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "组ID不得为空");
|
||||
}
|
||||
log.info("[getGroupUser] gid={}, role={}, 用户中心组内用户开放接口尚未接入", gid, role);
|
||||
// 显式报错而非静默返回空列表:原空桩会让前端误以为组内无用户
|
||||
throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "组内用户查询接口暂未接入用户中心,请联系管理员配置后重试");
|
||||
}
|
||||
|
||||
// ============================ 用户模块树 ============================
|
||||
|
||||
@Override
|
||||
public List<BoaModule> getUserModuleTree(Integer userId, Integer schId) {
|
||||
if (userId == null) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "用户ID不得为空");
|
||||
}
|
||||
return YschBaseOpenApi.getUserModule(userId, schId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将扁平模块列表组装为树(对应原 NoticeController.buildModuleTree)。
|
||||
* 顶层取 parentId=0 或父节点不在列表中的节点,子节点按 orderNumber 排序。
|
||||
*/
|
||||
private List<Map<String, Object>> buildModuleTree(List<BoaModule> modules) {
|
||||
Map<Integer, BoaModule> moduleMap = modules.stream()
|
||||
.collect(Collectors.toMap(BoaModule::getId, m -> m, (a, b) -> a));
|
||||
|
||||
// 每个模块节点(含 children 占位)
|
||||
Map<Integer, Map<String, Object>> nodeMap = new LinkedHashMap<>();
|
||||
for (BoaModule m : modules) {
|
||||
Map<String, Object> node = new LinkedHashMap<>();
|
||||
node.put("id", m.getId());
|
||||
node.put("modId", m.getId());
|
||||
node.put("name", m.getName());
|
||||
node.put("parentId", m.getParentId() != null ? m.getParentId() : 0);
|
||||
node.put("page", m.getPath() != null ? m.getPath() : "");
|
||||
node.put("level", m.getLevel() != null ? m.getLevel() : 0);
|
||||
node.put("icon", m.getIcon() != null ? m.getIcon() : "");
|
||||
node.put("orderNumber", m.getSortNo() != null ? m.getSortNo() : 0);
|
||||
node.put("usable", true);
|
||||
node.put("children", new ArrayList<>());
|
||||
nodeMap.put(m.getId(), node);
|
||||
}
|
||||
|
||||
// 挂父子关系
|
||||
List<Map<String, Object>> roots = new ArrayList<>();
|
||||
for (BoaModule m : modules) {
|
||||
Map<String, Object> node = nodeMap.get(m.getId());
|
||||
int parentId = m.getParentId() != null ? m.getParentId() : 0;
|
||||
if (parentId == 0 || !moduleMap.containsKey(parentId)) {
|
||||
roots.add(node);
|
||||
} else {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> children = (List<Map<String, Object>>) nodeMap.get(parentId).get("children");
|
||||
children.add(node);
|
||||
}
|
||||
}
|
||||
|
||||
// 保持 SDK(用户中心)返回顺序,不再按 orderNumber 重排
|
||||
return roots;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.yunhai.familyactivity.utils;
|
||||
|
||||
import com.yunhai.familyactivity.model.BasePageQuery;
|
||||
import com.yunhai.familyactivity.model.BasePageResult;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
|
||||
/**
|
||||
* 分页辅助类:BasePageQuery <-> Pageable,Page <-> BasePageResult。
|
||||
*/
|
||||
public class PageUtils {
|
||||
|
||||
private PageUtils() {
|
||||
}
|
||||
|
||||
public static Pageable toPageable(BasePageQuery query, Sort sort) {
|
||||
int page = Math.max(query.getPageIndex() - 1, 0);
|
||||
int size = Math.max(query.getPageSize(), 1);
|
||||
if (sort == null) {
|
||||
sort = Sort.unsorted();
|
||||
}
|
||||
return PageRequest.of(page, size, sort);
|
||||
}
|
||||
|
||||
public static <T> BasePageResult<T> toResult(Page<T> page) {
|
||||
return new BasePageResult<T>(
|
||||
page.getContent(),
|
||||
(int) page.getTotalElements(),
|
||||
page.getNumber() + 1,
|
||||
page.getSize(),
|
||||
page.getTotalPages(),
|
||||
page.hasNext()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package com.yunhai.familyactivity.utils;
|
||||
|
||||
import lombok.Value;
|
||||
|
||||
/**
|
||||
* 当前登录用户上下文(ThreadLocal)。
|
||||
* <p>由 {@link com.yunhai.familyactivity.aspect.ControllerUserContextAspect} 在请求进入控制器前填充、
|
||||
* 请求结束后清理;Service 层统一通过本类获取当前用户(发布人账号、schId 数据隔离、操作日志)。
|
||||
* 参考实现:UserclassService {@code com.yunhai.bschclassmanage.utils.UserContext}。</p>
|
||||
*/
|
||||
public class UserContext {
|
||||
|
||||
private static final ThreadLocal<Integer> USER_ID = new ThreadLocal<>();
|
||||
private static final ThreadLocal<String> USER_NAME = new ThreadLocal<>();
|
||||
private static final ThreadLocal<String> ACCOUNT = new ThreadLocal<>();
|
||||
private static final ThreadLocal<Integer> ROLE_ID = new ThreadLocal<>();
|
||||
private static final ThreadLocal<Integer> SCH_ID = new ThreadLocal<>();
|
||||
private static final ThreadLocal<String> IP = new ThreadLocal<>();
|
||||
private static final ThreadLocal<Integer> TYPE = new ThreadLocal<>();
|
||||
|
||||
public static void setUserId(Integer userId) {
|
||||
USER_ID.set(userId);
|
||||
}
|
||||
public static Integer getUserId() {
|
||||
return USER_ID.get();
|
||||
}
|
||||
|
||||
public static void setUserName(String userName) {
|
||||
USER_NAME.set(userName);
|
||||
}
|
||||
public static String getUserName() {
|
||||
return USER_NAME.get();
|
||||
}
|
||||
|
||||
public static void setAccount(String account) {
|
||||
ACCOUNT.set(account);
|
||||
}
|
||||
/** 当前登录账号(家长端=学生账号;createBy/updateBy、publisherAccount 取自此) */
|
||||
public static String getAccount() {
|
||||
return ACCOUNT.get();
|
||||
}
|
||||
|
||||
public static void setRoleId(Integer roleId) {
|
||||
ROLE_ID.set(roleId);
|
||||
}
|
||||
public static Integer getRoleId() {
|
||||
return ROLE_ID.get();
|
||||
}
|
||||
|
||||
public static void setSchId(Integer schId) {
|
||||
SCH_ID.set(schId);
|
||||
}
|
||||
/** 当前学校ID(数据隔离口径;超管固定为 0) */
|
||||
public static Integer getSchId() {
|
||||
return SCH_ID.get();
|
||||
}
|
||||
|
||||
public static void setIp(String ip) {
|
||||
IP.set(ip);
|
||||
}
|
||||
public static String getIp() {
|
||||
return IP.get();
|
||||
}
|
||||
|
||||
public static void setType(Integer type) {
|
||||
TYPE.set(type);
|
||||
}
|
||||
/** 用户类型(平台 BoaUser.type;1=超级管理员) */
|
||||
public static Integer getType() {
|
||||
return TYPE.get();
|
||||
}
|
||||
|
||||
@Value
|
||||
public static class UserInfo {
|
||||
Integer userId;
|
||||
String userName;
|
||||
String account;
|
||||
Integer roleId;
|
||||
Integer schId;
|
||||
String ip;
|
||||
Integer type;
|
||||
}
|
||||
|
||||
public static UserInfo getCurrentUser() {
|
||||
return new UserInfo(
|
||||
USER_ID.get(),
|
||||
USER_NAME.get(),
|
||||
ACCOUNT.get(),
|
||||
ROLE_ID.get(),
|
||||
SCH_ID.get(),
|
||||
IP.get(),
|
||||
TYPE.get()
|
||||
);
|
||||
}
|
||||
|
||||
public static void setUserInfo(UserInfo info) {
|
||||
if (info == null) return;
|
||||
USER_ID.set(info.getUserId());
|
||||
USER_NAME.set(info.getUserName());
|
||||
ACCOUNT.set(info.getAccount());
|
||||
ROLE_ID.set(info.getRoleId());
|
||||
SCH_ID.set(info.getSchId());
|
||||
IP.set(info.getIp());
|
||||
TYPE.set(info.getType());
|
||||
}
|
||||
|
||||
/**
|
||||
* 包装异步任务:把当前用户上下文带入子线程执行,执行完自动清理,避免线程复用串号。
|
||||
*/
|
||||
public static Runnable wrap(Runnable task) {
|
||||
UserInfo info = getCurrentUser();
|
||||
return () -> {
|
||||
try {
|
||||
setUserInfo(info);
|
||||
task.run();
|
||||
} finally {
|
||||
clear();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static void clear() {
|
||||
USER_ID.remove();
|
||||
USER_NAME.remove();
|
||||
ACCOUNT.remove();
|
||||
ROLE_ID.remove();
|
||||
SCH_ID.remove();
|
||||
IP.remove();
|
||||
TYPE.remove();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
server:
|
||||
port: 8104
|
||||
|
||||
spring:
|
||||
mvc:
|
||||
date-format: yyyy-MM-dd HH:mm:ss
|
||||
jackson:
|
||||
time-zone: GMT+8
|
||||
date-format: yyyy-MM-dd HH:mm:ss
|
||||
|
||||
datasource:
|
||||
url: jdbc:mysql://localhost:3306/family_activity?useUnicode=true&characterEncoding=UTF-8&connectionCollation=utf8mb4_general_ci&serverTimezone=Asia/Shanghai
|
||||
username: root
|
||||
password: 123123
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
min-idle: 10
|
||||
max-active: 30
|
||||
#jpa
|
||||
jpa:
|
||||
database: MySQL
|
||||
show-sql: true
|
||||
hibernate:
|
||||
# 开发期使用 update 自动建表;生产环境设置成 none
|
||||
ddl-auto: update
|
||||
database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
|
||||
properties:
|
||||
hibernate.order_by.default_null_ordering: last
|
||||
open-in-view: true
|
||||
|
||||
#文件上传
|
||||
servlet:
|
||||
multipart:
|
||||
max-file-size: 100MB
|
||||
max-request-size: 100MB
|
||||
|
||||
#本地关闭 rabbit
|
||||
rabbitmq:
|
||||
listener:
|
||||
direct:
|
||||
auto-startup: false
|
||||
simple:
|
||||
auto-startup: false
|
||||
|
||||
# 文件上传存储路径(Docker 部署时与 volume 挂载一致)
|
||||
file:
|
||||
upload-path: D:/data/upload
|
||||
|
||||
# 短信模块配置
|
||||
sms:
|
||||
enabled: true
|
||||
module-id: notice
|
||||
|
||||
# 单点登录 / 应用间签名鉴权(基础平台 yh-auth / yh-app-auth)
|
||||
yh:
|
||||
sso:
|
||||
server-url-prefix: http://sso.yunhaixingtong.com
|
||||
client-host-url: http://localhost:8104
|
||||
allow-headers: all
|
||||
allow-origins: all
|
||||
url-pattern: /api/*
|
||||
mobi-api-pattern: /mobiapi/*
|
||||
app-auth:
|
||||
enabled: true
|
||||
# 本应用自身身份(客户端签名用)
|
||||
app-key: 100010
|
||||
app-secret: 07e3526c655b484caade38ce0484d917
|
||||
open:
|
||||
enabled: true
|
||||
base-host: http://base.yunhaixingtong.com
|
||||
user-center-url: http://base.yunhaixingtong.com/userbase
|
||||
# 班级管理(bschclass)服务基址
|
||||
bschclass: http://base.yunhaixingtong.com/userclass
|
||||
@@ -0,0 +1,33 @@
|
||||
spring:
|
||||
application:
|
||||
name: notice
|
||||
profiles:
|
||||
active: ${SPRING_PROFILES_ACTIVE:local}
|
||||
cloud:
|
||||
nacos:
|
||||
config:
|
||||
enabled: ${NACOS_CONFIG_ENABLED:false}
|
||||
server-addr: ${NACOS_ADDR:nacos.yunhaixingtong.com:8848}
|
||||
namespace: ${SPRING_CLOUD_NACOS_CONFIG_NAMESPACE:6196f0de-a28d-4894-8f10-e2982b57c6bc}
|
||||
# 应用自身配置所在分组
|
||||
group: OA_APP
|
||||
# 配置文件格式;最终主配置 dataId = ${spring.application.name}.${file-extension},即 notice.yaml
|
||||
file-extension: yaml
|
||||
encode: UTF-8
|
||||
# nacos 不可达时不强制退出,回退到本地 application.yml 兜底
|
||||
fail-fast: false
|
||||
# 只读账号
|
||||
username: ${SPRING_CLOUD_NACOS_USERNAME:yh-reader}
|
||||
password: ${SPRING_CLOUD_NACOS_PASSWORD:123456}
|
||||
# 共享公共配置:public 空间下的 common.properties
|
||||
shared-configs:
|
||||
# ① 1 号(最低优先级):public 命名空间(namespace="" 固定写空字符串)的 application-default.yaml
|
||||
- data-id: application-default.yaml
|
||||
group: DEFAULT_GROUP
|
||||
namespace: "" # ⭐ 固定 = "",强制指向 Nacos "public" 命名空间,不读上面的全局 prod namespace
|
||||
refresh: false
|
||||
|
||||
# ② 2 号(中等优先级):prod 命名空间(不写 namespace = 继承顶部全局 prod namespace id)的 application-infra.yaml
|
||||
- data-id: application-infra.yaml
|
||||
group: DEFAULT_GROUP
|
||||
refresh: false
|
||||
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<!-- 引入 Spring Boot 默认彩色日志变量 -->
|
||||
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
|
||||
|
||||
<!-- 控制台:所有环境都有,且带颜色2 -->
|
||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
|
||||
<charset>${CONSOLE_LOG_CHARSET}</charset>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<!-- 文件日志:只有非 local 环境才启用 -->
|
||||
<springProfile name="!local">
|
||||
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>/app/logs/app.log</file>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<fileNamePattern>/app/logs/app.%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||
<maxHistory>30</maxHistory>
|
||||
<totalSizeCap>5GB</totalSizeCap>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
</springProfile>
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="CONSOLE" />
|
||||
<springProfile name="!local">
|
||||
<appender-ref ref="FILE" />
|
||||
</springProfile>
|
||||
</root>
|
||||
</configuration>
|
||||
Reference in New Issue
Block a user