test(my): 我的填写六端点单测闭环 P70/P75(my 面实现随批入库)
MyAnswerIT 12 例(真库 @Transactional 回滚,沿 T3/T4 IT 惯例): - 身份全会话推导:六端点首参恒为 BoaUser、入参无 id 型身份位、请求体只有 answerId/values/summary;未登录一律 UNAUTHORIZED;跨校同 uuid 读不到(V17) - 越权无探测面:他人答卷与"不存在"同码同形(PARAM_ERROR + 同一句话,仅 uuid 不同); 非提交人改/删 FORBIDDEN;被代填人可读不可改(V29 的我的面形态) - 软删释放名额:本人删自己的行 → isDeleted=1、索引行清空、名额释放、可重填, 已删行在列表/两视图/详情四处置为不可见(V30/V32 我的面半边) - 两视图谓词分离:代填行只进操作人 submitList 与被代填人 aboutList, 自填行(subject=本人)两视图都在;零模块授权学生两视图照常(与数据管理面相反) - myDetail 归属:提交人与被代填人都可读,返回钉住版本 groups + 值文档,外层无库内自增 id(V5); 升版后老行仍按 v1 呈现与校验(P20) - V14 结构半边:本面无任何改状态端点(映射表断言死锁为六条) - AnswerReadService.myDetail javadoc 修正:读路径没有 editable 字段, 前端按 ItemView.submitterId 判 isMine,拒绝在 editBySubmitter/softDeleteBySubmitter(P75 附带项) 门禁:mvn -B -o clean test → Tests run: 190, Failures: 0, Errors: 0, Skipped: 0(原 178 + 本批 12)
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
package com.yunhai.formcenter.controller.api;
|
||||
|
||||
import com.yunhai.formcenter.model.MyModels;
|
||||
import com.yunhai.formcenter.model.ReadModels;
|
||||
import com.yunhai.formcenter.service.AnswerReadService;
|
||||
import com.yunhai.platform.apistd.BizException;
|
||||
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||
import com.yunhai.platform.utils.model.BoaUser;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 填报面的"我的"数据读改端点:T7 的填报页内嵌"我的提交"段(按单表单)+
|
||||
* T8 的全局两视图 {@code /my/submit}(我提交的)与 {@code /my/about}(关于我的,V18)。
|
||||
*
|
||||
* <p>纪律:</p>
|
||||
* <ul>
|
||||
* <li><b>身份一律会话推导</b>:不接受外部传 submitterId/schId(V17);</li>
|
||||
* <li><b>数据面身份只用 uuid</b>:answerId 是答卷 uuid,库内 id 不出现在任何外层(契约 §1/V5);</li>
|
||||
* <li><b>权限闸门</b>:普通用户只能看/改/删<b>自己提交</b>的答卷(submitter_id=会话用户),
|
||||
* "关于我的"只读(我是被代填人时可看不可改——改是填表人或管理员的动作);
|
||||
* 有管理权者另有 /api/form/admin/data/**(T4 已落地,scope 矩阵生效),两套口径不混用;</li>
|
||||
* <li><b>本控制器没有状态端点</b>:填表人改状态在接口层即不存在(V14 的实现形态)。</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/form/my")
|
||||
@RequiredArgsConstructor
|
||||
public class FormMyAnswerController {
|
||||
|
||||
private final AnswerReadService readService;
|
||||
|
||||
/** 我的答卷列表(按 submitter 过滤,"我的提交"段) */
|
||||
@GetMapping("/list")
|
||||
public List<ReadModels.RowView> list(BoaUser user,
|
||||
@RequestParam("formCode") String formCode,
|
||||
@RequestParam(value = "appCode", required = false) String appCode) {
|
||||
requireSession(user);
|
||||
return readService.listBySubmitter(user, formCode, appCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* "我提交的"视图({@code /my/submit},T8):跨表单、按 {@code submitter_id}=会话用户。
|
||||
*
|
||||
* <p>与 {@code /my/about} 是<b>两个端点、两个谓词</b>,不是一个端点加 view 参数
|
||||
* (方案 §6.3 原文:"做成两个视图不做一个接口加参数(鉴权分支容易写漏)")。
|
||||
* 这里<b>不看模块行、不看契约 §8 范围矩阵</b>——数据管理面那套范围管的是"管理员能看谁的行",
|
||||
* 本面管的是"我自己的行",两套口径混用会让填表人不授权模块行就看不见自己的数据。</p>
|
||||
*/
|
||||
@GetMapping("/submit-list")
|
||||
public List<MyModels.ItemView> submitList(BoaUser user) {
|
||||
requireSession(user);
|
||||
return readService.listMine(user, false);
|
||||
}
|
||||
|
||||
/** "关于我的"视图({@code /my/about},T8、V18):跨表单、按 {@code subject_id}=会话用户(含他人代填) */
|
||||
@GetMapping("/about-list")
|
||||
public List<MyModels.ItemView> aboutList(BoaUser user) {
|
||||
requireSession(user);
|
||||
return readService.listMine(user, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 我的答卷详情(uuid 寻址):<b>钉住版本</b>的内联展开 schema + 值文档(形状与
|
||||
* {@code /api/form/admin/data/detail} 同一个 {@link ReadModels.DetailView},渲染与校验同源)。
|
||||
*
|
||||
* <p>归属判定=我是提交人或我是被代填人,别人的答卷按"不存在"回答(不给探测面);
|
||||
* 不在这里的动作:改状态(V14——本控制器没有任何状态端点,结构即证据)。</p>
|
||||
*/
|
||||
@GetMapping("/detail")
|
||||
public ReadModels.DetailView detail(BoaUser user, @RequestParam("answerId") String answerId) {
|
||||
requireSession(user);
|
||||
return readService.myDetail(user, answerId);
|
||||
}
|
||||
|
||||
/** 软删我的答卷(确认对话框后调用;软删释放限次名额,契约 §5) */
|
||||
@PostMapping("/delete")
|
||||
public void delete(BoaUser user, @RequestBody DeleteRequest body) {
|
||||
requireSession(user);
|
||||
readService.softDeleteBySubmitter(user, body.getAnswerId());
|
||||
}
|
||||
|
||||
/** 编辑我的答卷(按钉住 version 校验;不改状态、不改身份) */
|
||||
@PostMapping("/edit")
|
||||
public ReadModels.RowView edit(BoaUser user, @RequestBody EditRequest body) {
|
||||
requireSession(user);
|
||||
return readService.editBySubmitter(user, body.getAnswerId(), body.getValues(), body.getSummary());
|
||||
}
|
||||
|
||||
private static void requireSession(BoaUser user) {
|
||||
if (user == null || user.getId() == null) {
|
||||
throw new BizException(ErrorCode.UNAUTHORIZED, "未登录");
|
||||
}
|
||||
}
|
||||
|
||||
@lombok.Getter
|
||||
@lombok.Setter
|
||||
public static class DeleteRequest {
|
||||
private String answerId;
|
||||
}
|
||||
|
||||
@lombok.Getter
|
||||
@lombok.Setter
|
||||
public static class EditRequest {
|
||||
private String answerId;
|
||||
private com.fasterxml.jackson.databind.JsonNode values;
|
||||
private String summary;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.yunhai.formcenter.model;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* "我的填写"两视图({@code /my/submit} 与 {@code /my/about})的列表出参(方案 §6.3、任务书 T8)。
|
||||
*
|
||||
* <p><b>为什么不是 {@link ReadModels.RowView}</b>:那两个视图是<b>跨表单</b>的——
|
||||
* 一个学生/老师的名下可能挂着好几张表单的答卷,行上必须带表单身份(名称/code/归属应用)才认得出是哪张表。
|
||||
* {@code RowView} 是"单表单数据列表"的行形状(列由前端按该表单 schema 展),没有这两列。</p>
|
||||
*
|
||||
* <p><b>为什么不含 {@code values}</b>:跨表单列表没有统一列可展,值文档在这里没有展示位;
|
||||
* 要看值走 {@code /api/form/my/detail}(钉住版本 + 值文档一起给,渲染与校验同源)。
|
||||
* 少带一个 MEDIUMTEXT 列也与方案 §5.5 铁律①同向。</p>
|
||||
*
|
||||
* <p>对外身份仍然只有 uuid:{@code answerId}/{@code formUuid};库内 {@code id} 不外露(契约 §1、V5)。
|
||||
* {@code submitterId}/{@code subjectId} 是<b>用户</b> id(平台全局身份,不是本库自增主键),
|
||||
* 与数据管理面 {@code RowView} 同口径。</p>
|
||||
*/
|
||||
public final class MyModels {
|
||||
|
||||
private MyModels() {
|
||||
}
|
||||
|
||||
/** 一行"我的答卷":答卷身份 + 归属表单身份 + 状态/报名号/提交时间 */
|
||||
@Getter
|
||||
@Setter
|
||||
public static class ItemView {
|
||||
private String answerId;
|
||||
private String formUuid;
|
||||
/** 归属应用(中心=form-center,快应用=该应用 app_code) */
|
||||
private String appCode;
|
||||
private String code;
|
||||
/** 表单名称;归属表单已被彻底取不到时为 null,前端降级显示 code */
|
||||
private String formName;
|
||||
/** 该答卷钉住的版本号(详情/编辑按它取 schema) */
|
||||
private Integer version;
|
||||
private String status;
|
||||
/** 状态 label;已废弃 code 为 null,前端标"已废弃:code"(方案 §5.3①) */
|
||||
private String statusLabel;
|
||||
private Integer submitterId;
|
||||
private Integer subjectId;
|
||||
private String answerNo;
|
||||
/** yyyy-MM-dd HH:mm:ss */
|
||||
private String submitTime;
|
||||
}
|
||||
}
|
||||
@@ -57,4 +57,14 @@ public interface AnswerRepository extends JpaRepository<Answer, Long> {
|
||||
+ "order by a.id asc")
|
||||
List<Answer> findLiveByFormUuidAfterId(@Param("formUuid") String formUuid, @Param("cursor") Long cursor,
|
||||
Pageable pageable);
|
||||
|
||||
/** 我的提交段(T7)/ "我提交的"视图(T8):按 submitter 过滤本人答卷,按 id 降序(最新在前) */
|
||||
List<Answer> findBySchIdAndSubmitterIdOrderByIdDesc(Long schId, Integer submitterId);
|
||||
|
||||
/**
|
||||
* "关于我的"视图(T8,V18):按 subject 过滤——别人<b>代我填</b>的答卷也在这里
|
||||
* (方案 §6.3:谁提交与关于谁是两个角色,学生看到的是"关于我"的那几条,不限于自己填的)。
|
||||
* 与上一条是两个方法而非一个方法加 flag(同视图侧"两个入口不做参数分支"的口径)。
|
||||
*/
|
||||
List<Answer> findBySchIdAndSubjectIdOrderByIdDesc(Long schId, Integer subjectId);
|
||||
}
|
||||
|
||||
@@ -2,18 +2,27 @@ package com.yunhai.formcenter.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.yunhai.formcenter.common.FormFilterOperators;
|
||||
import com.yunhai.formcenter.common.FormRuntimeRules;
|
||||
import com.yunhai.formcenter.common.FormSchemaValidator;
|
||||
import com.yunhai.formcenter.common.FormStatusDefs;
|
||||
import com.yunhai.formcenter.common.FormValueValidator;
|
||||
import com.yunhai.formcenter.entity.Answer;
|
||||
import com.yunhai.formcenter.entity.AnswerOpLog;
|
||||
import com.yunhai.formcenter.entity.Form;
|
||||
import com.yunhai.formcenter.entity.FormFile;
|
||||
import com.yunhai.formcenter.model.MyModels;
|
||||
import com.yunhai.formcenter.model.ReadModels;
|
||||
import com.yunhai.formcenter.repository.AnswerOpLogRepository;
|
||||
import com.yunhai.formcenter.repository.AnswerRepository;
|
||||
import com.yunhai.formcenter.repository.FillQuotaRepository;
|
||||
import com.yunhai.formcenter.repository.FormFileRepository;
|
||||
import com.yunhai.platform.apistd.BizException;
|
||||
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||
import com.yunhai.platform.utils.model.BoaUser;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
@@ -35,6 +44,11 @@ import java.util.Map;
|
||||
* <p>三处纪律:列表走<b>两段式</b>(先取当页 id、再按 id 批量取值文档,铁律①);
|
||||
* 翻页走 <b>keyset</b> 且游标对外是 uuid(铁律② + 契约 §1 id 不外露);
|
||||
* 软删答卷在四条路径一律不可见(V30,谓词在构造器里)。</p>
|
||||
*
|
||||
* <p><b>末尾的"我的填写"段是另一条线,不要与上面四条混读</b>:那里的可见集来自
|
||||
* {@code submitter_id}/{@code subject_id}=会话用户的<b>归属判定</b>,不是契约 §8 的范围矩阵
|
||||
* (填表人不授权任何模块行也能看自己的数据,方案 §6.3"我的填写两层落点");
|
||||
* 它也不接受 {@code schId} 外部传入,租户域同样只从会话推导(V17)。</p>
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@@ -48,6 +62,10 @@ public class AnswerReadService {
|
||||
private final DataScopeService scopeService;
|
||||
private final AnswerScopeQueryBuilder queryBuilder;
|
||||
private final AnswerRepository answerRepository;
|
||||
private final AnswerIndexService indexService;
|
||||
private final FillQuotaRepository quotaRepository;
|
||||
private final FormFileRepository fileRepository;
|
||||
private final AnswerOpLogRepository opLogRepository;
|
||||
|
||||
// ==================== 规格构造(四路径共用) ====================
|
||||
|
||||
@@ -255,4 +273,230 @@ public class AnswerReadService {
|
||||
}
|
||||
return labels;
|
||||
}
|
||||
|
||||
// ==================== 我的提交(T7 填报页内嵌段) ====================
|
||||
|
||||
/**
|
||||
* 我的答卷列表(按 submitter 过滤,"我的提交"段)。
|
||||
* 普通用户只能看自己的答卷(submitter_id=会话用户),无数据管理权。
|
||||
*/
|
||||
public List<ReadModels.RowView> listBySubmitter(BoaUser user, String formCode, String appCode) {
|
||||
if (user == null || user.getId() == null) {
|
||||
throw new BizException(ErrorCode.UNAUTHORIZED, "未登录");
|
||||
}
|
||||
Long schId = sessionSchId(user);
|
||||
if (schId == null) {
|
||||
throw new BizException(ErrorCode.FORBIDDEN, "当前身份无学校归属");
|
||||
}
|
||||
String app = (appCode == null || appCode.trim().isEmpty()) ? "form-center" : appCode.trim();
|
||||
// 与填报页加载同一条解析(契约 §1 resolve 是读接口唯一入口):不存在=PARAM_ERROR、未发布/停用=CUSTOM
|
||||
Form form = defService.resolvePublished(schId, app, formCode).form();
|
||||
List<Answer> answers = answerRepository.findBySchIdAndSubmitterIdOrderByIdDesc(schId, user.getId());
|
||||
List<ReadModels.RowView> rows = new ArrayList<>();
|
||||
for (Answer answer : answers) {
|
||||
if (!answer.getAppCode().equals(app) || !answer.getCode().equals(formCode)) {
|
||||
continue;
|
||||
}
|
||||
if (Integer.valueOf(1).equals(answer.getIsDeleted())) {
|
||||
continue;
|
||||
}
|
||||
rows.add(toRow(answer, form));
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* 软删我的答卷(确认对话框后调用;软删释放限次名额,契约 §5)。
|
||||
* 普通用户只能删自己提交的答卷(submitter_id=会话用户;被代填的答卷删不动——语义上是"我填的")。
|
||||
*/
|
||||
@Transactional
|
||||
public void softDeleteBySubmitter(BoaUser user, String answerId) {
|
||||
if (user == null || user.getId() == null) {
|
||||
throw new BizException(ErrorCode.UNAUTHORIZED, "未登录");
|
||||
}
|
||||
Long schId = sessionSchId(user);
|
||||
if (schId == null) {
|
||||
throw new BizException(ErrorCode.FORBIDDEN, "当前身份无学校归属");
|
||||
}
|
||||
Answer answer = requireLiveAnswer(schId, answerId);
|
||||
if (!answer.getSubmitterId().equals(user.getId())) {
|
||||
throw new BizException(ErrorCode.FORBIDDEN, "只能删除自己提交的答卷");
|
||||
}
|
||||
answer.setIsDeleted(1);
|
||||
answerRepository.save(answer);
|
||||
// 与管理员软删同口径:索引行同步删、限次名额释放(V30/V32),否则删了行仍挡着本人重填
|
||||
indexService.clear(answer.getId());
|
||||
quotaRepository.releaseQuota(
|
||||
defService.pinnedForData(answer.getFormUuid(), answer.getVersion()).form().getId(),
|
||||
FormRuntimeRules.quotaKey(answer.getSubjectId(), answer.getSubmitterId()));
|
||||
opLog(answer.getId(), AnswerOpLog.OP_SOFT_DELETE, user.getId(), "本人删除自己的提交(软删)");
|
||||
log.info("[我的提交] 软删答卷 answerId={} submitterId={}", answerId, user.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑我的答卷(按钉住 version 校验;不改状态、不改身份)。
|
||||
* 普通用户只能编辑自己提交的答卷(submitter_id=会话用户)。
|
||||
*/
|
||||
@Transactional
|
||||
public ReadModels.RowView editBySubmitter(BoaUser user, String answerId, JsonNode values, String summary) {
|
||||
if (user == null || user.getId() == null) {
|
||||
throw new BizException(ErrorCode.UNAUTHORIZED, "未登录");
|
||||
}
|
||||
Long schId = sessionSchId(user);
|
||||
if (schId == null) {
|
||||
throw new BizException(ErrorCode.FORBIDDEN, "当前身份无学校归属");
|
||||
}
|
||||
Answer answer = requireLiveAnswer(schId, answerId);
|
||||
if (!answer.getSubmitterId().equals(user.getId())) {
|
||||
throw new BizException(ErrorCode.FORBIDDEN, "只能编辑自己提交的答卷");
|
||||
}
|
||||
FormDefService.Published pinned = defService.pinnedForData(answer.getFormUuid(), answer.getVersion());
|
||||
Form form = pinned.form();
|
||||
String valuesJson = values == null || values.isNull() ? "{}" : values.toString();
|
||||
// 与管理员编辑同一条值契约(按钉住版本、字典放行集);P34:附件三件套必须对得上本校上传记录
|
||||
FormValueValidator.validate(valuesJson, pinned.versionRow().getSchemaJson(),
|
||||
dictCode -> dictService.allowedValues(answer.getSchId(), dictCode));
|
||||
assertAttachmentsOwned(answer.getSchId(), valuesJson, pinned.versionRow().getSchemaJson());
|
||||
answer.setValuesJson(valuesJson);
|
||||
answerRepository.save(answer);
|
||||
// 索引按钉住版本重建(方案 §5.4),否则改了可筛字段的值而筛选结果不变
|
||||
indexService.rebuild(answer, pinned.versionRow().getSchemaJson());
|
||||
opLog(answer.getId(), AnswerOpLog.OP_EDIT, user.getId(), summary == null || summary.trim().isEmpty()
|
||||
? "本人编辑自己的提交(钉住版本 v" + answer.getVersion() + ")"
|
||||
: "本人编辑:钉住版本 v" + answer.getVersion() + ";" + summary.trim());
|
||||
log.info("[我的提交] 编辑答卷 answerId={} submitterId={} v={}", answerId, user.getId(),
|
||||
answer.getVersion());
|
||||
return toRow(answer, form);
|
||||
}
|
||||
|
||||
// ==================== 我的填写两视图(T8:/my/submit 按 submitter、/my/about 按 subject) ====================
|
||||
|
||||
/**
|
||||
* 全局"我的填写"两视图的列表读取。<b>两个视图 = 两个谓词、两个入口方法</b>(方案 §6.3:
|
||||
* "做成两个视图不做一个接口加参数"),鉴权分支不写在同一个方法体里靠 flag 分岔:
|
||||
* {@code bySubject=false} 按 {@code submitter_id}=会话用户(我提交的),
|
||||
* {@code bySubject=true} 按 {@code subject_id}=会话用户(关于我的,含管理员代填的那几条,V18)。
|
||||
*
|
||||
* <p>与数据管理面的区别:那条路径的范围来自契约 §8 矩阵({@link DataScopeService})且要先有
|
||||
* {@code /admin/data} 模块行;这里<b>不看模块行、不看范围矩阵</b>,只看"这条数据是不是我的",
|
||||
* 所以填表人(学生)不授权任何管理模块行也能用自己的两视图。</p>
|
||||
*/
|
||||
public List<MyModels.ItemView> listMine(BoaUser user, boolean bySubject) {
|
||||
if (user == null || user.getId() == null) {
|
||||
throw new BizException(ErrorCode.UNAUTHORIZED, "未登录");
|
||||
}
|
||||
Long schId = sessionSchId(user);
|
||||
if (schId == null) {
|
||||
throw new BizException(ErrorCode.FORBIDDEN, "当前身份无学校归属");
|
||||
}
|
||||
List<Answer> answers = bySubject
|
||||
? answerRepository.findBySchIdAndSubjectIdOrderByIdDesc(schId, user.getId())
|
||||
: answerRepository.findBySchIdAndSubmitterIdOrderByIdDesc(schId, user.getId());
|
||||
// 表头信息按 form_uuid 去重取(跨表单列表,一页内同表单多行时不重复查头表)
|
||||
Map<String, Form> forms = new HashMap<>();
|
||||
List<MyModels.ItemView> items = new ArrayList<>();
|
||||
for (Answer answer : answers) {
|
||||
if (Integer.valueOf(1).equals(answer.getIsDeleted())) {
|
||||
continue;
|
||||
}
|
||||
Form form = forms.computeIfAbsent(answer.getFormUuid(), uuid -> findFormQuietly(schId, uuid));
|
||||
items.add(toMyItem(answer, form));
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* 我的答卷详情(uuid 寻址):<b>钉住版本</b>的内联展开 schema + 值文档,
|
||||
* 与 {@link #detail} 同一份形状(渲染与校验同源——两视图的编辑弹层直接按它出控件)。
|
||||
* 归属判定=我是提交人 <em>或</em> 我是被代填人;后者只读——读路径上<b>没有</b> editable 之类的
|
||||
* 服务端判定字段(P75 修正:前端按列表行的 {@code submitterId} 是否等于会话用户决定编辑入口显隐,
|
||||
* 真正的拒绝在 {@link #editBySubmitter}/{@link #softDeleteBySubmitter} 的提交人判定里);
|
||||
* 改状态在这一面压根没有入口——V14/V29。
|
||||
*/
|
||||
public ReadModels.DetailView myDetail(BoaUser user, String answerId) {
|
||||
if (user == null || user.getId() == null) {
|
||||
throw new BizException(ErrorCode.UNAUTHORIZED, "未登录");
|
||||
}
|
||||
Long schId = sessionSchId(user);
|
||||
if (schId == null) {
|
||||
throw new BizException(ErrorCode.FORBIDDEN, "当前身份无学校归属");
|
||||
}
|
||||
Answer answer = requireLiveAnswer(schId, answerId);
|
||||
boolean mine = user.getId().equals(answer.getSubmitterId()) || user.getId().equals(answer.getSubjectId());
|
||||
if (!mine) {
|
||||
// 别人的答卷与不存在的答卷同一口径回答,差别信息就是探测面(契约 §1:uuid 不可枚举)
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "答卷不存在或已删除:" + answerId);
|
||||
}
|
||||
FormDefService.Published pinned = defService.pinnedForData(answer.getFormUuid(), answer.getVersion());
|
||||
Form form = pinned.form();
|
||||
ReadModels.DetailView view = new ReadModels.DetailView();
|
||||
copyInto(answer, form, view);
|
||||
view.setAppCode(form.getAppCode());
|
||||
view.setCode(form.getCode());
|
||||
view.setFormName(form.getName());
|
||||
view.setGroups(groupsNode(dictService.expandSchema(form.getSchId(), pinned.versionRow().getSchemaJson())));
|
||||
return view;
|
||||
}
|
||||
|
||||
private MyModels.ItemView toMyItem(Answer answer, Form form) {
|
||||
MyModels.ItemView item = new MyModels.ItemView();
|
||||
item.setAnswerId(answer.getUuid());
|
||||
item.setFormUuid(answer.getFormUuid());
|
||||
item.setAppCode(answer.getAppCode());
|
||||
item.setCode(answer.getCode());
|
||||
item.setFormName(form == null ? null : form.getName());
|
||||
item.setVersion(answer.getVersion());
|
||||
item.setStatus(answer.getStatus());
|
||||
item.setStatusLabel(form == null ? null : FormStatusDefs.labelOf(form.getStatusDefs(), answer.getStatus()));
|
||||
item.setSubmitterId(answer.getSubmitterId());
|
||||
item.setSubjectId(answer.getSubjectId());
|
||||
item.setAnswerNo(answer.getAnswerNo());
|
||||
item.setSubmitTime(format(answer.getCreateTime()));
|
||||
return item;
|
||||
}
|
||||
|
||||
/** 会话校(Integer→Long 换算走已有字段,契约 §7 身份行:外部不传) */
|
||||
private static Long sessionSchId(BoaUser user) {
|
||||
Integer schId = user.getCurrentRole() == null ? null : user.getCurrentRole().getSchId();
|
||||
return schId == null ? null : Long.valueOf(schId);
|
||||
}
|
||||
|
||||
/** 表单头表静默取数:答卷归属的表单理论上一定在(P32 连软删的都还在),取不到就让表名列降级为空 */
|
||||
private Form findFormQuietly(Long schId, String formUuid) {
|
||||
try {
|
||||
return defService.readDomainForm(schId, formUuid);
|
||||
} catch (BizException e) {
|
||||
log.warn("[我的填写] 答卷归属表单取不到,表名列降级显示 code:formUuid={} 原因={}", formUuid, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** 附件归属与一致性(P24 同校 + P34 key/fileName 与上传记录一致),判据与管理员编辑侧同源 */
|
||||
private void assertAttachmentsOwned(Long schId, String valuesJson, String schemaJson) {
|
||||
for (JsonNode item : FormValueValidator.attachments(valuesJson, schemaJson)) {
|
||||
long id = item.get("id").longValue();
|
||||
FormFile file = fileRepository.findByIdAndSchId(id, schId).orElse(null);
|
||||
if (file == null) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "附件不存在或不属于本校:id=" + id);
|
||||
}
|
||||
String key = item.path("key").asText(null);
|
||||
if (key == null || !key.equals(file.getStoreKey())) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "附件 key 与上传记录不一致:id=" + id);
|
||||
}
|
||||
String fileName = item.path("fileName").asText(null);
|
||||
if (fileName == null || !fileName.equals(file.getFileName())) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "附件文件名与上传记录不一致:id=" + id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 留痕形态与管理员侧同源(方案 §5.4:编辑/删除都进 answer_op_log),只是操作人=本人 */
|
||||
private void opLog(Long answerId, String op, Integer operatorId, String summary) {
|
||||
AnswerOpLog entry = new AnswerOpLog();
|
||||
entry.setAnswerId(answerId);
|
||||
entry.setOp(op);
|
||||
entry.setOperatorId(operatorId);
|
||||
entry.setSummary(AnswerOpLog.summarize(summary));
|
||||
opLogRepository.save(entry);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user