|
|
@@ -254,4 +254,78 @@ public class GrowthPlanService {
|
|
|
plan.setUpdatedAt(new Date());
|
|
|
return growthPlanMapper.updateById(plan) > 0;
|
|
|
}
|
|
|
+
|
|
|
+ /** 草稿有效期默认 7 天(SC-P-81 边界:草稿有效期)。 */
|
|
|
+ private static final long DRAFT_VALIDITY_DAYS = 7L;
|
|
|
+ private static final long DRAFT_VALIDITY_MS = DRAFT_VALIDITY_DAYS * 24L * 3600_000L;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 存草稿(SC-P-81 正常流):暂存未完方案,持久化 status=draft,
|
|
|
+ * 设置有效期 draftExpiresAt = now + 7 天,返回已存草稿(含 id/版本)。
|
|
|
+ * <p>新建草稿版本号从 1 起;更新已存草稿版本号递增(并发两版:后者覆盖且提示冲突)。</p>
|
|
|
+ */
|
|
|
+ public GrowthPlan saveDraft(GrowthPlan plan) {
|
|
|
+ plan.setStatus("draft");
|
|
|
+ Date now = new Date();
|
|
|
+ plan.setDraftExpiresAt(new Date(now.getTime() + DRAFT_VALIDITY_MS));
|
|
|
+ if (plan.getId() != null) {
|
|
|
+ GrowthPlan existing = growthPlanMapper.selectById(plan.getId());
|
|
|
+ int baseVersion = existing != null && existing.getDraftVersion() != null ? existing.getDraftVersion() : 0;
|
|
|
+ plan.setDraftVersion(baseVersion + 1);
|
|
|
+ plan.setUpdatedAt(now);
|
|
|
+ growthPlanMapper.updateById(plan);
|
|
|
+ } else {
|
|
|
+ plan.setDraftVersion(1);
|
|
|
+ plan.setReviewCount(plan.getReviewCount() != null ? plan.getReviewCount() : 0);
|
|
|
+ plan.setCreatedAt(now);
|
|
|
+ plan.setUpdatedAt(now);
|
|
|
+ growthPlanMapper.insert(plan);
|
|
|
+ }
|
|
|
+ return plan;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 恢复草稿继续编辑(SC-P-81 正常流);草稿已超有效期 → 提示『草稿已失效』。 */
|
|
|
+ public GrowthPlan restoreDraft(Long draftId) {
|
|
|
+ GrowthPlan plan = growthPlanMapper.selectById(draftId);
|
|
|
+ if (plan == null) {
|
|
|
+ throw new IllegalArgumentException("方案不存在");
|
|
|
+ }
|
|
|
+ if (isDraftExpired(plan)) {
|
|
|
+ throw new IllegalStateException("草稿已失效");
|
|
|
+ }
|
|
|
+ return plan;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 权限恢复(SC-P-81 权限流):非归属(非本人创建)→ 403/401 拒绝,不返回他人草稿。 */
|
|
|
+ public GrowthPlan restoreDraft(Long draftId, Long userId) {
|
|
|
+ GrowthPlan plan = growthPlanMapper.selectById(draftId);
|
|
|
+ if (plan == null) {
|
|
|
+ return null; // 草稿不存在 → 无数据可泄露
|
|
|
+ }
|
|
|
+ if (userId == null || !userId.equals(plan.getTeacherId())) {
|
|
|
+ throw new IllegalStateException("无权限恢复他人草稿");
|
|
|
+ }
|
|
|
+ if (isDraftExpired(plan)) {
|
|
|
+ throw new IllegalStateException("草稿已失效");
|
|
|
+ }
|
|
|
+ return plan;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 草稿是否已超有效期(SC-P-81 边界:草稿有效期);无有效期视作未过期。 */
|
|
|
+ public boolean isDraftExpired(GrowthPlan draft) {
|
|
|
+ if (draft == null || draft.getDraftExpiresAt() == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return draft.getDraftExpiresAt().getTime() < System.currentTimeMillis();
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 并发检测(SC-P-81 异常流):服务端已存版本 > 本地版本 → 冲突(后存覆盖前存且提示冲突)。 */
|
|
|
+ public boolean isDraftVersionConflict(Long draftId, long localVersion) {
|
|
|
+ GrowthPlan existing = growthPlanMapper.selectById(draftId);
|
|
|
+ if (existing == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ int serverVersion = existing.getDraftVersion() != null ? existing.getDraftVersion() : 0;
|
|
|
+ return serverVersion > localVersion;
|
|
|
+ }
|
|
|
}
|