From 07c409c83c38b102802d67c49d1626884352362e Mon Sep 17 00:00:00 2001 From: wanghu Date: Wed, 23 Sep 2026 10:56:26 +0800 Subject: [PATCH] =?UTF-8?q?=E9=98=85=E8=AF=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/后端重构计划-活动信息发布.md | 6 ++--- .../model/activity/ActivityProgress.java | 8 ++++++- .../model/activity/SignupBrief.java | 3 +++ .../service/impl/ActivityService.java | 24 +++++++++++++++---- 4 files changed, 33 insertions(+), 8 deletions(-) diff --git a/docs/后端重构计划-活动信息发布.md b/docs/后端重构计划-活动信息发布.md index bb0ce6b..282d358 100644 --- a/docs/后端重构计划-活动信息发布.md +++ b/docs/后端重构计划-活动信息发布.md @@ -210,7 +210,7 @@ com.yunhai.familyactivity 1. **发布**(Create):draft=true 存草稿;否则置已发;`targetClassIds()` 统一解析 `receiveList`(`"ALL"`=全校班级);发布时按班级×学生**预生成回执记录**(receiptMode≠none 时,status=none),保证「应收人数」「未回执名单」可查。 2. **状态机**:`actStatus` 推导规则 —— status=草稿→`draft`;now < signupDeadline→`signup`;signupDeadline ≤ now < endTime→`closed`;now ≥ endTime→`finished`(receiptMode=none 时无报名态语义,仅展示)。 -3. **进度统计**(`ActivityProgress`):`should`=应回执人数;`signed/confirmed/canceled` 按 signup 表 status 计数;`unsigned=should-signed-confirmed-canceled`;`usedQuota`=ΣjoinCount(canceled 不计);`full = quota>0 && usedQuota≥quota`;`signupRate`。`receiptMode=none` 或 `draft` 时 `progress=null`。 +3. **进度统计**(`ActivityProgress`):`should`=应回执人数;`signed/confirmed/canceled` 按 signup 表 status 计数;`unsigned=should-signed-confirmed-canceled`;`usedQuota`=ΣjoinCount(canceled 不计);`full = quota>0 && usedQuota≥quota`;`signupRate`;`readCount`=已读人数、`readRate`=阅读率%(readCount/should 取整,仅 `/Signups` 汇总填充,口径同详情 `readnumcount`)。`receiptMode=none` 或 `draft` 时 `progress=null`。 4. **已读**:`Detail(type=1)` / `MySignup` 标读(家长端打开即确认模式:`receiptMode=confirm` 且状态为 none 时同步置 `confirmed`);`InitializeRed` 一键全部已读;`ReadDetail` 按年级→班级分组返回读/未读。 5. **报名**(Signup/CancelSignup):校验活动状态(signup 中)、截止时间、名额(quota>0 时 usedQuota+joinCount≤quota)、随行(allowAccompany);成功/失败回执业务码。 6. **管理动作**:Edit(含 sourceId 复用)、Delete(批量 + 级联删 read/signup)、UserPull 撤回、SetTopmost、ReceiveClasses 补发(新班级追加并补回执记录)、Expedite/Remind 催办(本期日志占位)、ExportSignups 导出 CSV(easyexcel 已在 pom,CSV 直接拼文本返回)。 @@ -238,7 +238,7 @@ com.yunhai.familyactivity | POST | `/SetTopmost` | serid, topmost | 置顶 | | POST | `/Expedite` | serid | 催办未读(日志占位) | | POST | `/ReceiveClasses` | serid, classes | 追加接收班级(补回执) | -| GET | `/Signups` | serid, signupStatus | 回执名单(按班级分组+汇总) | +| GET | `/Signups` | serid, signupStatus | 回执名单(按班级分组+汇总;summary 含 `readCount`/`readRate`,名单项含 `read` 已读/未读) | | GET | `/SignupDetail` | signupId | 单条回执详情 | | POST | `/Review` | ReviewRequest | 处理回执 confirm/cancel/waitlist | | POST | `/Remind` | serid | 催报名(日志占位,返回 {reminded}) | @@ -444,4 +444,4 @@ npm run dev # 首次需 npm install(Node≥20 建议 --ignore-scripts - `ActivityVo.createBy/updateBy` 同步改 `Integer`,JSON 输出为数字;前端契约 `types/activity.ts` 改 `number | null`(mock 演示数据置 null)。 - 历史行为账号文本,无法在本库映射为用户ID:迁移脚本先将非纯数字文本置 NULL,再把列 `VARCHAR(50)` 改为 `INT`(涉及继承 BaseEntity 的全部 7 张表),纯数字历史值直接转换保留。 -验收:`mvn compile` BUILD SUCCESS。注意:`createBy/updateBy` 改 INT 后前端契约同步为 `number | null`,`fa_sys_dict_sch_id_and_audit.sql` 需由 DBA 执行(含审计列 VARCHAR→INT 转换)。 +验收:`mvn compile` BUILD SUCCESS。注意:`createBy/updateBy` 改 INT 后前端契约同步为 `number | null`,`fa_sys_dict_sch_id_and_audit.sql` 需由 DBA 执行(含审计列 VARCHAR→INT 转换)。**本地库 `family_activity` 已于 2026-09-20 执行**(`sch_id` 列此前已由 ddl-auto 自动添加,脚本跳过加列直接归零回填;存量账号文本 `lijing` 置 NULL 后 14 个审计列转为 `int(11)`,并建 `idx_sys_dict_sch_type` 索引;因库中已有真实字典数据(发文部/运动,归入全局种子),未执行种子 INSERT 段)。 diff --git a/src/main/java/com/yunhai/familyactivity/model/activity/ActivityProgress.java b/src/main/java/com/yunhai/familyactivity/model/activity/ActivityProgress.java index 0ae3bbe..5dd32dc 100644 --- a/src/main/java/com/yunhai/familyactivity/model/activity/ActivityProgress.java +++ b/src/main/java/com/yunhai/familyactivity/model/activity/ActivityProgress.java @@ -43,8 +43,14 @@ public class ActivityProgress { /** 待处理回执(名额超额/待定),统计用,列表场景恒为 0 */ private int pendingReview; + /** 已读人数(fa_activity_read 班级粒度投影到学生,与详情 readnumcount 同口径) */ + private int readCount; + + /** 阅读率(readCount / should,四舍五入取整 %) */ + private int readRate; + 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); + this(should, signed, confirmed, canceled, unsigned, signupRate, quota, usedQuota, full, 0, 0, 0); } } diff --git a/src/main/java/com/yunhai/familyactivity/model/activity/SignupBrief.java b/src/main/java/com/yunhai/familyactivity/model/activity/SignupBrief.java index 161fd1b..b600e4d 100644 --- a/src/main/java/com/yunhai/familyactivity/model/activity/SignupBrief.java +++ b/src/main/java/com/yunhai/familyactivity/model/activity/SignupBrief.java @@ -40,6 +40,9 @@ public class SignupBrief { /** 是否已回执/已报名 */ private boolean hasReceipt; + /** 阅读状态:该生所在班是否已读(fa_activity_read 班级粒度投影) */ + private boolean read; + /** 备注(如过敏史、随行人数说明) */ private String remark; } diff --git a/src/main/java/com/yunhai/familyactivity/service/impl/ActivityService.java b/src/main/java/com/yunhai/familyactivity/service/impl/ActivityService.java index b16a36d..74e7f64 100644 --- a/src/main/java/com/yunhai/familyactivity/service/impl/ActivityService.java +++ b/src/main/java/com/yunhai/familyactivity/service/impl/ActivityService.java @@ -504,13 +504,21 @@ public class ActivityService implements IActivityService { .collect(Collectors.toList()); Map classes = schoolClasses(schId); + + // 已读统计:fa_activity_read 为班级粒度,投影到学生数并与接收班级求交(与详情 readnumcount 同口径); + // readClassIds 供名单行按"该生所在班是否已读"填充 read(与 SignupDetail 的 findRead 判定一致) + List reads = activityReadRepository.findByActivityId(serid); + Set readClassIds = reads.stream() + .map(ActivityRead::getClassId) + .collect(Collectors.toSet()); + Map> byClass = filtered.stream() .collect(Collectors.groupingBy(r -> r.getClassName() != null ? r.getClassName() : classNameOf(classes, parseSafe(r.getClassId())), LinkedHashMap::new, Collectors.toList())); List groups = byClass.entrySet().stream() .map(e -> new SignupGroup(e.getKey(), e.getValue().stream() - .map(this::toBrief).collect(Collectors.toList()))) + .map(r -> toBrief(r, readClassIds)).collect(Collectors.toList()))) .collect(Collectors.toList()); Map> byClassStudents = studentsByClass(schId, targetClassIds(activity, classes)); @@ -518,6 +526,9 @@ public class ActivityService implements IActivityService { if (should == 0 && !records.isEmpty()) { should = records.size(); // 班级数据不可达时降级为回执表实有人数 } + int readCount = reads.stream() + .mapToInt(r -> byClassStudents.getOrDefault(parseSafe(r.getClassId()), Collections.emptyList()).size()) + .sum(); SignupsResultVo vo = new SignupsResultVo(); vo.setActivityId(activity.getId()); @@ -532,7 +543,10 @@ public class ActivityService implements IActivityService { ReceiptModeEnum mode = ReceiptModeEnum.of(activity.getReceiptMode()); vo.setReceiptMode(mode.getCode()); vo.setReceiptModeDesc(mode.getDesc()); - vo.setSummary(computeProgress(activity, records, should)); + ActivityProgress summary = computeProgress(activity, records, should); + summary.setReadCount(readCount); + summary.setReadRate(should > 0 ? (int) Math.round(readCount * 100.0 / should) : 0); + vo.setSummary(summary); vo.setGroups(groups); return vo; } @@ -1272,7 +1286,8 @@ public class ActivityService implements IActivityService { boolean full = quota > 0 && usedQuota >= quota; double rate = should > 0 ? Math.round((signed + confirmed) * 1000.0 / should) / 10.0 : 0; int pending = quota > 0 ? Math.max(0, usedQuota - quota) : 0; - return new ActivityProgress(should, signed, confirmed, canceled, unsigned, rate, quota, usedQuota, full, pending); + // readCount/readRate 由调用方按 fa_activity_read 投影填充(如 Signups 汇总),此处缺省 0 + return new ActivityProgress(should, signed, confirmed, canceled, unsigned, rate, quota, usedQuota, full, pending, 0, 0); } private int usedQuota(List records) { @@ -1591,7 +1606,7 @@ public class ActivityService implements IActivityService { return vo; } - private SignupBrief toBrief(ActivitySignup r) { + private SignupBrief toBrief(ActivitySignup r, Set readClassIds) { SignupBrief brief = new SignupBrief(); brief.setSignupId(r.getId()); brief.setStudentId(r.getStudentId()); @@ -1605,6 +1620,7 @@ public class ActivityService implements IActivityService { brief.setSignedAt(fmt(r.getSignedAt())); boolean receipt = !SignupStatusEnum.NONE.getCode().equalsIgnoreCase(r.getStatus()); brief.setHasReceipt(receipt); + brief.setRead(readClassIds.contains(r.getClassId())); brief.setRemark(r.getRemark()); return brief; }