0020_sub_plan_to_attachment.py 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. """配套方案并入共享资料库:SUB_PLAN → ATTACHMENT(ACCOMPANYING_PLAN)。
  2. Revision ID: 0020_sub_plan_to_attachment
  3. Revises: 0019_content_extract_error
  4. Create Date: 2026-08-29
  5. 变更要点:
  6. 1. doc_document 附件类型CHECK扩四值:新名 ck_doc_document_attachment_type_v2
  7. (旧名 DROP IF EXISTS,新名 ADD IF NOT EXISTS,按名幂等,与 Flyway V5 一致);
  8. 2. 同名消歧:存活子案与存活附件同名、或与更小id存活子案同名者,追加"(id)"后缀;
  9. 3. 全部 SUB_PLAN(含软删)→ ATTACHMENT + attachment_type=ACCOMPANYING_PLAN,
  10. visibility 统一 ALL_AUTHENTICATED(附件可见性口径);
  11. 4. 自有子案关系转挂载:存活子案按 parent_document_id 补 doc_attachment_binding
  12. (sort_no 沿用原排序,NOT EXISTS 防重);
  13. 5. doc_sub_plan_binding 活跃行转 doc_attachment_binding 后软删(防重跑双转);
  14. 6. 重算全部主案 attachment_count;child_count 清零(子案不再按父子计数);
  15. 7. 降级保护:已产生 ACCOMPANYING_PLAN 数据时拒绝有损回退。
  16. """
  17. from __future__ import annotations
  18. from collections.abc import Sequence
  19. import sqlalchemy as sa
  20. from alembic import op
  21. revision: str = "0020_sub_plan_to_attachment"
  22. down_revision: str | None = "0019_content_extract_error"
  23. branch_labels: str | Sequence[str] | None = None
  24. depends_on: str | Sequence[str] | None = None
  25. _OLD_CHECK = "ck_doc_document_document_attachment_type"
  26. _NEW_CHECK = "ck_doc_document_attachment_type_v2"
  27. _NEW_CHECK_DDL = (
  28. "ALTER TABLE doc_document ADD CONSTRAINT " + _NEW_CHECK + " "
  29. "CHECK (attachment_type IS NULL OR attachment_type IN "
  30. "('SUPPORT_ACHIEVEMENT','TRAINING_MATERIAL','TYPICAL_CASE','ACCOMPANYING_PLAN'))"
  31. )
  32. # 附件shape约束:放宽 security_level(配套方案保留原密级,读路径由密级前置拦截)
  33. _OLD_SHAPE = "ck_doc_document_document_type_shape"
  34. _NEW_SHAPE = "ck_doc_document_type_shape_v2"
  35. _NEW_SHAPE_DDL = (
  36. "ALTER TABLE doc_document ADD CONSTRAINT " + _NEW_SHAPE + " CHECK ("
  37. "((document_type = 'MAIN') AND (parent_document_id IS NULL) "
  38. "AND (category_id IS NOT NULL) AND (attachment_type IS NULL)) "
  39. "OR ((document_type = 'SUB_PLAN') AND (parent_document_id IS NOT NULL) "
  40. "AND (root_document_id IS NOT NULL) AND (category_id IS NOT NULL) "
  41. "AND (attachment_type IS NULL)) "
  42. "OR ((document_type = 'ATTACHMENT') AND (parent_document_id IS NULL) "
  43. "AND (root_document_id IS NULL) AND (category_id IS NULL) "
  44. "AND (attachment_type IS NOT NULL) "
  45. "AND (visibility_type = 'ALL_AUTHENTICATED')))"
  46. )
  47. def _constraint_exists(name: str) -> bool:
  48. bind = op.get_bind()
  49. return bool(
  50. bind.scalar(
  51. sa.text(
  52. "SELECT COUNT(*) FROM information_schema.TABLE_CONSTRAINTS "
  53. "WHERE TABLE_SCHEMA = DATABASE() "
  54. "AND TABLE_NAME = 'doc_document' AND CONSTRAINT_NAME = :name"
  55. ),
  56. {"name": name},
  57. )
  58. )
  59. def _run(statement: str) -> None:
  60. op.get_bind().execute(sa.text(statement))
  61. def upgrade() -> None:
  62. # 1) CHECK 约束扩四值(按名幂等)
  63. if _constraint_exists(_OLD_CHECK):
  64. _run(f"ALTER TABLE doc_document DROP CHECK {_OLD_CHECK}")
  65. if not _constraint_exists(_NEW_CHECK):
  66. _run(_NEW_CHECK_DDL)
  67. # 1b) shape约束放宽:附件允许任意密级(存量子案密级保留,读路径密级前置)
  68. if _constraint_exists(_OLD_SHAPE):
  69. _run(f"ALTER TABLE doc_document DROP CHECK {_OLD_SHAPE}")
  70. if not _constraint_exists(_NEW_SHAPE):
  71. _run(_NEW_SHAPE_DDL)
  72. # 2) 同名消歧(仅存活行;后缀含id保证确定性且可重跑无冲突)。
  73. # 候选id先落临时表再JOIN更新,规避 MySQL 1093(UPDATE 目标表不可出现在子查询)。
  74. _run("DROP TEMPORARY TABLE IF EXISTS tmp_0020_sub_dedupe")
  75. _run(
  76. "CREATE TEMPORARY TABLE tmp_0020_sub_dedupe (id BIGINT PRIMARY KEY)"
  77. )
  78. _run(
  79. """
  80. INSERT INTO tmp_0020_sub_dedupe (id)
  81. SELECT a.id FROM doc_document a
  82. WHERE a.document_type = 'SUB_PLAN' AND a.is_deleted = 0 AND (
  83. EXISTS (
  84. SELECT 1 FROM doc_document b
  85. WHERE b.is_deleted = 0 AND b.document_type = 'ATTACHMENT'
  86. AND b.document_name = a.document_name
  87. )
  88. OR EXISTS (
  89. SELECT 1 FROM doc_document c
  90. WHERE c.document_type = 'SUB_PLAN'
  91. AND c.document_name = a.document_name AND c.id < a.id
  92. )
  93. )
  94. """
  95. )
  96. _run(
  97. """
  98. UPDATE doc_document d JOIN tmp_0020_sub_dedupe t ON d.id = t.id
  99. SET d.document_name = CONCAT(d.document_name, '(', d.id, ')')
  100. """
  101. )
  102. _run("DROP TEMPORARY TABLE IF EXISTS tmp_0020_sub_dedupe")
  103. # 3) 自有子案关系先转挂载(需 parent_document_id,父主案存活的存活子案)
  104. _run(
  105. """
  106. INSERT INTO doc_attachment_binding
  107. (main_document_id, attachment_document_id, sort_no, created_by, updated_by)
  108. SELECT d.parent_document_id, d.id, d.sort_no, d.created_by, d.updated_by
  109. FROM doc_document d
  110. WHERE d.document_type = 'SUB_PLAN'
  111. AND d.is_deleted = 0
  112. AND d.parent_document_id IS NOT NULL
  113. AND EXISTS (
  114. SELECT 1 FROM doc_document m
  115. WHERE m.id = d.parent_document_id AND m.is_deleted = 0
  116. )
  117. AND NOT EXISTS (
  118. SELECT 1 FROM doc_attachment_binding b
  119. WHERE b.main_document_id = d.parent_document_id
  120. AND b.attachment_document_id = d.id AND b.is_deleted = 0
  121. )
  122. """
  123. )
  124. # 4) 子案跨主案挂载关系转附件挂载后软删(防重跑双转)
  125. _run(
  126. """
  127. INSERT INTO doc_attachment_binding
  128. (main_document_id, attachment_document_id, sort_no, created_by, updated_by)
  129. SELECT s.main_document_id, s.sub_document_id, s.sort_no, s.created_by, s.updated_by
  130. FROM doc_sub_plan_binding s
  131. WHERE s.is_deleted = 0
  132. AND NOT EXISTS (
  133. SELECT 1 FROM doc_attachment_binding b
  134. WHERE b.main_document_id = s.main_document_id
  135. AND b.attachment_document_id = s.sub_document_id AND b.is_deleted = 0
  136. )
  137. """
  138. )
  139. _run(
  140. """
  141. UPDATE doc_sub_plan_binding
  142. SET is_deleted = 1, deleted_at = NOW(3), updated_at = NOW(3),
  143. row_version = row_version + 1
  144. WHERE is_deleted = 0
  145. """
  146. )
  147. # 5) 类型转换(全部 SUB_PLAN,含软删,重跑自然无目标行):
  148. # 置空父/根/分类(附件shape),保留密级(读路径密级前置拦截)
  149. _run(
  150. """
  151. UPDATE doc_document
  152. SET document_type = 'ATTACHMENT',
  153. attachment_type = 'ACCOMPANYING_PLAN',
  154. visibility_type = 'ALL_AUTHENTICATED',
  155. parent_document_id = NULL,
  156. root_document_id = NULL,
  157. category_id = NULL,
  158. category_name = NULL,
  159. category_path = NULL
  160. WHERE document_type = 'SUB_PLAN'
  161. """
  162. )
  163. # 6) 计数重算与清零
  164. _run(
  165. """
  166. UPDATE doc_document m
  167. SET m.attachment_count = (
  168. SELECT COUNT(*) FROM doc_attachment_binding b
  169. WHERE b.main_document_id = m.id AND b.is_deleted = 0
  170. )
  171. WHERE m.document_type = 'MAIN'
  172. AND m.attachment_count <> (
  173. SELECT COUNT(*) FROM doc_attachment_binding b
  174. WHERE b.main_document_id = m.id AND b.is_deleted = 0
  175. )
  176. """
  177. )
  178. _run(
  179. "UPDATE doc_document SET child_count = 0 "
  180. "WHERE document_type = 'MAIN' AND child_count <> 0"
  181. )
  182. def downgrade() -> None:
  183. bind = op.get_bind()
  184. converted = bind.scalar(
  185. sa.text(
  186. "SELECT COUNT(*) FROM doc_document "
  187. "WHERE attachment_type = 'ACCOMPANYING_PLAN'"
  188. )
  189. )
  190. if converted:
  191. raise RuntimeError(
  192. "0020已产生配套方案(ACCOMPANYING_PLAN)数据,拒绝有损降级;"
  193. "请先备份并按数据治理流程处理"
  194. )
  195. if _constraint_exists(_NEW_CHECK):
  196. _run(f"ALTER TABLE doc_document DROP CHECK {_NEW_CHECK}")
  197. if not _constraint_exists(_OLD_CHECK):
  198. _run(
  199. "ALTER TABLE doc_document ADD CONSTRAINT " + _OLD_CHECK + " "
  200. "CHECK (attachment_type IS NULL OR attachment_type IN "
  201. "('SUPPORT_ACHIEVEMENT','TRAINING_MATERIAL','TYPICAL_CASE'))"
  202. )
  203. # shape约束回旧版(附件强制PUBLIC密级);已拒绝并轨数据,安全恢复
  204. if _constraint_exists(_NEW_SHAPE):
  205. _run(f"ALTER TABLE doc_document DROP CHECK {_NEW_SHAPE}")
  206. if not _constraint_exists(_OLD_SHAPE):
  207. _run(
  208. "ALTER TABLE doc_document ADD CONSTRAINT " + _OLD_SHAPE + " "
  209. "CHECK (((document_type = 'MAIN') AND (parent_document_id IS NULL) "
  210. "AND (category_id IS NOT NULL) AND (attachment_type IS NULL)) "
  211. "OR ((document_type = 'SUB_PLAN') AND (parent_document_id IS NOT NULL) "
  212. "AND (root_document_id IS NOT NULL) AND (category_id IS NOT NULL) "
  213. "AND (attachment_type IS NULL)) "
  214. "OR ((document_type = 'ATTACHMENT') AND (parent_document_id IS NULL) "
  215. "AND (root_document_id IS NULL) AND (category_id IS NULL) "
  216. "AND (attachment_type IS NOT NULL) "
  217. "AND (security_level = 'PUBLIC') "
  218. "AND (visibility_type = 'ALL_AUTHENTICATED')))"
  219. )