DocumentMapper.java 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  1. package com.dms.mapper;
  2. import com.dms.model.DocumentEntity;
  3. import java.time.LocalDateTime;
  4. import java.util.List;
  5. import java.util.Map;
  6. import org.apache.ibatis.annotations.Insert;
  7. import org.apache.ibatis.annotations.Options;
  8. import org.apache.ibatis.annotations.Param;
  9. import org.apache.ibatis.annotations.Select;
  10. import org.apache.ibatis.annotations.Update;
  11. /** doc_document 表访问(按阶段逐步扩展)。 */
  12. public interface DocumentMapper {
  13. /** 路径级联:同步挂载文档的 organization_name 快照(row_version+1)。 */
  14. @Update("UPDATE doc_document SET organization_name = #{orgName}, "
  15. + "updated_by = #{actorId}, updated_at = #{now}, "
  16. + "row_version = row_version + 1 "
  17. + "WHERE organization_id = #{organizationId} AND is_deleted = 0")
  18. int syncOrganizationName(@Param("organizationId") long organizationId,
  19. @Param("orgName") String orgName,
  20. @Param("actorId") Long actorId,
  21. @Param("now") LocalDateTime now);
  22. // ------------------------------------------------------------ 基础读取(阶段5.2/5.3)
  23. @Select("SELECT * FROM doc_document WHERE id = #{id} AND is_deleted = 0")
  24. DocumentEntity findByIdNotDeleted(@Param("id") long id);
  25. /** 子方案权限源:root 主案(有效 MAIN)。 */
  26. @Select("SELECT * FROM doc_document WHERE id = #{id} AND document_type = 'MAIN' "
  27. + "AND is_deleted = 0")
  28. DocumentEntity findActiveMainById(@Param("id") long id);
  29. /** 分类父节点检查:直挂有效主案数(_valid_parent / update_category)。 */
  30. @Select("SELECT COUNT(*) FROM doc_document WHERE category_id = #{categoryId} "
  31. + "AND document_type = 'MAIN' AND is_deleted = 0")
  32. long countActiveMainByCategory(@Param("categoryId") long categoryId);
  33. /** 分类删除占用:直挂方案(MAIN+SUB_PLAN)数。 */
  34. @Select("SELECT COUNT(*) FROM doc_document WHERE category_id = #{categoryId} "
  35. + "AND is_deleted = 0 AND document_type IN ('MAIN', 'SUB_PLAN')")
  36. long countActivePlansByCategory(@Param("categoryId") long categoryId);
  37. /** 分类路径级联:同步直挂方案快照(rv+1),返回受影响行数。 */
  38. @Update("UPDATE doc_document SET category_name = #{categoryName}, "
  39. + "category_path = #{categoryPath}, updated_by = #{actorId}, "
  40. + "updated_at = #{now}, row_version = row_version + 1 "
  41. + "WHERE category_id = #{categoryId} AND is_deleted = 0 "
  42. + "AND document_type IN ('MAIN', 'SUB_PLAN')")
  43. int syncCategorySnapshot(@Param("categoryId") long categoryId,
  44. @Param("categoryName") String categoryName,
  45. @Param("categoryPath") String categoryPath,
  46. @Param("actorId") Long actorId,
  47. @Param("now") LocalDateTime now);
  48. // ------------------------------------------------------------ 密码锁(阶段5.3)
  49. /** 密码锁维护目标:锁定有效主案行。 */
  50. @Select("SELECT * FROM doc_document WHERE id = #{id} AND document_type = 'MAIN' "
  51. + "AND is_deleted = 0 FOR UPDATE")
  52. DocumentEntity findActiveMainForUpdate(@Param("id") long id);
  53. /** 密码锁整体写入(哈希 + 7 勾选位 + rv+1)。 */
  54. @Update("UPDATE doc_document SET password_hash = #{passwordHash}, "
  55. + "protect_view = #{protectView}, protect_download = #{protectDownload}, "
  56. + "protect_edit = #{protectEdit}, protect_delete = #{protectDelete}, "
  57. + "protect_config_permission = #{protectConfigPermission}, "
  58. + "protect_bind_attachment = #{protectBindAttachment}, "
  59. + "protect_unbind_attachment = #{protectUnbindAttachment}, "
  60. + "updated_by = #{updatedBy}, updated_at = #{updatedAt}, "
  61. + "row_version = row_version + 1 WHERE id = #{id}")
  62. int updatePasswordLock(DocumentEntity document);
  63. // ------------------------------------------------------------ 只读查询(阶段5.4)
  64. /**
  65. * 主案/子方案通用动态查询,对齐 document_query_service 的组合过滤:
  66. * types 必填;categoryIds/parentDocumentId/parentIds/titlePattern/contentPattern/
  67. * securityLevels/orgPattern/tags(配tagsMatch)/securityLevel/status/updated范围可选;
  68. * orderBy 为服务层白名单校验后的排序子串,缺省不排序(对齐 Python 无 order_by 场景)。
  69. */
  70. @Select("<script>SELECT * FROM doc_document WHERE is_deleted = 0 AND document_type IN "
  71. + "<foreach collection='types' item='t' open='(' separator=',' close=')'>#{t}</foreach>"
  72. + "<if test='ids != null'>AND id IN "
  73. + "<foreach collection='ids' item='i' open='(' separator=',' close=')'>"
  74. + "#{i}</foreach></if>"
  75. + "<if test='categoryIds != null'>AND category_id IN "
  76. + "<foreach collection='categoryIds' item='c' open='(' separator=',' close=')'>"
  77. + "#{c}</foreach></if>"
  78. + "<if test='parentDocumentId != null'>AND parent_document_id = #{parentDocumentId}</if>"
  79. + "<if test='parentIds != null'>AND parent_document_id IN "
  80. + "<foreach collection='parentIds' item='p' open='(' separator=',' close=')'>"
  81. + "#{p}</foreach></if>"
  82. + "<if test='titlePattern != null'>AND document_name LIKE #{titlePattern} ESCAPE '\\\\'</if>"
  83. + "<if test='contentPattern != null'>AND (content_text LIKE #{contentPattern} ESCAPE '\\\\' "
  84. + "OR search_text LIKE #{contentPattern} ESCAPE '\\\\')</if>"
  85. + "<if test='securityLevels != null'>AND security_level IN "
  86. + "<foreach collection='securityLevels' item='l' open='(' separator=',' close=')'>"
  87. + "#{l}</foreach></if>"
  88. + "<if test='orgPattern != null'>AND organization_name LIKE #{orgPattern} ESCAPE '\\\\'</if>"
  89. + "<if test='attachmentType != null'>AND attachment_type = #{attachmentType}</if>"
  90. + "<if test='fileExtension != null'>AND file_extension = #{fileExtension}</if>"
  91. + "<if test='tags != null'>AND (<choose>"
  92. + "<when test='tagsMatch == \"all\"'>"
  93. + "<foreach collection='tags' item='tg' separator=' AND '>JSON_CONTAINS(tags, #{tg})"
  94. + "</foreach></when><otherwise>"
  95. + "<foreach collection='tags' item='tg' separator=' OR '>JSON_CONTAINS(tags, #{tg})"
  96. + "</foreach></otherwise></choose>)</if>"
  97. + "<if test='securityLevel != null'>AND security_level = #{securityLevel}</if>"
  98. + "<if test='status != null'>AND document_status = #{status}</if>"
  99. + "<if test='updatedFrom != null'>AND updated_at &gt;= #{updatedFrom}</if>"
  100. + "<if test='updatedTo != null'>AND updated_at &lt;= #{updatedTo}</if>"
  101. + "<if test='orderBy != null'>ORDER BY ${orderBy}, id ASC</if>"
  102. + "</script>")
  103. List<DocumentEntity> listByFilters(Map<String, Object> filters);
  104. /** 对齐 _active_plan:详情目标(MAIN/SUB_PLAN)。 */
  105. @Select("SELECT * FROM doc_document WHERE id = #{id} AND is_deleted = 0 "
  106. + "AND document_type IN ('MAIN', 'SUB_PLAN')")
  107. DocumentEntity findActivePlanById(@Param("id") long id);
  108. /** _record_view 锁定行。 */
  109. @Select("SELECT * FROM doc_document WHERE id = #{id} AND is_deleted = 0 FOR UPDATE")
  110. DocumentEntity findNotDeletedForUpdate(@Param("id") long id);
  111. /** _record_view 查看计数自增。 */
  112. @Update("UPDATE doc_document SET view_count = view_count + 1 WHERE id = #{id}")
  113. int incrementViewCount(@Param("id") long id);
  114. // ------------------------------------------------------------ 写操作(阶段5.5)
  115. /** 新建方案/附件(密码锁/计数等由库默认承担,对齐 Python session.add)。 */
  116. @Insert("INSERT INTO doc_document (document_name, summary, document_type, "
  117. + "document_status, security_level, visibility_type, attachment_type, "
  118. + "category_id, category_name, category_path, organization_id, "
  119. + "organization_name, parent_document_id, root_document_id, tags, "
  120. + "original_file_name, file_relative_path, file_extension, mime_type, "
  121. + "file_size, file_hash, sort_no, content_text, content_extract_status, "
  122. + "content_extract_error, content_extracted_at, search_text, created_by, updated_by, "
  123. + "created_by_name, updated_by_name) VALUES (#{documentName}, #{summary}, "
  124. + "#{documentType}, #{documentStatus}, #{securityLevel}, #{visibilityType}, "
  125. + "#{attachmentType}, #{categoryId}, #{categoryName}, #{categoryPath}, "
  126. + "#{organizationId}, #{organizationName}, #{parentDocumentId}, "
  127. + "#{rootDocumentId}, #{tags}, #{originalFileName}, #{fileRelativePath}, "
  128. + "#{fileExtension}, #{mimeType}, #{fileSize}, #{fileHash}, #{sortNo}, "
  129. + "#{contentText}, #{contentExtractStatus}, #{contentExtractError}, "
  130. + "#{contentExtractedAt}, "
  131. + "#{searchText}, #{createdBy}, #{updatedBy}, #{createdByName}, "
  132. + "#{updatedByName})")
  133. @Options(useGeneratedKeys = true, keyProperty = "id")
  134. int insert(DocumentEntity document);
  135. /** 上传查重锁定:同名同类型未删(SUB_PLAN 限定父内,MAIN 全局),对齐 Python duplicate 条件。 */
  136. @Select("<script>SELECT * FROM doc_document WHERE document_name = #{name} "
  137. + "AND document_type = #{type} AND is_deleted = 0 "
  138. + "<if test='parentId != null'>AND parent_document_id = #{parentId}</if> "
  139. + "FOR UPDATE</script>")
  140. DocumentEntity findDuplicateForUpdate(@Param("name") String name, @Param("type") String type,
  141. @Param("parentId") Long parentId);
  142. /** 覆盖上传:全文件与元数据字段 + search_text + rv+1(document_name 不变)。 */
  143. @Update("UPDATE doc_document SET summary = #{summary}, security_level = #{securityLevel}, "
  144. + "category_id = #{categoryId}, category_name = #{categoryName}, "
  145. + "category_path = #{categoryPath}, organization_id = #{organizationId}, "
  146. + "organization_name = #{organizationName}, tags = #{tags}, "
  147. + "original_file_name = #{originalFileName}, "
  148. + "file_relative_path = #{fileRelativePath}, file_extension = #{fileExtension}, "
  149. + "mime_type = #{mimeType}, file_size = #{fileSize}, file_hash = #{fileHash}, "
  150. + "content_text = #{contentText}, content_extract_status = #{contentExtractStatus}, "
  151. + "content_extract_error = #{contentExtractError}, "
  152. + "content_extracted_at = #{contentExtractedAt}, search_text = #{searchText}, "
  153. + "updated_by = #{updatedBy}, updated_by_name = #{updatedByName}, "
  154. + "updated_at = #{updatedAt}, row_version = row_version + 1 WHERE id = #{id}")
  155. int updateOverwrite(DocumentEntity document);
  156. /** 上传时密码锁写入:仅密码字段(rv 不递增,对齐 Python 上传后 apply_password_lock 语义)。 */
  157. @Update("UPDATE doc_document SET password_hash = #{passwordHash}, "
  158. + "protect_view = #{protectView}, protect_download = #{protectDownload}, "
  159. + "protect_edit = #{protectEdit}, protect_delete = #{protectDelete}, "
  160. + "protect_config_permission = #{protectConfigPermission}, "
  161. + "protect_bind_attachment = #{protectBindAttachment}, "
  162. + "protect_unbind_attachment = #{protectUnbindAttachment} WHERE id = #{id}")
  163. int insertPasswordLock(DocumentEntity document);
  164. /** MAIN 换分类:子方案分类快照级联(rv+1)。 */
  165. @Update("UPDATE doc_document SET category_id = #{categoryId}, "
  166. + "category_name = #{categoryName}, category_path = #{categoryPath}, "
  167. + "updated_by = #{updatedBy}, updated_by_name = #{updatedByName}, "
  168. + "updated_at = #{updatedAt}, row_version = row_version + 1 "
  169. + "WHERE parent_document_id = #{mainId} AND document_type = 'SUB_PLAN' "
  170. + "AND is_deleted = 0")
  171. int syncSubPlanCategory(@Param("mainId") long mainId, @Param("categoryId") Long categoryId,
  172. @Param("categoryName") String categoryName,
  173. @Param("categoryPath") String categoryPath,
  174. @Param("updatedBy") Long updatedBy,
  175. @Param("updatedByName") String updatedByName,
  176. @Param("updatedAt") LocalDateTime updatedAt);
  177. /** 新建主案排序末尾:全局 MAIN 最大 sort_no。 */
  178. @Select("SELECT COALESCE(MAX(sort_no), 0) FROM doc_document "
  179. + "WHERE document_type = 'MAIN' AND parent_document_id IS NULL AND is_deleted = 0")
  180. int maxMainSortNo();
  181. /** 新建子方案排序末尾:父内 SUB_PLAN 最大 sort_no。 */
  182. @Select("SELECT COALESCE(MAX(sort_no), 0) FROM doc_document "
  183. + "WHERE document_type = 'SUB_PLAN' AND parent_document_id = #{parentId} "
  184. + "AND is_deleted = 0")
  185. int maxSubPlanSortNo(@Param("parentId") long parentId);
  186. /** 方案编辑:元数据字段 + search_text + rv+1(对齐 update_document)。 */
  187. @Update("UPDATE doc_document SET document_name = #{documentName}, summary = #{summary}, "
  188. + "tags = #{tags}, category_id = #{categoryId}, category_name = #{categoryName}, "
  189. + "category_path = #{categoryPath}, organization_id = #{organizationId}, "
  190. + "organization_name = #{organizationName}, security_level = #{securityLevel}, "
  191. + "visibility_type = #{visibilityType}, search_text = #{searchText}, "
  192. + "updated_by = #{updatedBy}, updated_by_name = #{updatedByName}, "
  193. + "updated_at = #{updatedAt}, row_version = row_version + 1 WHERE id = #{id}")
  194. int updateMeta(DocumentEntity document);
  195. /** 附件编辑:元数据字段 + search_text + rv+1(对齐 update_attachment)。 */
  196. @Update("UPDATE doc_document SET document_name = #{documentName}, "
  197. + "attachment_type = #{attachmentType}, summary = #{summary}, tags = #{tags}, "
  198. + "organization_id = #{organizationId}, organization_name = #{organizationName}, "
  199. + "search_text = #{searchText}, updated_by = #{updatedBy}, "
  200. + "updated_by_name = #{updatedByName}, updated_at = #{updatedAt}, "
  201. + "row_version = row_version + 1 WHERE id = #{id}")
  202. int updateAttachmentMeta(DocumentEntity document);
  203. /** 描述编辑:仅 description(rv+1,对齐 update_document_description)。 */
  204. @Update("UPDATE doc_document SET description = #{description}, "
  205. + "updated_by = #{updatedBy}, updated_by_name = #{updatedByName}, "
  206. + "updated_at = #{updatedAt}, row_version = row_version + 1 WHERE id = #{id}")
  207. int updateDescription(DocumentEntity document);
  208. /**
  209. * re_extract_content:覆盖写回提取结果(含失败原因)并同步重建 search_text,
  210. * touch updated_by/name/at 与 rv+1。
  211. */
  212. @Update("UPDATE doc_document SET content_text = #{contentText}, "
  213. + "content_extract_status = #{contentExtractStatus}, "
  214. + "content_extract_error = #{contentExtractError}, "
  215. + "content_extracted_at = #{contentExtractedAt}, search_text = #{searchText}, "
  216. + "updated_by = #{updatedBy}, updated_by_name = #{updatedByName}, "
  217. + "updated_at = #{updatedAt}, row_version = row_version + 1 WHERE id = #{id}")
  218. int updateContentExtraction(DocumentEntity document);
  219. /** update_document 锁定行(MAIN/SUB_PLAN)。 */
  220. @Select("SELECT * FROM doc_document WHERE id = #{id} "
  221. + "AND document_type IN ('MAIN', 'SUB_PLAN') AND is_deleted = 0 FOR UPDATE")
  222. DocumentEntity findPlanForUpdate(@Param("id") long id);
  223. /** update_attachment/delete_attachment 锁定行。 */
  224. @Select("SELECT * FROM doc_document WHERE id = #{id} AND document_type = 'ATTACHMENT' "
  225. + "AND is_deleted = 0 FOR UPDATE")
  226. DocumentEntity findAttachmentForUpdate(@Param("id") long id);
  227. /** get_attachment 读取行(无锁)。 */
  228. @Select("SELECT * FROM doc_document WHERE id = #{id} AND document_type = 'ATTACHMENT' "
  229. + "AND is_deleted = 0")
  230. DocumentEntity findActiveAttachmentById(@Param("id") long id);
  231. /** 对齐 session.get(Document, id):主键读取,不过滤任何状态。 */
  232. @Select("SELECT * FROM doc_document WHERE id = #{id}")
  233. DocumentEntity findByIdNoFilter(@Param("id") long id);
  234. /** 新建子方案:父 child_count+1、rv+1、updated_by/by_name(不触碰 updated_at)。 */
  235. @Update("UPDATE doc_document SET child_count = child_count + 1, "
  236. + "updated_by = #{updatedBy}, updated_by_name = #{updatedByName}, "
  237. + "row_version = row_version + 1 WHERE id = #{id}")
  238. int incrementChildCount(@Param("id") long id, @Param("updatedBy") Long updatedBy,
  239. @Param("updatedByName") String updatedByName);
  240. /** 删除子方案:父 child_count=GREATEST(0,-1)、rv+1、updated_*。 */
  241. @Update("UPDATE doc_document SET child_count = GREATEST(0, child_count - 1), "
  242. + "updated_by = #{updatedBy}, updated_by_name = #{updatedByName}, "
  243. + "updated_at = #{updatedAt}, row_version = row_version + 1 WHERE id = #{id}")
  244. int decrementChildCount(@Param("id") long id, @Param("updatedBy") Long updatedBy,
  245. @Param("updatedByName") String updatedByName,
  246. @Param("updatedAt") LocalDateTime updatedAt);
  247. /** 删除主案:attachment_count 清零。 */
  248. @Update("UPDATE doc_document SET attachment_count = 0 WHERE id = #{id}")
  249. int setAttachmentCountZero(@Param("id") long id);
  250. /** 删除主案前置:有效子方案计数(parent 或 root 指向主案)。 */
  251. @Select("SELECT COUNT(*) FROM doc_document WHERE (parent_document_id = #{id} "
  252. + "OR root_document_id = #{id}) AND document_type = 'SUB_PLAN' AND is_deleted = 0")
  253. long countSubPlansByMain(@Param("id") long id);
  254. /** 软删文档:is_deleted、deleted_at、updated_by、updated_at、rv+1。 */
  255. @Update("UPDATE doc_document SET is_deleted = 1, deleted_at = #{now}, "
  256. + "updated_at = #{now}, updated_by = #{actorId}, updated_by_name = #{actorName}, "
  257. + "row_version = row_version + 1 WHERE id = #{id}")
  258. int softDelete(@Param("id") long id, @Param("now") LocalDateTime now,
  259. @Param("actorId") Long actorId, @Param("actorName") String actorName);
  260. /** 重排:ids 集合行锁定(含软删行,对齐 Python select...in 不加 is_deleted 过滤)。 */
  261. @Select("<script>SELECT * FROM doc_document WHERE id IN "
  262. + "<foreach collection='ids' item='i' open='(' separator=',' close=')'>"
  263. + "#{i}</foreach> ORDER BY id ASC FOR UPDATE</script>")
  264. List<DocumentEntity> listForUpdateByIds(@Param("ids") List<Long> ids);
  265. /** 重排:兄弟全集锁定(MAIN 全局 / SUB_PLAN 父内)。 */
  266. @Select("<script>SELECT * FROM doc_document WHERE is_deleted = 0 "
  267. + "<choose><when test='parentId != null'>AND document_type = 'SUB_PLAN' "
  268. + "AND parent_document_id = #{parentId}</when>"
  269. + "<otherwise>AND document_type = 'MAIN' AND parent_document_id IS NULL"
  270. + "</otherwise></choose> ORDER BY sort_no ASC, id ASC FOR UPDATE</script>")
  271. List<DocumentEntity> listSiblingsForOrder(@Param("parentId") Long parentId);
  272. /** 重排:仅写变化行(sort_no + updated_* + rv+1)。 */
  273. @Update("UPDATE doc_document SET sort_no = #{sortNo}, updated_by = #{updatedBy}, "
  274. + "updated_by_name = #{updatedByName}, updated_at = #{updatedAt}, "
  275. + "row_version = row_version + 1 WHERE id = #{id}")
  276. int updateSortNo(@Param("id") long id, @Param("sortNo") int sortNo,
  277. @Param("updatedBy") Long updatedBy,
  278. @Param("updatedByName") String updatedByName,
  279. @Param("updatedAt") LocalDateTime updatedAt);
  280. // ------------------------------------------------------------ 下载/预览与挂载域(阶段5.6)
  281. /** 预览/下载入口:三类通用有效行(对齐 _document)。 */
  282. @Select("SELECT * FROM doc_document WHERE id = #{id} AND is_deleted = 0 "
  283. + "AND document_type IN ('MAIN', 'SUB_PLAN', 'ATTACHMENT')")
  284. DocumentEntity findReadableById(@Param("id") long id);
  285. /** _record_download 查看计数同款:下载计数自增。 */
  286. @Update("UPDATE doc_document SET download_count = download_count + 1 WHERE id = #{id}")
  287. int incrementDownloadCount(@Param("id") long id);
  288. /** 挂载/解挂后主案回写:attachment_count + rv+1 + updated_*。 */
  289. @Update("UPDATE doc_document SET attachment_count = #{attachmentCount}, "
  290. + "row_version = row_version + 1, updated_by = #{updatedBy}, "
  291. + "updated_by_name = #{updatedByName}, updated_at = #{updatedAt} WHERE id = #{id}")
  292. int updateAttachmentCount(@Param("id") long id,
  293. @Param("attachmentCount") long attachmentCount,
  294. @Param("updatedBy") Long updatedBy,
  295. @Param("updatedByName") String updatedByName,
  296. @Param("updatedAt") LocalDateTime updatedAt);
  297. /** 挂载重排后主案元数据触碰:rv+1 + updated_*(不动 attachment_count)。 */
  298. @Update("UPDATE doc_document SET row_version = row_version + 1, "
  299. + "updated_by = #{updatedBy}, updated_by_name = #{updatedByName}, "
  300. + "updated_at = #{updatedAt} WHERE id = #{id}")
  301. int bumpMeta(@Param("id") long id, @Param("updatedBy") Long updatedBy,
  302. @Param("updatedByName") String updatedByName,
  303. @Param("updatedAt") LocalDateTime updatedAt);
  304. /**
  305. * has_any_manage_action:任一"已发布主案"命中(密级×部门范围×查看权三来源)。
  306. * scope 为 OWN/OTHER;集合为空时调用方传 [-1] 占位(对齐 Python
  307. * in_(空集)恒假、not_in(空集)恒真语义);canView=1 时矩阵行自身即查看来源。
  308. */
  309. @Select("<script>SELECT EXISTS(SELECT 1 FROM doc_document d "
  310. + "WHERE d.is_deleted = 0 AND d.document_status = 'PUBLISHED' "
  311. + "AND d.document_type = 'MAIN' AND d.security_level = #{securityLevel} "
  312. + "<choose>"
  313. + "<when test='scope == \"OWN\"'>AND d.organization_id IN "
  314. + "<foreach item='i' collection='ownIds' open='(' separator=',' close=')'>#{i}</foreach></when>"
  315. + "<otherwise>AND (d.organization_id IS NULL OR d.organization_id NOT IN "
  316. + "<foreach item='i' collection='ownIds' open='(' separator=',' close=')'>#{i}</foreach>)</otherwise>"
  317. + "</choose>"
  318. + "AND (d.visibility_type = 'ALL_AUTHENTICATED' "
  319. + "OR EXISTS(SELECT 1 FROM doc_permission p WHERE p.document_id = d.id "
  320. + "AND p.is_deleted = 0 AND p.can_view = 1 AND ("
  321. + "(p.subject_type = 'ORG' AND p.subject_id IN "
  322. + "<foreach item='i' collection='ancestorIds' open='(' separator=',' close=')'>#{i}</foreach>) "
  323. + "OR (p.subject_type = 'USER' AND p.subject_id = #{userId} "
  324. + "AND d.visibility_type = 'CUSTOM')))"
  325. + "<if test='canView'>OR TRUE</if>))</script>")
  326. boolean existsManageHit(@Param("securityLevel") String securityLevel,
  327. @Param("scope") String scope,
  328. @Param("ownIds") List<Long> ownIds,
  329. @Param("ancestorIds") List<Long> ancestorIds,
  330. @Param("userId") long userId,
  331. @Param("canView") boolean canView);
  332. /** _current_version:挂载唯一键冲突兜底的当前版本读取。 */
  333. @Select("SELECT row_version FROM doc_document WHERE id = #{id} AND is_deleted = 0")
  334. Integer currentRowVersion(@Param("id") long id);
  335. /** 权限保存:visibility + updated_* + rv+1(对齐 save_permissions 的文档 touch)。 */
  336. @Update("UPDATE doc_document SET visibility_type = #{visibility}, "
  337. + "updated_by = #{updatedBy}, updated_by_name = #{updatedByName}, "
  338. + "updated_at = #{now}, row_version = row_version + 1 WHERE id = #{id}")
  339. int updateVisibility(@Param("id") long id, @Param("visibility") String visibility,
  340. @Param("updatedBy") Long updatedBy,
  341. @Param("updatedByName") String updatedByName,
  342. @Param("now") LocalDateTime now);
  343. // ------------------------------------------------------------ 回收站(阶段7,对齐 recycle_bin_service.py)
  344. /**
  345. * 回收站分页查询(对齐 list_recycle_bin_documents):软删 + deleted_at 非空 +
  346. * 类型白名单;可选 documentType/categoryId/keyword 四字段 LIKE/deletedFrom≥/deletedTo<。
  347. * orderBy/idOrder 为服务层白名单校验后的 "列 方向" 子串。
  348. */
  349. @Select("<script>SELECT * FROM doc_document WHERE is_deleted = 1 "
  350. + "AND deleted_at IS NOT NULL "
  351. + "AND document_type IN ('ATTACHMENT', 'MAIN', 'SUB_PLAN')"
  352. + "<if test='f.documentType != null'>AND document_type = #{f.documentType}</if>"
  353. + "<if test='f.categoryId != null'>AND category_id = #{f.categoryId}</if>"
  354. + "<if test='f.keywordPattern != null'>AND (document_name LIKE #{f.keywordPattern} "
  355. + "ESCAPE '\\\\' OR summary LIKE #{f.keywordPattern} ESCAPE '\\\\' "
  356. + "OR search_text LIKE #{f.keywordPattern} ESCAPE '\\\\' "
  357. + "OR original_file_name LIKE #{f.keywordPattern} ESCAPE '\\\\')</if>"
  358. + "<if test='f.deletedFrom != null'>AND deleted_at &gt;= #{f.deletedFrom}</if>"
  359. + "<if test='f.deletedTo != null'>AND deleted_at &lt; #{f.deletedTo}</if>"
  360. + " ORDER BY ${f.orderBy}, id ${f.idOrder} "
  361. + "LIMIT #{f.pageSize} OFFSET #{f.offset}</script>")
  362. List<DocumentEntity> listDeletedPage(@Param("f") Map<String, Object> filters);
  363. /** 回收站计数(与 listDeletedPage 同条件,无分页/排序)。 */
  364. @Select("<script>SELECT COUNT(*) FROM doc_document WHERE is_deleted = 1 "
  365. + "AND deleted_at IS NOT NULL "
  366. + "AND document_type IN ('ATTACHMENT', 'MAIN', 'SUB_PLAN')"
  367. + "<if test='f.documentType != null'>AND document_type = #{f.documentType}</if>"
  368. + "<if test='f.categoryId != null'>AND category_id = #{f.categoryId}</if>"
  369. + "<if test='f.keywordPattern != null'>AND (document_name LIKE #{f.keywordPattern} "
  370. + "ESCAPE '\\\\' OR summary LIKE #{f.keywordPattern} ESCAPE '\\\\' "
  371. + "OR search_text LIKE #{f.keywordPattern} ESCAPE '\\\\' "
  372. + "OR original_file_name LIKE #{f.keywordPattern} ESCAPE '\\\\')</if>"
  373. + "<if test='f.deletedFrom != null'>AND deleted_at &gt;= #{f.deletedFrom}</if>"
  374. + "<if test='f.deletedTo != null'>AND deleted_at &lt; #{f.deletedTo}</if>"
  375. + "</script>")
  376. long countDeletedPage(@Param("f") Map<String, Object> filters);
  377. /** 对齐 _restore_operation 行锁读:主键 FOR UPDATE,不过滤删除状态。 */
  378. @Select("SELECT * FROM doc_document WHERE id = #{id} FOR UPDATE")
  379. DocumentEntity findByIdForUpdate(@Param("id") long id);
  380. /** 恢复后主案子案重数(AND 语义,区别于 countSubPlansByMain 的 OR 版)。 */
  381. @Select("SELECT COUNT(*) FROM doc_document WHERE document_type = 'SUB_PLAN' "
  382. + "AND parent_document_id = #{mainId} AND root_document_id = #{mainId} "
  383. + "AND is_deleted = 0")
  384. long countActiveSubPlansUnderMain(@Param("mainId") long mainId);
  385. /** 主案恢复批次附件锁定(对齐 attachments 锁读:in + id 升序 FOR UPDATE,不过滤)。 */
  386. @Select("<script>SELECT * FROM doc_document WHERE id IN "
  387. + "<foreach item='item' collection='ids' open='(' separator=',' close=')'>#{item}"
  388. + "</foreach> ORDER BY id ASC FOR UPDATE</script>")
  389. List<DocumentEntity> listByIdsForUpdate(@Param("ids") java.util.Collection<Long> ids);
  390. /** 子方案恢复:继承主案分类快照(对齐 Python 对象赋值 flush)。 */
  391. @Update("UPDATE doc_document SET category_id = #{categoryId}, "
  392. + "category_name = #{categoryName}, category_path = #{categoryPath} "
  393. + "WHERE id = #{id}")
  394. int updateCategorySnapshot(@Param("id") long id, @Param("categoryId") Long categoryId,
  395. @Param("categoryName") String categoryName,
  396. @Param("categoryPath") String categoryPath);
  397. /** 文档复活:is_deleted=0、deleted_at=NULL、updated_*、rv+1。 */
  398. @Update("UPDATE doc_document SET is_deleted = 0, deleted_at = NULL, "
  399. + "updated_at = #{now}, updated_by = #{actorId}, "
  400. + "updated_by_name = #{actorName}, row_version = row_version + 1 "
  401. + "WHERE id = #{id}")
  402. int restoreDocument(@Param("id") long id, @Param("now") LocalDateTime now,
  403. @Param("actorId") Long actorId, @Param("actorName") String actorName);
  404. /**
  405. * 主案恢复后 child_count/attachment_count 重数(对齐 SQLAlchemy 仅变化时落库:
  406. * 值不变则 0 行受影响,不触碰 updated_*)。
  407. */
  408. @Update("UPDATE doc_document SET child_count = #{childCount}, "
  409. + "attachment_count = #{attachmentCount} WHERE id = #{id} "
  410. + "AND (child_count != #{childCount} OR attachment_count != #{attachmentCount})")
  411. int restoreMainCounts(@Param("id") long id,
  412. @Param("childCount") long childCount,
  413. @Param("attachmentCount") long attachmentCount);
  414. /** 子方案恢复后主案 touch:child_count 重数 + updated_* + rv+1(无条件)。 */
  415. @Update("UPDATE doc_document SET child_count = #{childCount}, "
  416. + "updated_at = #{now}, updated_by = #{actorId}, "
  417. + "updated_by_name = #{actorName}, row_version = row_version + 1 "
  418. + "WHERE id = #{id}")
  419. int restoreParentCounts(@Param("id") long id, @Param("childCount") long childCount,
  420. @Param("now") LocalDateTime now, @Param("actorId") Long actorId,
  421. @Param("actorName") String actorName);
  422. }