@@ -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 段)。
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,9 @@ public class SignupBrief {
|
||||
/** 是否已回执/已报名 */
|
||||
private boolean hasReceipt;
|
||||
|
||||
/** 阅读状态:该生所在班是否已读(fa_activity_read 班级粒度投影) */
|
||||
private boolean read;
|
||||
|
||||
/** 备注(如过敏史、随行人数说明) */
|
||||
private String remark;
|
||||
}
|
||||
|
||||
@@ -504,13 +504,21 @@ public class ActivityService implements IActivityService {
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Map<Integer, SchClassDto> classes = schoolClasses(schId);
|
||||
|
||||
// 已读统计:fa_activity_read 为班级粒度,投影到学生数并与接收班级求交(与详情 readnumcount 同口径);
|
||||
// readClassIds 供名单行按"该生所在班是否已读"填充 read(与 SignupDetail 的 findRead 判定一致)
|
||||
List<ActivityRead> reads = activityReadRepository.findByActivityId(serid);
|
||||
Set<String> readClassIds = reads.stream()
|
||||
.map(ActivityRead::getClassId)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
Map<String, List<ActivitySignup>> byClass = filtered.stream()
|
||||
.collect(Collectors.groupingBy(r -> r.getClassName() != null ? r.getClassName()
|
||||
: classNameOf(classes, parseSafe(r.getClassId())), LinkedHashMap::new, Collectors.toList()));
|
||||
|
||||
List<SignupGroup> 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<Integer, List<SchStudentDto>> 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<ActivitySignup> records) {
|
||||
@@ -1591,7 +1606,7 @@ public class ActivityService implements IActivityService {
|
||||
return vo;
|
||||
}
|
||||
|
||||
private SignupBrief toBrief(ActivitySignup r) {
|
||||
private SignupBrief toBrief(ActivitySignup r, Set<String> 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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user