diff --git a/api-curl-test.sh b/api-curl-test.sh new file mode 100644 index 0000000..e80f3da --- /dev/null +++ b/api-curl-test.sh @@ -0,0 +1,124 @@ +#!/bin/bash +# CampusNotice 接口 curl 测试脚本 v2 +# 用法: ./api-curl-test.sh [BASE] [PLATFORM] +# BASE 默认 http://localhost:8101 +# PLATFORM 默认 manage(CAS 仅拦 /api/* 与 /mobiapi/*,manage 为匿名通道;生产环境应封死,见安全提醒) +# 注意:中文 JSON 体必须走 UTF-8 文件(--data-binary @file), +# 直接 -d '中文' 会被 Git Bash→curl.exe 的 argv 转换变成 GBK,服务端报 +# Invalid UTF-8 start byte(HttpMessageNotReadable → code=6),属测试工具问题。 +BASE="${1:-http://localhost:8101}" +PF="${2:-manage}" +U="$BASE/$PF" +TMPB="$(dirname "$0")/.curl-body.json" +PASS=0; BIZ=0; FAIL=0 +CREATED_IDS=() + +req() { # req 名称 方法 路径 [json体] + local name="$1" method="$2" path="$3" body="$4" + local args=(-s --max-time 15 -w "\n%{http_code}" -H "Accept: application/json") + if [ -n "$body" ]; then + printf '%s' "$body" > "$TMPB" # UTF-8 原样字节落盘 + args+=(-X POST -H "Content-Type: application/json; charset=utf-8" --data-binary "@$(cygpath -w "$TMPB" 2>/dev/null || echo "$TMPB")") + elif [ "$method" = "POST" ]; then + args+=(-X POST) + fi + local out; out=$(curl "${args[@]}" "$U$path" 2>/dev/null) + local http="${out##*$'\n'}"; local json="${out%$'\n'*}" + local parsed; parsed=$(node -e " + try{const j=JSON.parse(process.argv[1]); + const d=j.data; + const ds=d===null||d===undefined?'null':Array.isArray(d)?'array['+d.length+']':typeof d==='object'?'{'+Object.keys(d).slice(0,4).join(',')+'}':String(d).slice(0,30); + console.log([j.code,(j.msg||'').slice(0,46),ds].join('\u0001')); + }catch(e){console.log(['-','-','-'].join('\u0001'))}" "$json" 2>/dev/null) + local code msg data + code=$(echo "$parsed" | cut -d$'\001' -f1) + msg=$(echo "$parsed" | cut -d$'\001' -f2) + data=$(echo "$parsed" | cut -d$'\001' -f3) + local st + if [ "$code" = "0" ]; then PASS=$((PASS+1)); st="✅PASS"; + elif [ "$code" = "-" ] || [ "$http" != "200" ]; then FAIL=$((FAIL+1)); st="❌FAIL"; + else BIZ=$((BIZ+1)); st="🟡BIZ"; fi + printf "%-26s | HTTP %s | code=%-3s | msg=%-46s | data=%s\n" "$name" "$http" "$code" "$msg" "$data" +} + +extract_id() { # 从最近一次输出无法跨函数取值,改为调用方直接用 node 从 curl 结果提取 + node -e "try{console.log(JSON.parse(require('fs').readFileSync(process.argv[1],'utf8')).data ?? '')}catch(e){console.log('')}" "$1" 2>/dev/null +} + +echo "================ CampusNotice curl 矩阵 (BASE=$BASE PF=$PF) ================" + +echo "---- A. 静态(直连 BASE,不经 platform 前缀) ----" +for p in "/" "/api-test.html"; do + http=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 "$BASE$p") + if [ "$http" = "200" ]; then PASS=$((PASS+1)); printf "%-26s | HTTP %s\n" "静态 $p" "$http"; else FAIL=$((FAIL+1)); printf "%-26s | HTTP %s ❌\n" "静态 $p" "$http"; fi +done +http=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 "$BASE/api/notice/Statis") +if [ "$http" = "302" ]; then PASS=$((PASS+1)); printf "%-26s | HTTP 302 → CAS 登录墙(符合预期)\n" "/api 登录墙对照"; else FAIL=$((FAIL+1)); printf "%-26s | HTTP $http ❌(预期302)\n" "/api 登录墙对照"; fi + +echo "---- B. 平台/基础(匿名语义) ----" +req "GetUserInfo" GET "/base/GetUserInfo" +req "GetUserMod(匿名预期拒)" GET "/base/GetUserMod" +req "GetSchoolAll" GET "/base/GetSchoolAll" +req "GetGroupUser(未接入桩)" GET "/base/GetGroupUser?gid=1&role=2" + +echo "---- C. 通知查询 ----" +req "notice/List" GET "/notice/List?pageIndex=1&pageSize=3" +req "notice/SendList" GET "/notice/SendList?pageIndex=1&pageSize=3&status=1&scope=user" +req "notice/Statis" GET "/notice/Statis" +req "notice/GetNextPrev" GET "/notice/GetNextPrev?serid=1&type=0" +req "notice/DraftList" GET "/notice/DraftList?pageIndex=1&pageSize=3" +req "notice/ApproverSuggest" GET "/notice/ApproverSuggest" +req "ApprovalList(pending)" GET "/notice/ApprovalList?auditState=pending&pageIndex=1&pageSize=3" +req "ApprovalList(reviewed)" GET "/notice/ApprovalList?auditState=reviewed&pageIndex=1&pageSize=3" + +echo "---- D. 推送范围(匿名预期拒) ----" +req "notice/ScopeTree" GET "/notice/ScopeTree" +req "notice/ScopePreview" GET "/notice/ScopePreview?scopes=%5B%7B%22scopeType%22%3A1%2C%22scopeId%22%3A0%2C%22scopeName%22%3A%22%E5%85%A8%E6%A0%A1%22%7D%5D" + +echo "---- E. 写链路(UTF-8 文件体,测后自清理) ----" +printf '%s' '{"title":"【测试】curl直发","content":"
curl
","type":1,"deptId":"1","deptName":"校长室","topmost":0,"receiveList":"ALL","fileIds":"","timingSendTime":null,"smsFlag":0,"receiptFlag":1,"publishType":"direct"}' > "$TMPB" +resp=$(curl -s --max-time 15 -X POST -H "Content-Type: application/json; charset=utf-8" --data-binary "@$(cygpath -w "$TMPB" 2>/dev/null || echo "$TMPB")" "$U/notice/Create") +echo "$resp" > "$TMPB.out" +ID_DIRECT=$(extract_id "$TMPB.out") +if [ -n "$ID_DIRECT" ]; then PASS=$((PASS+1)); printf "%-26s | code=0 | id=%s ✅\n" "Create(direct)" "$ID_DIRECT"; else FAIL=$((FAIL+1)); printf "%-26s | ❌ 未返回id\n" "Create(direct)"; fi +CREATED_IDS+=("$ID_DIRECT") + +printf '%s' '{"title":"【测试】curl草稿","content":"curl
","type":1,"deptId":"1","deptName":"校长室","topmost":0,"receiveList":"ALL","fileIds":"","timingSendTime":null,"smsFlag":0,"receiptFlag":1}' > "$TMPB" +resp=$(curl -s --max-time 15 -X POST -H "Content-Type: application/json; charset=utf-8" --data-binary "@$(cygpath -w "$TMPB" 2>/dev/null || echo "$TMPB")" "$U/notice/SaveDraft") +echo "$resp" > "$TMPB.out" +ID_DRAFT=$(extract_id "$TMPB.out") +if [ -n "$ID_DRAFT" ]; then PASS=$((PASS+1)); printf "%-26s | code=0 | id=%s ✅\n" "SaveDraft" "$ID_DRAFT"; else FAIL=$((FAIL+1)); printf "%-26s | ❌ 未返回id\n" "SaveDraft"; fi +CREATED_IDS+=("$ID_DRAFT") + +if [ -n "$ID_DRAFT" ]; then + printf '%s' "{\"serid\":$ID_DRAFT,\"approverAccounts\":[\"lijing\"],\"opinion\":\"请审批\"}" > "$TMPB" + resp=$(curl -s --max-time 15 -X POST -H "Content-Type: application/json; charset=utf-8" --data-binary "@$(cygpath -w "$TMPB" 2>/dev/null || echo "$TMPB")" "$U/notice/Submit") + code=$(node -e "try{console.log(JSON.parse(process.argv[1]).code)}catch(e){console.log('-')}" "$resp") + if [ "$code" = "0" ]; then PASS=$((PASS+1)); printf "%-26s | code=0 | 草稿→待审批 ✅\n" "Submit"; else BIZ=$((BIZ+1)); printf "%-26s | code=%s 🟡\n" "Submit" "$code"; fi +fi +req "Approve(匿名预期拒)" POST "/notice/Approve?serid=${ID_DRAFT:-0}&opinion=x" +req "Reject(匿名预期拒)" POST "/notice/Reject?serid=${ID_DRAFT:-0}&opinion=x" + +echo "---- F. 其他模块 ----" +req "dict/ManageList" GET "/dict/ManageList" +req "template/Typelist" GET "/template/Typelist" +req "contacts/GetGroup" GET "/contacts/GetGroup" +req "sms/GetSchool" GET "/sms/GetSchool" + +echo "---- G. 附件 ----" +f="$(dirname "$0")/.curl-upload-sample.txt"; echo "curl upload sample $(date +%s)" > "$f" +req "base/FileUpload" POST "/base/FileUpload" "" "file=@$(cygpath -w "$f" 2>/dev/null || echo "$f")" +rm -f "$f" + +echo "---- H. 清理测试数据 ----" +if [ "${#CREATED_IDS[@]}" -gt 0 ]; then + ids=$(IFS=,; echo "${CREATED_IDS[*]}") + printf '%s' "[$ids]" > "$TMPB" + resp=$(curl -s --max-time 15 -X POST -H "Content-Type: application/json; charset=utf-8" --data-binary "@$(cygpath -w "$TMPB" 2>/dev/null || echo "$TMPB")" "$U/notice/Delete") + code=$(node -e "try{console.log(JSON.parse(process.argv[1]).code)}catch(e){console.log('-')}" "$resp") + if [ "$code" = "0" ]; then PASS=$((PASS+1)); printf "%-26s | code=0 | ids=[$ids] 已清理 ✅\n" "Delete清理"; else FAIL=$((FAIL+1)); printf "%-26s | code=%s ❌\n" "Delete清理" "$code"; fi +fi +rm -f "$TMPB" "$TMPB.out" + +echo "================ 统计: PASS=$PASS BIZ(业务拒绝/环境预期)=$BIZ FAIL=$FAIL ================" +echo "提醒: /manage 匿名可访问全部业务接口——CAS 仅拦 /api/* 与 /mobiapi/*,生产环境务必封死(安全项)。" diff --git a/sql/fix_db_charset_utf8mb4.sql b/sql/fix_db_charset_utf8mb4.sql new file mode 100644 index 0000000..043f4fe --- /dev/null +++ b/sql/fix_db_charset_utf8mb4.sql @@ -0,0 +1,30 @@ +-- ===================================================================== +-- 修复:campus_notice 库字符集 latin1 → utf8mb4(2026-09-09 接口检测发现) +-- 现象:所有含中文的写入(notice Create/SaveDraft/Forward/Edit、审批人姓名、 +-- 审批意见等)报 Incorrect string value → 全局兜底为「内部服务错误」。 +-- 根因:建库未指定字符集(latin1_swedish_ci),Hibernate ddl-auto=update +-- 建表继承库默认,全部 14 张表均为 latin1。 +-- 修复:在线转换(存量数据仅 ASCII,转换无损);应用无需重启。 +-- DBA 建议:生产建库统一 utf8mb4_general_ci;新表继承库默认后即为 utf8mb4。 +-- ===================================================================== + +ALTER DATABASE campus_notice CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci; + +ALTER TABLE campus_notice.notice CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; +ALTER TABLE campus_notice.notice_read CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; +ALTER TABLE campus_notice.notice_receiver CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; +ALTER TABLE campus_notice.notice_push_scope CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; +ALTER TABLE campus_notice.notice_approval CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; +ALTER TABLE campus_notice.notice_approver_memory CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; +ALTER TABLE campus_notice.sys_dict CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; +ALTER TABLE campus_notice.template_type CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; +ALTER TABLE campus_notice.notice_template CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; +ALTER TABLE campus_notice.up_file CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; +ALTER TABLE campus_notice.contacts_group CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; +ALTER TABLE campus_notice.contacts_group_user CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; +ALTER TABLE campus_notice.sms_module CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; +ALTER TABLE campus_notice.sms_school_config CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; + +-- 核查(预期全部 utf8mb4): +-- SELECT table_name, table_collation FROM information_schema.TABLES +-- WHERE table_schema='campus_notice' ORDER BY table_name; diff --git a/src/main/java/com/yunhai/campusnotice/entity/NoticeApproval.java b/src/main/java/com/yunhai/campusnotice/entity/NoticeApproval.java new file mode 100644 index 0000000..cfce6fc --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/NoticeApproval.java @@ -0,0 +1,59 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Index; +import javax.persistence.Table; +import java.time.LocalDateTime; + +/** + * 通知审批记录表 notice_approval(改造方案 2.4)。 + *+ * 审批轨迹全量落本表(提交行含审批人快照,通过/驳回行记录实际处理人), + * 驳回重提产生新一轮提交行,历史轮次永久保留(详情页按时间线分组展示)。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "notice_approval", + indexes = @Index(name = "idx_approval_notice", columnList = "notice_id")) +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class NoticeApproval extends BaseEntity { + + /** 审批动作:1 提交审批 / 2 通过 / 3 驳回(见 ApprovalActionEnum) */ + public static final int ACTION_SUBMIT = 1; + public static final int ACTION_APPROVE = 2; + public static final int ACTION_REJECT = 3; + + /** 通知ID(关联 notice.id) */ + @Column(name = "notice_id", nullable = false) + private Integer noticeId; + + /** 审批动作:1 提交审批 / 2 通过 / 3 驳回 */ + @Column(name = "action", nullable = false) + private Integer action; + + /** 操作人账号 */ + @Column(name = "operator_account", length = 64) + private String operatorAccount; + + /** 操作人姓名 */ + @Column(name = "operator_name", length = 64) + private String operatorName; + + /** 审批意见 */ + @Column(name = "opinion", length = 1000) + private String opinion; + + /** 仅 action=1 提交行使用:选定审批人快照 JSON [{account,name}] */ + @Column(name = "assignees", columnDefinition = "LONGTEXT") + private String assignees; + + /** 操作时间 */ + @Column(name = "action_time") + private LocalDateTime actionTime; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/NoticeApproverMemory.java b/src/main/java/com/yunhai/campusnotice/entity/NoticeApproverMemory.java new file mode 100644 index 0000000..c21734f --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/NoticeApproverMemory.java @@ -0,0 +1,44 @@ +package com.yunhai.campusnotice.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; + +/** + * 审批人记忆表 notice_approver_memory(改造方案 2.5)。 + *
+ * 记住发布人最近一次提交所选的审批人组合,表单再次进入审批流程时 + * 经 ApproverSuggest 默认回显,免去重复选人。记忆仅是默认值: + * Submit 时后端仍逐个校验被选人资格,防止记忆中的人被撤销角色后带病提交。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "notice_approver_memory", + uniqueConstraints = @UniqueConstraint( + name = "uk_approver_memory_sch_publisher", + columnNames = {"sch_id", "publisher_account"})) +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class NoticeApproverMemory extends BaseEntity { + + /** 学校ID(多租户隔离) */ + @Column(name = "sch_id", nullable = false) + private Integer schId; + + /** 发布人账号(按人记忆) */ + @Column(name = "publisher_account", nullable = false, length = 64) + private String publisherAccount; + + /** 最近一次所选审批人账号 JSON ["a","b"] */ + @Column(name = "approver_accounts", columnDefinition = "LONGTEXT") + private String approverAccounts; + + /** 对应姓名快照 JSON(回显免二次查询) */ + @Column(name = "approver_names", columnDefinition = "LONGTEXT") + private String approverNames; +} diff --git a/src/main/java/com/yunhai/campusnotice/entity/NoticePushScope.java b/src/main/java/com/yunhai/campusnotice/entity/NoticePushScope.java new file mode 100644 index 0000000..79f0fa1 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/entity/NoticePushScope.java @@ -0,0 +1,41 @@ +package com.yunhai.campusnotice.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Index; +import javax.persistence.Table; + +/** + * 通知推送范围表 notice_push_scope(改造方案 2.2)。 + *
+ * 范围选择本质是圈定一组学校组织节点,随通知快照保存(防止后续组织调整影响历史通知): + * 判定用 scopeId,展示用 scopeName。推送范围不在主表冗余 JSON 字段,统一以本表为准。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "notice_push_scope", + indexes = @Index(name = "idx_push_scope_notice", columnList = "notice_id")) +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class NoticePushScope extends BaseEntity { + + /** 通知ID(关联 notice.id) */ + @Column(name = "notice_id", nullable = false) + private Integer noticeId; + + /** 范围类型:1 全校 / 2 学部 / 3 年级 / 4 班级 / 5 训练队 / 6 指定学员(见 PushScopeTypeEnum) */ + @Column(name = "scope_type", nullable = false) + private Integer scopeType; + + /** 对应组织节点ID:学部=学制ID、年级=gradeId、班级=class_id、学员=stuId、全校= schId(仅语义占位) */ + @Column(name = "scope_id") + private Integer scopeId; + + /** 快照名称(如"初二年级"),组织改名不影响历史通知展示 */ + @Column(name = "scope_name", length = 128) + private String scopeName; +} diff --git a/src/main/java/com/yunhai/campusnotice/enums/ApprovalActionEnum.java b/src/main/java/com/yunhai/campusnotice/enums/ApprovalActionEnum.java new file mode 100644 index 0000000..03fb52a --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/enums/ApprovalActionEnum.java @@ -0,0 +1,34 @@ +package com.yunhai.campusnotice.enums; + +import lombok.Getter; + +/** + * 通知审批动作(notice_approval.action)。 + */ +@Getter +public enum ApprovalActionEnum { + + SUBMIT(1, "提交审批"), + APPROVE(2, "审批通过"), + REJECT(3, "审批驳回"); + + private final int code; + private final String desc; + + ApprovalActionEnum(int code, String desc) { + this.code = code; + this.desc = desc; + } + + public static ApprovalActionEnum of(Integer code) { + if (code == null) { + return null; + } + for (ApprovalActionEnum e : values()) { + if (e.code == code) { + return e; + } + } + return null; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/enums/PushScopeTypeEnum.java b/src/main/java/com/yunhai/campusnotice/enums/PushScopeTypeEnum.java new file mode 100644 index 0000000..228fa52 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/enums/PushScopeTypeEnum.java @@ -0,0 +1,39 @@ +package com.yunhai.campusnotice.enums; + +import lombok.Getter; + +/** + * 推送范围节点类型(notice_push_scope.scope_type)。 + * scopeId 对应 bschclass 组织节点:学段=学制ID、年级=gradeId、班级=class_id; + * 训练队数据源待确认(改造方案 5.1-#3),暂不支持圈人展开。 + */ +@Getter +public enum PushScopeTypeEnum { + + SCHOOL(1, "全校"), + SUB_SCHOOL(2, "学部"), + GRADE(3, "年级"), + CLAZZ(4, "班级"), + TEAM(5, "训练队"), + STUDENT(6, "指定学员"); + + private final int code; + private final String desc; + + PushScopeTypeEnum(int code, String desc) { + this.code = code; + this.desc = desc; + } + + public static PushScopeTypeEnum of(Integer code) { + if (code == null) { + return null; + } + for (PushScopeTypeEnum e : values()) { + if (e.code == code) { + return e; + } + } + return null; + } +} diff --git a/src/main/java/com/yunhai/campusnotice/job/TimingPublishJob.java b/src/main/java/com/yunhai/campusnotice/job/TimingPublishJob.java new file mode 100644 index 0000000..b2804a2 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/job/TimingPublishJob.java @@ -0,0 +1,38 @@ +package com.yunhai.campusnotice.job; + +import com.yunhai.campusnotice.service.INoticeService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +/** + * 定时发送调度任务(改造方案 4.2)。 + *
每分钟扫描 status=1 且 timingSendTime<=now 且 timing_published=0 的通知, + * 逐条 CAS 抢占(timing_published 0→1)后执行真实发布(圈人/落学员快照/发短信/统计口径对齐)。 + * 幂等由 CAS 保证:重复扫描同一通知只触发一次真实发布。
+ *注意:当前按单实例部署实现(无分布式锁);若后续多实例部署, + * 需引入分布式锁(DB 行锁/Redis)或保留 CAS 抢占语义由数据库串行化。
+ */ +@Component +public class TimingPublishJob { + + private static final Logger log = LoggerFactory.getLogger(TimingPublishJob.class); + + @Autowired + private INoticeService noticeService; + + @Scheduled(fixedDelay = 60_000, initialDelay = 30_000) + public void publishDue() { + try { + int count = noticeService.timingPublishDue(); + if (count > 0) { + log.info("[TimingPublishJob] 本轮定时发布 {} 条通知", count); + } + } catch (Exception e) { + // 单条失败不影响下一轮扫描(事务内单条异常会整体回滚该条,由日志暴露) + log.error("[TimingPublishJob] 定时发布调度异常", e); + } + } +} diff --git a/src/main/java/com/yunhai/campusnotice/model/bsch/SchClassDto.java b/src/main/java/com/yunhai/campusnotice/model/bsch/SchClassDto.java new file mode 100644 index 0000000..2c51b20 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/bsch/SchClassDto.java @@ -0,0 +1,29 @@ +package com.yunhai.campusnotice.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,scopeType=班级 的 scopeId;学生 ClassID 指向它) */ + @JsonProperty("ClassID") + private Integer classId; + + /** 班级名称 */ + @JsonProperty("ClassName") + private String className; + + /** 年级ID(= gradeId) */ + @JsonProperty("Levell") + private Integer gradeId; + + /** 排序号 */ + @JsonProperty("sx") + private Integer sort; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/bsch/SchGradeDto.java b/src/main/java/com/yunhai/campusnotice/model/bsch/SchGradeDto.java new file mode 100644 index 0000000..fdb75db --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/bsch/SchGradeDto.java @@ -0,0 +1,25 @@ +package com.yunhai.campusnotice.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,scopeType=年级 的 scopeId;班级 Levell 指向它) */ + @JsonProperty("Id") + private Integer id; + + /** 年级名称(gradeNickname,如「初一」) */ + @JsonProperty("Name") + private String name; + + /** 所属学段(学制)ID */ + @JsonProperty("OwnerId") + private Integer ownerId; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/bsch/SchInfoDto.java b/src/main/java/com/yunhai/campusnotice/model/bsch/SchInfoDto.java new file mode 100644 index 0000000..f683b91 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/bsch/SchInfoDto.java @@ -0,0 +1,25 @@ +package com.yunhai.campusnotice.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(与 CampusNotice schId 同域) */ + @JsonProperty("UserSchSerId") + private Integer userSchSerId; + + /** 学校名称 */ + @JsonProperty("SchName") + private String schName; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/bsch/SchStudentDto.java b/src/main/java/com/yunhai/campusnotice/model/bsch/SchStudentDto.java new file mode 100644 index 0000000..cd45856 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/bsch/SchStudentDto.java @@ -0,0 +1,34 @@ +package com.yunhai.campusnotice.model.bsch; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +/** + * bschclass 学生 DTO(/oapi/OAStu/*,SchStudent 视图)。 + * 登录账号口径:学员端账号 = SchStudent.account(改造方案 5.1-#1)。 + */ +@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; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/bsch/SchSystemDto.java b/src/main/java/com/yunhai/campusnotice/model/bsch/SchSystemDto.java new file mode 100644 index 0000000..7084aa9 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/bsch/SchSystemDto.java @@ -0,0 +1,25 @@ +package com.yunhai.campusnotice.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(scopeType=学部 的 scopeId) */ + @JsonProperty("Id") + private Integer id; + + /** 学段名称(如「小学部」「初中部」) */ + @JsonProperty("Name") + private String name; + + /** 学制类型 */ + @JsonProperty("Type") + private Integer type; +} diff --git a/src/main/java/com/yunhai/campusnotice/model/bsch/SchTreeNodeDto.java b/src/main/java/com/yunhai/campusnotice/model/bsch/SchTreeNodeDto.java new file mode 100644 index 0000000..0d38a76 --- /dev/null +++ b/src/main/java/com/yunhai/campusnotice/model/bsch/SchTreeNodeDto.java @@ -0,0 +1,41 @@ +package com.yunhai.campusnotice.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已审行携带最新处理信息:或签下含「他人已处理」(handledByMe=false),
+ * 实际处理人/动作/时间/意见取 notice_approval 最新通过/驳回行。
+ */
+@Data
+public class ApprovalListVo {
+
+ private Integer id;
+
+ private String title;
+
+ /** 通知类型:1 普通 / 2 重要 */
+ private Integer type;
+
+ private String typeDesc;
+
+ /** 状态:2 待审批 / 1 已发布 / 3 已驳回 */
+ private Integer status;
+
+ private String statusDesc;
+
+ /** 发布人账号 */
+ private String publisherAccount;
+
+ /** 发布人姓名 */
+ private String publisherName;
+
+ /** 本轮审批人姓名(来自提交行 assignees 快照,列表页免查用户中心) */
+ private List 教师树(base/GetSchoolAll)叶子仅携带教师用户ID与姓名,审批快照按登录账号存储;
+ * 前端选人后经 ApproverResolve 换算账号提交,编辑回显时按账号换回节点ID勾树。
+ * 按账号解析失败时 id 为 null,按ID解析失败时 account 为 null(调用方据此提示)。