@@ -99,23 +99,28 @@ public class DictController {
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增审批人(JSON body:account/name/deptId)。
|
||||
* 新增审批人(JSON body:account/name/deptId/teaId;account 可空,空时后端按 teaId 经用户中心解析)。
|
||||
*/
|
||||
@PostMapping("AddApprover")
|
||||
public void addApprover(@RequestBody Map<String, String> body) {
|
||||
dictService.addApprover(body.get("account"), body.get("name"), body.get("deptId"));
|
||||
public void addApprover(@RequestBody Map<String, Object> body) {
|
||||
dictService.addApprover(
|
||||
asString(body.get("account")),
|
||||
asString(body.get("name")),
|
||||
asString(body.get("deptId")),
|
||||
toInteger(body.get("teaId")));
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新审批人(JSON body:id + 可选 name/deptId/enabled)。
|
||||
* 更新审批人(JSON body:id + 可选 name/deptId/enabled/teaId;teaId=0 清空教师绑定)。
|
||||
*/
|
||||
@PostMapping("UpdateApprover")
|
||||
public void updateApprover(@RequestBody Map<String, Object> body) {
|
||||
dictService.updateApprover(
|
||||
toInteger(body.get("id")),
|
||||
(String) body.get("name"),
|
||||
(String) body.get("deptId"),
|
||||
toInteger(body.get("enabled")));
|
||||
asString(body.get("name")),
|
||||
asString(body.get("deptId")),
|
||||
toInteger(body.get("enabled")),
|
||||
toInteger(body.get("teaId")));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -140,4 +145,9 @@ public class DictController {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Map 值转 String(null 返回 null,避免 String.valueOf(null) 产出 "null" 串) */
|
||||
private String asString(Object value) {
|
||||
return value == null ? null : String.valueOf(value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,18 @@ public class NoticeController {
|
||||
return noticeService.userList(query, PageUtils.toPageable(query, sort));
|
||||
}
|
||||
|
||||
// ============================ 收件箱三 tab 统计 ============================
|
||||
|
||||
/**
|
||||
* 「已收到」全部 / 未读 / 已读 三个 tab 的条数(v17-B1)。
|
||||
* <p>入参与 List 相同(keyword/type/deptId/startTime/endTime;分页与 readType 字段被忽略),
|
||||
* 统计结果与对应 tab 下列表的 total 一一对应,恒满足 {@code all = unread + read}。
|
||||
*/
|
||||
@GetMapping("ReadTabCount")
|
||||
public ReadTabCountVo readTabCount(NoticeQuery query) {
|
||||
return noticeService.readTabCount(query);
|
||||
}
|
||||
|
||||
// ============================ 已发/全部通知列表 ============================
|
||||
|
||||
/**
|
||||
@@ -182,11 +194,22 @@ public class NoticeController {
|
||||
|
||||
/**
|
||||
* 上一篇/下一篇通知ID。
|
||||
* <p>v17:已收序列(type=1)沿用与 List 相同的 keyword / 部门 / 时间范围筛选,
|
||||
* <b>公告重要程度在本接口用 noticeType 传</b>——同一请求里 `type` 已被「序列类型」占用,
|
||||
* 若让 NoticeQuery 直接绑定同名 type 会把 "0/1" 误当成重要程度筛选。
|
||||
* 参数均可选,不传即整条可见已收序列,旧调用方行为不变。
|
||||
*/
|
||||
@GetMapping("GetNextPrev")
|
||||
public Map<String, Object> getNextPrev(@RequestParam("serid") Integer serid,
|
||||
@RequestParam("type") String type) {
|
||||
return noticeService.getNextPrev(serid, type);
|
||||
@RequestParam("type") String type,
|
||||
@RequestParam(value = "noticeType", required = false) Integer noticeType,
|
||||
NoticeQuery query) {
|
||||
if (noticeType != null) {
|
||||
query.setType(noticeType);
|
||||
}
|
||||
// 已收序列刻意不受未读/已读影响(v17 决策 2);服务层也已固定按「全部」桶取序列
|
||||
query.setReadType(null);
|
||||
return noticeService.getNextPrev(serid, type, query);
|
||||
}
|
||||
|
||||
// ============================ 附件打包下载 ============================
|
||||
@@ -346,7 +369,7 @@ public class NoticeController {
|
||||
// ============================ 审批列表(待审/已审合一) ============================
|
||||
|
||||
/**
|
||||
* 审批列表:auditState/status=pending(默认) 待我审批 / approved 走流程已发布 /
|
||||
* 审批列表:auditState/status=pending(默认) 待审批(当前步审批人快照含我) / approved 走流程已发布 /
|
||||
* rejected 已驳回 / all|reviewed 已审合一。行 VO 为 NoticeVo,多步审批字段:
|
||||
* approvalStatus/approvalStatusDesc(进度文本)/approvalLevel/canApprove。
|
||||
*/
|
||||
@@ -370,6 +393,17 @@ public class NoticeController {
|
||||
return noticeService.approvalCount();
|
||||
}
|
||||
|
||||
// ============================ 审批中心 tab 统计数字 ============================
|
||||
|
||||
/**
|
||||
* 审批中心四 tab 统计数字(v13-E3):{pending,approved,rejected,all},
|
||||
* 口径与 ApprovalList 各 tab 默认过滤一致(不含 keyword 搜索条件)。
|
||||
*/
|
||||
@GetMapping("ApprovalTabCount")
|
||||
public ApprovalTabCountVo approvalTabCount() {
|
||||
return noticeService.approvalTabCount();
|
||||
}
|
||||
|
||||
/** 解析 scopes JSON 参数为范围列表(空/非法返回 null = 无范围) */
|
||||
private List<PushScopeItem> parseScopes(String scopes) {
|
||||
if (scopes == null || scopes.trim().isEmpty()) {
|
||||
|
||||
@@ -45,6 +45,13 @@ public class NoticeApproval extends BaseEntity {
|
||||
@Column(name = "operator_name", length = 64)
|
||||
private String operatorName;
|
||||
|
||||
/**
|
||||
* 操作人同步教师ID(用户中心 teaId,与教师树叶子、{@code notice_approval_step.approver_tea_ids} 同一 ID 空间;
|
||||
* 非本表/池表自增 id)。v15 新增:审批轨迹按人名展示、按教师ID留痕,账号仅作登录标识。
|
||||
*/
|
||||
@Column(name = "operator_tea_id")
|
||||
private Integer operatorTeaId;
|
||||
|
||||
/** 审批意见 */
|
||||
@Column(name = "opinion", length = 1000)
|
||||
private String opinion;
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.yunhai.campusnotice.model.notice;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 通知视图对象(列表/详情返回)。
|
||||
@@ -74,6 +75,12 @@ public class NoticeVo {
|
||||
/** 当前用户是否为本单当前步待签审批人(审批列表/详情操作按钮用) */
|
||||
private Boolean canApprove;
|
||||
|
||||
/** 当前步待签审批人姓名(审批列表「当前审批人」列;非待审批态为 null,对齐详情 approvalFlow[].pending) */
|
||||
private List<String> pendingApproverNames;
|
||||
|
||||
/** 最新轮末条处理意见(驳回行=驳回意见、通过行=末步通过意见;待审批/免审为 null,审批列表「审批意见」列) */
|
||||
private String lastOpinion;
|
||||
|
||||
/** 附件ID串("1,2,3",列表页下载入口使用;对齐旧 .NET UpFile 字段) */
|
||||
private String UpFile;
|
||||
|
||||
|
||||
@@ -84,19 +84,26 @@ public interface IDictService {
|
||||
/**
|
||||
* 审批人管理列表(含停用)。
|
||||
*
|
||||
* @return List<Map> 每项 {id, account, name, deptId, deptName, enabled}
|
||||
* @return List<Map> 每项 {id, account, name, deptId, deptName, enabled, teaId}
|
||||
*/
|
||||
List<Map<String, Object>> getApproverList();
|
||||
|
||||
/**
|
||||
* 新增审批人(本校唯一账号校验,部门名称按字典解析快照)。
|
||||
* <p>v7:teaId 非空时落教师ID绑定;account 为空且 teaId 非空时按教师ID经用户中心解析登录账号,
|
||||
* 解析失败报错提示手填账号。
|
||||
*
|
||||
* @param account 登录账号(可空,空时按 teaId 自动解析)
|
||||
* @param name 姓名(可空,回退账号展示)
|
||||
* @param deptId 发文部门字典 key(可空)
|
||||
* @param teaId 教师树叶子ID(可空;审核设置「选择教师」带入)
|
||||
*/
|
||||
void addApprover(String account, String name, String deptId);
|
||||
void addApprover(String account, String name, String deptId, Integer teaId);
|
||||
|
||||
/**
|
||||
* 更新审批人(null 参数不改动;enabled 仅接受 0/1)。
|
||||
* 更新审批人(null 参数不改动;enabled 仅接受 0/1;teaId 传 0 表示清空教师绑定)。
|
||||
*/
|
||||
void updateApprover(Integer id, String name, String deptId, Integer enabled);
|
||||
void updateApprover(Integer id, String name, String deptId, Integer enabled, Integer teaId);
|
||||
|
||||
/**
|
||||
* 删除审批人(仅本校记录)。
|
||||
|
||||
@@ -54,7 +54,7 @@ public interface INoticeService {
|
||||
void reject(Integer serid, String opinion);
|
||||
|
||||
/**
|
||||
* 审批列表:pending(默认)=待我审批 status=2;approved=走流程已发布;rejected=已驳回;
|
||||
* 审批列表:pending(默认)=待审批(当前步审批人快照含我)status=2;approved=走流程已发布;rejected=已驳回;
|
||||
* all/reviewed=已审合一。行 VO 为 NoticeVo(多步审批字段:approvalStatus/approvalStatusDesc/
|
||||
* approvalLevel/canApprove)。
|
||||
*/
|
||||
@@ -65,6 +65,12 @@ public interface INoticeService {
|
||||
*/
|
||||
int approvalCount();
|
||||
|
||||
/**
|
||||
* 审批中心四 tab 统计数字(v13-E3):pending/approved/rejected/all,
|
||||
* 口径与 ApprovalList 各 tab 默认过滤一致(不含 keyword)。
|
||||
*/
|
||||
ApprovalTabCountVo approvalTabCount();
|
||||
|
||||
/**
|
||||
* 审批人记忆回显:当前发布人最近一次所选审批教师ID组合,无记忆返回空。
|
||||
*/
|
||||
@@ -110,6 +116,13 @@ public interface INoticeService {
|
||||
*/
|
||||
NoticePageResult<NoticeVo> userList(NoticeQuery query, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 「已收到」三 tab 统计(v17-B1):全部 / 未读 / 已读。
|
||||
* 口径与 {@link #userList} 同源(同一可见性谓词 + 同一筛选条件),
|
||||
* <b>readType 不作为入参</b>(它就是被统计的三个桶),故恒满足 {@code all = unread + read}。
|
||||
*/
|
||||
ReadTabCountVo readTabCount(NoticeQuery query);
|
||||
|
||||
/**
|
||||
* 管理端列表:按条件查询全部通知(管理员查看全部)。
|
||||
*/
|
||||
@@ -165,10 +178,14 @@ public interface INoticeService {
|
||||
* 上一篇/下一篇通知ID(SQL 相邻查询)。
|
||||
*
|
||||
* @param serid 当前通知ID
|
||||
* @param type 0 已发(当前用户发布的序列)/ 1 已收(阅读记录序列)
|
||||
* @return {prevId, nextId}
|
||||
* @param type 0 已发(当前用户发布的序列,遗留 E1:仍无筛选且整表加载)/ 1 已收
|
||||
* @param query 已收序列的筛选条件(keyword/type/deptId/时间范围,与 List 同源,可为 null)。
|
||||
* 已收序列 v17 起为「当前用户可见的已收公告」(含未读),
|
||||
* <b>刻意忽略 query.readType</b>:未读公告点开即被标读,
|
||||
* 若序列带未读过滤会把当前条挤出集合、上下篇同时置灰。
|
||||
* @return {prevId, nextId},0 表示该方向没有可跳的公告
|
||||
*/
|
||||
Map<String, Object> getNextPrev(Integer serid, String type);
|
||||
Map<String, Object> getNextPrev(Integer serid, String type, NoticeQuery query);
|
||||
|
||||
/**
|
||||
* 获取某通知的阅读明细列表。
|
||||
@@ -195,6 +212,17 @@ public interface INoticeService {
|
||||
*/
|
||||
int timingPublishDue();
|
||||
|
||||
/**
|
||||
* 历史审批身份回填(v15):把「姓名=登录名」的历史留痕换成用户中心真实姓名,
|
||||
* 并补齐 {@code notice_approval.operator_tea_id}、{@code notice_approval_record.approver_tea_id}
|
||||
* 与 {@code notice_approval_step.approver_names}(按 teaId 对齐本校教师名单)。
|
||||
* <p>幂等:仅改「姓名为空/等于账号」与「teaId 为 NULL」的行,可重复执行。
|
||||
* 由启动开关 {@code campus-notice.backfill-approver-identity=true} 触发一次。
|
||||
*
|
||||
* @return 各表更新条数统计(trail/record/step/notice/unresolved)
|
||||
*/
|
||||
Map<String, Integer> backfillApproverIdentity();
|
||||
|
||||
/**
|
||||
* 通知统计:总数/已发布、累计阅读人次、当前用户未读、按发布人、按月。
|
||||
*/
|
||||
|
||||
@@ -11,6 +11,7 @@ import com.yunhai.platform.auth.LoginUser;
|
||||
import com.yunhai.platform.auth.UserContextHolder;
|
||||
import com.yunhai.platform.auth.spring.CurrentBoaUser;
|
||||
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.BoaUser;
|
||||
import org.slf4j.Logger;
|
||||
@@ -290,6 +291,8 @@ public class DictService implements IDictService {
|
||||
item.put("name", StringUtils.hasText(approver.getName()) ? approver.getName() : approver.getAccount());
|
||||
item.put("account", approver.getAccount());
|
||||
item.put("deptName", approver.getDeptName());
|
||||
// 教师ID:ApproverSuggest 记忆回显按 teaId 映射回池内账号(新版前端账号路径选人)
|
||||
item.put("teaId", approver.getTeaId());
|
||||
item.put("isParent", false);
|
||||
result.add(item);
|
||||
}
|
||||
@@ -308,6 +311,8 @@ public class DictService implements IDictService {
|
||||
item.put("deptId", approver.getDeptId());
|
||||
item.put("deptName", approver.getDeptName());
|
||||
item.put("enabled", approver.getEnabled());
|
||||
// v7:教师ID绑定(审核设置「选择教师」落库;白名单过滤与回显用)
|
||||
item.put("teaId", approver.getTeaId());
|
||||
result.add(item);
|
||||
}
|
||||
return result;
|
||||
@@ -315,12 +320,19 @@ public class DictService implements IDictService {
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void addApprover(String account, String name, String deptId) {
|
||||
public void addApprover(String account, String name, String deptId, Integer teaId) {
|
||||
Integer schId = requireSchId();
|
||||
if (!StringUtils.hasText(account)) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "请填写审批人账号");
|
||||
String acc = account == null ? "" : account.trim();
|
||||
if (!StringUtils.hasText(acc) && teaId != null && teaId > 0) {
|
||||
// v7:选择教师带入 teaId 但未填账号 → 按教师ID经用户中心解析登录账号
|
||||
acc = resolveAccountByTeaId(teaId);
|
||||
if (!StringUtils.hasText(acc)) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "无法自动获取教师登录账号(teaId=" + teaId + "),请手动填写账号");
|
||||
}
|
||||
}
|
||||
if (!StringUtils.hasText(acc)) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "请填写审批人账号,或选择本校教师自动关联");
|
||||
}
|
||||
String acc = account.trim();
|
||||
if (sysApproverRepository.findBySchIdAndAccount(schId, acc).isPresent()) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "审批人「" + acc + "」已存在");
|
||||
}
|
||||
@@ -330,13 +342,14 @@ public class DictService implements IDictService {
|
||||
approver.setName(StringUtils.hasText(name) ? name.trim() : acc);
|
||||
approver.setDeptId(StringUtils.hasText(deptId) ? deptId.trim() : null);
|
||||
approver.setDeptName(resolveDeptName(approver.getDeptId()));
|
||||
approver.setTeaId(teaId != null && teaId > 0 ? teaId : null);
|
||||
approver.setEnabled(1);
|
||||
sysApproverRepository.save(approver);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void updateApprover(Integer id, String name, String deptId, Integer enabled) {
|
||||
public void updateApprover(Integer id, String name, String deptId, Integer enabled, Integer teaId) {
|
||||
Integer schId = requireSchId();
|
||||
SysApprover approver = requireOwnSchool(schId, id);
|
||||
if (StringUtils.hasText(name)) {
|
||||
@@ -349,9 +362,29 @@ public class DictService implements IDictService {
|
||||
if (enabled != null) {
|
||||
approver.setEnabled(enabled == 0 ? 0 : 1);
|
||||
}
|
||||
if (teaId != null) {
|
||||
// v7:teaId=0 语义为清空教师绑定(存量记录补绑/解绑)
|
||||
approver.setTeaId(teaId > 0 ? teaId : null);
|
||||
}
|
||||
sysApproverRepository.save(approver);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按教师ID经用户中心解析登录账号(v7)。
|
||||
* <p>bschclass 教师名单(SchTeaList)无登录账号字段,故走用户中心;取 {@code BoaUser.getAccount()}。解析失败返回 null。
|
||||
*/
|
||||
private String resolveAccountByTeaId(Integer teaId) {
|
||||
try {
|
||||
BoaUser user = YschBaseOpenApi.getUserById(String.valueOf(teaId));
|
||||
if (user != null && StringUtils.hasText(user.getAccount())) {
|
||||
return user.getAccount().trim();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[addApprover] 用户中心解析教师账号失败, teaId={}", teaId, e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void delApprover(Integer id) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -62,8 +62,8 @@ yh:
|
||||
app-auth:
|
||||
enabled: true
|
||||
# 本应用自身身份(客户端签名用)
|
||||
app-key: 100001
|
||||
app-secret: ec4e860206374c80938719fff8e14a86
|
||||
app-key: 100006
|
||||
app-secret: e053f7d98518407ab0e69f0500820d17
|
||||
open:
|
||||
enabled: true
|
||||
base-host: http://base.yunhaixingtong.com
|
||||
|
||||
@@ -47,6 +47,8 @@
|
||||
.seg { display: inline-flex; background: #e2e8f0; border-radius: 8px; padding: 3px; }
|
||||
.seg button { border: none; background: none; padding: 5px 14px; border-radius: 6px; cursor: pointer; font-size: 13px; color: var(--text2); }
|
||||
.seg button.on { background: #fff; color: var(--primary); font-weight: 600; box-shadow: var(--shadow); }
|
||||
/* tab 条数(v17 ReadTabCount):空内容时不占位 */
|
||||
.seg .seg-cnt { margin-left: 5px; font-size: 12px; color: var(--text2); font-weight: 400; }
|
||||
.input { border: 1px solid var(--border); border-radius: 8px; padding: 7px 12px; font-size: 13px; background: #fff; color: var(--text); outline: none; }
|
||||
.input:focus { border-color: var(--primary); box-shadow: 0 0 0 3px rgba(37,99,235,.1); }
|
||||
select.input { cursor: pointer; }
|
||||
@@ -270,9 +272,9 @@
|
||||
</div>
|
||||
<div class="toolbar">
|
||||
<div class="seg" id="readSeg">
|
||||
<button class="on" data-v="0" onclick="inboxFilter('readType',0,this)">全部</button>
|
||||
<button data-v="1" onclick="inboxFilter('readType',1,this)">未读</button>
|
||||
<button data-v="2" onclick="inboxFilter('readType',2,this)">已读</button>
|
||||
<button class="on" data-v="0" onclick="inboxFilter('readType',0,this)">全部<span class="seg-cnt" id="cntAll"></span></button>
|
||||
<button data-v="1" onclick="inboxFilter('readType',1,this)">未读<span class="seg-cnt" id="cntUnread"></span></button>
|
||||
<button data-v="2" onclick="inboxFilter('readType',2,this)">已读<span class="seg-cnt" id="cntRead"></span></button>
|
||||
</div>
|
||||
<select class="input" id="inboxType" onchange="reloadInbox()">
|
||||
<option value="">全部类型</option>
|
||||
@@ -727,6 +729,17 @@ function renderUnreadBadge() {
|
||||
const n = inboxState.unreadCount;
|
||||
$('unreadBadge').textContent = n > 0 ? (n > 99 ? '99+' : n) : '';
|
||||
}
|
||||
/* 三 tab 条数(v17 ReadTabCount):与列表同一 keyword/type 过滤,readType 不参与 */
|
||||
async function loadInboxCounts() {
|
||||
const put = (id, n) => { const el = $(id); if (el) el.textContent = n > 0 ? '(' + n + ')' : ''; };
|
||||
try {
|
||||
const c = await api('/notice/ReadTabCount', {
|
||||
params: { type: $('inboxType').value, keyword: $('inboxKw').value.trim() }
|
||||
});
|
||||
if (!c) return;
|
||||
put('cntAll', c.all); put('cntUnread', c.unread); put('cntRead', c.read);
|
||||
} catch (e) { /* 统计失败不影响列表 */ }
|
||||
}
|
||||
async function loadInbox() {
|
||||
const box = $('inboxList');
|
||||
box.innerHTML = '<div class="empty">加载中…</div>';
|
||||
@@ -743,6 +756,7 @@ async function loadInbox() {
|
||||
const list = (r && r.content) || [];
|
||||
inboxState.unreadCount = (r && r.unreadCount) || inboxState.unreadCount;
|
||||
renderUnreadBadge();
|
||||
loadInboxCounts();
|
||||
$('inboxSub').textContent = r ? ('共 ' + r.total + ' 条 · 未读 ' + r.unreadCount + ' 条') : '';
|
||||
|
||||
if (!list.length) {
|
||||
@@ -819,9 +833,14 @@ async function openDetail(serid, from) {
|
||||
$('dFiles').style.display = 'none';
|
||||
}
|
||||
|
||||
// 上一篇/下一篇
|
||||
// 上一篇/下一篇(v17:已收序列沿用收件箱当前筛选;重要程度走 noticeType,`type` 已被序列类型占用)
|
||||
let np = {};
|
||||
try { np = await api('/notice/GetNextPrev', { params: { serid, type: 1 } }) || {}; } catch (e) { /* 忽略 */ }
|
||||
const navParams = { serid, type: 1 };
|
||||
if (from === 'inbox') {
|
||||
navParams.keyword = $('inboxKw').value.trim();
|
||||
if ($('inboxType').value) navParams.noticeType = $('inboxType').value;
|
||||
}
|
||||
try { np = await api('/notice/GetNextPrev', { params: navParams }) || {}; } catch (e) { /* 忽略 */ }
|
||||
setupNavBtn('dPrev', np.prev || np.preId || np.prevId, '上一篇');
|
||||
setupNavBtn('dNext', np.next || np.nextId, '下一篇');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user