document.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. """主案、子方案和共享附件统一文档模型。"""
  2. from __future__ import annotations
  3. from datetime import datetime
  4. from sqlalchemy import (
  5. BigInteger,
  6. Boolean,
  7. CheckConstraint,
  8. ForeignKey,
  9. Index,
  10. Integer,
  11. JSON,
  12. String,
  13. Text,
  14. text,
  15. )
  16. from sqlalchemy.orm import Mapped, mapped_column
  17. from sqlalchemy.dialects import mysql
  18. from dms.database.base import BusinessTableMixin
  19. from dms.extensions import db
  20. class Document(BusinessTableMixin, db.Model):
  21. __tablename__ = "doc_document"
  22. __table_args__ = (
  23. CheckConstraint(
  24. "document_type IN ('MAIN', 'SUB_PLAN', 'ATTACHMENT')",
  25. name="document_type",
  26. ),
  27. CheckConstraint(
  28. "document_status IN ('DRAFT', 'PUBLISHED', 'ARCHIVED')",
  29. name="document_status",
  30. ),
  31. CheckConstraint(
  32. "security_level IN "
  33. "('PUBLIC', 'INTERNAL', 'SECRET', 'CONFIDENTIAL', 'TOP_SECRET')",
  34. name="document_security_level",
  35. ),
  36. CheckConstraint(
  37. "visibility_type IN ('ALL_AUTHENTICATED', 'ORGANIZATION', 'CUSTOM')",
  38. name="document_visibility_type",
  39. ),
  40. CheckConstraint(
  41. "attachment_type IS NULL OR attachment_type IN "
  42. "('SUPPORT_ACHIEVEMENT', 'TRAINING_MATERIAL', 'TYPICAL_CASE')",
  43. name="document_attachment_type",
  44. ),
  45. CheckConstraint(
  46. "content_extract_status IN "
  47. "('SUCCESS', 'EMPTY', 'UNSUPPORTED', 'FAILED')",
  48. name="document_content_extract_status",
  49. ),
  50. # 方案密码锁:仅主案可设密(password_hash 非空时其余类型被拒绝)
  51. CheckConstraint(
  52. "(password_hash IS NULL) OR (document_type = 'MAIN')",
  53. name="document_password_main_only",
  54. ),
  55. CheckConstraint(
  56. "("
  57. "document_type = 'MAIN' "
  58. "AND parent_document_id IS NULL "
  59. "AND category_id IS NOT NULL "
  60. "AND attachment_type IS NULL"
  61. ") OR ("
  62. "document_type = 'SUB_PLAN' "
  63. "AND parent_document_id IS NOT NULL "
  64. "AND root_document_id IS NOT NULL "
  65. "AND category_id IS NOT NULL "
  66. "AND attachment_type IS NULL"
  67. ") OR ("
  68. "document_type = 'ATTACHMENT' "
  69. "AND parent_document_id IS NULL "
  70. "AND root_document_id IS NULL "
  71. "AND category_id IS NULL "
  72. "AND attachment_type IS NOT NULL "
  73. "AND security_level = 'PUBLIC' "
  74. "AND visibility_type = 'ALL_AUTHENTICATED'"
  75. ")",
  76. name="document_type_shape",
  77. ),
  78. Index(
  79. "ix_doc_document_type_active_updated",
  80. "document_type",
  81. "is_deleted",
  82. "updated_at",
  83. ),
  84. Index(
  85. "ix_doc_document_parent_type_active",
  86. "parent_document_id",
  87. "document_type",
  88. "is_deleted",
  89. ),
  90. Index(
  91. "ix_doc_document_root_active",
  92. "root_document_id",
  93. "is_deleted",
  94. ),
  95. Index(
  96. "ix_doc_document_category_active",
  97. "category_id",
  98. "is_deleted",
  99. ),
  100. Index(
  101. "ix_doc_document_organization_active",
  102. "organization_id",
  103. "is_deleted",
  104. ),
  105. Index(
  106. "ix_doc_document_status_active",
  107. "document_status",
  108. "is_deleted",
  109. ),
  110. Index(
  111. "ix_doc_document_security_active",
  112. "security_level",
  113. "is_deleted",
  114. ),
  115. Index("ix_doc_document_file_hash", "file_hash"),
  116. Index(
  117. "ix_doc_document_creator_active",
  118. "created_by",
  119. "is_deleted",
  120. ),
  121. Index(
  122. "ix_doc_document_attachment_type_active_updated",
  123. "attachment_type",
  124. "is_deleted",
  125. "updated_at",
  126. ),
  127. Index(
  128. "ix_doc_document_file_extension_active_updated",
  129. "file_extension",
  130. "is_deleted",
  131. "updated_at",
  132. ),
  133. Index(
  134. "ix_doc_document_recycle_deleted",
  135. "is_deleted",
  136. "deleted_at",
  137. "id",
  138. ),
  139. )
  140. document_name: Mapped[str] = mapped_column(String(255), nullable=False)
  141. summary: Mapped[str | None] = mapped_column(Text, nullable=True)
  142. # 详细说明:管理员在"查看"对话框维护,供普通用户阅读;不参与全文检索
  143. description: Mapped[str | None] = mapped_column(Text, nullable=True)
  144. document_type: Mapped[str] = mapped_column(String(32), nullable=False)
  145. document_status: Mapped[str] = mapped_column(
  146. String(32),
  147. nullable=False,
  148. server_default=text("'DRAFT'"),
  149. )
  150. security_level: Mapped[str] = mapped_column(
  151. String(32),
  152. nullable=False,
  153. server_default=text("'INTERNAL'"),
  154. )
  155. visibility_type: Mapped[str] = mapped_column(
  156. String(32),
  157. nullable=False,
  158. server_default=text("'CUSTOM'"),
  159. )
  160. attachment_type: Mapped[str | None] = mapped_column(String(32), nullable=True)
  161. category_id: Mapped[int | None] = mapped_column(
  162. BigInteger,
  163. ForeignKey("doc_category.id", ondelete="RESTRICT"),
  164. nullable=True,
  165. )
  166. category_name: Mapped[str | None] = mapped_column(String(128), nullable=True)
  167. category_path: Mapped[str | None] = mapped_column(String(1000), nullable=True)
  168. organization_id: Mapped[int | None] = mapped_column(
  169. BigInteger,
  170. ForeignKey("sys_organization.id", ondelete="RESTRICT"),
  171. nullable=True,
  172. )
  173. organization_name: Mapped[str | None] = mapped_column(String(128), nullable=True)
  174. parent_document_id: Mapped[int | None] = mapped_column(
  175. BigInteger,
  176. ForeignKey("doc_document.id", ondelete="RESTRICT"),
  177. nullable=True,
  178. )
  179. root_document_id: Mapped[int | None] = mapped_column(
  180. BigInteger,
  181. ForeignKey("doc_document.id", ondelete="RESTRICT"),
  182. nullable=True,
  183. )
  184. tags: Mapped[list[str] | None] = mapped_column(JSON, nullable=True)
  185. original_file_name: Mapped[str] = mapped_column(String(255), nullable=False)
  186. file_relative_path: Mapped[str] = mapped_column(String(1000), nullable=False)
  187. file_extension: Mapped[str] = mapped_column(String(20), nullable=False)
  188. mime_type: Mapped[str | None] = mapped_column(String(128), nullable=True)
  189. file_size: Mapped[int] = mapped_column(
  190. BigInteger,
  191. nullable=False,
  192. server_default=text("0"),
  193. )
  194. file_hash: Mapped[str] = mapped_column(String(64), nullable=False)
  195. # 管理员拖动排序:主案范围内全局排序、子方案在所属主案内排序;步长10便于插入。
  196. sort_no: Mapped[int] = mapped_column(
  197. Integer,
  198. nullable=False,
  199. server_default=text("0"),
  200. )
  201. child_count: Mapped[int] = mapped_column(
  202. Integer,
  203. nullable=False,
  204. server_default=text("0"),
  205. )
  206. attachment_count: Mapped[int] = mapped_column(
  207. Integer,
  208. nullable=False,
  209. server_default=text("0"),
  210. )
  211. view_count: Mapped[int] = mapped_column(
  212. BigInteger,
  213. nullable=False,
  214. server_default=text("0"),
  215. )
  216. download_count: Mapped[int] = mapped_column(
  217. BigInteger,
  218. nullable=False,
  219. server_default=text("0"),
  220. )
  221. search_text: Mapped[str | None] = mapped_column(mysql.LONGTEXT, nullable=True)
  222. content_text: Mapped[str | None] = mapped_column(mysql.LONGTEXT, nullable=True)
  223. content_extract_status: Mapped[str] = mapped_column(
  224. String(32),
  225. nullable=False,
  226. server_default=text("'UNSUPPORTED'"),
  227. )
  228. content_extracted_at: Mapped[datetime | None] = mapped_column(
  229. mysql.DATETIME(fsp=3),
  230. nullable=True,
  231. )
  232. created_by_name: Mapped[str | None] = mapped_column(String(64), nullable=True)
  233. updated_by_name: Mapped[str | None] = mapped_column(String(64), nullable=True)
  234. # 方案密码锁:Argon2id 哈希(NULL=未设密);保护位对普通用户生效,
  235. # 管理员对设密主案的除删除外全部操作均需密码(服务层判定)。
  236. password_hash: Mapped[str | None] = mapped_column(String(255), nullable=True)
  237. protect_view: Mapped[bool] = mapped_column(
  238. Boolean,
  239. nullable=False,
  240. server_default=text("0"),
  241. )
  242. protect_download: Mapped[bool] = mapped_column(
  243. Boolean,
  244. nullable=False,
  245. server_default=text("0"),
  246. )
  247. protect_edit: Mapped[bool] = mapped_column(
  248. Boolean,
  249. nullable=False,
  250. server_default=text("0"),
  251. )
  252. protect_delete: Mapped[bool] = mapped_column(
  253. Boolean,
  254. nullable=False,
  255. server_default=text("0"),
  256. )
  257. protect_config_permission: Mapped[bool] = mapped_column(
  258. Boolean,
  259. nullable=False,
  260. server_default=text("0"),
  261. )
  262. protect_bind_attachment: Mapped[bool] = mapped_column(
  263. Boolean,
  264. nullable=False,
  265. server_default=text("0"),
  266. )
  267. protect_unbind_attachment: Mapped[bool] = mapped_column(
  268. Boolean,
  269. nullable=False,
  270. server_default=text("0"),
  271. )