ordloom 1.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of ordloom might be problematic. Click here for more details.

Files changed (285) hide show
  1. ordloom-1.2.0/PKG-INFO +2551 -0
  2. ordloom-1.2.0/README.md +2528 -0
  3. ordloom-1.2.0/ordloom.egg-info/PKG-INFO +2551 -0
  4. ordloom-1.2.0/ordloom.egg-info/SOURCES.txt +283 -0
  5. ordloom-1.2.0/ordloom.egg-info/dependency_links.txt +1 -0
  6. ordloom-1.2.0/ordloom.egg-info/requires.txt +5 -0
  7. ordloom-1.2.0/ordloom.egg-info/top_level.txt +1 -0
  8. ordloom-1.2.0/pyproject.toml +44 -0
  9. ordloom-1.2.0/setup.cfg +4 -0
  10. ordloom-1.2.0/simpleui_antd/__init__.py +3 -0
  11. ordloom-1.2.0/simpleui_antd/account_security.py +96 -0
  12. ordloom-1.2.0/simpleui_antd/api/__init__.py +0 -0
  13. ordloom-1.2.0/simpleui_antd/api/account_admin.py +66 -0
  14. ordloom-1.2.0/simpleui_antd/api/accounts.py +42 -0
  15. ordloom-1.2.0/simpleui_antd/api/admin_forms.py +59 -0
  16. ordloom-1.2.0/simpleui_antd/api/audit_retention.py +18 -0
  17. ordloom-1.2.0/simpleui_antd/api/auth.py +224 -0
  18. ordloom-1.2.0/simpleui_antd/api/base.py +113 -0
  19. ordloom-1.2.0/simpleui_antd/api/collaboration.py +269 -0
  20. ordloom-1.2.0/simpleui_antd/api/config.py +59 -0
  21. ordloom-1.2.0/simpleui_antd/api/crud.py +474 -0
  22. ordloom-1.2.0/simpleui_antd/api/dashboard.py +90 -0
  23. ordloom-1.2.0/simpleui_antd/api/data_services.py +231 -0
  24. ordloom-1.2.0/simpleui_antd/api/data_templates.py +21 -0
  25. ordloom-1.2.0/simpleui_antd/api/dataset_usage.py +49 -0
  26. ordloom-1.2.0/simpleui_antd/api/datasets.py +302 -0
  27. ordloom-1.2.0/simpleui_antd/api/events.py +33 -0
  28. ordloom-1.2.0/simpleui_antd/api/field_scope.py +93 -0
  29. ordloom-1.2.0/simpleui_antd/api/file_values.py +49 -0
  30. ordloom-1.2.0/simpleui_antd/api/form_exports.py +57 -0
  31. ordloom-1.2.0/simpleui_antd/api/form_summary.py +54 -0
  32. ordloom-1.2.0/simpleui_antd/api/grant_audits.py +80 -0
  33. ordloom-1.2.0/simpleui_antd/api/identifiers.py +24 -0
  34. ordloom-1.2.0/simpleui_antd/api/inline_forms.py +248 -0
  35. ordloom-1.2.0/simpleui_antd/api/json_fields.py +22 -0
  36. ordloom-1.2.0/simpleui_antd/api/list_query.py +68 -0
  37. ordloom-1.2.0/simpleui_antd/api/listing.py +159 -0
  38. ordloom-1.2.0/simpleui_antd/api/notifications.py +152 -0
  39. ordloom-1.2.0/simpleui_antd/api/offline.py +72 -0
  40. ordloom-1.2.0/simpleui_antd/api/offline_snapshots.py +106 -0
  41. ordloom-1.2.0/simpleui_antd/api/operations.py +59 -0
  42. ordloom-1.2.0/simpleui_antd/api/overview.py +148 -0
  43. ordloom-1.2.0/simpleui_antd/api/ownership.py +80 -0
  44. ordloom-1.2.0/simpleui_antd/api/page_bindings.py +140 -0
  45. ordloom-1.2.0/simpleui_antd/api/page_charts.py +102 -0
  46. ordloom-1.2.0/simpleui_antd/api/page_forms.py +225 -0
  47. ordloom-1.2.0/simpleui_antd/api/page_grants.py +297 -0
  48. ordloom-1.2.0/simpleui_antd/api/pages.py +291 -0
  49. ordloom-1.2.0/simpleui_antd/api/password_fields.py +182 -0
  50. ordloom-1.2.0/simpleui_antd/api/rate_limit.py +24 -0
  51. ordloom-1.2.0/simpleui_antd/api/readonly_fields.py +31 -0
  52. ordloom-1.2.0/simpleui_antd/api/registry.py +696 -0
  53. ordloom-1.2.0/simpleui_antd/api/report_bindings.py +38 -0
  54. ordloom-1.2.0/simpleui_antd/api/report_boards.py +128 -0
  55. ordloom-1.2.0/simpleui_antd/api/report_exports.py +216 -0
  56. ordloom-1.2.0/simpleui_antd/api/reports.py +176 -0
  57. ordloom-1.2.0/simpleui_antd/api/screen_playlists.py +134 -0
  58. ordloom-1.2.0/simpleui_antd/api/security.py +159 -0
  59. ordloom-1.2.0/simpleui_antd/api/share_management.py +59 -0
  60. ordloom-1.2.0/simpleui_antd/api/share_settings.py +118 -0
  61. ordloom-1.2.0/simpleui_antd/api/task_diagnostics.py +22 -0
  62. ordloom-1.2.0/simpleui_antd/api/totp.py +75 -0
  63. ordloom-1.2.0/simpleui_antd/api/webhooks.py +247 -0
  64. ordloom-1.2.0/simpleui_antd/apps.py +18 -0
  65. ordloom-1.2.0/simpleui_antd/asgi.py +12 -0
  66. ordloom-1.2.0/simpleui_antd/audit_retention.py +78 -0
  67. ordloom-1.2.0/simpleui_antd/branding.py +4 -0
  68. ordloom-1.2.0/simpleui_antd/collaboration.py +168 -0
  69. ordloom-1.2.0/simpleui_antd/components/__init__.py +1 -0
  70. ordloom-1.2.0/simpleui_antd/components/fields.py +257 -0
  71. ordloom-1.2.0/simpleui_antd/conf.py +61 -0
  72. ordloom-1.2.0/simpleui_antd/consumers.py +92 -0
  73. ordloom-1.2.0/simpleui_antd/data_services.py +228 -0
  74. ordloom-1.2.0/simpleui_antd/diagnostics.py +84 -0
  75. ordloom-1.2.0/simpleui_antd/editor/__init__.py +0 -0
  76. ordloom-1.2.0/simpleui_antd/editor/fields.py +30 -0
  77. ordloom-1.2.0/simpleui_antd/graphql/__init__.py +1 -0
  78. ordloom-1.2.0/simpleui_antd/graphql/access.py +279 -0
  79. ordloom-1.2.0/simpleui_antd/graphql/checks.py +16 -0
  80. ordloom-1.2.0/simpleui_antd/graphql/datasets.py +325 -0
  81. ordloom-1.2.0/simpleui_antd/graphql/durations.py +43 -0
  82. ordloom-1.2.0/simpleui_antd/graphql/event_access.py +45 -0
  83. ordloom-1.2.0/simpleui_antd/graphql/event_limits.py +12 -0
  84. ordloom-1.2.0/simpleui_antd/graphql/events.py +118 -0
  85. ordloom-1.2.0/simpleui_antd/graphql/inputs.py +133 -0
  86. ordloom-1.2.0/simpleui_antd/graphql/json_projection.py +141 -0
  87. ordloom-1.2.0/simpleui_antd/graphql/query.py +235 -0
  88. ordloom-1.2.0/simpleui_antd/graphql/registry.py +160 -0
  89. ordloom-1.2.0/simpleui_antd/graphql/relations.py +82 -0
  90. ordloom-1.2.0/simpleui_antd/graphql/schema.py +217 -0
  91. ordloom-1.2.0/simpleui_antd/graphql/scopes.py +64 -0
  92. ordloom-1.2.0/simpleui_antd/graphql/subscriptions.py +59 -0
  93. ordloom-1.2.0/simpleui_antd/graphql/views.py +89 -0
  94. ordloom-1.2.0/simpleui_antd/management/__init__.py +0 -0
  95. ordloom-1.2.0/simpleui_antd/management/commands/__init__.py +0 -0
  96. ordloom-1.2.0/simpleui_antd/management/commands/simpleui_antd_check.py +31 -0
  97. ordloom-1.2.0/simpleui_antd/management/commands/simpleui_antd_deliver_webhooks.py +25 -0
  98. ordloom-1.2.0/simpleui_antd/management/commands/simpleui_antd_export_reports.py +24 -0
  99. ordloom-1.2.0/simpleui_antd/management/commands/simpleui_antd_init.py +42 -0
  100. ordloom-1.2.0/simpleui_antd/management/commands/simpleui_antd_prune_audit.py +25 -0
  101. ordloom-1.2.0/simpleui_antd/management/commands/simpleui_antd_prune_share_visits.py +42 -0
  102. ordloom-1.2.0/simpleui_antd/management/commands/simpleui_antd_reindex_visual.py +45 -0
  103. ordloom-1.2.0/simpleui_antd/management/commands/simpleui_antd_upgrade.py +56 -0
  104. ordloom-1.2.0/simpleui_antd/middleware.py +23 -0
  105. ordloom-1.2.0/simpleui_antd/migrations/0001_initial.py +30 -0
  106. ordloom-1.2.0/simpleui_antd/migrations/0002_siteconfig.py +25 -0
  107. ordloom-1.2.0/simpleui_antd/migrations/0003_visualpage.py +33 -0
  108. ordloom-1.2.0/simpleui_antd/migrations/0004_visualpage_uuid.py +31 -0
  109. ordloom-1.2.0/simpleui_antd/migrations/0005_visualsubmission.py +23 -0
  110. ordloom-1.2.0/simpleui_antd/migrations/0006_screenplaylist.py +21 -0
  111. ordloom-1.2.0/simpleui_antd/migrations/0007_visualdataset.py +38 -0
  112. ordloom-1.2.0/simpleui_antd/migrations/0008_share_controls.py +19 -0
  113. ordloom-1.2.0/simpleui_antd/migrations/0009_report.py +21 -0
  114. ordloom-1.2.0/simpleui_antd/migrations/0010_page_grant.py +21 -0
  115. ordloom-1.2.0/simpleui_antd/migrations/0011_usersecurity.py +17 -0
  116. ordloom-1.2.0/simpleui_antd/migrations/0012_visualgrantaudit.py +27 -0
  117. ordloom-1.2.0/simpleui_antd/migrations/0013_visual_usage.py +55 -0
  118. ordloom-1.2.0/simpleui_antd/migrations/0014_notifications_retention.py +28 -0
  119. ordloom-1.2.0/simpleui_antd/migrations/0015_webhooks.py +97 -0
  120. ordloom-1.2.0/simpleui_antd/migrations/0016_data_services.py +38 -0
  121. ordloom-1.2.0/simpleui_antd/migrations/0017_dataset_archive.py +18 -0
  122. ordloom-1.2.0/simpleui_antd/migrations/0018_recovery_devices.py +53 -0
  123. ordloom-1.2.0/simpleui_antd/migrations/0019_share_access_visits.py +57 -0
  124. ordloom-1.2.0/simpleui_antd/migrations/0020_initialization_batch.py +30 -0
  125. ordloom-1.2.0/simpleui_antd/migrations/0021_offline_page_import.py +31 -0
  126. ordloom-1.2.0/simpleui_antd/migrations/0022_report_board.py +32 -0
  127. ordloom-1.2.0/simpleui_antd/migrations/0023_report_exports.py +64 -0
  128. ordloom-1.2.0/simpleui_antd/migrations/0024_collaboration.py +66 -0
  129. ordloom-1.2.0/simpleui_antd/migrations/0025_organization_members.py +12 -0
  130. ordloom-1.2.0/simpleui_antd/migrations/0026_ownership_handover.py +161 -0
  131. ordloom-1.2.0/simpleui_antd/migrations/__init__.py +0 -0
  132. ordloom-1.2.0/simpleui_antd/models.py +709 -0
  133. ordloom-1.2.0/simpleui_antd/notifications.py +60 -0
  134. ordloom-1.2.0/simpleui_antd/offline_worker.js +40 -0
  135. ordloom-1.2.0/simpleui_antd/organizations.py +181 -0
  136. ordloom-1.2.0/simpleui_antd/ownership.py +268 -0
  137. ordloom-1.2.0/simpleui_antd/pwa.py +92 -0
  138. ordloom-1.2.0/simpleui_antd/report_exports/__init__.py +1 -0
  139. ordloom-1.2.0/simpleui_antd/report_exports/calendar.py +57 -0
  140. ordloom-1.2.0/simpleui_antd/report_exports/config.py +31 -0
  141. ordloom-1.2.0/simpleui_antd/report_exports/files.py +35 -0
  142. ordloom-1.2.0/simpleui_antd/report_exports/service.py +269 -0
  143. ordloom-1.2.0/simpleui_antd/routing.py +20 -0
  144. ordloom-1.2.0/simpleui_antd/session_devices.py +120 -0
  145. ordloom-1.2.0/simpleui_antd/setup_tools.py +268 -0
  146. ordloom-1.2.0/simpleui_antd/share_access.py +45 -0
  147. ordloom-1.2.0/simpleui_antd/share_analytics.py +67 -0
  148. ordloom-1.2.0/simpleui_antd/share_limits.py +93 -0
  149. ordloom-1.2.0/simpleui_antd/starter_templates.json +1828 -0
  150. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/assets/AntVChart-DJXYP1w1.js +2 -0
  151. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/assets/ArrowUpOutlined-YUinJllQ.js +1 -0
  152. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/assets/CollaborationWorkspaces-BZe-W3GA.js +1 -0
  153. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/assets/ComponentGallery-PmHB-aME.js +17 -0
  154. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/assets/DataServicesPage-DnEA6vWK.js +1 -0
  155. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/assets/DataTemplateWizard-CTuc5MHT.js +1 -0
  156. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/assets/DatasetDefinitionEditor-CpYbTwu4.js +1 -0
  157. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/assets/DatasetDiffDialog-gL21OxRw.js +1 -0
  158. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/assets/DatasetManager-Ce8a34FQ.js +1 -0
  159. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/assets/FieldHelp-ByCZGwlD.js +1 -0
  160. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/assets/ListPage-0vUEYDtV.js +3 -0
  161. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/assets/MapInput-C5WPSlgA.js +1 -0
  162. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/assets/MapInput-CIGW-MKW.css +1 -0
  163. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/assets/MarkdownEditor-Yd3YGlor.js +27 -0
  164. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/assets/MarkdownPreview-CqfWLDBk.js +1 -0
  165. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/assets/OfflineManager-CaXwAiy8.js +1 -0
  166. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/assets/OfflineStudio-CHMOkowM.css +1 -0
  167. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/assets/OfflineStudio-ChYD3Nzu.js +2 -0
  168. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/assets/PasswordResetModal-Dp8lxenK.js +1 -0
  169. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/assets/ReportBoard-fJiKrPlp.js +1 -0
  170. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/assets/ReportBoardsPage-L8QlyTtw.js +1 -0
  171. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/assets/ReportBuilder-6YjMXv4W.js +1 -0
  172. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/assets/ReportsPage-B8HgL-Y7.css +1 -0
  173. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/assets/ReportsPage-DdHAzX-H.js +1 -0
  174. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/assets/ScreenPlaylistManager-D1-6oIav.js +1 -0
  175. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/assets/ScreenPlaylistPlayer-DsXUSgDc.js +1 -0
  176. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/assets/ShareAltOutlined-d3DM7J-p.js +1 -0
  177. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/assets/SnapshotCapture-BQQ4rBEe.js +1 -0
  178. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/assets/TaskDiagnosticsPage-C698suOP.js +1 -0
  179. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/assets/UploadOutlined-DF9avbAg.js +4 -0
  180. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/assets/VisualEditor-G6HHOLvg.js +4 -0
  181. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/assets/WebhooksPage-BWMHwr1y.js +1 -0
  182. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/assets/antvRuntime-COO-f2fZ.js +52 -0
  183. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/assets/bootstrap-D2zNl10I.css +1 -0
  184. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/assets/bootstrap-ORzXA_T-.js +410 -0
  185. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/assets/chartRuntime-BDiSlyel.js +1 -0
  186. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/assets/chartRuntime-Co53qbqJ.js +1 -0
  187. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/assets/client-BPfy2HtM.js +1 -0
  188. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/assets/document-DXtU3i12.js +1 -0
  189. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/assets/index-BpL76UWO.js +1 -0
  190. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/assets/index.css +1 -0
  191. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/assets/index.js +2 -0
  192. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/assets/installCanvasRenderer-Cyw3KEQY.js +18 -0
  193. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/assets/markdown.worker-BzqxKdkw.js +71 -0
  194. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/assets/preload-C3bFygIu.js +1 -0
  195. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/assets/reportBoards-DSTrDDne.css +1 -0
  196. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/assets/snapshotStore-CaOklyUa.js +1 -0
  197. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/assets/tslib.es6-1xh5QPgC.js +1 -0
  198. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/assets/useConfigurationAccess-NgA1ANl0.js +1 -0
  199. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/assets/widgets-DdAQVwr-.js +36 -0
  200. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/component-cover.svg +11 -0
  201. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/icons/icon-192.png +0 -0
  202. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/icons/icon-512.png +0 -0
  203. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/index.html +14 -0
  204. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/offline-assets.json +301 -0
  205. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/visual-assets/SOURCES.md +15 -0
  206. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/visual-assets/city.jpg +0 -0
  207. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/visual-assets/coffee.jpg +0 -0
  208. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/visual-assets/event.jpg +0 -0
  209. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/visual-assets/headphones.jpg +0 -0
  210. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/visual-assets/nature.jpg +0 -0
  211. ordloom-1.2.0/simpleui_antd/static/simpleui_antd/visual-assets/workspace.jpg +0 -0
  212. ordloom-1.2.0/simpleui_antd/task_runtime.py +297 -0
  213. ordloom-1.2.0/simpleui_antd/templates/simpleui_antd/index.html +22 -0
  214. ordloom-1.2.0/simpleui_antd/templates/simpleui_antd/offline.html +3 -0
  215. ordloom-1.2.0/simpleui_antd/tests/__init__.py +0 -0
  216. ordloom-1.2.0/simpleui_antd/tests/admin.py +461 -0
  217. ordloom-1.2.0/simpleui_antd/tests/apps.py +7 -0
  218. ordloom-1.2.0/simpleui_antd/tests/email_account.py +35 -0
  219. ordloom-1.2.0/simpleui_antd/tests/models.py +323 -0
  220. ordloom-1.2.0/simpleui_antd/tests/test_account_admin.py +375 -0
  221. ordloom-1.2.0/simpleui_antd/tests/test_accounts.py +257 -0
  222. ordloom-1.2.0/simpleui_antd/tests/test_audit_retention.py +145 -0
  223. ordloom-1.2.0/simpleui_antd/tests/test_collaboration.py +413 -0
  224. ordloom-1.2.0/simpleui_antd/tests/test_create_only.py +73 -0
  225. ordloom-1.2.0/simpleui_antd/tests/test_data_services.py +273 -0
  226. ordloom-1.2.0/simpleui_antd/tests/test_data_templates.py +97 -0
  227. ordloom-1.2.0/simpleui_antd/tests/test_dataset_archive.py +235 -0
  228. ordloom-1.2.0/simpleui_antd/tests/test_dataset_usage.py +147 -0
  229. ordloom-1.2.0/simpleui_antd/tests/test_datasets.py +416 -0
  230. ordloom-1.2.0/simpleui_antd/tests/test_durations.py +205 -0
  231. ordloom-1.2.0/simpleui_antd/tests/test_events.py +179 -0
  232. ordloom-1.2.0/simpleui_antd/tests/test_events_channels.py +233 -0
  233. ordloom-1.2.0/simpleui_antd/tests/test_fields.py +371 -0
  234. ordloom-1.2.0/simpleui_antd/tests/test_form_results.py +130 -0
  235. ordloom-1.2.0/simpleui_antd/tests/test_generic_inline.py +318 -0
  236. ordloom-1.2.0/simpleui_antd/tests/test_grant_audits.py +123 -0
  237. ordloom-1.2.0/simpleui_antd/tests/test_graphql.py +361 -0
  238. ordloom-1.2.0/simpleui_antd/tests/test_graphql_queries.py +358 -0
  239. ordloom-1.2.0/simpleui_antd/tests/test_host_setup.py +272 -0
  240. ordloom-1.2.0/simpleui_antd/tests/test_identifiers.py +141 -0
  241. ordloom-1.2.0/simpleui_antd/tests/test_inline_contract.py +220 -0
  242. ordloom-1.2.0/simpleui_antd/tests/test_json_fields.py +175 -0
  243. ordloom-1.2.0/simpleui_antd/tests/test_json_projection.py +272 -0
  244. ordloom-1.2.0/simpleui_antd/tests/test_management.py +73 -0
  245. ordloom-1.2.0/simpleui_antd/tests/test_markdown.py +131 -0
  246. ordloom-1.2.0/simpleui_antd/tests/test_notifications.py +183 -0
  247. ordloom-1.2.0/simpleui_antd/tests/test_offline.py +139 -0
  248. ordloom-1.2.0/simpleui_antd/tests/test_offline_snapshots.py +194 -0
  249. ordloom-1.2.0/simpleui_antd/tests/test_operations.py +203 -0
  250. ordloom-1.2.0/simpleui_antd/tests/test_organizations.py +290 -0
  251. ordloom-1.2.0/simpleui_antd/tests/test_overview.py +236 -0
  252. ordloom-1.2.0/simpleui_antd/tests/test_ownership.py +285 -0
  253. ordloom-1.2.0/simpleui_antd/tests/test_page_bindings.py +161 -0
  254. ordloom-1.2.0/simpleui_antd/tests/test_page_grants.py +300 -0
  255. ordloom-1.2.0/simpleui_antd/tests/test_pages.py +320 -0
  256. ordloom-1.2.0/simpleui_antd/tests/test_recovery_devices.py +304 -0
  257. ordloom-1.2.0/simpleui_antd/tests/test_relations.py +313 -0
  258. ordloom-1.2.0/simpleui_antd/tests/test_report_boards.py +152 -0
  259. ordloom-1.2.0/simpleui_antd/tests/test_report_exports.py +418 -0
  260. ordloom-1.2.0/simpleui_antd/tests/test_report_initialization.py +236 -0
  261. ordloom-1.2.0/simpleui_antd/tests/test_report_reuse.py +233 -0
  262. ordloom-1.2.0/simpleui_antd/tests/test_reports.py +193 -0
  263. ordloom-1.2.0/simpleui_antd/tests/test_request_body.py +69 -0
  264. ordloom-1.2.0/simpleui_antd/tests/test_screen_playlists.py +141 -0
  265. ordloom-1.2.0/simpleui_antd/tests/test_secret_fields.py +192 -0
  266. ordloom-1.2.0/simpleui_antd/tests/test_share_access.py +188 -0
  267. ordloom-1.2.0/simpleui_antd/tests/test_share_limits.py +165 -0
  268. ordloom-1.2.0/simpleui_antd/tests/test_share_settings.py +120 -0
  269. ordloom-1.2.0/simpleui_antd/tests/test_sql_scopes.py +259 -0
  270. ordloom-1.2.0/simpleui_antd/tests/test_task_diagnostics.py +216 -0
  271. ordloom-1.2.0/simpleui_antd/tests/test_totp.py +206 -0
  272. ordloom-1.2.0/simpleui_antd/tests/test_webhook_bots.py +280 -0
  273. ordloom-1.2.0/simpleui_antd/tests/test_webhooks.py +349 -0
  274. ordloom-1.2.0/simpleui_antd/tests/test_workspace.py +65 -0
  275. ordloom-1.2.0/simpleui_antd/urls.py +152 -0
  276. ordloom-1.2.0/simpleui_antd/views.py +115 -0
  277. ordloom-1.2.0/simpleui_antd/visual_templates.py +118 -0
  278. ordloom-1.2.0/simpleui_antd/visual_usage.py +61 -0
  279. ordloom-1.2.0/simpleui_antd/webhooks/__init__.py +1 -0
  280. ordloom-1.2.0/simpleui_antd/webhooks/access.py +41 -0
  281. ordloom-1.2.0/simpleui_antd/webhooks/bots.py +135 -0
  282. ordloom-1.2.0/simpleui_antd/webhooks/config.py +79 -0
  283. ordloom-1.2.0/simpleui_antd/webhooks/signals.py +70 -0
  284. ordloom-1.2.0/simpleui_antd/webhooks/transport.py +78 -0
  285. ordloom-1.2.0/simpleui_antd/webhooks/worker.py +149 -0
ordloom-1.2.0/PKG-INFO ADDED
@@ -0,0 +1,2551 @@
1
+ Metadata-Version: 2.4
2
+ Name: ordloom
3
+ Version: 1.2.0
4
+ Summary: 序衡 Ordloom:可配置的 React 19 + Ant Design 6 Django Admin 工作台
5
+ Author: neekin
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/neekin/simpleui-antd
8
+ Project-URL: Repository, https://github.com/neekin/simpleui-antd
9
+ Project-URL: Issues, https://github.com/neekin/simpleui-antd/issues
10
+ Classifier: Framework :: Django
11
+ Classifier: Framework :: Django :: 4.2
12
+ Classifier: Framework :: Django :: 5.0
13
+ Classifier: Framework :: Django :: 5.1
14
+ Classifier: Framework :: Django :: 5.2
15
+ Classifier: Framework :: Django :: 6.0
16
+ Classifier: Programming Language :: Python :: 3
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+ Requires-Dist: django>=4.2
20
+ Requires-Dist: pillow>=9.0
21
+ Provides-Extra: graphql
22
+ Requires-Dist: strawberry-graphql-django<0.97,>=0.96.3; extra == "graphql"
23
+
24
+ # 序衡 Ordloom · Django
25
+
26
+ 完整使用手册:[Ordloom Docs](https://github.com/neekin/ordloom-docs/blob/main/docs/README.md)。
27
+
28
+ 基于 **React 19 + Ant Design 6** 的 Django Admin 工作台,提供可配置品牌、主题和业务组件。
29
+
30
+ `pip install` 后在 `INSTALLED_APPS` 注册应用,React 界面**直接接管 `/admin/`**,
31
+ 数据来自你注册到 Django Admin 的模型,
32
+ 复用其 queryset、权限与 ModelForm 校验。
33
+
34
+ ## 特性
35
+
36
+ - **接管 /admin/**:可选 `SIMPLEUI_ANTD_FULL_TAKEOVER = False`
37
+ 切回「入口接管 + 原生 admin 兜底」模式
38
+ - **拖拽式数据大屏**:首页卡片拖拽排序、宽度调整、隐藏恢复,布局按用户持久化;
39
+ ECharts 柱状图 / 占比环形图 / 近 7 日动作趋势
40
+ - **全模型管理**:列表(搜索 / 排序 / 分页 / 勾选 / 批量 Action)、新增 / 编辑 / 删除、
41
+ **内联 InlineModelAdmin**(弹窗内子记录增删改)、**文件 / 图片上传**、**多对多多选**
42
+ - **Pro 组件字段**:复选框 / 单选 / 切换 / 数字 /
43
+ 滑块 / 评分 / 日期 / 日期时间 / 时间 / **穿梭框 TransferField** / **树形下拉 TreeSelectField** /
44
+ **地图取点 MapField** / **视频 VideoField**
45
+ - **编辑器**:富文本 RichTextField(内置工具栏)/ Markdown MDTextField
46
+ - **登录验证码开关**:后台「系统设置」一键开启/关闭,图片验证码(Pillow 生成),
47
+ 会话绑定、一次性,登录接口强制校验
48
+ - **主题与外观**:8 种主题色,浅色/深色/跟随系统;导航、密度、字号、圆角和页面宽度可调整,偏好在浏览器保存
49
+ - **多标签页栏**、字体大小、全屏、最近动作(Django Admin LogEntry)、登录页分栏大气布局
50
+
51
+ ## 操作演示
52
+
53
+ 套用模板、拖入组件、调整布局、保存预览与全屏放映;采用静态模板示例数据,来自 1.1.0 功能构建的真实浏览器录制。
54
+ [查看操作动图](https://github.com/neekin/simpleui-antd/blob/v1.1.0/docs/images/visual-builder-demo.gif);素材位于源码仓库 `docs/images/`,私有仓库访问需要授权。
55
+
56
+ ## 安装
57
+
58
+ Python 发行包名统一为 `ordloom`,与序衡 Ordloom 品牌一致;Django 应用名、Python 导入路径和配置键继续兼容。
59
+ 从 1.2.0 起以新名称发布;本版包含分享密码限速和可选的后台任务运行诊断,无新增迁移。
60
+
61
+ ```bash
62
+ pip install ordloom
63
+ ```
64
+
65
+ ### 从旧发行包切换
66
+
67
+ 已安装旧发行包的环境,先卸载旧包,再安装新包;两个发行包包含同一个 `simpleui_antd` 应用,不要同时安装。
68
+
69
+ ```bash
70
+ python -m pip uninstall django-simpleui-antd
71
+ python -m pip install ordloom
72
+ ```
73
+
74
+ 无需改写 `INSTALLED_APPS`、URL 导入、`SIMPLEUI_*` 设置或已有 `simpleui_antd_*` 管理命令。
75
+ 应用标签、数据库表和迁移历史保持原名;包名切换本身不需要数据迁移,升级包含的新功能仍按正常流程执行 `python manage.py migrate`。
76
+
77
+ ## 快速开始
78
+
79
+ ```python
80
+ # settings.py
81
+ INSTALLED_APPS = [
82
+ ...,
83
+ "simpleui_antd", # 放在你自己的 app 之后即可
84
+ ]
85
+
86
+ # urls.py —— React 界面接管 /admin/
87
+ urlpatterns = [
88
+ path("admin/", include("simpleui_antd.urls")), # 必须写在 admin.site.urls 之前
89
+ path("admin/", admin.site.urls), # 原生 admin 兜底(可选)
90
+ ]
91
+ ```
92
+
93
+ ```bash
94
+ python manage.py migrate # 创建大屏布局 / 站点配置表
95
+ python manage.py runserver
96
+ ```
97
+
98
+ 访问 <http://127.0.0.1:8000/admin/>,用**任意 is_staff 账号**登录即可。
99
+
100
+ 不希望完全接管时,在 settings.py 设置
101
+ `SIMPLEUI_ANTD_FULL_TAKEOVER = False`,则仅 `/admin/` 入口为 React 界面,
102
+ 其余子路径走原生 admin;也可独立挂载
103
+ `path("simpleui/", include("simpleui_antd.urls"))` 两者并存。
104
+
105
+ ## 可选 GraphQL 数据层
106
+
107
+ 已提供模型自动注册、字段目录、强类型分页明细、按 ID 查询,以及第二阶段的通用筛选、排序和聚合。GraphQL 默认关闭,
108
+ 已有 REST API、页面和核心依赖继续使用原来的配置。本扩展需要 Python ≥ 3.10、Django ≥ 5.2。
109
+
110
+ ```bash
111
+ pip install 'ordloom[graphql]'
112
+ ```
113
+
114
+ 在宿主项目中启用,并明确列出允许开放的模型字段;模型必须已经注册到 Django Admin:
115
+
116
+ ```python
117
+ SIMPLEUI_ANTD_GRAPHQL = {
118
+ "enabled": True,
119
+ "models": {
120
+ "sales.order": {
121
+ "fields": ["id", "number", "amount", "paid", "created_at"],
122
+ },
123
+ },
124
+ "max_page_size": 100, # 默认 100,可配置 1–1000
125
+ "max_permission_scan": 1000, # 对象/动态字段权限扫描上限,可配置 1–10000
126
+ "max_filter_depth": 5, # 筛选树深度,可配置 1–10
127
+ "max_filter_nodes": 100, # 筛选节点和操作数量,可配置 1–500
128
+ "max_aggregate_groups": 100, # 每次最多返回的分组,可配置 1–1000
129
+ }
130
+ ```
131
+
132
+ 也可以让模型随其 ModelAdmin 自动发现,settings 中只需设置 `{"enabled": True}`:
133
+
134
+ ```python
135
+ @admin.register(Order)
136
+ class OrderAdmin(admin.ModelAdmin):
137
+ simpleui_antd_graphql = {
138
+ "fields": ["id", "number", "amount", "paid", "created_at"],
139
+ }
140
+ ```
141
+
142
+ 同一模型的 settings 声明优先于 ModelAdmin 声明;将该模型配置为 `False` 可关闭开放。
143
+ `fields` 必须是字段白名单,不接受 `__all__`,不会因为模型新增字段而自动扩大开放范围。
144
+ 不支持的自定义字段或计算展示列会在 Django system check 中明确报错。
145
+
146
+ 接口地址为挂载点下的 `api/graphql/`,例如 `/admin/api/graphql/`。
147
+ 使用现有 Django 登录会话,仅已启用的 staff/superuser 可访问。查询只接受 POST JSON,
148
+ 保留 Django CSRF;浏览器应携带 CSRF cookie 对应的 `X-CSRFToken` 请求头。
149
+
150
+ 查询有权限的模型和字段目录:
151
+
152
+ ```graphql
153
+ query {
154
+ visualDataCatalog {
155
+ key
156
+ label
157
+ queryField
158
+ detailField
159
+ aggregateField
160
+ maxPageSize
161
+ maxAggregateGroups
162
+ fields {
163
+ name
164
+ graphqlName
165
+ label
166
+ kind
167
+ graphqlType
168
+ nullable
169
+ primaryKey
170
+ choices { value label }
171
+ relatedModel
172
+ multiple
173
+ filterOperators
174
+ sortable
175
+ aggregateOperations
176
+ groupable
177
+ dateGranularities
178
+ }
179
+ supportsFiltering
180
+ supportsSorting
181
+ supportsAggregation
182
+ supportsPush
183
+ }
184
+ }
185
+ ```
186
+
187
+ 目录按当前请求过滤。`nullable` 表示数据库字段是否允许 null;GraphQL 输出字段均可为空,
188
+ 使对象相关的字段拒读可以返回局部错误,而不清除整条记录。GraphQL 的 Schema 固定包含已配置的
189
+ 字段,introspection 不代替授权;直接请求已知字段仍执行同样的权限检查。
190
+
191
+ 查询名从 `app_label.model_name` 自动生成,并在 `queryField` / `detailField` 返回。
192
+ 例如 `sales.order` 对应 `salesOrder` / `salesOrderById`:
193
+
194
+ ```graphql
195
+ query Orders($id: ID!) {
196
+ salesOrder(offset: 0, limit: 20) {
197
+ items { id number amount paid createdAt }
198
+ totalCount
199
+ offset
200
+ limit
201
+ hasNextPage
202
+ }
203
+ salesOrderById(id: $id) { id number amount }
204
+ }
205
+ ```
206
+
207
+ 字段名采用 camelCase;主键统一为字符串 ID,兼容 0、UUID 和字符串主键;金额用 Decimal
208
+ 字符串,整数用 BigInt 字符串,避免 JavaScript 数值精度损失。布尔值和 JSON 保留原类型,
209
+ 日期/时间采用 ISO 格式。数据库本身已经丢失的精度无法由传输层恢复。
210
+
211
+ 读取遵守 `has_module_permission()`、`has_view_permission()`、原始完整 request、
212
+ `get_queryset(request)` 和软删除范围。字段白名单还会与原生字段布局、排除字段、动态表单及
213
+ 凭据识别规则取交集。`readonly_fields` 仍可以读取;密码/密钥字段禁止开放,
214
+ 请求期间才被识别为密码的动态字段也不会返回值。
215
+
216
+ 需要附加按用户或按对象的字段限制时,可实现只会收窄白名单的钩子:
217
+
218
+ ```python
219
+ def get_graphql_fields(self, request, obj=None):
220
+ fields = ["id", "number", "paid", "created_at"]
221
+ if request.user.has_perm("sales.view_order_amount"):
222
+ fields.append("amount")
223
+ return fields
224
+ ```
225
+
226
+ 自定义对象查看权限会先过滤,再执行用户条件、排序、计数和分页;超出 `max_permission_scan` 时返回
227
+ `POLICY_SCOPE_REQUIRED`,避免静默漏检或扫描全表。可通过原来的 `get_queryset(request)`
228
+ 缩小候选范围。无权查看的对象与不存在的对象均返回 null。
229
+
230
+ 关联字段继续只返回授权后的 ID 或 ID 列表:检查关联模型的查看权限、QuerySet、
231
+ 软删除范围及原生表单的可选范围;不开放任意嵌套关系遍历。关联结果超过读取上限时明确报错。
232
+
233
+ 每次查询单独保存权限计算状态,不跨用户共享结果缓存;响应禁止缓存。
234
+ 请求上限为 64 KiB、2000 个 GraphQL tokens、8 层查询深度和 20 个数据操作。
235
+ 分页默认最多 100 条;错误通过 GraphQL `errors[].extensions.code` 返回,包括
236
+ `FORBIDDEN`、`FORBIDDEN_FIELD`、`INVALID_ARGUMENT`、`POLICY_SCOPE_REQUIRED`、
237
+ `QUERY_TOO_LARGE` 等。内部异常消息不返回客户端。关闭扩展时端点返回 404,
238
+ 缺少依赖或配置有误时 system check 提示原因,原有 API 不依赖此扩展。
239
+
240
+ ### 通用筛选与排序
241
+
242
+ 原明细查询增加可选 `filters`、`orderBy`,不传参数时继续按主键稳定分页。
243
+ `filters.where` 根据已开放字段生成强类型输入:条件字段采用 camelCase;
244
+ `AND`、`OR` 为子条件数组,`NOT` 为单个子条件,同层各条件按 AND 组合。
245
+
246
+ ```graphql
247
+ query {
248
+ salesOrder(
249
+ filters: {
250
+ where: { amount: { gte: "100", lt: "1000" }, paid: { eq: true } }
251
+ OR: [
252
+ { where: { number: { startsWith: "WEB-" } } }
253
+ { where: { number: { inList: ["VIP-001", "VIP-002"] } } }
254
+ ]
255
+ NOT: { where: { number: { eq: "WEB-CANCELLED" } } }
256
+ }
257
+ orderBy: [{ field: "amount", direction: DESC }, { field: "created_at", direction: ASC }]
258
+ limit: 20
259
+ ) {
260
+ items { id number amount paid createdAt }
261
+ totalCount
262
+ hasNextPage
263
+ }
264
+ }
265
+ ```
266
+
267
+ - 基础操作:`eq`、`ne`、`inList`、`notIn`、`isNull`。`eq: null` / `ne: null` 用于判断数据库空值;其他操作不接受 null。
268
+ - 数值、日期、时间和时长增加 `gt`、`gte`、`lt`、`lte`;文本增加 `contains`、`iContains`、`startsWith`、`endsWith`。
269
+ - `inList` / `notIn` 每次接受 1–100 个不含 null 的值,文本筛选值不超过 2000 字;空条件、空 AND/OR 和空集合返回明确错误。`ne`、`notIn` 和 `NOT` 沿用 Django 排除语义,对非 null 条件取反时可包含 null 记录。
270
+ - 筛选值遵守字段类型。BigInt 接受整数或十进制整数字符串;Decimal 建议使用字符串。NaN/Infinity 和超出有符号 64 位范围的整数筛选被拒绝。
271
+ - `orderBy.field` 使用目录的原生 `name`(如 `created_at`),最多 5 个、不重复;空值始终排最后,并自动补主键打破并列,保持跨页顺序稳定。
272
+ - 开启 `USE_TZ` 时 DateTime 筛选必须携带时区;关闭时接受无时区时间。Time 字段不接受时区偏移。
273
+
274
+ 多值关联、原始 JSON 和文件字段保留原有读取能力,不支持直接查询运算或任意 ORM lookup。
275
+ 数据集与报表可显式声明 JSON 标量子字段,见「JSON 子字段」。
276
+ 正向单值关联按目标主键查询,见「单值关联字段」。
277
+ `DurationField` 的筛选、排序、分组与聚合见下节「持续时间字段」。
278
+ 目录逐字段返回 `filterOperators`、`sortable`、`aggregateOperations`、`groupable` 和 `dateGranularities`。
279
+ 目录只列出当前账号可读字段;Schema 中存在输入字段不代表账号可以使用它。
280
+
281
+ ### JSON 子字段
282
+
283
+ 数据集工作室和报表构建器可将已开放的原生 JSONField 中的对象键、数组位置声明为类型化列,
284
+ 用于明细、筛选、排序、分组和标量类型允许的聚合。配置保存在各自的 `definition.jsonFields`,
285
+ 不会推断或修改全局模型目录、模型 Schema 或业务记录,也不接受代码、SQL 或 ORM lookup。
286
+
287
+ 例如,业务 JSON 为 `{"stats":{"amount":"12.50"},"daily":[{"units":"9007199254740993"}]}`,
288
+ 在已有模型与源字段白名单下可配置:
289
+
290
+ ```json
291
+ {
292
+ "model": "sales.order",
293
+ "mode": "records",
294
+ "jsonFields": [
295
+ {"name": "price", "source": "preferences", "path": ["stats", "amount"], "type": "Decimal"},
296
+ {"name": "dailyUnits", "source": "preferences", "path": ["daily", 0, "units"], "type": "BigInt"}
297
+ ],
298
+ "fields": ["id", "price", "dailyUnits"],
299
+ "filters": {"where": {"price": {"gte": "10"}}},
300
+ "orderBy": [{"field": "price", "direction": "DESC"}]
301
+ }
302
+ ```
303
+
304
+ 模型与 `preferences` 应替换为宿主实际开放字段;`fields`/排序/分组/指标使用声明名称,
305
+ 筛选及明细输出沿用 camelCase 规则。标量为 `String/Boolean/BigInt/Decimal/Float`,
306
+ 参数也用相应标量或其列表;发布版本冻结路径、类型及源 JSON 字段依赖,草稿修改不影响旧版本。
307
+ 发布快照仍冻结查询配置,不冻结结果、权限或业务 JSON 类型。
308
+
309
+ - 每定义最多 20 列、每路径 1–8 层。对象键为 1–64 字字符串,支持 Unicode、点和数字键,
310
+ 不含双引号、反斜线及控制字符;数组序号为 0–1000000 整数。字符串 `"0"` 与整数 `0` 明确区分。
311
+ 名称以英文字母开头,最多 64 位字母/数字/下划线,不含双下划线,不能覆盖模型字段、属性、
312
+ 宿主 QuerySet 注解或其他列的 camelCase 名称。
313
+ - 精确整数、小数的**源 JSON 值使用字符串**:BigInt 为规范十进制有符号 64 位整数,
314
+ 不接受前导零、`-0`、小数或指数;Decimal 最多 26 位整数与 12 位小数,不接受指数。
315
+ `Float` 使用有限 JSON 数字,文本和布尔值必须是对应 JSON 类型,不自动将文本转为布尔值或数字。
316
+ BigInt/Decimal 沿用字符串传输;图表继续检查有限值及安全数值范围。
317
+ - 缺失路径、JSON null 和 SQL null 返回空值。已有非空值或路径容器类型不符合声明时,
318
+ 预览/执行返回 `INVALID_VALUE`,不会静默转零;检查覆盖**用户筛选之前的完整授权对象范围**。
319
+ 保存/发布验证结构与权限,不扫描实际值类型,允许修复源数据后重新预览,无需重发相同查询。
320
+ - 当前支持 PostgreSQL 与 SQLite。PostgreSQL Decimal 使用 `numeric(38,12)`;SQLite 的 Decimal
321
+ 转换和聚合使用其原生数值计算,大额或高精度小数会出现约 15 位有效数字及二进制舍入限制。
322
+ JSON 中用字符串保存并不能消除 SQLite 查询计算损失。需完整小数精度时使用已验证的 PostgreSQL,
323
+ 并验收实际数据规模;BigInt 字符串在两个数据库中均保持 64 位整数精度。查询展开超过数据库参数预算
324
+ 返回 `QUERY_TOO_LARGE`,减少列、路径或条件后重试;原有无 JSON 子字段查询不受此扩展限制。
325
+ - 子字段继承**整个源 JSON 字段**的权限,不提供键级独立授权。模型/模块、Admin QuerySet、
326
+ 租户/软删除、对象、动态表单、私密/PasswordInput 字段规则继续生效。SQL 权限证明收到原始源字段名称,
327
+ 权限钩子读取原始模型对象,声明的列不会变成权限判断所用的对象属性。
328
+ 固定数据集服务授权仍授予该版本完整输出,公开页面仍仅返回映射字段;外发前均复核当前权限。
329
+ 含子字段的公开查询始终实时执行,避免已有结果缓存掩盖当前值类型变化;原生查询缓存规则不变。
330
+ - 大屏/H5、报表沿用现有映射及预览,轮询/模型变更重读策略不变。历史报表导出下载继续检查
331
+ 当前配置、字段及贡献记录权限,不因源 JSON 当前值类型变化重算或破坏已生成文件。
332
+
333
+ 专项:`python scripts/test_backend.py simpleui_antd.tests.test_json_projection`;
334
+ 真实 Session/CSRF 与隔离合成数据:`python scripts/test_json_projection.py`(需可选 GraphQL 依赖);
335
+ 专用临时 PostgreSQL:`python scripts/test_ownership_postgres.py --json-fields`(还需 psycopg)。
336
+ 演示可用 `scripts/dev_fields.py --json-fields --production`;不写实际宿主数据库,无新增宿主迁移。
337
+ JSON 编辑器 DOM 回归使用模拟 API,不能替代真实浏览器、Canvas 或宿主数据库验收。
338
+
339
+ ### 单值关联字段
340
+
341
+ 作为业务字段的正向 ForeignKey/OneToOne 支持 `eq/ne/inList/notIn/isNull`、排序、原值分组及 COUNT(含 distinct)。
342
+ 输入为目标记录的主键 `ID`,参数可用 `ID` 或 `[ID!]`;即使外键存储 `to_field="code"`,仍按目标主键操作及返回。
343
+ 空值保持 Django 的排除语义,排序始终将 null 放最后。关系本身作为根模型主键时继续按原根 ID 行为处理。
344
+ 多对多不参与这些运算,不接受任意 `owner__name`、ORM lookup 或关系遍历;SUM/AVG/MIN/MAX 也不用于关联 ID。
345
+
346
+ ```graphql
347
+ query ByTeam($team: ID!) {
348
+ projectRecord(
349
+ filters: {where: {owner: {eq: $team}}}
350
+ orderBy: [{field: "owner", direction: ASC}]
351
+ ) { items { id owner } totalCount }
352
+ }
353
+ ```
354
+
355
+ `projectRecord/owner` 需替换为宿主已开放模型的 `queryField` 和字段。UUID、整数及字符主键保留身份;
356
+ 不能把显示名称或存储业务码冒充主键。目录的 `filterValueType` / REST `filter_value_type` 为 `ID`,
357
+ 界面按 ID 匹配运行参数,并允许关联 ID 作为图表类目、COUNT 作为数值。查询定义和组件中不保存实际结果。
358
+
359
+ 权限在用户筛选前判定:
360
+
361
+ - 根模型沿用当前模型/模块、Admin QuerySet、软删除、对象与字段规则。目标需已注册 Django Admin 且可查看,
362
+ 使用目标 Admin 范围、对象权限及源字段 ModelForm 的 queryset 选项,不借用创建人或超级管理员身份。
363
+ - 对完整授权根范围中参与运算的字段,任一非空关联值不可见、已悬空或不在表单选项中,整体返回 `FORBIDDEN_FIELD`。
364
+ 不能先用名称/参数过滤掉该记录后绕过校验,也不会把不可见 ID 合并成可统计的 null 组。
365
+ 没有参与筛选/排序/分组/计数的只读关联继续按原规则返回可见 ID 或 null,旧明细发布版本保持兼容。
366
+ - 静态原生表单用 SQL 集合核对,源数据可超过 Python 扫描预算;动态 ModelForm/字段布局采用已有
367
+ `max_permission_scan`(默认 1000)。目标自定义对象权限也在原范围中有界检查,可用目标自己的 SQL 范围适配。
368
+ 单独的根 SQL 证明不能替代目标权限或逐对象表单选项。跨数据库的目标或选项范围拒绝处理。
369
+ - 关联权限范围不可验证时返回 `POLICY_SCOPE_INVALID`,组件清除旧结果;真实权限变化在下次请求重新检查。
370
+ 窗口排名权限先在原目标范围判断,避免按单个外键筛选后重新编号而放行不可见目标;
371
+ 服务身份对目标主键字段布局的判断也保持原窗口范围。
372
+ - 外部服务的模型字段授权还需明确包含目标模型主键,不能利用关联运算绕过原服务字段白名单;
373
+ 显式固定数据集版本的完整查询授权继续按原契约执行,仍复核当前 Django 权限。
374
+
375
+ 新定义仅对实际关联运算追加 `dependencies.relationOperands`,冻结目标主键与引用字段的结构;改变引用键/类型后
376
+ 原版本返回 `SCHEMA_CHANGED`,需修订发布。旧的只读关联依赖不重写。含关联依赖的公开数据授权不复用结果缓存,
377
+ 目标范围变化后立即重读或拒绝,避免仅依据根模型权限复用旧关联 ID。
378
+
379
+ 私有数据集/报表执行响应可带 `models`,包括根模型和关联目标模型;已有 `model` 保留。
380
+ 大屏、H5 和报表组件订阅这些模型的变更,替换绑定时清理旧订阅,在途通知合并后重读。
381
+ 事件只覆盖已显式注册到 GraphQL、启用事件且当前可读的模型;需要目标变更触发重读时,也应显式开放目标模型
382
+ 至少主键字段。未开放的目标仍可按 Django 权限读取关联 ID,但不产生目标模型通知,可选择轮询更新。
383
+ 匿名公开页面继续使用原定时更新,不订阅模型通知。
384
+
385
+ 专项 `python scripts/test_backend.py simpleui_antd.tests.test_relations` 共 23 项;
386
+ `python scripts/test_relations.py` 使用自己专用的临时服务、真实 Session/CSRF 和合成记录,验证 19 项查询、版本、
387
+ 大屏/H5/报表映射、目标权限变化及变更签名。`scripts/test_ownership_postgres.py --relations` 使用专用临时
388
+ PostgreSQL 跑 122 项关联、通用查询、服务字段与 SQL 权限检查;依赖见脚本头部,不接受外部数据库。
389
+ 演示可用 `dev_fields.py --relations --events --production`;无新增宿主迁移,不写宿主数据。
390
+ DOM 检查使用模拟 API,不代替真实浏览器、Canvas、真实宿主字段与容量验收。
391
+
392
+ ### 持续时间字段
393
+
394
+ 已开放的 `DurationField` 保持原明细接口:`graphqlType: String`,值沿用 Django 的时长字符串,旧发布版本不改写。
395
+ 筛选输入另用 `Duration` 标量,目录通过 `filterValueType`(REST:`filter_value_type`)声明;不能根据明细的 `String` 类型猜测筛选参数。
396
+ 支持等值、集合、空值、范围、排序、原值分组及 COUNT/SUM/AVG/MIN/MAX,所有运算字段沿用当前模型、对象、租户和字段权限;
397
+ SQL 权限适配也必须证明时长字段的读取能力。
398
+
399
+ - 输入使用 ISO 8601 天/小时/分钟/秒,如 `PT1H30M`、`P2DT3H`、`PT0.000001S` 或 `-PT0.000001S`。
400
+ 最多六位小数秒,范围为有符号 64 位微秒;不接受年/月/周、数字秒、指数写法及更细精度,不静默截断或舍入实参。
401
+ - 参数声明使用 `Duration` 或 `[Duration!]`,如 `{"minimum":{"type":"Duration","default":"PT1H"}}`;
402
+ 同时在 `filters` 中绑定 `{"where":{"elapsed":{"gte":{"$param":"minimum"}}}}`。`String` 参数不能绑定到时长操作。
403
+ - 聚合中时长维度及非 COUNT 指标以**秒的十进制字符串**返回,如 `"5400"`、`"-0.000001"`;
404
+ 目录的 `aggregateUnit`(REST:`aggregate_unit`)为 `seconds`,COUNT 仍为数量。输出字段路径保持原规则。
405
+ 工作室、预览、字段映射及报表支持这类数值;界面列名标明「秒」,可用于图表、指标、排行和其他数值组件。
406
+ 原明细时长文本仍可映射到文本/表格,不能直接作为图表数值。
407
+ - 序列化用整数分解保留微秒,不经过浮点或宿主 Decimal 上下文。SUM/MIN/MAX 的实际容量仍取决于数据库;
408
+ SQLite 按整数微秒存储,SUM 超过其 64 位范围明确返回 `INVALID_VALUE`,不会显示为零或空成功结果。
409
+ PostgreSQL 的 interval 容量与 SQLite 不同。AVG 使用数据库原生计算,再按 Django 的微秒精度转换;
410
+ SQLite 在很大时长求 AVG 时会先转为浮点,可能损失微秒(例如单值 `9007199254.740993` 秒的平均数为 `9007199254.740992`)。
411
+ 需要微秒级平均值精度时使用已验证的 PostgreSQL,仍须针对实际数据量和精度验收。
412
+ - 数据库数值/interval 溢出返回 `INVALID_VALUE`;SQL 执行使用保存点,失败不会损坏同一外层事务中的后续查询。
413
+ 没有新增包内模型或宿主迁移;示例的 `elapsed` 只存在于测试业务模型。
414
+
415
+ ```graphql
416
+ query Elapsed($minimum: Duration!) {
417
+ supportTicketAggregate(
418
+ filters: {where: {elapsed: {gte: $minimum}}}
419
+ groupBy: [{field: "status"}]
420
+ metrics: [{name: "averageSeconds", operation: AVG, field: "elapsed"}]
421
+ orderBy: [{field: "averageSeconds", direction: DESC}]
422
+ ) { rows { dimensions metrics } totalCount hasMore }
423
+ }
424
+ ```
425
+
426
+ 示例 `supportTicket` 需替换为宿主真实开放模型的 `queryField`,`elapsed/status` 替换为已开放字段,变量如 `{"minimum":"PT1H"}`。
427
+ 专项:`python scripts/test_backend.py simpleui_antd.tests.test_durations`(13 项);真实 HTTP:`python scripts/test_durations.py`(18 项)。
428
+ `dev_fields.py --durations --production` 提供临时合成时长记录,不写宿主数据;`scripts/test_ownership_postgres.py --durations`
429
+ 使用经归属校验的专用临时 PostgreSQL 复验时长、通用查询和 SQL 权限(60 项通过),依赖与命令见脚本头部;不接受外部数据库。
430
+ 组件检查使用模拟 API,不替代真实浏览器或实际宿主验收。
431
+
432
+ ### 分组与聚合
433
+
434
+ 每个模型增加 `<queryField>Aggregate`,目录通过 `aggregateField` 返回名称;例如 `salesOrderAggregate`。
435
+ 它复用明细的 `filters`。下面的例子按付款状态和月份统计授权范围内的订单:
436
+
437
+ ```graphql
438
+ query {
439
+ salesOrderAggregate(
440
+ filters: { where: { amount: { gte: "0" } } }
441
+ groupBy: [
442
+ { field: "paid" }
443
+ { field: "created_at", name: "month", granularity: MONTH }
444
+ ]
445
+ metrics: [
446
+ { name: "orders", operation: COUNT }
447
+ { name: "revenue", operation: SUM, field: "amount" }
448
+ { name: "average", operation: AVG, field: "amount" }
449
+ { name: "minimum", operation: MIN, field: "amount" }
450
+ { name: "maximum", operation: MAX, field: "amount" }
451
+ ]
452
+ orderBy: [{ field: "revenue", direction: DESC }]
453
+ timezone: "Asia/Shanghai"
454
+ limit: 50
455
+ ) {
456
+ rows { dimensions metrics }
457
+ totalCount
458
+ hasMore
459
+ }
460
+ }
461
+ ```
462
+
463
+ - 需要 1–20 个指标,最多 3 个分组维度和 5 个结果排序键。维度/指标名称须为 1–64 位字母、数字或下划线,字母开头且互不重复;分组名默认使用字段的 camelCase 名。
464
+ - `COUNT` 省略字段时计记录数;指定字段时计非 null 值,支持 `distinct: true`。`SUM`/`AVG` 用于数值和时长,`MIN`/`MAX` 用于数值、文本、日期时间和时长;实际操作以目录为准。
465
+ - `groupBy.field`、指标 `field` 使用原生字段名;聚合 `orderBy.field` 使用自定义的维度或指标名称。未指定排序时按维度升序,空值在后;指定排序时仍补齐维度打破并列。
466
+ - 日期支持 YEAR/QUARTER/MONTH/WEEK/DAY,日期时间还支持 HOUR/MINUTE;默认 VALUE 按原值分组。WEEK 从周一开始。时区默认跟随 Django 当前时区,可使用 IANA 名覆盖;`USE_TZ=False` 时不能指定时区。
467
+ - `rows` 中每行包含 JSON 对象 `dimensions` 与 `metrics`。整数、计数和 Decimal 以字符串返回,浮点数和布尔值保留原类型,日期时间采用 ISO 格式。整数 AVG 也返回 Decimal 字符串;实际精度仍受数据库运算能力影响,SQLite 的小数计算不保证精确财务统计。
468
+ - `totalCount` 为**授权且筛选后的源记录数**,采用 BigInt 字符串,不是分组数量。返回行数受 `limit` 和 `max_aggregate_groups` 限制;`hasMore` 表示还有分组,不返回未限定数量的结果。
469
+ - 无分组的空结果返回一行:COUNT 为 `"0"`,其余指标为 null;分组空结果返回空数组。统计基于去重后的授权主键,Admin QuerySet 中的多对多连接不会重复累计同一记录。
470
+
471
+ 筛选、排序、维度和指标也执行字段权限检查。遇到自定义表单、对象相关字段布局或
472
+ `get_graphql_fields` 等动态规则时,先按 `max_permission_scan` 扫描授权候选记录;
473
+ 任一候选记录不允许读取所用字段,就拒绝本次查询并返回 `FORBIDDEN_FIELD`,不会先按该字段筛选再跳过检查。
474
+ 扫描超限返回 `POLICY_SCOPE_REQUIRED`;可通过 Admin 的 SQL QuerySet 缩小范围。
475
+ 使用原生权限方法、默认表单和原生字段方法时,走 SQL 范围与聚合,不受 Python 扫描条数限制。
476
+ 原 QuerySet 的注解仍保留给明细对象、权限方法与动态表单使用。
477
+
478
+ #### 大范围对象权限的 SQL 适配
479
+
480
+ 宿主可在 `ModelAdmin` 实现 `get_graphql_query_scope(request, *, queryset, fields)`,
481
+ 显式提供与现有对象权限及运算字段权限**等价**的 SQL 范围。无此方法,或返回 `None`,继续使用原来的有界检查。
482
+ 不会自动把任意 Python 权限代码转换成 SQL;此钩子只能由可信宿主代码提供,不能由页面配置上传 SQL 或代码。
483
+
484
+ `queryset` 是已应用 Admin、Manager、租户和软删除规则、尚未应用调用者筛选条件的基础范围。
485
+ `fields` 为不可变集合,包含原生筛选、排序、分组及指标字段;数据集校验还包含配置的输出字段。
486
+ 它不代表 GraphQL 响应中选中的所有字段。空集合要求证明对象可见范围,仍可用于列表、详情及关联 ID。
487
+
488
+ 例如,宿主原有规则为“仅可看所属区域;保密记录不可读取金额”,可使用如下等价适配。
489
+ `request.allowed_region_ids` 必须由宿主根据当前账号建立可信上下文,示例字段需替换为实际模型字段:
490
+
491
+ ```python
492
+ from simpleui_antd.graphql.scopes import SQLPermissionScope
493
+
494
+ class OrderAdmin(admin.ModelAdmin):
495
+ def has_view_permission(self, request, obj=None):
496
+ return super().has_view_permission(request, obj) and (
497
+ obj is None or obj.region_id in request.allowed_region_ids
498
+ )
499
+
500
+ def get_graphql_fields(self, request, obj=None):
501
+ fields = {field.name for field in self.model._meta.concrete_fields}
502
+ if obj is not None and obj.confidential:
503
+ fields.discard('amount')
504
+ return fields
505
+
506
+ def get_graphql_query_scope(self, request, *, queryset, fields):
507
+ authorized = queryset.filter(region_id__in=request.allowed_region_ids)
508
+ approved = fields
509
+ if 'amount' in fields and authorized.filter(confidential=True).exists():
510
+ approved = fields - {'amount'}
511
+ return SQLPermissionScope(queryset=authorized, fields=approved)
512
+ ```
513
+
514
+ 正常证书的 `fields` 必须与请求集合完全一致;少一个字段返回 `FORBIDDEN_FIELD`,额外字段属于无效配置。
515
+ 拒读判断必须覆盖**调用者筛选之前的完整授权对象范围**:不能删除保密对象后再声称金额可用,
516
+ 也不能随所请求的运算字段改变对象可见范围来规避拒绝。无法提供等价证明的策略应返回 `None`。
517
+
518
+ 返回 QuerySet 必须惰性、同模型且同数据库,不能切片、`values()`、union、加行锁、使用字段级 distinct 或窗口注解。
519
+ 普通注解可保留;窗口权限策略可返回 `None` 继续走原检查。
520
+ 平台将证书范围与原始范围取交集,按主键去重执行计数及聚合,证书不能扩大已有范围或字段白名单。
521
+ 返回明细的对象和字段仍执行原 `has_view_permission` 与动态字段检查;宿主仍需保证计数和聚合范围与旧规则一致。
522
+
523
+ 无效证书或钩子异常返回 `POLICY_SCOPE_INVALID`(数据集 REST 为 503),不返回底层异常内容;
524
+ 原路径扫描超限仍返回 `POLICY_SCOPE_REQUIRED`。组件收到这两类权限校验失败会清除旧结果,普通临时网络故障仍保留结果并标记过期。
525
+ 目录新增 `supportsSqlPermissionScope` / `maxPermissionScan`(REST 为 `supports_sql_permission_scope` / `max_permission_scan`),
526
+ 前者只表明安装了钩子,不保证每次请求都能适配。安装钩子的公开授权查询始终实时执行,不复用结果缓存。
527
+ 固定数据集版本、页面归属及服务身份范围保持原语义;适配没有新增迁移,也不要求安装 Strawberry 才能导入证书类型。
528
+ 回归见 [`test_sql_scopes.py`](simpleui_antd/tests/test_sql_scopes.py),隔离双数据库验证见 [容量文档](../docs/capacity.md)。
529
+
530
+ 当前目录根据字段类型开放筛选和排序,聚合已实现,`supportsPush` 仍为 false。
531
+ 数据集保存、版本与可视化绑定见下节;组件支持独立更新策略,事件通知通过可选扩展启用。
532
+
533
+ ### 数据集草稿与发布版本
534
+
535
+ 数据集接口与可视化工作室复用现有查询引擎和 Session/CSRF。仍需启用
536
+ `SIMPLEUI_ANTD_GRAPHQL` 并安装可选依赖;关闭时数据集接口返回 404,核心功能不加载 Strawberry。
537
+ 升级后执行 `python manage.py migrate`,应用 `0007_visualdataset`,新增 `VisualDataset` 与 `VisualDatasetVersion`。
538
+
539
+ 默认普通 staff 只能管理和执行自己的数据集,超级管理员可管理全部,包括创建人已删除的数据集。
540
+ 0024 的可选协作工作区可显式授予配置读取/编辑,查询仍使用访问者自己的数据权限;删除及归档仍为创建人/超级管理员操作。
541
+ 数据集归属不授予业务数据权限:保存、发布、预览和按版本执行均检查当前账号的模型、QuerySet、
542
+ 对象与字段规则。执行身份始终是当前调用者,不冒用创建人或发布人,不提供匿名业务数据读取。
543
+
544
+ 以下地址均位于配置的挂载点下,例如 `/admin/api/datasets/`:
545
+
546
+ | 接口 | 用途 |
547
+ | --- | --- |
548
+ | `GET/POST api/datasets/` | 分页列表、新建有效草稿 |
549
+ | `GET api/datasets/catalog/` | 当前账号可查询的模型、字段、类型和查询能力,REST 元数据使用 snake_case |
550
+ | `POST api/datasets/preview/` | 预览未保存的 `definition` 和 `params`,不创建或修改数据集 |
551
+ | `GET/PUT/DELETE api/datasets/<id>/` | 查看/更新草稿;只有未发布的数据集可删除 |
552
+ | `POST api/datasets/<id>/preview/` | 按草稿修订号预览,检查当前权限 |
553
+ | `POST api/datasets/<id>/publish/` | 发布指定草稿修订号,返回版本号与指纹 |
554
+ | `GET api/datasets/<id>/versions/` | 分页列出历史版本,摘要不包含完整定义 |
555
+ | `GET api/datasets/<id>/versions/<number>/` | 查看不可变的发布快照及字段依赖 |
556
+ | `POST api/datasets/<id>/restore/` | 将历史定义复制为新草稿,不改写任何发布版本 |
557
+ | `POST api/datasets/<id>/execute/` | 指定发布版本执行,不接受临时修改查询结构 |
558
+
559
+ 列表使用 `?page=1`,每页 20 条。业务数据与查询结果不保存在草稿或版本中。
560
+ 元数据按数据集归属可见;失去模型权限后仍可查看自己的查询定义,但不能借此执行查询。
561
+
562
+ 新建请求示例:
563
+
564
+ ```json
565
+ {
566
+ "name": "已付款订单",
567
+ "description": "按金额筛选的订单明细",
568
+ "definition": {
569
+ "schemaVersion": 1,
570
+ "model": "sales.order",
571
+ "mode": "records",
572
+ "fields": ["id", "number", "amount", "paid"],
573
+ "filters": {
574
+ "where": {"amount": {"gte": {"$param": "minimumAmount"}}, "paid": {"eq": true}}
575
+ },
576
+ "orderBy": [{"field": "created_at", "direction": "DESC"}],
577
+ "limit": 20,
578
+ "parameters": {"minimumAmount": {"type": "Decimal", "default": "100"}}
579
+ }
580
+ }
581
+ ```
582
+
583
+ `mode: "records"` 必须明确选择 `fields`;它们使用原生字段名,结果字段仍采用 camelCase。
584
+ `mode: "aggregate"` 使用现有 `metrics`、`groupBy`、`orderBy` 和 `timezone`,不接受 `fields`。
585
+ 例如指标 `[{"name":"orders","operation":"COUNT"},{"name":"revenue","operation":"SUM","field":"amount"}]`,
586
+ 可按 `[{"field":"paid"}]` 分组并按 `[{"field":"revenue","direction":"DESC"}]` 排序。
587
+ 省略的查询上限和聚合时区在保存时填入,发布后不会随新的默认设置悄悄变化;平台调低硬性限额时,
588
+ 旧定义若超限会明确报错,不自动改变原配置。
589
+
590
+ 新建响应返回字符串 `id`、`revision: 1`、`latest_version: 0`、完整规范化定义及字段依赖。
591
+ PUT 可更新名称、说明和完整 `definition`,必须携带当前 `revision`;定义不做隐式深度合并。
592
+ 每次保存将 revision 加一,旧修订请求返回 HTTP 409 / `CONFLICT`,失败不覆盖草稿。
593
+ 创建人由登录账号决定,不能通过请求转移归属。
594
+
595
+ 对数据集 12 发布 `POST api/datasets/12/publish/`、请求体 `{"revision":1}`:
596
+
597
+ - 同一草稿修订重复发布返回同一版本,支持请求重试;即使已经保存新草稿,也可重试获取此前已发布的版本。
598
+ - 新修订发布得到递增的版本号。发布事务、条件更新和唯一约束共同避免版本覆盖;失败不会留下半发布状态。
599
+ - 快照冻结定义、参数声明、字段类型依赖与 SHA-256 指纹;版本只读,不提供覆盖或删除接口。
600
+ - `latest_version` 只用于管理摘要;执行必须明确指定版本号。修改草稿和发布新版本都不会改变旧版本定义。
601
+ - `POST restore/` 请求体为 `{"revision":2,"version":1}`,生成 revision 3 的草稿;再次发布产生新版本,不回退或复用版本号。
602
+ - 已发布的数据集禁止删除,防止引用失效;未发布草稿可用 DELETE 请求体 `{"revision":1}` 删除。
603
+
604
+ 运行预览:`POST api/datasets/12/preview/`,请求体 `{"revision":1,"params":{"minimumAmount":"200"}}`。
605
+ 运行发布版本:`POST api/datasets/12/execute/`,请求体 `{"version":1,"params":{"minimumAmount":"200"}}`。
606
+ 运行时只可提供声明的 `params`,以及明细 `offset` 和不超过保存上限的 `limit`,不能更换模型、字段、条件或指标。
607
+ 聚合不支持 offset。预览和执行均返回现有明细/聚合结果结构;发布版本另返回版本号、模式与指纹。
608
+
609
+ 也可通过已有只读 GraphQL 端点执行发布版本:
610
+
611
+ ```graphql
612
+ query {
613
+ visualDataset(id: "12", version: 1, params: {minimumAmount: "200"}, limit: 20) {
614
+ datasetId
615
+ version
616
+ fingerprint
617
+ mode
618
+ data
619
+ }
620
+ }
621
+ ```
622
+
623
+ `data` 为 JSON,保留原查询的 ID、BigInt、Decimal 字符串和布尔/日期格式。
624
+ 新接口不增加 GraphQL Mutation,也不改变原模型明细、按 ID 和聚合接口。
625
+
626
+ 参数仅能通过 `filters` 中的 `{"$param":"参数名"}` 绑定到标量操作值或集合,不允许绑定整棵条件树或查询结构。
627
+ 声明最多 20 个,名称须以字母开头,最多 64 位字母/数字/下划线,且每项必须实际使用。
628
+ `type` 支持 String/Boolean/ID/BigInt/Decimal/Float/Date/DateTime/Time/UUID/Duration,及对应 `[String!]` 等集合类型;
629
+ 类型必须与绑定操作精确匹配,值继续经过原 GraphQL 类型、时间/数值和查询限额校验。
630
+ `required` 默认为 true;有 `default` 时可省略参数,无默认值的必填参数必须在执行时提供。
631
+ `required:false` 且无默认值时按 null 处理,仅适用于接受 null 的条件。未知参数、错误类型及必填 null 被拒绝。
632
+ 保存和发布使用类型样例验证无默认值的必填参数,不执行结果查询;预览和执行必须提供有效实参。
633
+
634
+ 字段依赖覆盖输出列、筛选、排序、分组和指标;运行时会重新检查权限及结构。
635
+ 字段撤销开放、类型/精度改变、时区模式改变或快照指纹不符时返回 `SCHEMA_CHANGED`,不会静默重解释旧版本。
636
+ 可编辑并保存新草稿后重新发布。版本冻结的是查询配置,业务结果会随数据和当前权限变化。
637
+ 配置定义最多 48 KiB,管理请求最多 64 KiB;保留原查询的条件、分组、扫描与请求操作限额。
638
+ REST 错误代码位于 `context.code`,GraphQL 位于 `errors[].extensions.code`;内部异常不返回客户端。
639
+ 常规 ORM 写入也禁止覆盖或删除快照;宿主直接 SQL、底层写入与迁移脚本仍属于可信运维范围。
640
+
641
+ ### 数据集归档与恢复(0017)
642
+
643
+ 应用 `0017_dataset_archive` 迁移后,数据集工作室提供「在用数据集 / 已归档 / 全部数据集」筛选。
644
+ 归档不删除数据,不改写草稿定义、字段依赖或不可变发布快照;恢复后重新开放编辑和新增引用。
645
+
646
+ ```bash
647
+ python manage.py migrate
648
+ ```
649
+
650
+ - 创建人或超级管理员可归档/恢复;不读取业务数据,业务源字段权限撤销后仍可整理元数据。GraphQL 功能和模型注册配置仍须有效。
651
+ - 归档后草稿名称、说明、定义、保存、预览、发布、历史恢复为草稿及删除均冻结;仍能查看版本、比较差异和使用关系。
652
+ 工作室的未保存输入在确认归档时放弃,取消归档确认则保留输入。
653
+ - 已有固定版本 REST/GraphQL 查询、后台大屏/H5、轮播、公开授权和服务凭据继续按当前权限读取;归档不会停止现有集成。
654
+ 要停止业务读取,请撤销对应公开授权/服务凭据或数据权限。
655
+ - 保存已有页面时,同一个组件 ID 引用同数据集与同发布版本可保留、修改布局/映射/类型化参数,继续复核当前字段权限。
656
+ 复制页面、增加组件或切换固定版本属于新引用,已归档时拒绝;多个组件共享预览缓存也不能绕过检查。
657
+ - 已配置公开/服务授权可保留、撤销、恢复或轮换;新增归档数据集或其他版本的授权范围拒绝。
658
+ 公开授权即使以 `revoked:true` 暂存也不能新增归档范围,服务直接模型字段授权不受数据集归档影响。
659
+ - 归档/恢复检查当前 `revision`,实际状态变更递增修订号并与 Django 操作记录同事务提交;同状态、同修订的重试不重复记账。
660
+ 来源校验期间发生归档的草稿保存或发布不能覆盖归档状态;发布事务再次检查状态。普通引用保存按本次校验读取的状态判断,已通过校验的在途保存可能稍后完成。
661
+ - 恢复不授予额外权限,也不自动升级组件或创建新快照;已有快照响应、指纹、序号及使用索引保持不变。
662
+ 已发布数据集始终禁止删除,未发布数据集需要先恢复再删除;此生命周期策略由管理 API 实施,可信 ORM/SQL 运维需自行保持修订号和状态一致。
663
+
664
+ | 接口 | 行为 |
665
+ | --- | --- |
666
+ | `GET /api/datasets/?archived=false` | 默认在用列表,每页 20 条;`true` 归档,`all` 全部,先按账号与状态筛选再分页 |
667
+ | `POST /api/datasets/<id>/archive/` | `{"revision":3,"archived":true}` 归档,`false` 恢复 |
668
+ | `GET /api/datasets/<id>/` | 含 `archived` / `archived_at`;归档后仍按创建人/超级管理员读取 |
669
+ | `GET /api/datasets/<id>/versions/` | 版本列表额外提供 `dataset_archived` / `dataset_name`;单版本接口保留原不可变快照结构 |
670
+
671
+ 管理写入沿用 staff Session、CSRF 和可选 GraphQL 开关,归档冲突返回 409,冻结操作返回 `context.code=ARCHIVED`。
672
+ 既有 `items/page/total`、快照、执行及静态页面契约保持兼容,旧数据迁移后默认在用。
673
+ 真实界面验收见 `frontend` 的 `npm run test:dataset-archive`,后端启动参数为
674
+ `scripts/dev_fields.py --datasets --data-services --production --port=8660`(隔离 SQLite、合成业务记录)。
675
+
676
+ ### 可视化数据集与组件绑定
677
+
678
+ 「我的页面 → 业务数据模板」新增零售经营大屏、服务工单大屏与移动经营简报。选择已授权模型,
679
+ 将分类/状态、金额(经营模板必填)、日期和明细编号映射到现有字段,即可预览实际业务查询结果。
680
+ 创建操作在同一事务中生成 3–4 个数据集及各自的不可变 v1,再生成绑定页面,不修改业务记录,
681
+ 不把预览结果存进页面。页面默认关闭公开分享,组件默认手动更新;后续可在编辑器配置轮询或系统通知。
682
+ 预览、创建和后续执行均复用当前账号权限,动态对象/字段规则同样生效;列表、分组和日期结果遵守现有限额。
683
+ 模板中的“返回范围”表示实际有限结果,完整范围与 `hasMore` / `hasNextPage` 可在预览中检查。
684
+
685
+ 接口:`GET/POST /api/data-templates/` 与 `POST /api/data-templates/preview/`。
686
+ 提交 `{"template":"commerce-screen","name":"经营看板","model":"sales.order","fields":{"category":"status","value":"amount","date":"created_at","label":"code"}}`;
687
+ 模型及字段必须已开放,日期与明细字段可省略;`service-screen` 不使用 `value`,`business-h5` 用于移动简报。
688
+
689
+ 在「页面搭建 → 我的页面 → 数据集工作室」中,选择模型及明细字段,或配置分组维度和统计指标。
690
+ 界面支持 AND/OR/NOT 条件组、按类型输入的筛选值、多字段排序、日期粒度、时区和类型化参数。
691
+ 筛选字段、操作、值来源、参数选择和条件组合为必选项;占位项不能清空已有条件,删除筛选请使用「删除筛选条件」或「删除条件组」。
692
+ 「预览数据」查询当前未保存的定义,不写入数据集;保存草稿后再发布固定版本。历史版本可恢复为新草稿。
693
+ 数据集列表、版本列表与绑定选择均支持分页;无权限或未开放的模型不会出现在可用数据源目录中。
694
+ 版本列表在每页额外返回 `latest_version`,保留原有 `items`、`page`、`total`;翻到旧版本页后仍提示实际最新版本,
695
+ 不会自动升级已绑定组件。修改预览定义、实参或绑定版本,以及关闭配置面板时,会取消未完成的预览请求;
696
+ 即使服务端已经完成查询,迟到响应也不会覆盖新的配置或恢复已清空的结果。
697
+
698
+ 每个发布版本提供「比较差异」:可对照当前草稿(包括未保存修改),或切换到其他发布版本。
699
+ 比较展示模型、字段、筛选、排序、聚合、参数与已保存字段依赖的变化,不执行业务查询。
700
+ 输出字段移除/类型变化、必填参数与参数类型变化会提示绑定升级检查;未保存草稿不会被描述为已确认类型兼容。
701
+ 差异超过 200 项时可显示全部。绑定面板显式切换版本后,可用「比较绑定升级差异」对照原绑定与目标版本。
702
+ 比较不修改任何草稿、发布快照或组件;应用升级仍须预览并重新校验映射、实参与当前权限。
703
+
704
+ #### 依赖与使用关系(0013 迁移)
705
+
706
+ 工作室的「依赖与使用」按发布版本展示源模型字段、页面组件与间接引用的轮播,支持分页。
707
+ `GET /api/datasets/<id>/usage/` 接受 `version`(省略为所有发布版本)、`section=pages|playlists`、`page`,每页 20 项。
708
+ 所有计数和明细先限定当前账号可管理的页面、轮播及数据集;普通 staff 不会看到其他人的引用,超级管理员可看全部。
709
+ 接口只返回引用元数据,不含运行实参、业务结果或分享令牌;授权标签仅反映配置,实际执行继续复核当前权限。
710
+ 未指定版本时字段依赖来自已保存草稿,选择具体版本时来自该发布快照;报表独立存储查询,不属于数据集版本的引用。
711
+
712
+ `0013_visual_usage` 为现有 JSON 文档回填 `VisualDatasetUsage` 与 `VisualPlaylistPage` 索引。
713
+ 页面/轮播正常 `save()` 与索引在同一事务保存,删除会清理引用;索引不是新的权限来源,运行时仍以页面与原权限链为准。
714
+ 可信批量导入、QuerySet `update()`、`bulk_create/update` 和原始 SQL 不触发模型保存钩子,导入后运行:
715
+
716
+ ```bash
717
+ python manage.py simpleui_antd_reindex_visual --check --json # 只检查,有差异返回非零退出码
718
+ python manage.py simpleui_antd_reindex_visual --json # 重建派生索引,不改源文档/版本/更新时间
719
+ # 多数据库工程可传 --database <alias>,索引与源文档必须位于同一数据库
720
+ ```
721
+
722
+ #### 运行数据库驱动示例
723
+
724
+ 在仓库根目录执行以下命令。示例使用隔离的临时 SQLite 和合成业务记录,不连接宿主项目数据库:
725
+
726
+ ```bash
727
+ bash scripts/sync-dist.sh
728
+ uv run --python 3.12 --with 'django>=5.2,<5.3' --with pillow \
729
+ --with 'strawberry-graphql-django>=0.96.3,<0.97' \
730
+ python scripts/dev_fields.py --visual-examples --production
731
+ ```
732
+
733
+ 打开 `http://127.0.0.1:8650/`,使用 `demo / demo-only` 登录。会预置 3 张页面和 12 个已发布数据集,
734
+ 可在模型管理中修改业务金额,再点击页面「刷新数据」验证汇总、趋势和明细变化。日期示例分布在多个日期,
735
+ 生成页面仅保存查询与映射;停止脚本即清理数据库与媒体。需要通知测试时另装 `channels daphne` 并加 `--events`。
736
+
737
+ 大屏或 H5 编辑器中选中组件,进入右侧「数据」页签:
738
+
739
+ 1. 选择自己的数据集(超级管理员可选全部)及明确的发布版本。
740
+ 2. 填写声明的运行参数,将输出字段映射到组件需要的角色。
741
+ 3. 预览查询结果和实际组件,确认后应用绑定并保存页面。
742
+
743
+ 文本、指标卡、进度条、排行榜、表格及全部 11 类 ECharts、7 类 AntV 图表支持绑定。
744
+ 饼图使用 `chartType: "pieSolid"`;原有 `chartType: "pie"` 继续显示环形图,已有页面与报表无需转换。
745
+ 报表的 `chart.type` 使用相同值,两者沿用类目/数值映射和当前权限;离线只读快照保留所选形状。
746
+ 明细字段使用查询输出的 camelCase 名;聚合字段为 `dimensions.<维度名>` 和 `metrics.<指标名>`。
747
+ 表格可配置列顺序、标题;关系图映射起点/终点/权重,层级图映射 1–3 层维度和数值。
748
+ 文本、指标和进度取第一行;图表、排行、表格使用返回的有限结果集,不自动拉取全部分页。
749
+
750
+ 绑定保存于组件的可选 `dataBinding` 属性,例如:
751
+
752
+ ```json
753
+ {
754
+ "datasetId": "12",
755
+ "version": 1,
756
+ "params": {"minimumAmount": "200"},
757
+ "mapping": {"category": "dimensions.paid", "value": "metrics.revenue"}
758
+ }
759
+ ```
760
+
761
+ 页面保存重新校验数据集归属、发布快照、当前业务权限、参数、字段及映射类型,不查询或保存业务结果。
762
+ 未映射的样式和静态属性继续生效;原有无绑定页面无需转换。撤销、重做、复制和 JSON 导入导出保留绑定配置,
763
+ 不包含查询结果。升级发布版本须显式选择,草稿修改或发布新版本不会自动改变现有页面绑定。
764
+
765
+ 文本和表格保留 BigInt/Decimal 字符串精度,正确区分 0、false 和 null。
766
+ 图表会将有效数值转换为 JavaScript Number,可能舍入小数;超出安全范围、非数值或不适用的空值会提示错误,
767
+ 进度/仪表盘要求 0–100。加载、空结果和错误有独立状态,失败时可重试,不用示例数据冒充业务结果。
768
+
769
+ 授权后台的编辑、预览及轮播在进入时执行绑定;编辑器提供「刷新数据」。同屏相同数据集版本和参数合并在途请求,
770
+ 刷新、卸载与轮播切屏会取消所属请求,旧响应不会覆盖新配置。不持久缓存业务结果。
771
+ 绑定的可选 `updateMode` 支持 `manual`(手动)、`poll`(定时请求)、`push`(系统通知)与 `auto`(请求和通知)。
772
+ `poll` 必须设置 `refreshSeconds`(10–3600 整数秒);`push` 在事件扩展关闭时按该间隔兜底,默认 30 秒。
773
+ 旧绑定不含 `updateMode` 时沿用原有行为。手动组件不响应模型事件,定时请求组件不订阅事件,彼此独立。
774
+ 隐藏时暂停定时任务,恢复可见立即刷新;网络错误保留实际业务结果并标记过期,权限失效或结构漂移清除结果。
775
+ 公开单页/轮播默认显示授权提示;仅明确开启页面数据授权后读取绑定字段,公开端使用安全的定时兜底,
776
+ 不连接管理员事件总线。0024 已提供可选协作及工作区/资源授权界面(当前浏览器已验证组织授权、撤销与确认交接);页面公开数据授权和独立报表见后续章节。
777
+
778
+ 本阶段没有新增数据库迁移;已有数据集工程沿用 `0007_visualdataset`。关闭可选 GraphQL 后静态编辑、分享、表单与轮播仍可使用,
779
+ 可移除已有绑定,但保存新的业务绑定需要重新启用扩展。
780
+
781
+ ### 自助报表(业务报表)
782
+
783
+ 升级后执行 `python manage.py migrate`,加载 `0009_report`。新增 `Report` 模型与 `/api/reports/` 端点,
784
+ 复用数据集聚合层、字段目录与权限栈;需要启用可选 GraphQL 扩展。
785
+
786
+ - `GET/POST /api/reports/`、`GET/PUT/DELETE /api/reports/<id>/`:报表 CRUD,按创建人或超级管理员隔离。
787
+ 报表保存聚合 `definition`(仅支持聚合模式)与 `chart` 图表映射;定义经 `prepare` 规范化并冻结字段依赖。
788
+ 列表支持 `page`(每页 20 条)与 `q`(最多 100 字的名称搜索),先限定权限范围再筛选和分页。
789
+ - `POST /api/reports/preview/`:校验并执行未保存配置,返回定义、依赖与数据,不落库。
790
+ - `POST /api/reports/<id>/execute/`:执行已保存报表,始终按当前调用者的模型/对象/字段权限读取;
791
+ 模型字段或类型漂移时明确报错,不冒用创建人权限,不保存业务结果。
792
+ - 图表映射服务端校验:类目必须为维度输出、数值必须为数值型指标,表格列 1–20 列且引用输出字段,
793
+ 图表配置上限 8 KiB。前端入口在「页面搭建 → 业务报表」。
794
+
795
+ 报表还支持「大数字」展示:选择数值型汇总指标,保留空的分组维度,可添加标题和最多 30 字的单位。
796
+ COUNT/SUM/AVG/MIN/MAX 等沿用同一聚合层;只接受单行完整汇总。示例:
797
+
798
+ ```json
799
+ {
800
+ "name": "订单总额",
801
+ "definition": {
802
+ "model": "sales.order",
803
+ "mode": "aggregate",
804
+ "groupBy": [],
805
+ "metrics": [{"name": "total", "operation": "SUM", "field": "amount"}]
806
+ },
807
+ "chart": {"type": "stat", "value": "metrics.total", "unit": "元"}
808
+ }
809
+ ```
810
+
811
+ 将模型和字段替换为宿主白名单中的值。大数字不需要 `category`;标题省略时使用报表名称,单位只添加文字,不进行数值换算。
812
+ BigInt/Decimal 字符串原样展示,COUNT 空集显示 `0`,SUM 等空值显示 `—`;结果仍受数据库原有数值精度限制。
813
+ 同一报表可加入工作台、大屏/H5、报表看板及显式只读离线快照;只保存查询/展示配置和引用,不将实时结果写入在线页面或工作台。
814
+ CSV/Excel 继续导出原始指标列和值,显示单位不进入数值。已有图表与表格报表继续兼容,无新增迁移。
815
+
816
+ 工作台编辑模式的「添加报表」与大屏/H5 报表组件使用同一分页/搜索选择器,支持类型化实参和真实数据预览。
817
+ 工作台可添加同一报表的多个独立卡片,并通过卡片「参数」按钮修改实参;预览与配置验证失败会保留输入。
818
+ 实参存于工作台卡片的 `reportParams` 或页面报表组件的 `props.reportParams`,例如 `{"minimum":"200.0001"}`,
819
+ 最多 16 KiB;Decimal/BigInt 继续使用字符串。省略实参时保持原有默认参数行为,不强制改写旧文档。
820
+ 新增或修改实参保存时验证报表归属、字段依赖、参数声明/类型及当前业务权限,但不执行或保存业务结果;
821
+ 工作台添加/应用配置按钮先执行一次当前参数查询,确认可用后才将引用加入待保存布局。
822
+ 切换引用/实参取消旧运行请求并隔离迟到结果,页面隐藏与后台标签失活时仍暂停读取。
823
+ 报表声明发生变化后可刷新配置、清除已移除的实参或重置实参;实际查询始终重新检查当前定义和权限。
824
+ 公开分享仍不执行私有报表,且分享载荷会移除 `reportParams`。此增强不增加数据库迁移。
825
+
826
+ #### 报表组件的刷新策略
827
+
828
+ 工作台卡片、大屏/H5 报表组件及看板卡片可保存可选 `reportRefresh`,例如:
829
+
830
+ ```json
831
+ {"reportId":"21","reportParams":{},"reportRefresh":{"updateMode":"poll","refreshSeconds":60}}
832
+ ```
833
+
834
+ 大屏/H5 将这些字段置于报表组件的 `props`;报表看板置于各卡片。`updateMode` 支持 `manual`(手动)、
835
+ `poll`(定时请求)、`push`(系统通知)与 `auto`(通知与可选轮询)。间隔必须是 10–3600 的整数秒;
836
+ `poll` 必填,`auto` 不填则只响应模型通知,`manual` 不启用定时或订阅。UI 提供 30 秒、1 分钟和 5 分钟快捷项。
837
+ `push` 在事件服务未启用时按指定间隔或默认 30 秒兜底;已启用的事件服务自身可使用原有签名轮询传输。
838
+ 初次显示及恢复激活均读取一次。旧引用省略 `reportRefresh` 时保留原有通知方式,不自动增加定时请求。
839
+
840
+ 显示的更新时间是上次成功读取时间。网络失败保留该结果并标记过期;权限失效、报表删除、结构变化或权限范围检查失败清除旧值与时间。
841
+ 隐藏/切走时暂停并取消,恢复后重读;参数变化隔离旧响应。页面或看板内相同报表与参数仅合并在途请求,最多 3 路报表并发,
842
+ 每卡片慢请求期间最多合并一次补读,不缓存完成结果。每次请求仍使用当前账号权限,系统通知不携带业务结果。
843
+
844
+ 服务端验证策略字段与间隔,保存新策略时同时复核当前查询权限/依赖/参数,不执行查询结果。
845
+ 只保存引用、实参与策略,不存结果或运行时间戳;UUID 分享移除报表策略且不执行私有报表,离线副本移除在线报表调度。
846
+ 不增加迁移,不影响数据集固定发布版本或现有静态页面。当前浏览器已验证真实计时、WS 和网络过期;目标设备长时运行仍按部署验收。
847
+
848
+ 开发验证:
849
+
850
+ ```bash
851
+ uv run --python 3.12 --with 'django>=5.2,<5.3' --with pillow \
852
+ --with 'strawberry-graphql-django>=0.96.3,<0.97' \
853
+ python scripts/test_backend.py simpleui_antd.tests.test_graphql simpleui_antd.tests.test_graphql_queries simpleui_antd.tests.test_datasets simpleui_antd.tests.test_page_bindings
854
+ ```
855
+
856
+ ### 跨报表看板(0022)
857
+
858
+ 执行 `python manage.py migrate` 加载 `0022_report_board`。在「页面搭建 → 报表看板」新建命名看板,
859
+ 添加已保存的业务报表、填写每卡片参数并预览;编辑模式可调整顺序、1–3 列宽度和 240–720 像素高度。
860
+ 支持查看、保存、复制、删除及跨页名称搜索。看板需要启用现有 GraphQL 数据层,未启用时接口继续返回 404。
861
+
862
+ - `GET/POST /api/report-boards/`:按创建人/超级管理员列出或新建看板,`page` 每页 20 条,`q` 搜索最多 100 字。
863
+ - `GET/PUT/DELETE /api/report-boards/<id>/`:详情、修改和删除。PUT 与 DELETE 均需当前 `revision`;
864
+ 修订过期返回 409,不覆盖新配置。创建人由服务器确定,不接受转移归属。
865
+ - 配置仅保存报表引用、实参、标题和布局:1–20 张卡片、总配置最多 48 KiB,每卡片实参最多 16 KiB。
866
+ 未支持的字段(包括 `result`、`data`)被拒绝。示例:
867
+
868
+ ```json
869
+ {
870
+ "name": "经营看板",
871
+ "description": "按状态查看经营金额",
872
+ "content": {
873
+ "schemaVersion": 1,
874
+ "cards": [{
875
+ "id": "bd768592-29b5-4ba1-b330-5bc05876f807",
876
+ "reportId": "12",
877
+ "reportParams": {"minimum": "100.0000"},
878
+ "title": "经营金额",
879
+ "width": 2,
880
+ "height": 400
881
+ }]
882
+ }
883
+ }
884
+ ```
885
+
886
+ 将示例中的 `reportId` 与参数替换为自己可管理的报表及其声明。更新时在请求顶层增加当前 `revision`,
887
+ 删除仅提交 `{"revision":1}`。服务端保存会验证报表归属、依赖结构、参数类型及当前模型/QuerySet/对象/字段权限,
888
+ 不执行或持久化业务结果。报表已删除、权限撤销或结构变化时,保存失败并保留原配置;可以移除失效卡片后再保存,或删除看板。
889
+
890
+ 读取继续使用 `/api/reports/<id>/execute/`,权限与直接执行报表一致,不因看板引用而扩大权限。
891
+ 每次读取使用报表当前保存定义;同一报表可以配多张不同参数卡片。看板最多 3 路并发读取,
892
+ 仅合并当前看板中相同报表/参数的在途请求,不缓存已完成结果。单卡片失败不影响其他卡片,失败状态不显示示例数据。
893
+ 切走工作区标签或隐藏浏览器时取消读取,返回后重读;可点击「刷新全部数据」,启用事件服务时也响应模型变更。
894
+ 单个重复卡片取消不会中断其他消费者;布局变更不触发重新查询。独立看板按授权后台管理,不提供公开 URL,
895
+ 已有工作台、大屏/H5 报表组件和数据集发布版本绑定保持原有行为。
896
+
897
+ ### 报表定时导出(0023)
898
+
899
+ 执行 `python manage.py migrate` 加载 `0023_report_exports`,并显式启用:
900
+
901
+ ```python
902
+ SIMPLEUI_ANTD_REPORT_EXPORTS = {
903
+ 'enabled': True, # 默认 False;需要已有 GraphQL 运行时
904
+ 'retention_days': 7,
905
+ 'max_schedules_per_user': 20,
906
+ 'max_artifacts_per_user': 10, # 全账号所有计划合计,不是每个计划
907
+ 'max_runs_per_user': 100, # 已结束记录;另限每账号最多 20 个待执行/执行中任务
908
+ 'max_file_bytes': 2097152,
909
+ 'max_source_rows': 10000,
910
+ 'lease_seconds': 300,
911
+ 'max_attempts': 3, # 中断任务的租约恢复次数;失败任务可手动重新排队
912
+ 'manual_per_minute': 10,
913
+ # 'permission_context': 'myapp.exports.permission_request',
914
+ }
915
+ ```
916
+
917
+ 「业务报表」卡片的「定时导出」可配置计划名称、CSV/Excel、类型化参数,以及每日、每周或固定间隔;
918
+ 可预览当前数据、保存、暂停/恢复、删除、立即加入队列、刷新执行记录并下载已生成文件。
919
+ 修订冲突保留输入,需选择左侧计划重新载入;立即排队使用 UUID 重试去重。
920
+ 当前构建已完成真实浏览器界面与 Canvas 复验,范围见仓库验收矩阵。
921
+
922
+ 后台命令默认只预览。宿主使用现有 cron、systemd timer、Celery Beat 或其他调度器定期调用显式执行命令:
923
+
924
+ ```bash
925
+ python manage.py simpleui_antd_export_reports --json # 只读检查
926
+ python manage.py simpleui_antd_export_reports --apply --limit 100 --json
927
+ python manage.py simpleui_antd_export_reports --prune-only --json
928
+ python manage.py simpleui_antd_export_reports --prune-only --apply --json
929
+ ```
930
+
931
+ 例如每分钟调用一次 `--apply`。库不启动常驻线程,也不自动安装宿主定时任务;命令必须在事务外运行。
932
+ 同一计划到期只建立一条任务,停机期间错过的周期合并为一次,下一次从恢复时计算;间隔须为 15–43200 分钟。
933
+ 每日/每周使用 IANA 时区和当地时间,周一为 0;夏令时缺失时间跳过当天,重复时间只选择第一次。
934
+ 执行记录采用随机租约和比较更新,进程中断后可恢复,迟到的旧进程不能覆盖新结果;编辑/暂停会取消在途任务。
935
+ 查询异常写安全错误码,不将异常详情、字段值、文件字节或实参写入审计日志。出现失败任务时命令返回非零。
936
+
937
+ 权限与快照约定:
938
+
939
+ - 普通 staff 仅管理本人计划和文件,超级管理员可管理全部。创建、修改、手动排队均验证报表归属、字段及实参,
940
+ 同时验证后台身份;超级管理员编辑别人的计划仍按原创建账号执行。权限已撤销时仍可暂停或删除计划。
941
+ - 后台重新读取当前账号,不转发浏览器 Cookie、Session、租户头或登录状态。
942
+ 依赖租户/区域等请求上下文的宿主应提供可信 `permission_context(user, schedule)`,返回 `HttpRequest`;
943
+ `schedule` 在保存校验时可能尚未落库,不能依赖其主键。库会将 `request.user` 替换为新读出的账号。
944
+ 默认使用空 Session 请求;多租户宿主应先验证该上下文与预期业务范围一致,不能依赖浏览器当前选择。
945
+ - 每次排队冻结当时的报表定义、依赖和实参,执行读取当前授权数据。执行中捕获参与查询的源主键,
946
+ 实际聚合进一步限制在该清单内,避免范围在捕获后扩大;生成后再次复核创建账号和数据权限才发布文件。
947
+ - 下载同时复核创建账号与当前下载者,对历史查询做字段/类型检查,并检查所有贡献源记录仍在当前授权范围内。
948
+ 金额等业务值变化不改写历史文件;任一源记录被删除、软删除、移出租户、对象/字段拒读或源数据库改变则拒绝下载,需重新生成。
949
+ 权限检查使用当前源记录状态和主键身份,宿主应保持业务主键稳定。文件已返回或请求正在执行时不能撤回。
950
+ - 私有文件、历史定义和授权主键清单存于数据库,不写公开 `MEDIA_URL`,不提供匿名令牌或公开下载地址,
951
+ 历史列表只返回状态、时间、大小、行数和安全错误码。CSV 防公式注入,Excel 使用文本单元格保留 BigInt/Decimal 精度。
952
+ 导出包含全部维度和指标;`hasMore=true`、源记录或文件超限时明确失败,不静默输出截断文件。
953
+ - worker 清除过期或超出全账号文件数上限的字节/授权清单,再按历史条数清理已结束记录;失败/取消立即清除快照。
954
+ 过期后下载立即拒绝,物理清理需要运行命令;关闭功能后仍可用 `--prune-only --apply` 清理。
955
+ 删除报表、计划或创建账号会级联删除文件。数据库备份中的历史字节沿用宿主备份保留策略。
956
+
957
+ 控制表目前要求 `default` 数据库,查询仍复用现有业务数据层和显式 SQL 权限范围适配;
958
+ 不增加 Celery、Excel 或云函数依赖,不更改已有报表直接执行、数据集版本及组件绑定行为。
959
+
960
+ 管理 API(Session/CSRF;全部相对当前挂载点):
961
+
962
+ | 端点 | 行为 |
963
+ | --- | --- |
964
+ | `GET/POST /api/report-exports/` | 分页列出或创建;可用 `report_id` 限定当前可管理报表 |
965
+ | `GET/PUT/DELETE /api/report-exports/<id>/` | 查看、修改或删除;修改/删除需当前 `revision` |
966
+ | `GET/POST /api/report-exports/<id>/runs/` | 分页历史,或用 `revision` 与标准 UUID `request_id` 立即排队 |
967
+ | `GET /api/report-exports/runs/<id>/download/` | 当前权限验证后的私有下载;未就绪 409,过期 410 |
968
+
969
+ 创建示例:`{"name":"每日经营","report_id":"12","params":{},"format":"xlsx","enabled":true,"cadence":{"kind":"daily","time":"09:00","timezone":"Asia/Shanghai"}}`。
970
+ 周计划增加 `weekday:0` 并改为 `kind:"weekly"`;间隔配置为 `{"kind":"interval","minutes":60}`。
971
+ 演示可使用 `scripts/dev_fields.py --datasets --report-exports --production`(临时 SQLite、合成记录);
972
+ 默认不自动执行队列。专项测试为 `python scripts/test_backend.py simpleui_antd.tests.test_report_exports`。
973
+ `python scripts/test_report_exports.py` 使用临时 SQLite、真实 HTTP/Session/CSRF 和当前构建产物,
974
+ 通过仅在演示脚本中的受保护控制端点调用同一管理命令,核对 CSV/Excel 排队及下载;结束清理自己的服务与数据库。
975
+ 导出管理界面在元数据复核失败或离开面板时清除预览和历史入口,迟到结果不能恢复旧数据;
976
+ 恢复面板先复核当前权限,未保存的输入和修订号保留。历史/下载检查失败隐藏旧下载入口;
977
+ 排队失败重试沿用同一请求 UUID,成功后的新操作使用新编号。模拟 API 的回归入口为前端
978
+ `npm run test:collaboration-ui`,当前浏览器已复验计划配置、排队和真实下载;生产调度需由部署工程接入。
979
+
980
+ ### 协作工作区(0024)
981
+
982
+ 先执行 `python manage.py migrate` 应用全部向前迁移(工作区 0024、组织成员 0025、所有权交接 0026),再显式启用:
983
+
984
+ ```python
985
+ SIMPLEUI_ANTD_COLLABORATION = {
986
+ 'enabled': True, # 默认 False,保留原有创建人隔离
987
+ 'max_workspaces_per_user': 20,
988
+ 'max_members': 200,
989
+ 'max_shares_per_resource': 20,
990
+ }
991
+ ```
992
+
993
+ 启用后「页面搭建 → 团队协作」支持创建/编辑工作区、加入已有账号/用户组、调整角色及移除成员。
994
+ 个人资源所有者/超级管理员从页面、数据集、报表、轮播和看板的「协作授权」逐项设置工作区 read/edit;资源也可经确认交由工作区持有。
995
+ 五类资源界面按 read/edit/manage 能力显示只读、编辑和当前所有者/工作区管理操作;只读访问仍可配置运行参数并按当前账号权限预览或查询。
996
+ 基础工作区 API 不依赖 GraphQL;
997
+ 数据集/报表的实际查询仍需已有可选运行时。配置无效时关闭协作扩权,原创建人范围保持;协作 API 报配置错误。
998
+
999
+ 工作区是配置资源的组织范围,支持独立账号和现有 Django 用户组。不会自动成为业务数据的租户边界,
1000
+ 也不会授予 Django 模型权限或更改项目已有 `ModelAdmin.get_queryset`、对象及字段规则。
1001
+ 个人资源由所有者/超级管理员逐项授权,加入工作区不会自动读到个人所有页面;工作区持有的资源按该工作区角色开放。
1002
+
1003
+ | 工作区成员角色 | 工作区内资源能力 |
1004
+ | --- | --- |
1005
+ | `viewer` | 读取已显式共享及工作区持有的配置;执行查询时仍检查自己的数据权限 |
1006
+ | `editor` | 还可编辑工作区持有及被授予 `edit` 的资源;个人资源仅授予 `read` 时仍为只读 |
1007
+ | `manager` | 同 editor,另外管理工作区名称、成员及工作区持有的资源;个人资源的分享/删除仍由其所有者控制 |
1008
+ | 工作区当前所有者 | 始终有工作区管理权;仍需资源自身的共享授权才能编辑别人个人所有的内容 |
1009
+
1010
+ 资源授权为 `read` 或 `edit`。有效编辑权限必须同时满足**同一个工作区**的 editor/manager 成员关系与 `edit` 授权,
1011
+ 不能把甲工作区的成员角色与乙工作区的授权拼在一起。重叠账号/用户组或多个工作区不会重复列出资源。
1012
+ 个人资源所有者保留原有权利,超级管理员保留全部管理权;无个人所有者且未由工作区持有的历史资源仅超级管理员可恢复,不接受普通协作扩权。
1013
+
1014
+ 管理 API 使用 Session/CSRF,列表每页 20 条,所有标识支持十进制字符串:
1015
+
1016
+ | 端点 | 行为 |
1017
+ | --- | --- |
1018
+ | `GET/POST /api/workspaces/` | 当前可见工作区或创建工作区 |
1019
+ | `GET/PUT/DELETE /api/workspaces/<id>/` | 读取;manager 修改;仅当前所有者/超级管理员删除;仍持有资源或待接交接时拒绝;写入需当前 `revision` |
1020
+ | `GET/POST /api/workspaces/<id>/members/` | manager 查看成员或加入账号/用户组 |
1021
+ | `PUT/DELETE /api/workspaces/<id>/members/<member>/` | manager 改角色/移除成员;需 `revision` |
1022
+ | `GET/POST /api/collaboration/<kind>/<resource>/` | 仅当前资源管理者列出或新增工作区授权 |
1023
+ | `PUT/DELETE /api/collaboration/<kind>/<resource>/<share>/` | 改访问级别或撤销;需 `revision` |
1024
+ | `GET /api/collaboration/<kind>/<resource>/access/` | 当前资源的 `read/edit/manage` 能力,界面同时读取各资源载荷中的 `permissions` |
1025
+
1026
+ `kind` 仅支持 `page/dataset/report/playlist/board`,不接受任意模型名。新增授权需要本人是目标工作区成员。
1027
+ 创建工作区提交 `{"name":"经营团队","description":"配置协作"}`;成员提交 `{"user":"现有登录账号","role":"editor"}`,
1028
+ `user` 按当前用户模型的 `USERNAME_FIELD` 精确匹配有效 staff;也可使用 `{"group":"已有用户组名称","role":"viewer"}`。
1029
+ 普通账号只能选择自己所属的组,超级管理员可选择全部组;不创建账号或改写 Django 用户组关系。
1030
+ 新增资源授权提交 `{"workspace_id":"12","access":"edit"}`。更新只提交相应可编辑字段与当前修订号,冲突返回 409。
1031
+
1032
+ 配置保存的冲突保护:
1033
+
1034
+ - 仅启用协作时,各资源列表/详情/保存响应带 `permissions: {read,edit,manage}`;关闭后保留原载荷。
1035
+ 界面能力只控制操作入口,服务端每次请求重新复核。数据集、看板继续使用已有整数 `revision`。
1036
+ - 页面、报表和轮播在详情响应中带 `configuration_token`。PUT 时提交同一次读取的
1037
+ `base_configuration_token`;保存成功响应返回新令牌。令牌绑定资源类型、ID 和完整配置,不依赖时钟精度。
1038
+ 检查与保存处于同一行锁事务;配置已变化返回 409,非法/缺少必需令牌返回 400,不覆盖现有配置。
1039
+ - 当资源由工作区持有或存在有效的 `edit` 工作区授权记录时,当前管理者/超级管理员也必须提交令牌;个人资源仅只读共享不强制要求。
1040
+ 协作关闭或个人资源未配置编辑共享的旧调用可继续省略令牌;已主动提交的令牌仍会校验。
1041
+ 新界面启用协作后始终携带读取到的令牌,覆盖未设置编辑共享时的多窗口保存。
1042
+ - 工作区/成员/授权保留未应用输入,冲突后显示当前已保存角色与修订号,需再次操作才会应用。
1043
+ 页面/报表/轮播冲突保留草稿,「重新载入」在有修改时先确认放弃。成员与授权分页保留当前输入。
1044
+ 只读数据集使用 `/datasets/<id>/preview/` 和当前草稿修订号,避免改走任意未保存定义;可编辑数据集预览前刷新资源能力。
1045
+
1046
+ 行为边界:
1047
+
1048
+ - 读取配置会包含布局、查询定义、筛选和已配置参数。数据集的共享覆盖草稿、已有与未来版本;旧组件仍固定原发布号。
1049
+ 不隐式共享页面绑定的数据集、引用报表或轮播中的页面;需要分别授权。查询和保存复用访问者自己的字段/对象/租户权限,
1050
+ 不使用资源创建人的身份。归档数据集拒绝新增工作区授权或提高既有授权级别,既有组织范围仍按当前成员关系生效。
1051
+ - 个人资源的删除、归档、公开分享、答卷及访问统计由当前所有者/超级管理员管理;工作区持有的资源由该工作区管理员管理。
1052
+ 协作者修改页面时会撤销该页面已有公开数据授权,需当前管理者重新确认;公开静态页面仍遵守原分享设置。
1053
+ 非本次确认账号修改页面也会撤销有效数据授权。重新确认/调整有效授权会使用确认者的权限;执行账号变化时轮换数据令牌,
1054
+ 旧令牌不能继续读取之前管理员的数据。纯撤销不转移执行身份,审计保存执行账号标识,不保存令牌。
1055
+ - 新增轮播选页仍要求当前操作者管理该页面。协作编辑可保留原选页、修改时长,但不能刷新他人页面的受保护链接代次;
1056
+ 页面令牌轮换后旧公开轮播仍失效,协作者不能通过重新保存恢复它。
1057
+ - 账号/组成员、角色或资源授权变化在后续请求重新计算,不缓存协作授权结果;启用协作时公开数据授权也不复用结果缓存。
1058
+ 已返回或正在处理的数据不能撤回;当前页面是否立即刷新仍由已有组件更新策略决定。
1059
+ - 删除未持有资源且无待接交接的工作区只撤销其授权,不删除页面或业务数据;资源删除级联清理授权,避免主键复用留下旧访问权。
1060
+ 0026 后删除工作区所有者账号会清空所有者引用,保留工作区与其他成员;授权操作者删除仅清空操作归属,已批准授权保留。
1061
+ 工作区持有的资源不依赖原个人账号。未交接个人资源仍遵循原模型删除规则:页面/数据集/报表/看板可成为待管理员恢复的记录,私人轮播仍按原级联规则处理。
1062
+ 可信批量/原始 SQL 运维需保持成员恰为账号、用户组或有效组织来源之一、授权恰为一种资源;运行时也拒绝多目标或无目标的异常授权。
1063
+ - 0025 已提供下面的宿主组织只读适配、实时层级与组织成员授权,0026 已支持工作区持有、确认交接与管理员恢复;仓库测试宿主的组织选择、成员撤销和确认交接已通过当前浏览器检查;其他业务工程映射与生产容量按部署验证。
1064
+
1065
+ 源码见 `collaboration.py` / `api/collaboration.py`,专项测试:
1066
+ `python scripts/test_backend.py simpleui_antd.tests.test_collaboration`。
1067
+ 前端另有 `cd frontend && npm run test:collaboration-ui`:28 项实际 React/Ant Design DOM 组件检查(API 用夹具,Canvas 不在检查范围),
1068
+ 不能替代真实浏览器、真实 Session/CSRF 或宿主并发验收。
1069
+ 临时演示可加 `scripts/dev_fields.py --collaboration --production`;不写宿主数据库,不自动建立组织或授权。
1070
+
1071
+
1072
+ ### 宿主组织目录与层级(0025)
1073
+
1074
+ 组织目录作为只读数据源接入,工作区成员仍由工作区管理员显式加入。管理员可以选择部门的直接成员,
1075
+ 或勾选「包含下级部门成员」。默认关闭;基础协作以及原来的账号/用户组授权均保留。
1076
+ 应用 0025 后配置宿主已有部门模型与成员关系,例如:
1077
+
1078
+ ```python
1079
+ SIMPLEUI_ANTD_ORGANIZATIONS = {
1080
+ 'enabled': True, # 同时需要 SIMPLEUI_ANTD_COLLABORATION['enabled'] = True
1081
+ 'organization_model': 'company.Department',
1082
+ 'membership_model': 'company.DepartmentMember',
1083
+ 'key_field': 'public_id', # UUIDField(unique=True, editable=False),非空且不可重用
1084
+ 'name_field': 'name',
1085
+ 'parent_field': 'parent', # 自关联 ForeignKey;平面目录明确设置 None
1086
+ 'user_field': 'account', # ForeignKey(settings.AUTH_USER_MODEL)
1087
+ 'organization_field': 'department', # ForeignKey(Department)
1088
+ 'organization_active_field': 'is_active', # BooleanField;无停用字段明确设置 None
1089
+ 'membership_active_field': 'is_active', # BooleanField;无停用字段明确设置 None
1090
+ 'max_nodes': 2000, # 1–10000,活动目录节点预算
1091
+ 'max_depth': 32, # 1–64,含当前节点
1092
+ }
1093
+ ```
1094
+
1095
+ 示例宿主关系(已有模型符合契约时仅需配置映射):
1096
+
1097
+ ```python
1098
+ import uuid
1099
+ from django.conf import settings
1100
+ from django.db import models
1101
+
1102
+ class Department(models.Model):
1103
+ public_id = models.UUIDField(default=uuid.uuid4, unique=True, editable=False)
1104
+ name = models.CharField(max_length=100)
1105
+ parent = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
1106
+ is_active = models.BooleanField(default=True)
1107
+
1108
+ class DepartmentMember(models.Model):
1109
+ department = models.ForeignKey(Department, on_delete=models.CASCADE)
1110
+ account = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
1111
+ is_active = models.BooleanField(default=True)
1112
+ class Meta:
1113
+ indexes = [models.Index(fields=['account', 'department', 'is_active'])]
1114
+ ```
1115
+
1116
+ 两个宿主模型须为 default 数据库中的实体模型;组织模型使用单列整数、UUID 或最多 512 字的字符主键,
1117
+ 名称为文本、关系为相应 ForeignKey、停用标志为 BooleanField;`_su_org_member/_su_org_name` 为目录保留别名。
1118
+ 组织名称在 SQL 中截取前 100 字后传输,避免读取完整长文本;组织身份仍为不可重用的 UUID。
1119
+ 包不会创建/修改宿主部门、人员归属、账号、Django 用户组或模型权限。缺少稳定 UUID 的工程需由宿主补充身份字段,
1120
+ 不能把可复用的数字主键当作组织身份。字段设置变更由宿主自行管理迁移;本包仅新增自己的组织成员授权字段。
1121
+ 部门 UUID 不可编辑、不可复用;删除并复用数字主键会得到新的 UUID,旧授权继续失效。
1122
+
1123
+ 启用后「团队协作 → 成员类型 → 组织部门」可搜索组织路径和分页选择。
1124
+ 普通账号可选本人直接所属部门及其活动下级,不能选择祖先或其他分支;超级管理员可选全部活动目录。
1125
+ 列表路径包含祖先名称以提供上下文,但不会提供宿主员工花名册、业务字段或任意模型查询。
1126
+ 名称与路径按本节的组织归属规则可见,其元数据范围独立于宿主组织管理页面的权限;其他业务字段仍使用原接口。
1127
+ `GET /api/collaboration/organizations/?page=1&q=名称` 返回 `key/source/name/parent/path`,每页 20 条;
1128
+ 仍使用 Session,非 staff 和匿名拒绝,修改成员需要正常 CSRF。
1129
+
1130
+ 加入组织提交:
1131
+
1132
+ ```json
1133
+ {"organization":{"source":"目录返回的来源指纹","key":"目录返回的UUID"},"role":"editor","include_descendants":false}
1134
+ ```
1135
+
1136
+ 来源指纹绑定宿主模型、组织身份、关系与停用规则。授权后更换这些映射会使旧来源记录失效,需管理员显式撤销/重建,
1137
+ 不会将旧部门授权自动解释为另一套组织。部门名称变化实时显示,不改变授权身份。
1138
+ 成员更新继续提交当前 `revision`,可提交 `role` 和/或 `include_descendants`;省略角色时保留原角色。
1139
+ 从直接成员扩大至下级范围时重新检查操作者的可选部门范围,缩小或移除不受该扩展检查限制。
1140
+ 目录失效、停用或已删除的组织关系在界面标为不可用,保留明确撤销入口。
1141
+
1142
+ 每次构造组织权限范围,用一个有界 SQL SELECT 同时读取活动树和当前账号的成员标记,在同一语句快照上计算祖先,
1143
+ 避免从多次读取的层级版本拼接路径。组织成员、停用、删除及移动在后续请求重新读取,不依赖同步任务或结果缓存。
1144
+ 未勾选下级时仅匹配直接部门成员;勾选后匹配当前活动祖先链。停用/默认 manager 不可见的父节点切断继承,
1145
+ 子节点的直接授权仍按自己的活动状态生效。整个活动目录循环、超预算、无效映射或查询失败时组织权限不生效,
1146
+ 原创建人及有效的账号/用户组授权继续工作;SQL 失败用保存点隔离,目录 API 明确返回 503。
1147
+ 已开始或已返回的请求数据无法撤回;业务查询仍叠加当前访问者的模型/模块/租户/对象/字段规则。
1148
+ 一个部门的角色不能与另一工作区的资源授权拼接,组织身份也不授予公开分享、删除资源或答卷管理权。
1149
+
1150
+ 目录一次最多读取 `max_nodes + 1` 行,不会因分页放宽预算;超额时先调整宿主目录/索引并核验实际开销。
1151
+ 建议给宿主成员表的账号、部门与活动条件提供适合实际数据库的索引。此实现支持 UUID 身份与配置字段映射,
1152
+ 并不代替宿主组织 CRUD、任意外部目录协议、多数据库同步或无限规模树引擎。
1153
+ 0026 后工作区所有者与授权操作者的删除清空相应归属,工作区、既有协作授权与工作区持有的资源继续保留;所有权交接见下一节。
1154
+
1155
+ 检查与验收:
1156
+
1157
+ ```bash
1158
+ python manage.py simpleui_antd_check --json
1159
+ python scripts/test_backend.py simpleui_antd.tests.test_organizations
1160
+ python scripts/test_organizations.py
1161
+ ```
1162
+
1163
+ 专项覆盖实时组织撤销、层级/停用、数字主键复用与旧 UUID 授权隔离、来源变更、预算/循环、原生授权兜底,以及数据集当前模型/字段/QuerySet 权限。
1164
+ 真实 HTTP 脚本创建并清理自己的临时数据库/服务,执行 17 项 Session/CSRF/实时撤销检查;不会连接宿主工程。
1165
+ 组件检查使用模拟 API,不替代真实浏览器和宿主容量验收。临时演示可用 `scripts/dev_fields.py --organizations --production`:
1166
+ creator 属于经营中心,viewer 属于其销售组,reviewer 属于独立团队;部门与成员是临时合成数据。
1167
+
1168
+
1169
+ ### 所有权交接与工作区持有(0026)
1170
+
1171
+ 启用协作并应用全部迁移后,个人页面、数据集、报表、轮播与看板可交给另一个有效 staff 账号,
1172
+ 也可交由本人可见的工作区持有。工作区本身只能交给当前工作区管理员账号。
1173
+ 发起时仅创建交接单;接收账号,或接收工作区的当前管理员,查看已保存配置并确认后才改变所有权。
1174
+ 超级管理员可安排无所有者资源/工作区的恢复,目标账号仍需接收;恢复工作区前先为目标账号配置管理员角色。
1175
+
1176
+ | 资源归属 | 读取/编辑/管理 |
1177
+ | --- | --- |
1178
+ | 个人所有 | 当前个人所有者管理;其他成员仍需原有逐项 read/edit 授权 |
1179
+ | 工作区持有 | viewer 读取;editor 编辑/发布;manager 管理资源、协作授权、公开分享、答卷及访问记录 |
1180
+ | 历史无归属 | 仅超级管理员可安排恢复;普通成员不能自行接管 |
1181
+
1182
+ 管理工作区持有的资源与接出其所有权是不同能力:接出仅该工作区当前所有者/超级管理员可发起,
1183
+ 普通 manager 可以确认资源进入工作区,不能自行将团队资产转为私人资产。个人资源原有权限保持兼容。
1184
+ 底层旧字段 `created_by` 在这五类资源及工作区中继续承载个人管理归属;交给账号时改为接收人,交给工作区时为空,
1185
+ 由 `owner_workspace` 指向工作区。`created_at`、发布版本的发布人/时间/定义不重写,交接操作另存无内容审计。
1186
+ 关闭协作不会把工作区资源还给原个人;普通账号对工作区资源暂停访问,超级管理员可管理。
1187
+
1188
+ 使用入口:资源「协作授权 → 交接所有权」;工作区详情「交接工作区所有权」;「团队协作 → 所有权交接单」查看/接收/拒绝/撤销。
1189
+ 有未保存配置或未应用授权时,先保存/应用或放弃再发起。交接预览只读取保存配置、已配置参数与授权范围,
1190
+ 不会执行数据集/报表或展示实际业务结果。元数据仅在交接仍有效且接收资格/发起权限仍满足时提供;
1191
+ 其他账号不能凭交接单 UUID 读取配置。参数与原协作范围在预览中明确展示。
1192
+
1193
+ API 使用 Session/CSRF:
1194
+
1195
+ | 端点 | 行为 |
1196
+ | --- | --- |
1197
+ | `GET /api/ownership/context/<kind>/<id>/` | 当前所有者读取保存配置与本次 `guard` |
1198
+ | `GET/POST /api/ownership/offers/` | 分页列出本人相关交接,或发起交接 |
1199
+ | `GET /api/ownership/offers/<uuid>/` | 查看交接及仍有效的保存配置 |
1200
+ | `POST /api/ownership/offers/<uuid>/` | `action=accept/reject/cancel` 与当前 `revision` |
1201
+
1202
+ `kind` 为 `workspace/page/dataset/report/playlist/board`。发起示例:
1203
+
1204
+ ```json
1205
+ {"kind":"page","resource_id":"12","guard":"context返回的令牌","destination":{"kind":"user","login":"接收账号"}}
1206
+ ```
1207
+
1208
+ 工作区接收目标为 `{"kind":"workspace","id":"8"}`。标识保留为字符串,接收账号按当前用户模型的登录字段精确匹配。
1209
+ `guard` 绑定资源身份、所有权修订、已保存配置与协作授权;工作区绑定其成员配置。
1210
+ 配置/授权改变、接收资格或发起人权限撤销后不能接收,返回 409 或范围内 403/404,原资源保持当前状态。
1211
+ 不把交接视为借用业务权限:接收后所有实际查询仍使用调用者自己的模型/模块/租户/对象/字段规则。
1212
+ 公开数据授权的确认者还须保有当前页面管理权,失去管理权后停止发放及执行授权,即使仍有数据集读取权。
1213
+
1214
+ 接收页面/轮播本身时,关闭公开分享、轮换链接代次并撤销该页面的数据授权;新所有者需重新配置。
1215
+ 工作区所有者变更不改变工作区对资源的持有,仍按当前角色判断管理和原公开授权确认者资格。
1216
+ 个人导出计划与私有文件、外部服务凭据及其执行身份不自动转移;页面引用的数据集、报表及轮播选页需分别安排权限/交接。
1217
+ 数据集与看板递增原配置修订号,所有资源递增独立所有权修订号;页面/报表/轮播配置令牌包含所有权身份,旧窗口不能覆盖接管后的资源。
1218
+ 界面切回标签或本机接收成功后刷新权限,保留旧草稿的修订号/令牌;所有权变化要求重新载入,未保存输入保留。
1219
+ 数据集预览刷新权限也不会将旧草稿升级为其他窗口的新修订号。
1220
+
1221
+ 工作区被删除会清除其成员及普通共享授权;仍持有资源或待接交接时通过 PROTECT 拒绝删除。
1222
+ 已有账号删除不清空工作区或已批准协作授权;保留的管理员可继续管理工作区资产,缺少管理员时由超级管理员安排恢复。
1223
+ 页面授权审计记录当前所有者/工作区快照,交接和删除追加记录;按该页面最后一次审计归属校验历史访问,
1224
+ 接管后可读取原历史,页面删除后仍由相应所有者/当前工作区管理员查看。工作区删除后无法再按其成员关系读取历史,超级管理员仍可查。
1225
+
1226
+ 每个资源只保留一张当前交接单,终态或失效交接可被新交接替换,资源删除级联清理;完整操作另写无内容 LogEntry,
1227
+ 不在交接表复制业务结果或凭据。默认有效 24 小时、每账号最多 20 个在途交接,可在协作配置中设置
1228
+ `handover_hours`(1–168)和 `max_pending_handovers`(1–100)。过期单不能接收,不因轮询续期。
1229
+ 控制表与账号须统一 default 数据库,路由不满足时拒绝处理;业务数据仍可遵循原访问层的数据库规则。
1230
+
1231
+ 交接在同一事务中校验并锁定;PostgreSQL 先按顺序锁账号(NO KEY UPDATE,允许外键引用写入),再锁相关工作区、资源和交接单。
1232
+ 修订冲突、审计异常或保存失败不留半次接管。专用临时 PostgreSQL 的 52 项检查已通过,含同时接收只提交一次所有权修订及审计;这不代替实际宿主并发/容量验收。
1233
+
1234
+ ```bash
1235
+ python scripts/test_backend.py simpleui_antd.tests.test_ownership
1236
+ python scripts/test_ownership.py
1237
+ # 可选:仅启动并清理自己专用的临时 PostgreSQL;具体依赖见脚本头部
1238
+ python scripts/test_ownership_postgres.py
1239
+ ```
1240
+
1241
+ 真实 HTTP 脚本验证 17 项跨账号接收、原权限撤销、工作区持有、真实账号删除保留与管理员恢复,使用随机身份校验自己的临时服务。
1242
+ 组件回归使用模拟 API,不替代真实浏览器及实际宿主接入/负载验收。预先明确资源交接与备份流程,再执行真实账号离职处理。
1243
+
1244
+ ## 品牌与工作空间
1245
+
1246
+ ```python
1247
+ SIMPLEUI_SITE_TITLE = "科研协作平台" # 默认使用 admin.site.site_header
1248
+ SIMPLEUI_SITE_SUBTITLE = "课题 · 资料 · 研究进展"
1249
+ SIMPLEUI_LOGO = "/static/brand/logo.svg" # 图片路径或文字,兼容原版配置
1250
+ SIMPLEUI_HOME_TITLE = "研究工作台"
1251
+ SIMPLEUI_DEFAULT_THEME = "teal" # 保留既有主题 key
1252
+ ```
1253
+
1254
+ 工作台入口由当前账号可访问的模型生成,没有业务模型名称依赖。
1255
+ 个人外观偏好优先于项目默认主题,可通过“外观与体验”恢复默认。
1256
+ `Ctrl/Cmd+K` 按中文名称、分组或 `app.model` 搜索;`/admin/#page=m:app.model`
1257
+ 可直接打开指定模块。手机端自动切换抽屉导航。
1258
+
1259
+ 表格使用服务端分页、排序与筛选,支持每页条数和跨页选择;
1260
+ 表单提供只读详情、字段错误回显和未保存修改确认。
1261
+ 工作台卡片的顺序、宽度及隐藏状态按账号保存。
1262
+
1263
+ 工作台统计复用当前请求的 `ModelAdmin.get_queryset(request)`,排除回收站记录;
1264
+ 菜单、自定义入口和图表遵守账号的模型与模块权限。最近操作、日志页和七日趋势共用
1265
+ 日志可见范围,支持自定义对象查看权限、UUID 主键及未开启 `USE_TZ` 的项目。
1266
+ 日志页提供服务端分页、操作者/动作组合筛选、搜索、失败重试与业务模块跳转。
1267
+
1268
+ 默认日志只展示当前业务范围中仍可访问的对象,包括可访问的回收站记录。对象彻底删除后,
1269
+ 原始 `LogEntry` 仍然保留,但工作台不再展示其名称。若项目需要查看已归档或硬删除对象的历史,
1270
+ 可以实现 `ModelAdmin.get_log_queryset(request)`,返回已完成租户、组织和查看权限过滤的
1271
+ `LogEntry QuerySet`;平台会继续限定为该 Admin 对应的内容类型。此钩子接管该模型的日志范围,
1272
+ 项目应利用审计侧保留的业务归属信息进行过滤。
1273
+
1274
+ 个人布局读取失败会明确提示并提供重试;读取成功后才能修改和保存,避免覆盖尚未读取的偏好。
1275
+ 保存校验卡片标识、类型、宽度和隐藏状态,异常请求不会修改已有布局。
1276
+
1277
+ ## 原生列表与批量操作
1278
+
1279
+ 列表复用 `ModelAdmin` 的 `get_list_display`、`get_search_results`、`list_filter` 和排序配置,
1280
+ 支持带 `@admin.display` 的方法列、`SimpleListFilter`、关联筛选与日期筛选。
1281
+ 行内编辑使用 `get_changelist_formset()` 的表单,执行字段与跨字段校验,调用
1282
+ `save_form`、`save_model`、`save_related`;失败保留输入,Decimal 金额保持字符串精度。
1283
+
1284
+ 行按钮和批量操作调用同一个 Admin Action。HTML/TemplateResponse 中间表单在弹窗中继续提交,
1285
+ 保留选中记录、CSRF 与失败后的输入;附件响应下载原文件,JSON 响应显示结果,重定向提供结果链接。
1286
+ 返回的 HTML 来自项目自身的 Admin 模板,按可信页面运行;模板仍需按 Django 规则转义业务输入。
1287
+
1288
+ 每项操作检查当前 `get_queryset(request)` 范围和所选对象权限。存在不可用或无权限的记录时,
1289
+ 整批拒绝;未声明权限的 Action 默认需要修改权限。只读下载等操作可显式声明:
1290
+
1291
+ ```python
1292
+ @admin.action(description="导出报表", permissions=["view"])
1293
+ def export_report(self, request, queryset):
1294
+ ... # 返回带 Content-Disposition 的 HttpResponse
1295
+ ```
1296
+
1297
+ 自定义权限可使用 `permissions=["publish"]` 和 `has_publish_permission(request)`。
1298
+ Action 在数据库事务中运行,异常或错误响应回滚数据库变更;发送消息、调用外部服务和文件写入
1299
+ 等外部副作用由业务 Action 管理,可用 `transaction.on_commit()` 在成功提交后执行。
1300
+
1301
+ ## Pro 组件字段示例
1302
+
1303
+ ```python
1304
+ from django.db import models
1305
+ from simpleui_antd.components import fields
1306
+ from simpleui_antd.editor import fields as editor_fields
1307
+
1308
+ class Product(models.Model):
1309
+ STATUS = (("on", "在售"), ("off", "下架"))
1310
+ LABELS = (("hot", "热卖"), ("new", "新品"), ("free", "包邮"))
1311
+
1312
+ labels = fields.CheckboxField(choices=LABELS, max_length=128) # 复选框组
1313
+ status = fields.RadioField(choices=STATUS, default="on") # 单选框
1314
+ rating = fields.RateField("评分") # 星级评分
1315
+ weight = fields.SliderField("推荐权重") # 滑块
1316
+ released_on = fields.DateField("上架日期") # 日期选择器
1317
+ tags = fields.TransferField(Tag, titles=["待选", "已选"]) # 穿梭框(多对多)
1318
+ location = fields.MapField("仓库经纬度") # 地图取点(Leaflet + OSM,免 Key)
1319
+ video = fields.VideoField("宣传视频") # 视频播放器
1320
+ desc = editor_fields.RichTextField("图文详情") # 富文本
1321
+ md = editor_fields.MDTextField("说明") # Markdown
1322
+ ```
1323
+
1324
+ 在模型中定义后,管理界面自动渲染对应组件,零配置。
1325
+
1326
+ `MDTextField` 提供原文、预览、对照和全屏模式,支持表格、任务列表、代码、链接及图片。
1327
+ 默认保留前导空格、行尾双空格与末尾换行,保存、列表行内编辑和 CSV 导出均使用 Markdown 原文;
1328
+ 主从表单支持相同的编辑与上传流程。只读账号默认查看安全预览,也可切换原文复制。
1329
+ 可在 `simpleui_antd_fields` 中配置 `rows`、`placeholder` 和 `max_upload_size_mb`;
1330
+ 普通 `TextField` 也可设置 `type: 'markdown'`,其空白处理仍遵循项目的原生 ModelForm。
1331
+ 预览通过独立 Worker 解析,失败或超时可重试,不影响原文录入;HTML 预览统一清理。
1332
+ 部署的 CSP 需允许同源 Worker;使用跨域静态资源时,还需允许对应脚本来源、CORS 及 `worker-src blob:`。
1333
+
1334
+ ## 字段外观与表单契约
1335
+
1336
+ 普通 Django 字段和 Pro 字段都可以按项目配置外观,无需修改模型类型:
1337
+
1338
+ ```python
1339
+ @admin.register(Project)
1340
+ class ProjectAdmin(admin.ModelAdmin):
1341
+ readonly_fields = ["audit_number"]
1342
+ fieldsets = [
1343
+ ("基本信息", {"fields": [("name", "audit_number"), "budget"]}),
1344
+ ("资料", {"fields": ["settings", "teams", "attachment"]}),
1345
+ ]
1346
+ simpleui_antd_fields = {
1347
+ "name": {"placeholder": "请输入项目名称"},
1348
+ "settings": {"span": 2, "rows": 8},
1349
+ "teams": {"type": "transfer", "titles": ["可选团队", "协作团队"]},
1350
+ "attachment": {"accept": ".pdf,.txt", "max_upload_size_mb": 20},
1351
+ }
1352
+ ```
1353
+
1354
+ `type` 可以选择内置控件或前端 `registerField()` 注册的类型;常用选项还包括 `rows`、
1355
+ `span`(1/2 列)、`min/max/step/precision`、`placeholder`、上传格式/大小与地图瓦片配置。
1356
+ `InlineModelAdmin.simpleui_antd_fields` 使用相同配置。界面禁用可用 `disabled`;
1357
+ 需要服务端写入保护时使用 `readonly_fields` 或 ModelForm 字段的 `disabled=True`。
1358
+
1359
+ 元数据、保存与关联搜索复用实际 ModelForm,支持 `get_form`、实例化表单动态字段、
1360
+ `get_readonly_fields` 与关联字段 queryset;主记录保存调用 `save_form`、`save_model` 和
1361
+ `save_related`。Decimal/BigInteger 以字符串传输,日期使用标准日期格式,时间/日期时间保留秒,JSON 保留原生值,
1362
+ 层级 choices 使用 `(值, 名称, 父值)`,Cascader 保存完整的 `/` 分隔路径。
1363
+
1364
+ 离开保护按打开时的值比较实际草稿:恢复原值、撤销编辑、增添后移除新明细或恢复排序后,
1365
+ 不会误报未保存修改;不同明细组分别跟踪。多选项按成员比较,金额比较不损失大整数精度,
1366
+ JSON/Markdown/HTML 保留原文差异;比较不改写提交内容。文件上传未完成或仍有其他改动时继续提示。
1367
+
1368
+ JSON 编辑器使用原生 JSONField 准备的文本,保留大整数和项目的编解码规则;支持格式化、
1369
+ 压缩、语法提示、原生撤销及只读选择复制。格式化只改变空白,实际保存仍由项目字段和表单
1370
+ 校验决定。JSON 字段也可以在 Form 的 `__init__` 中动态添加,参与绑定、校验及保存钩子;
1371
+ 可调用默认值使用同一次原生字段初始值,避免文本和默认值出现差异。
1372
+
1373
+ 字段元数据额外返回 `json_source`,列表中可编辑的 JSON 字段提供 `value_sources`,
1374
+ 行内保存响应提供 `value_source`。这些文本用于回填;原有 JSON 数据结构继续保留。
1375
+ 自定义 JSON 编辑器应优先使用文本来源,避免先解析为 JavaScript Number 再重新编码。
1376
+ 只写字段与已存密码不会通过这些文本来源回填;运行时新增的密码哈希字段也受到写入保护。
1377
+
1378
+ 业务凭据使用 `PasswordField` 或模型字段上的 `PasswordInput(render_value=False)` 时,管理接口
1379
+ 只返回“已设置/未设置”状态,主表、明细、列表、CSV 与 JSON 文本来源均不回填凭据原值。
1380
+ 已有凭据留空表示保留;填写新内容表示替换;可选字段可以明确清除,并在保存前撤销清除。
1381
+ 必填、只读和原生 ModelForm 校验同样生效,保留操作不会重复规范化已有内容。
1382
+ 字段仍使用项目的 CharField 存储规则与保存钩子;用户账号密码继续由原生 UserAdmin 管理。
1383
+ 自定义控件可读取 `preserve_blank` / `secret_set` / `secret_can_clear`,显式 JSON `null`
1384
+ 表示清除,空字符串表示保留;列表行的 `editor_meta` 提供记录各自的编辑状态。
1385
+
1386
+ JSON 写入请求必须是对象,保存请求明确提供对象类型的 `data`,例如 `{ "data": {} }`。
1387
+ 损坏、空白或非对象的 JSON 返回明确的 400 错误,错误媒体类型返回 415;合法的空表单
1388
+ 仍可使用原生默认值新增记录。字段内部的 `false`、`0`、`null`、数组及对象继续由原生字段处理。
1389
+ 支持 `application/json` 和 `+json`;原生 Action 中间表单及上传仍使用表单格式。
1390
+
1391
+ 主记录与明细的新增、修改、删除在同一数据库事务中完成,任何明细失败会回滚本次数据库
1392
+ 变更,界面保留输入并指出对应行。明细支持多对多、JSON、时间、图片、文件等共用控件;
1393
+ 子模型不需要独立注册 Admin,关联查询、上传与写入沿父模型和 inline 权限检查。
1394
+ 明细使用完整的原生 FormSet,支持 `get_inlines()`、`get_formsets_with_inlines()`、
1395
+ `get_formset_kwargs()` 的表单上下文、跨行 `clean()`、唯一性校验、`min_num/max_num` 及
1396
+ `validate_min/validate_max`。明细构造回调和动态只读规则使用已校验的主表新值;未改动的明细也参与整体校验,
1397
+ 全部明细通过后才执行 `save_model()` 和 `save_related()`。
1398
+ `save_related()` 收到真实的全部表单集,项目的 `save_formset()` 会正常执行;单独的明细保存接口也使用相同流程。
1399
+
1400
+ 启用 FormSet 的 `can_order` 后,界面提供上移/下移;顺序通过 `formset.ordered_forms` 交给
1401
+ 项目的保存钩子。若需要持久化到 `position` 等模型字段,在 `save_formset()` 中保存该顺序。
1402
+ 删除遵守 `can_delete`、明细权限和受保护关联,失败时保留草稿并提示具体明细或表单集。
1403
+
1404
+ `readonly_fields` 支持 ModelAdmin 方法、模型方法/属性、布尔值、主键和使用 `format_html()`
1405
+ 生成的内容,保存后重新计算;HTML 按富文本安全策略呈现。实例级 `form.fields[name].disabled`
1406
+ 和隐藏字段同样保留,适合带审批状态、计算摘要和隐藏校验上下文的业务表单。
1407
+
1408
+ 通用关联明细也可直接使用 Django 的原生类:
1409
+
1410
+ ```python
1411
+ from django.contrib.contenttypes.admin import GenericTabularInline
1412
+
1413
+ class SharedAssetInline(GenericTabularInline):
1414
+ model = SharedAsset # 模型通过 GenericForeignKey 关联所属记录
1415
+ ct_field = "content_type" # 可使用项目自己的字段名
1416
+ ct_fk_field = "object_id"
1417
+ extra = 0
1418
+
1419
+ class ProjectAdmin(admin.ModelAdmin):
1420
+ inlines = [SharedAssetInline]
1421
+ ```
1422
+
1423
+ 通用明细支持整数/UUID 主记录及 UUID 明细标识,查询同时按内容类型和所属记录隔离;
1424
+ 内部关联字段不作为可编辑字段返回。动态 queryset、原生表单上下文、只读状态、关系查询及
1425
+ 文件/富文本上传与普通明细使用相同契约。已有主记录下新增明细时,上传路径函数也能取得主记录上下文。
1426
+
1427
+ 只读详情支持逐步浏览所有分组,校验失败后可直接定位到字段或明细行。
1428
+
1429
+ 用户管理复用实际 `UserAdmin` 的新建表单、字段分组和 `change_password_form`,兼容自定义
1430
+ 登录字段及 UUID 用户主键。密码输入不回填已存值;已存密码只显示状态,详情、列表与导出
1431
+ 不返回编码凭据。具备对象修改权限时,可在独立弹窗中重置密码或按原生表单规则停用密码登录,
1432
+ 保留 Django 的密码策略、审计钩子及当前会话。资料表单和密码弹窗分别保存,重置密码后
1433
+ 提交已有资料草稿不会覆盖新密码。修改自己的名称、权限或启用状态后,工作空间同步更新。
1434
+
1435
+ 原生 `FilteredSelectMultiple` 使用穿梭框,关系标签遵守 `label_from_instance()`;
1436
+ 大批已选项分批解析。权限搜索支持代号、模型及应用名称,并始终使用实际表单的 queryset。
1437
+ 仅有新增权限的账号可以从菜单进入录入页,保存后继续录入;列表、详情和统计仍遵守查看权限。
1438
+
1439
+ 支持整数、UUID 及字符业务主键。超过 JavaScript 安全整数范围的主键使用字符串返回,
1440
+ 小整数接口保持原类型;前端按原值传递,不转为浮点数。主从记录、行内编辑、文件上传、
1441
+ 日志和回收站遵守同一标识规则。长主键在列表和明细标题中省略显示并保留完整提示。
1442
+ 手机表单的网格允许控件缩小,长标识、关系穿梭框和附件不会撑宽或裁切表单。
1443
+
1444
+ 下列 `/api/` 地址相对 Ordloom 挂载点,例如挂载到 `/admin/` 时使用 `/admin/api/`。
1445
+ 自行请求时对查询值进行 URL 编码。自定义客户端可用
1446
+ `GET /api/detail/<model_key>/?object_id=...` 读取记录,密码表单也支持
1447
+ `/api/account-password/<model_key>/?object_id=...`。使用查询参数传递原始主键,避免斜杠、
1448
+ 问号和点号被当作地址结构;旧的带主键路径仍保留。关联选项及 CSV 导出可通过
1449
+ `ids_json` 传递 JSON 数组,例如 `["crm/2026,a","partner#1"]`,旧 `ids` 逗号列表仍兼容。
1450
+ 错误的 JSON 标识列表会明确拒绝,不会退回导出全部记录;空 JSON 数组只返回 CSV 表头。
1451
+
1452
+ 文件先上传到相应 FileField 的 `storage` 与 `upload_to`,随后保存存储名;支持存储返回的
1453
+ 绝对/CDN 地址、查询签名和编码文件名。上传执行表单文件校验,图片会验证实际内容;
1454
+ 自定义存储需能通过 `storage.url("")` 或 `MEDIA_URL` 确认文件 URL 前缀。
1455
+ 默认图片限制 10 MB、普通文件 50 MB,字段配置可调整,全局服务端上限由
1456
+ `SIMPLEUI_ANTD_MAX_UPLOAD_SIZE_MB` 设置(默认 50 MB)。富文本与 Markdown 图片也验证实际内容,
1457
+ 上传携带所属字段、主记录及明细行,检查写入权限与动态只读状态;默认上限 10 MB。
1458
+ 仅有查看权限的账号不能上传。无字段上下文的旧 `_rich` 上传仅供超级管理员使用。
1459
+ 文件上传独立于数据库事务,取消表单不会删除已上传的文件。
1460
+
1461
+ ## 登录验证码开关
1462
+
1463
+ ### 登录连续失败锁定
1464
+
1465
+ 同一账号 15 分钟内连续 5 次密码错误后,登录接口返回 429 并提示「账号已暂时锁定」,
1466
+ 锁定期间正确密码同样被拒绝;成功登录会重置计数。计数保存在共享 Django cache 中,
1467
+ 多实例部署一致生效;更换或清理 cache 后端会同时清除锁定状态。
1468
+
1469
+ 拥有系统设置权限的账号:右上角用户菜单 →「系统设置」→ 打开「登录验证码」→ 保存。
1470
+ 它是整个站点的登录策略。默认超级管理员可修改,其他 Staff 账号需要显式授予
1471
+ `simpleui_antd.change_siteconfig`;普通账号不显示系统设置入口,接口也执行相同校验。
1472
+
1473
+ ```python
1474
+ from django.contrib.auth.models import Permission
1475
+
1476
+ permission = Permission.objects.get(
1477
+ content_type__app_label="simpleui_antd", codename="change_siteconfig"
1478
+ )
1479
+ role.permissions.add(permission) # role 是项目中负责站点管理的用户组
1480
+ ```
1481
+
1482
+ 开启后登录接口校验会话绑定的单次图片验证码,默认 180 秒过期,可通过
1483
+ `SIMPLEUI_ANTD_CAPTCHA_TIMEOUT` 设置秒数;刷新会替换旧验证码。
1484
+ 验证码加载失败可重试,登录失败保留账号输入,验证码也可以使用键盘刷新。
1485
+
1486
+ ## 账号与密码
1487
+
1488
+ 登录使用 Django 原生认证表单和 `AUTHENTICATION_BACKENDS`。输入名称、类型和长度来自
1489
+ `AUTH_USER_MODEL.USERNAME_FIELD`,支持邮箱、工号等登录标识和 UUID 用户主键;
1490
+ 账号名称优先使用 `get_full_name()` / `get_short_name()`,其次使用 `get_username()`。
1491
+ 停用账号以及没有后台访问权限的账号无法进入工作台。
1492
+
1493
+ 修改密码执行 `AUTH_PASSWORD_VALIDATORS`,界面展示项目的密码规则,并将错误定位到对应字段。
1494
+ 校验或请求失败保留输入,取消/完成后清理表单;修改成功保持当前会话,并按 Django 的会话机制
1495
+ 使其他会话失效。未设置本地密码的账号不显示本地密码修改入口。
1496
+
1497
+ 请求层兼容 `CSRF_COOKIE_HTTPONLY`、`CSRF_USE_SESSIONS` 和自定义 `CSRF_COOKIE_NAME`,
1498
+ 登录后的 CSRF 令牌轮换也会同步到后续请求。登录配置、验证码和账号信息响应不缓存。
1499
+
1500
+ ## 软删除与回收站
1501
+
1502
+ 零耦合设计:不强依赖第三方插件,也不要求改模型基类。
1503
+
1504
+ ```python
1505
+ class Product(models.Model):
1506
+ ...
1507
+ deleted_at = models.DateTimeField("删除时间", blank=True, null=True) # 加这一个字段即启用
1508
+ ```
1509
+
1510
+ - 模型含 **`deleted_at`**(约定)或 **`deleted`**(兼容 django-safedelete 的 `SafeDeleteModel`)字段时自动启用;
1511
+ `ModelAdmin.simpleui_soft_delete = False` 可显式关闭
1512
+ - 启用后:删除 = 打时间戳入回收站(列表 / 筛选 / 导出默认只见未删记录),
1513
+ 列表页出现「回收站」按钮——支持搜索 / 分页 / 勾选恢复 / 勾选彻底删除 / 清空
1514
+ - 彻底删除与清空前展示**级联影响计数确认**(与删除保护同一套);恢复与彻底删除分别校验
1515
+ change / delete 权限,全部动作写入操作日志(LogEntry)
1516
+
1517
+ 回收站默认复用 `ModelAdmin.get_queryset(request)`,不会绕过项目的数据范围限制。
1518
+ 若默认 manager 已隐藏删除记录,请显式提供相同业务范围的查询:
1519
+
1520
+ ```python
1521
+ class ProductAdmin(admin.ModelAdmin):
1522
+ def get_trash_queryset(self, request):
1523
+ return Product.all_objects.filter(workspace=request.user.workspace)
1524
+ ```
1525
+
1526
+ `all_objects` 是项目自定义的 manager;查询应保留租户、组织及可见性限制。
1527
+ 清空作用于当前账号可访问的整个回收站,不受搜索词和分页影响,存在受保护关联时整批拒绝。
1528
+ 硬删除调用 `delete_model` / `delete_queryset`;软删除和恢复可分别实现
1529
+ `soft_delete_model(request, obj)` / `restore_model(request, obj)` 自定义业务逻辑。
1530
+
1531
+ ## 目录结构
1532
+
1533
+ ```
1534
+ frontend/ # Vite + React 19 + AntD 6 前端
1535
+ └── src/
1536
+ ├── api.js # 数据源抽象(Django API / 演示数据)
1537
+ ├── data.jsx # 主题与演示数据
1538
+ ├── motionShim.js # iframe / 截图型 webview 动画兜底(可移除)
1539
+ └── components/
1540
+ ├── AdminView.jsx # 管理端外壳(侧栏 / 顶栏 / 多标签页栏)
1541
+ ├── HomePage.jsx # 拖拽式数据大屏
1542
+ ├── ListPage.jsx # 通用列表页(meta 驱动)
1543
+ ├── EditModal.jsx # 新增 / 编辑弹窗(含内联)
1544
+ ├── TrashModal.jsx # 回收站(软删除恢复 / 彻底删除 / 清空)
1545
+ ├── InlineEditor.jsx # 内联子记录编辑器
1546
+ ├── LoginView.jsx # 登录页(分栏大气布局)
1547
+ ├── SettingsModal.jsx # 系统设置(验证码开关)
1548
+ ├── ThemeModal.jsx # 主题切换
1549
+ └── widgets.jsx # Pro 组件渲染器(上传 / 穿梭框 / 地图 / 富文本 …)
1550
+ django/ # ordloom Python 发行包
1551
+ └── simpleui_antd/
1552
+ ├── api/ # 管理接口(菜单 / 元数据 / 列表 / 增删改 / 上传 / 内联 / Action / 大屏)
1553
+ ├── components/fields.py # Pro 组件模型字段
1554
+ ├── editor/fields.py # 富文本 / Markdown 字段
1555
+ ├── models.py # DashboardLayout / SiteConfig
1556
+ └── templates, static/ # SPA 入口与构建产物
1557
+ scripts/sync-dist.sh # 构建前端并同步到 Python 包
1558
+ ```
1559
+
1560
+ ## 版本管理
1561
+
1562
+ - 版本号:`frontend/package.json` 与 `django/pyproject.toml` 保持一致,遵循 SemVer
1563
+ - Python 发行包名为 `ordloom`,前端发行文件为 `ordloom-frontend-<tag>.zip`;内部 Django 应用仍为 `simpleui_antd`。
1564
+ - 变更记录:[CHANGELOG.md](../CHANGELOG.md)(Keep a Changelog 格式)
1565
+ - 提交信息遵循 Conventional Commits(`feat:` / `fix:` / `chore:` …)
1566
+
1567
+ 发布(需仓库管理员):
1568
+
1569
+ 1. 同步更新版本号并打 tag:`git tag v1.x.y && git push origin main v1.x.y`
1570
+ 2. GitHub → Actions → **Release** → Run workflow,输入该 tag
1571
+ 3. 工作流:构建前端 → 同步产物 → 构建 wheel/sdist → 创建 GitHub Release →
1572
+ **twine 上传 PyPI**(需配置 `PYPI_API_KEY` Secret,未配置时自动跳过)
1573
+
1574
+ ## 已知边界
1575
+
1576
+ - 模块直达使用 `#page=m:app.model`;原生 admin 的对象详情 URL 暂未映射为 SPA 编辑弹窗
1577
+ - 自定义原生 Admin HTML 控件与第三方插件的界面可通过前端 `registerField()` 接入
1578
+ - 未配置 `search_fields` 且未覆盖 `get_search_results` 时,自动搜索可见表单中的文本字段
1579
+ - 地图为经纬度取点(Leaflet + OpenStreetMap),无 POI 搜索
1580
+ - 富文本为内置轻量编辑器(非 ueditor);Markdown 支持 GFM,保存原文,不执行脚本或渲染图表语法
1581
+
1582
+ ## 页面管理权限
1583
+
1584
+ 数字大屏和 H5 页面的管理接口 `/api/pages/`、`/api/pages/<id>/` 默认按创建人隔离:
1585
+
1586
+ - 普通 staff 只能列出、读取、保存、复制和删除自己创建的页面;访问他人页面返回 404。
1587
+ - 超级管理员可管理所有页面,包括创建人已删除或未填写的历史页面。
1588
+ - 新页面的创建人始终为当前登录账号,创建和保存请求不能通过 `created_by` 转移归属。
1589
+ - 轮播选页复用页面列表;创建和保存轮播时,后端再次检查当前操作者对每张大屏的管理权限。
1590
+
1591
+ 此变更不需要数据库迁移。已有 UUID 公开分享、匿名表单提交和轮播公开播放继续可用。
1592
+ 历史轮播若引用了其他人的页面,普通 staff 再次保存时需移除无管理权限的条目,或由超级管理员维护。
1593
+ 0024 的可选协作允许显式工作区读取/编辑;删除、答卷和公开分享仍按上述创建人规则。管理界面及只读/编辑入口已通过当前浏览器检查,查询仍按访问者数据权限执行。
1594
+
1595
+ ## 数字大屏轮播
1596
+
1597
+ 升级后执行 `python manage.py migrate`,加载 `0006_screenplaylist`。
1598
+ 页面管理中的「大屏轮播」保存 1–30 张数字大屏的顺序、每页停留时间(5–3600 秒)、循环与切换效果。
1599
+ `/api/screen-playlists/`、`/api/screen-playlists/<id>/` 用于管理,仅创建人及超级管理员可访问配置。
1600
+ `/api/screen-playlists/share/<uuid>/` 与 `/s/playlist/<uuid>/` 为免登录只读播放,显示页面最近保存的内容,
1601
+ 删除大屏会跳过该项,删除轮播不会删除页面。公开链接的访问范围与普通页面分享一致。
1602
+
1603
+ ## 分享管理(页面与轮播)
1604
+
1605
+ 升级后执行 `python manage.py migrate`,加载 `0008_share_controls`(`VisualPage` 与 `ScreenPlaylist`
1606
+ 新增 `share_enabled`、`share_password`、`share_expires_at` 字段,默认保持既有公开行为)。
1607
+
1608
+ - `GET/PUT /api/pages/<id>/share/`、`GET/PUT /api/screen-playlists/<id>/share/`:读取或更新分享设置,
1609
+ 仅创建人或超级管理员可访问。PUT 支持部分字段:`enabled`(布尔)、`password`(4–64 字符,`null` 或空串清除;
1610
+ 服务端仅保存哈希)、`expires_at`(ISO 时间,需晚于当前且最长 10 年,`null` 恢复永久)。
1611
+ - 公开端 `GET /api/share/<uuid>/` 与 `GET /api/screen-playlists/share/<uuid>/` 依次校验开关、有效期与密码:
1612
+ 关闭分享返回 404「分享已关闭或不存在」,过期返回 404「分享链接已过期」;设置了密码时,
1613
+ 无密码参数返回 `{"locked": true}`(前端展示密码输入),密码错误返回 403 且不返回任何页面内容,
1614
+ 密码正确才返回内容。新界面使用请求头 `X-Ordloom-Share-Password`(`encodeURIComponent` 编码后发送)或提交请求体传输密码,
1615
+ 旧 `?password=` 调用继续兼容;不要在发送给接收方的链接中拼接密码。密码尝试限速尚未提供。
1616
+
1617
+ ### 独立分享令牌与访问记录(0019)
1618
+
1619
+ 执行迁移后,原有 UUID 链接与 `share/` 的三字段响应保持兼容。分享面板可主动「启用独立链接」,随后可「轮换分享链接」。
1620
+ 启用/轮换产生新凭据,旧链接立即停止后续内容读取、公开授权取数与表单提交;不改变 UUID、访问密码、有效期、分享开关或数据授权。
1621
+ 保护启用后不能降级回无令牌的 UUID 访问,可通过关闭分享停止访问。已下载或当前浏览器已加载的内容无法远程撤回。
1622
+
1623
+ - `GET /api/pages/<id>/share-link/`,轮播对应 `/api/screen-playlists/<id>/share-link/`,返回 `protected/revision/generation/key`。
1624
+ `POST` 同一端点并提交 `{"revision":当前修订号}` 启用或轮换;冲突返回 409,令牌与无秘密操作审计同事务保存。
1625
+ 仅创建人/超级管理员能读取或轮换,未拥有的目标返回 404。原 `share/` 设置接口不返回此凭据。
1626
+ - 新链接为 `/s/<uuid>/#share_key=<key>` 或 `/s/playlist/<uuid>/#share_key=<key>`;片段不进入 HTTP URL/Referrer 的查询部分,
1627
+ 客户端通过 `X-Ordloom-Share-Key` 请求头发送。二维码和复制均使用完整链接。服务端不接受该令牌的 query 参数。
1628
+ 凭据由私有 nonce、目标类型、UUID 与站点 `SECRET_KEY` 派生;更换站点密钥后需重新复制保护链接。
1629
+ - 轮播保存时按当前操作者权限记录每张子页面的令牌代次。公开轮播只提供匹配代次的子页面凭据,仍检查各页面的开关、到期和密码。
1630
+ 子页面首次启用或再次轮换后,旧轮播选页失效,有管理权限的账号需重新保存轮播;知道旧轮播链接无法取得新子页面令牌。
1631
+ 轮播与子页面可分别设置密码,播放器会尝试输入的轮播密码;密码不同的子页面会显示错误并跳过,不绕过独立保护。
1632
+ 后台「保存并播放」通过已有管理接口获取配置及当前账号有权管理的页面,关闭公开分享不影响后台授权播放,且不计公开访问。
1633
+
1634
+ 访问采集默认关闭,按需在宿主设置中启用:
1635
+
1636
+ ```python
1637
+ SIMPLEUI_ANTD_SHARE_ANALYTICS = {
1638
+ 'enabled': True,
1639
+ 'retention_days': 30, # 1–365,查询窗口最多 90 天且不超过留存周期
1640
+ 'daily_budget': 10000, # 每个入口每天记录请求的近似预算,1–1000000
1641
+ 'per_ip_per_minute': 120, # 每入口/直接连接来源每分钟预算,1–10000
1642
+ }
1643
+ ```
1644
+
1645
+ 公开查看器每次进入生成临时 UUID,通过 `X-Ordloom-Visit-ID` 发送;仅内容入口请求采集,无此头的旧调用不记录。
1646
+ 密码提示、重试与成功合并为一次打开,首次成功标记不会被后续拒绝覆盖。轮播只统计轮播入口;切屏、预取、组件轮询和表单提交不增加浏览量。
1647
+ 这是可被客户端影响、受预算和缓存可用性约束的近似 PV/打开/拒绝/密码提示统计,不统计独立访客,也不作为不可篡改安全审计或认证限速。
1648
+ 多实例需共享 Redis 等原子 cache;缓存或数据库采集故障不阻断公开内容。来源使用 `REMOTE_ADDR`,不信任任意转发头。
1649
+
1650
+ 分享面板「访问记录与统计」对应 `GET /api/pages/<id>/visits/?days=7&page=1`(轮播同前缀),每页 20 条。
1651
+ 仅当前创建人或超级管理员查看;关闭采集后仍可读取留存中的记录。仅保存时间、结果计数、IPv4 /24 或 IPv6 /64 网段和有限浏览器/系统类别;
1652
+ 不保存完整 IP、原始 UA、来源网址、链接令牌、访问密码、问卷答案或业务结果。按目标 UUID 保存,删除页面不级联删除访问记录,但其管理接口不再可读。
1653
+
1654
+ 留存不依赖 HTTP 请求自动清理,由运维调度命令执行;默认预览、不触及业务记录或分享配置,支持 `--database`:
1655
+
1656
+ ```bash
1657
+ python manage.py simpleui_antd_prune_share_visits --json
1658
+ python manage.py simpleui_antd_prune_share_visits --apply --limit 10000 --json
1659
+ python manage.py simpleui_antd_check --json # 同时诊断分享统计设置
1660
+ ```
1661
+
1662
+ 每批最多 500 条,单次默认上限 10000、最大 100000;仅清理创建时间早于留存周期的记录,未来增加关联关系时拒绝级联清理。
1663
+ 隔离示例加 `--share-analytics` 可演示,使用临时 SQLite,不修改宿主数据库。
1664
+
1665
+ ## 公开数据授权(页面专用只读)
1666
+
1667
+ 升级后执行 `python manage.py migrate`,加载 `0010_page_grant`。页面创建人可在分享面板为该页面的
1668
+ 绑定组件开启公开数据授权,公开访客凭分享内容中的 token 读取绑定组件的数据;页面 UUID 本身
1669
+ 不授予任何业务数据权限。
1670
+
1671
+ - `GET/PUT /api/pages/<id>/grants/`:读取或更新授权(仅创建人或超级管理员)。`datasets` 为
1672
+ 最多 20 个 `{"datasetId","version"}` 的列表,且必须是页面已绑定的发布版本;`revoked` 为布尔。
1673
+ token 仅创建时生成并随 `GET /api/share/<uuid>/` 的 `content.dataGrant` 下发,管理接口不返回。
1674
+ - `POST /api/share/<uuid>/data/`:公开执行绑定查询。请求须携带分享载荷中的 `token` 与与页面绑定
1675
+ 完全一致的 `datasetId/version/params/mapping`;执行以最近确认有效授权的账号当前权限运行真实权限栈,
1676
+ 响应只保留映射引用的输出字段;客户端参数与绑定不一致直接拒绝。分享关闭、过期、密码缺失或
1677
+ 授权撤销时返回错误,不返回任何业务数据。
1678
+ - 每次读取都重新检查授权执行账号当前模型、对象、字段权限和结构,包括缓存命中路径。
1679
+ 确认者变化时重置执行账号并轮换数据令牌;修改他人确认的授权页面会先撤销数据授权,再由当前账号明确确认。
1680
+ 仅原生 QuerySet/Manager、无软删除和对象/动态字段规则的模型级读权限允许缓存结果 60 秒;其余策略实时执行。
1681
+ 缓存 key 含 token、版本、参数、映射与授权更新时间;撤销立即失效。公开端另有每分钟 120 次的基础限流。
1682
+ 执行请求使用创建者身份及完整的独立 HttpRequest,不传递匿名访问者的租户头、筛选参数或 Session。
1683
+ - **授权变更审计(0012 迁移)**:分享面板的「授权变更记录」显示创建、范围调整、撤销、恢复、
1684
+ 已授权绑定变更及删除页面。`GET /api/grant-audits/` 提供只读分页(`page`、`page_size` 1–100),
1685
+ 可按 `page_uuid`、`action` 筛选;普通 staff 只能读取自己创建页面的记录,超级管理员可读取全部。
1686
+ 授权操作及其审计记录在同一事务内保存,重复提交相同状态不产生新记录;页面删除后记录保留,
1687
+ 用户删除仅清空关联,保留操作人名称快照。记录不允许通过模型普通保存、更新、删除或批量覆盖修改。
1688
+ 只记录版本、映射字段和参数名,参数变化通过带密钥的摘要识别;不存 token、参数值、密码或业务结果。
1689
+ 审计仅覆盖启用此迁移后的管理 API 操作,不回填历史、不记录每次匿名访问,不能替代数据库级审计。
1690
+
1691
+ ## 变更订阅(可选事件扩展)
1692
+
1693
+ 在 `settings.py` 配置 `SIMPLEUI_ANTD_EVENTS = {'enabled': True}` 并安装 `channels`/`daphne` 后可用;
1694
+ 未启用时全部接口与行为保持不变。
1695
+
1696
+ - **事件总线**:`simpleui_antd/graphql/events.py`——已注册 GraphQL 模型的保存/删除在事务提交后
1697
+ (`transaction.on_commit`)写入共享 Django cache 序号(多实例一致)并按 channels 组广播;
1698
+ 批量 `update()`/`queryset.update` 与原始 SQL 不产生事件。
1699
+ - **WebSocket**:宿主可将 `ASGI_APPLICATION = 'simpleui_antd.asgi.application'`(或在自己的
1700
+ ProtocolTypeRouter 中组合 `simpleui_antd.routing.websocket_router`),路径 `<挂载点>/ws/visual-events/`。
1701
+ 连接即鉴权(staff),连接、签名与转发消息复核当前模型/字段权限;权限撤销或停用账号即停止通知。
1702
+ 路由校验 Origin,兼容 `/admin/` 等挂载点。业务重读继续执行完整权限检查。
1703
+ - **GraphQL Subscription**:启用事件扩展后 Schema 提供 `visualEvents` 订阅字段(`graphql/subscriptions.py`),
1704
+ 依赖宿主 channel layer。
1705
+ - **轮询兜底**:`GET /api/visual-events/signature/`(staff)校验当前账号权限,返回获授权模型的缓存序号与聚合摘要;
1706
+ 前端在未配置 channels 时自动退化为签名轮询(15 秒,页面隐藏暂停)。
1707
+ - **自动刷新集成**:`/api/datasets/<id>/execute/`、报表执行与公开授权数据响应均标注 `model`;
1708
+ 前端绑定组件收到所属模型事件后按与手动刷新相同的路径自动重读(含在途合并、切屏取消、迟到结果隔离)。
1709
+ - `/api/config/` 增加公开的 `events` 能力标志(`enabled`/`websocket`),不含任何数据或权限信息。
1710
+
1711
+ 同一数据库事务内对同一模型的多次保存/删除只生成一次提交后通知;回滚不会通知,发送失败不影响已提交业务操作。
1712
+ 信号按实际数据库连接注册提交回调。批量更新、批量创建、原始 SQL 与外部写库需要业务方显式接入变更通知。
1713
+ 客户端在 WebSocket 正常时通过签名心跳校验遗漏事件,断线时使用 HTTP 签名兜底;签名请求不堆积,退出时清理任务。
1714
+
1715
+ ### 订阅与签名限流
1716
+
1717
+ `SIMPLEUI_ANTD_EVENTS` 还支持以下选项,均为 1–100000 的整数:
1718
+
1719
+ ```python
1720
+ SIMPLEUI_ANTD_EVENTS = {
1721
+ 'enabled': True,
1722
+ 'signature_per_minute': 120, # 同账号 HTTP + WS 签名请求共用
1723
+ 'subscriptions_per_minute': 30, # 同账号 WS 连接 + GraphQL 订阅建立共用
1724
+ 'messages_per_minute': 120, # 同账号所有 WS 客户端消息共用
1725
+ 'max_message_bytes': 4096, # 单条 WS 文本消息的 UTF-8 字节数
1726
+ }
1727
+ ```
1728
+
1729
+ 计数复用 Django cache 的 `add/incr` 固定 60 秒窗口;多实例需共享 Redis。
1730
+ 超限的 HTTP 签名返回 `429` 与 `Retry-After: 60`,WS 关闭码 `4429`,GraphQL 返回
1731
+ `RATE_LIMITED` 和 `retryAfter: 60`;客户端会退避重试。非法 JSON、非对象或未知命令关闭码为
1732
+ `4400`,二进制消息为 `1003`,过大文本为 `1009`。限流不取消业务写入或已有合法订阅的变更通知。
1733
+ 握手前被拒的 WS 在浏览器中通常表现为 403/1006,客户端沿用指数退避;已建立连接收到 4429 则等待 60 秒。
1734
+ 这是请求及订阅建立频率限制,不是并发连接上限;ASGI/反向代理还需按容量设置连接和帧大小上限。
1735
+
1736
+ ### 多实例 Redis 与 ASGI 部署
1737
+
1738
+ 安装宿主需要的运行时(核心安装仍不要求事件依赖):
1739
+
1740
+ ```bash
1741
+ pip install 'ordloom[graphql]' channels channels-redis daphne redis
1742
+ ```
1743
+
1744
+ ```python
1745
+ import os
1746
+
1747
+ SIMPLEUI_ANTD_EVENTS = {'enabled': True}
1748
+ CACHES = {'default': {
1749
+ 'BACKEND': 'django.core.cache.backends.redis.RedisCache',
1750
+ 'LOCATION': os.environ['ORDLOOM_CACHE_URL'], # 例如 redis://127.0.0.1:6379/1
1751
+ 'KEY_PREFIX': 'ordloom-production',
1752
+ }}
1753
+ CHANNEL_LAYERS = {'default': {
1754
+ 'BACKEND': 'channels_redis.core.RedisChannelLayer',
1755
+ 'CONFIG': {'hosts': [os.environ['ORDLOOM_EVENTS_REDIS_URL']], 'prefix': 'ordloom-production-events'},
1756
+ }}
1757
+ ASGI_APPLICATION = 'myproject.asgi.application'
1758
+ ```
1759
+
1760
+ 宿主 `myproject/asgi.py` 先初始化 Django,再加载可选路由:
1761
+
1762
+ ```python
1763
+ import os
1764
+ from django.core.asgi import get_asgi_application
1765
+
1766
+ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
1767
+ django_http = get_asgi_application()
1768
+
1769
+ from channels.routing import ProtocolTypeRouter
1770
+ from simpleui_antd.routing import websocket_router
1771
+
1772
+ application = ProtocolTypeRouter({'http': django_http, 'websocket': websocket_router})
1773
+ ```
1774
+
1775
+ 启动 `daphne -b 127.0.0.1 -p 8001 myproject.asgi:application`;反向代理保留挂载点、Host、Upgrade 与 Connection。
1776
+ 例如部署在 `/admin/` 时,将 `/admin/ws/visual-events/` 代理到同一 ASGI 服务,并启用 WebSocket 升级。
1777
+ 各实例共享 cache 与 channel layer;内存后端只适合单进程开发,固定窗口计数的跨进程原子性依赖 Redis 等原生实现。
1778
+ 协议服务和 Redis 配置依据 [Channels 部署文档](https://channels.readthedocs.io/en/stable/deploying.html),
1779
+ 来源域限制依据 [Channels WebSocket 安全文档](https://channels.readthedocs.io/en/stable/topics/security.html)。
1780
+
1781
+ ## 问卷日期题、Excel 与汇总
1782
+
1783
+ 表单新增 `date`、`time`、`datetime`,合计 12 类题目。日期按真实日历校验,时间保留秒,日期时间为填写的本地时间,
1784
+ 不隐式换算时区。已有题目和答卷继续使用原 JSON 结构,无新增迁移;关闭/过期分享停止收集,密码分享提交时须携带访问密码。
1785
+
1786
+ 创建人/超级管理员的结果界面支持「提交明细 / 汇总统计」。`GET api/pages/<id>/submissions/` 沿用
1787
+ `widget_id`、`since`、`until`、`q`,新增 `format=xlsx` 与 `format=summary`,CSV 保持兼容。
1788
+ Excel 是真实 OOXML 工作簿,冻结标题行、设置列宽和换行;答案作为文本保存,保留大整数、小数和前导零,
1789
+ 不会将以 `=`、`+` 等开头的答案当作公式。生成使用标准库和临时文件,超过 8 MiB 时转存磁盘,下载关闭即释放。
1790
+ 超出 Excel 单表行数或单元格长度时明确返回错误,不静默截断。
1791
+
1792
+ 汇总按题目快照分别统计历史版本,提供填写/未填人数、选择题分布、多选人数与评分、数字的最小/最大/合计/平均。
1793
+ 合计使用 Decimal,平均为 34 位有效数字;不把答案先转为 JavaScript Number。每次汇总最多 5000 份答卷、200 个题目版本;
1794
+ 关键词筛选最多扫描 5000 份源提交,超限需先缩小时间或表单范围。所有导出和汇总复用相同的权限与筛选。
1795
+
1796
+ ## TOTP 与登录网络范围
1797
+
1798
+ 账号菜单的「安全设置」可扫码开启 TOTP;0018 后同时提供一次性恢复码,动态码或密码+恢复码可关闭,已启用时不能直接重新生成认证器密钥。
1799
+ 登录时密码与第二因素全部通过才清除失败计数;连续失败 5 次锁定 15 分钟。启用/关闭安全设置另有 5 次失败、5 分钟的限速。
1800
+ TOTP 使用 SHA-1、6 位码、30 秒步长和前后各一步时钟容差;需要应用 `0011_usersecurity` 及后续迁移;恢复码和设备管理需 `0018_recovery_devices`。
1801
+ 可配置 `SIMPLEUI_ANTD_LOGIN_IP_ALLOWLIST = ['10.0.0.0/8', '192.168.1.5']` 限制登录,空列表不限制。
1802
+ 按 `REMOTE_ADDR` 判断,宿主反向代理应正确设置真实客户端地址;不会直接信任访问者提交的 X-Forwarded-For。
1803
+
1804
+ ## PWA 与诊断命令
1805
+
1806
+ 数据层、事件和表单的双进程并发验证见仓库的 [容量验证方法与基线](../docs/capacity.md)。
1807
+ 包含 PostgreSQL/SQLite、真实 Redis、权限撤销、推送、限流及数值精度探针;需要单独的验证依赖和 Docker,
1808
+ 只创建自己的临时资源,不连接宿主已有数据库。结果不是生产容量承诺。
1809
+
1810
+ 消息中心与审计留存的配置也纳入诊断;完整说明见下文。
1811
+
1812
+ - `GET /api/manifest.webmanifest/`:动态 PWA 安装清单,`name`/`short_name` 取自站点标题,`start_url`/`scope`
1813
+ 为当前挂载点,图标指向包内 `static/simpleui_antd/icons/`。工作台入口页自动注入
1814
+ `<link rel="manifest">` 与 `theme-color`;公开分享页不注入。离线画布单独启用,见下文。
1815
+ - `python manage.py simpleui_antd_check [--json]`:配置诊断,覆盖应用注册、URL 挂载、
1816
+ 迁移状态(未应用时提示执行 `migrate`)、前端构建产物、可选 GraphQL 依赖(启用但不可用时判定失败)、
1817
+ 站点配置与可视化数据概况;`--json` 输出机器可读结果,存在失败项时退出码为 1,可直接用于部署脚本。
1818
+
1819
+ ### 离线画布与联网另存(0021)
1820
+
1821
+ 默认关闭。执行迁移及 `collectstatic` 后,在宿主配置:
1822
+
1823
+ ```python
1824
+ SIMPLEUI_ANTD_PWA = {'offline': True}
1825
+ ```
1826
+
1827
+ 使用 HTTPS 或 localhost,并从同源地址提供静态资源。在「页面搭建 → 本机画布与离线准备」点击
1828
+ 「准备或更新离线资源」,等待成功后打开「本机画布」。断网时可继续从 `<挂载点>/offline/` 打开,
1829
+ 也可在管理页面导航网络失败后进入通用画布;新建模板、导入 JSON、编辑排版、预览、保存本机修订及导出均可使用。
1830
+ 外链图片、音视频和外部链接不在离线预览中加载,包内六张图片及支持的图片 data URI 可使用。
1831
+
1832
+ 本机文档保存在此浏览器的 IndexedDB,按域名和挂载点隔离,**不随登录账号隔离**;界面明确提示其类似本机 JSON 文件。
1833
+ 不会自动下载私有页面或业务结果。每挂载点最多 20 份文档,每份 2 MiB、合计 20 MiB;多窗口写入校验修订号,
1834
+ 冲突不覆盖较新内容,当前修改可导出。浏览器可能回收本机存储,重要文档需自行导出备份。
1835
+
1836
+ 联网后登录 staff 账号,在上述面板显式选择文档修订「另存为新页面」。`POST /api/offline/import/` 使用当前
1837
+ Session/CSRF 和账号权限,创建本人新页面,默认关闭公开分享;不覆盖已有页面。相同账号、文档编号与修订重试返回原页面,
1838
+ 不会重放内容覆盖后续在线编辑;原目标已删除或不再属于本人时返回冲突,另一个账号可显式另存自己的副本。
1839
+ 本机导入和联网另存均清除数据集/报表业务引用及页面数据授权,表单停止收集;联网后需重新绑定发布版本、字段与实参。
1840
+
1841
+ 程序使用 `<挂载点>/ordloom-offline-worker.js`。同一精确 scope 已有其他 Service Worker 时拒绝准备,
1842
+ 可使用独立挂载点;更上层的宿主 PWA 可以保留。准备仅缓存清单内的包内静态资源及不含账号信息的通用画布入口,
1843
+ 逐项检查 SHA-256 和字节数,失败时保留旧缓存;不缓存管理 HTML、API、数据集查询结果、答卷、上传文件或凭据。
1844
+ 基础画布提供离线页面设计;只读业务导出需额外启用下节快照功能,仍不提供离线授权查询、表单提交队列或自动同步。程序更新遵循默认 Worker 生命周期,
1845
+ 提示更新已准备时关闭本站所有窗口并重新打开;关闭配置后 Worker 下次成功更新只清理自身缓存并注销,不删除本机文档。
1846
+
1847
+ ### 离线业务快照
1848
+
1849
+ 在已启用 GraphQL 数据层和离线画布的宿主上,可额外启用只读快照,默认关闭:
1850
+
1851
+ ```python
1852
+ SIMPLEUI_ANTD_PWA = {'offline': True, 'snapshots': True}
1853
+ ```
1854
+
1855
+ 无需新增模型或迁移。执行前端构建/`collectstatic`,使用同源静态资源与 HTTPS/localhost。
1856
+ 页面列表每张大屏/H5 的「业务快照」先显示当前保存页面及可见数据组件数量;
1857
+ 用户确认保存到设备后,准备程序资源,再读取业务数据并保存独立本机副本。
1858
+ 若已有程序更新仍在等待激活,先关闭本站窗口并重新打开,再生成快照,避免旧离线程序无法展示新格式。
1859
+
1860
+ - `GET api/offline/snapshots/<page_id>/` 仅读取页面名称、类型、可见数据组件数及当前配置令牌,不执行业务查询。
1861
+ `POST` 提交 `{"configuration_token":"<读取到的令牌>","acknowledged":true}`;仅接受已有保存页面,
1862
+ 不接受调用者上传查询或页面定义,复用 Session/CSRF、当前页面/数据集/报表的 read 权限与完整业务数据权限。
1863
+ 默认仍按创建人隔离;显式协作的只读使用者也只能导出自己当前有权读取的数据,不借用页面创建人或公开授权身份。
1864
+ - 只处理未隐藏、已绑定的组件:固定数据集发布版本按页面已保存实参执行,报表按生成时最新保存定义和实参执行。
1865
+ 同一数据集版本/实参、同一报表/实参在单次生成中合并查询,整页共用原有 20 次查询预算。
1866
+ 原有归档数据集的已保存引用可继续执行;权限/字段/类型失效、超限或任一组件失败时,整个生成失败,不保存半份快照。
1867
+ - 响应保留布局、采集时间、每个数据组件的映射与映射所需输出值;不包含查询定义、业务实参、
1868
+ 可继续执行的数据集/报表 ID、公开数据授权令牌或未映射输出列。表单停止收集,外链媒体不自动下载。
1869
+ 明细/统计沿用组件配置的第 1 页或分组上限,不遍历全部业务记录;仍有后续结果时展示截断提示。
1870
+ 字符串形式的 BigInt/Decimal 保持精度;图表沿用现有安全数值检查及数据库精度限制。
1871
+ - 页面或引用报表配置在采集过程中变化会拒绝生成;业务查询在一段时间内依次执行,
1872
+ **不承诺跨模型、跨数据库同一时刻的一致性快照**。重新采集创建新副本,不改写旧快照。
1873
+ - 每份最多 2 MiB、本机最多 20 份/合计 20 MiB,快照与可编辑画布使用独立 IndexedDB 库,均按源站及挂载点隔离。
1874
+ 本机画布中的「只读业务快照」可查看、全屏、导入/导出 `.snapshot.json`、按修订号删除。
1875
+ 数值、表格及 ECharts/AntV 数据复用现有渲染;报表转换为只读图表或表格,不再发起业务 API、轮询或订阅,表单不提交。
1876
+ - **本机副本采用导出文件的访问语义**:当前浏览器使用者可查看/导出,不随在线账号隔离;
1877
+ 断网后无法复核账号权限,也无法撤回已导出数据。生成前界面明确说明并由用户确认,按设备资料管理要求保留或删除。
1878
+ 关闭宿主功能、退出或撤销在线权限不会删除已有本机副本及用户导出的文件。
1879
+ 导入文件由使用者提供、可被编辑,不是服务器签名或审计证据,也不授予任何在线业务权限。
1880
+ - 业务值只在用户主动保存的快照库/文件中,不进入 Service Worker 的程序资源缓存;
1881
+ 管理 HTML、API、在线页面、表单答案和自动后台同步的原有边界不变。服务器不保存快照结果,不修改在线页面。
1882
+
1883
+ 开发验证(临时 SQLite/专用临时 PostgreSQL,不连接已有宿主数据库):
1884
+
1885
+ ```bash
1886
+ python scripts/test_backend.py simpleui_antd.tests.test_offline_snapshots
1887
+ python scripts/test_offline_snapshots.py
1888
+ # 可选 PostgreSQL 依赖和归属校验见脚本头部
1889
+ python scripts/test_ownership_postgres.py --snapshots
1890
+ # frontend:文档、Worker、IndexedDB 模拟事务、快照映射
1891
+ npm run test:offline-unit
1892
+ # frontend:快照确认/冲突/程序更新、只读渲染(模拟 API,无 Canvas)
1893
+ npm run test:collaboration-ui
1894
+ ```
1895
+
1896
+ 演示用 `scripts/dev_fields.py --snapshots --production`,沿用合成业务数据。
1897
+ 2026-11-11 已在本仓库 Django 测试宿主完成真实浏览器断网复验:停止自身服务器后编辑/重载/导出,恢复网络后显式另存;只读业务快照在模拟断网后重载、导入和导出,业务接口仍不可读取。记录见仓库 [验收矩阵](../docs/roadmap-acceptance.md)。移动宽度检查使用桌面浏览器模拟,真机安装与生产接入按部署验收。
1898
+
1899
+ ## 宿主工程初始化与升级(0020)
1900
+
1901
+ 先按接入说明配置应用和 URL。以下命令由已经获得宿主运维权限的操作者运行;默认输出计划,`--apply` 才写入。
1902
+ 支持 `--database <alias>`、`--json` 和文本输出,错误返回非零退出码,错误信息不包含数据库凭据或配方内容。
1903
+
1904
+ ### 迁移计划与升级执行
1905
+
1906
+ ```bash
1907
+ python manage.py simpleui_antd_upgrade --json
1908
+ # 核对 pending 列表,将下方值替换为上一步的 plan_id
1909
+ python manage.py simpleui_antd_upgrade --apply --plan-id '<plan_id>' --json
1910
+ python manage.py collectstatic --noinput
1911
+ python manage.py simpleui_antd_check --json
1912
+ python manage.py simpleui_antd_reindex_visual --check --json
1913
+ ```
1914
+
1915
+ 升级计划覆盖所选数据库中**全部已安装应用的待执行向前迁移**,包括 Django 认证/会话及宿主业务迁移,等同常规 `migrate` 的范围。
1916
+ 每项标明 `app/name/scope`(`package` 或 `host`);预览只读取迁移历史、代码和配置,不建表或补写站点配置。
1917
+ 签名绑定数据库目标、迁移源码、当前应用历史及目标节点;数据库、迁移文件或应用历史变化后,旧 `plan_id` 不能执行。
1918
+ 执行前运行 Django 工程检查,再调用标准迁移命令及 `post_migrate`;分支冲突、向后计划或不完整的应用数据库路由会拒绝。
1919
+
1920
+ 宿主部署流程需要串行安排迁移进程;签名复核不是跨进程数据库锁。迁移事务按各迁移声明及数据库能力执行,
1921
+ 失败时可能已有迁移提交,命令不自动回退;修复后重新预览当前状态。`applied` 表示迁移执行完成,`ok` 还检查包、URL 和可选配置,
1922
+ 迁移成功但接入检查失败会返回非零。静态收集、重启和对外发布由宿主部署流程执行,命令不会改写 `settings.py` 或 `urls.py`。
1923
+
1924
+ ### 默认配置与静态示例
1925
+
1926
+ ```bash
1927
+ python manage.py simpleui_antd_init --json # 仅预览站点默认配置
1928
+ python manage.py simpleui_antd_init --apply --json # 缺失时创建,已有验证码设置保留
1929
+ python manage.py simpleui_antd_init --list-templates --json
1930
+ python manage.py simpleui_antd_init --user '<已有账号>' --json
1931
+ python manage.py simpleui_antd_init --user '<已有账号>' --apply --json
1932
+ # 可选择静态模板,多个 --template 合并为一批
1933
+ python manage.py simpleui_antd_init --user '<已有账号>' --name team-demo \
1934
+ --template screen:antv --template h5:survey --apply --json
1935
+ ```
1936
+
1937
+ `--user` 使用当前认证模型的 `USERNAME_FIELD`,兼容用户名与邮箱等自定义账号;必须是已存在、有效的 staff/超级管理员。
1938
+ 命令不创建账号、默认密码、组或权限。没有 `--user` 时仅处理站点配置;指定账号后默认生成运营大屏与滚动叙事 H5 两张静态示例,
1939
+ 也可选择打包的 11 套模板,安装宿主无需 Node.js。模板来自编辑器同一份定义,素材通过宿主静态存储解析;生产环境应先完成 `collectstatic`。
1940
+ 页面名称带「静态示例」标识,图中数字为样例,未绑定业务数据;生成页面默认关闭分享,表单默认停止收集。
1941
+
1942
+ 0020 的 `InitializationBatch` 按账号和 `--name`(默认 `starter-v1`)保存配方指纹与页面/数据集/报表 ID。
1943
+ 相同配方重复执行返回原记录,不覆盖名称、布局、分享设置或验证码开关;被删除或移出归属的页面只报告不可用,不自动补建。
1944
+ 同名批次改用其他配方返回 `BATCH_CONFLICT`,请使用新名称;不根据页面名称猜测是否为历史示例,也不修复被手工删除的批次记录。
1945
+ 可将预览的 `plan_id` 传给初始化的 `--plan-id` 校验配置与字段定义仍一致;默认静态模板变化或业务数据源结构变化时需重新预览。
1946
+
1947
+ ### 业务数据配方
1948
+
1949
+ 启用已有 GraphQL 模型/字段白名单后,保存本地配方 JSON(最多 64 KiB,每批 1–3 套;名称不可重复):
1950
+
1951
+ ```json
1952
+ {
1953
+ "schemaVersion": 1,
1954
+ "templates": [{
1955
+ "template": "commerce-screen",
1956
+ "name": "经营看板",
1957
+ "model": "sales.order",
1958
+ "fields": {"category": "status", "value": "amount", "date": "created_at", "label": "code"}
1959
+ }]
1960
+ }
1961
+ ```
1962
+
1963
+ ```bash
1964
+ python manage.py simpleui_antd_init --user '<已有账号>' --name commerce-v1 --recipe ./recipe.json --json
1965
+ python manage.py simpleui_antd_init --user '<已有账号>' --name commerce-v1 --recipe ./recipe.json \
1966
+ --apply --plan-id '<plan_id>' --json
1967
+ ```
1968
+
1969
+ 模板键与字段要求沿用「业务数据模板」:`commerce-screen`、`service-screen`、`business-h5`。
1970
+ 业务模式使用同一 ModelAdmin、QuerySet、对象/字段权限和查询限额;校验后生成草稿、不可变 v1 和绑定页面,默认手动更新且关闭分享。
1971
+ 不会修改业务记录或保存查询结果;权限校验可能按现有对象规则读取业务对象。创建后再次核对发布查询与预览签名,变化则回滚整批。
1972
+ 一批中的页面、版本、索引、站点配置和批次记录同事务提交,任一创建失败全部回滚。
1973
+ 当前业务配方要求应用模型统一位于 `default`;静态初始化、诊断与迁移支持显式数据库别名,复杂业务路由可沿用宿主 API 创建流程。
1974
+
1975
+ ### 独立报表示例配方
1976
+
1977
+ `--report-recipe` 使用已启用的 GraphQL 模型/字段白名单和同一报表校验层,生成个人私有 `Report`。
1978
+ 它与 `--template`、`--recipe` 互斥,文件最多 64 KiB、每批 1–10 个报表,名称须唯一:
1979
+
1980
+ ```json
1981
+ {
1982
+ "schemaVersion": 1,
1983
+ "reports": [{
1984
+ "name": "订单状态统计",
1985
+ "description": "按状态汇总订单金额",
1986
+ "definition": {
1987
+ "model": "sales.order",
1988
+ "mode": "aggregate",
1989
+ "groupBy": [{"field": "status", "name": "state"}],
1990
+ "metrics": [{"name": "total", "operation": "SUM", "field": "amount"}]
1991
+ },
1992
+ "chart": {"type": "bar", "category": "dimensions.state", "value": "metrics.total"}
1993
+ }]
1994
+ }
1995
+ ```
1996
+
1997
+ ```bash
1998
+ python manage.py simpleui_antd_init --user '<已有账号>' --name report-demo-v1 \
1999
+ --report-recipe ./reports.json --json
2000
+ python manage.py simpleui_antd_init --user '<已有账号>' --name report-demo-v1 \
2001
+ --report-recipe ./reports.json --apply --plan-id '<plan_id>' --json
2002
+ ```
2003
+
2004
+ 将示例模型和字段改为宿主已开放的名称。`definition` 支持报表原有筛选、排序、分组、指标、类型化参数和 JSON 标量列;
2005
+ `chart` 沿用报表图表/表格映射约定。必填参数仅保存声明,查看报表时仍须传入实参。预览返回配置摘要,不执行汇总或保存业务结果;
2006
+ 权限检查可能读取授权范围内的原始对象。预览和创建分别校验当前账号、模型/模块、QuerySet、对象及字段权限,工厂不能替换账号。
2007
+
2008
+ 该模式仅在 `default` 数据库创建私有报表、缺失的站点配置及初始化批次,复用 0020,无新增迁移。
2009
+ 配方、规范化定义、字段依赖和图表共同参与计划签名;创建前结构或权限变化会拒绝,失败回滚整批。
2010
+ 相同账号/批次重试不覆盖报表编辑,被删除或转交的报表只标记不可用;切换配方或初始化模式须使用新 `--name`。
2011
+ JSON 输出包含 `mode: "reports"`、`reports` 摘要及 `created_reports`,原页面模式的输出与指纹格式保持兼容。
2012
+
2013
+ 专项检查:`python scripts/test_backend.py simpleui_antd.tests.test_report_initialization`;
2014
+ 真实临时宿主管理命令:`python scripts/test_report_initialization.py`;PostgreSQL 并发检查在隔离运行器中使用 `--report-init`。
2015
+ 这些检查不会连接用户宿主或迁移用户数据库。
2016
+
2017
+ ### 数据配方的权限上下文
2018
+
2019
+ 默认 CLI 请求只包含指定账号和空会话。如果权限逻辑需要租户、组织或其他中间件上下文,应在宿主配置可信请求工厂:
2020
+
2021
+ ```python
2022
+ # settings.py
2023
+ SIMPLEUI_ANTD_SETUP_REQUEST_FACTORY = 'project.setup_context.request_for_owner'
2024
+
2025
+ # project/setup_context.py
2026
+ from django.http import HttpRequest
2027
+
2028
+ def request_for_owner(*, user, using):
2029
+ request = HttpRequest()
2030
+ request.method = 'POST'
2031
+ request.session = {}
2032
+ request.tenant = user.tenant # 使用宿主已有的账号/租户关系
2033
+ return request
2034
+ ```
2035
+
2036
+ 工厂须返回 `HttpRequest`,仅补充上下文;命令重新加载指定账号作为 `request.user`,不接受工厂替换认证主体。
2037
+ 缺少宿主要求的上下文时校验失败,不猜测或放宽租户范围。工厂和现有权限钩子应保持只读。
2038
+
2039
+ 仓库验收脚本使用临时宿主工程和 SQLite,不连接已有项目数据库:
2040
+
2041
+ ```bash
2042
+ python scripts/test_host_setup.py
2043
+ python scripts/test_host_setup.py --serve --port=8662 # 验收后临时提供 /control/ 挂载点
2044
+ ```
2045
+
2046
+ ## 持久通知中心
2047
+
2048
+ 先执行 `python manage.py migrate` 应用 `0014_notifications_retention`。此能力不依赖 GraphQL、Channels 或外部消息服务。
2049
+ 顶栏「消息中心」显示当前账号的持久收件箱,支持未读计数、每页 20 条、状态筛选、标记已读/未读,以及当前快照的全部已读。
2050
+ 快照之后新到的消息保持未读。超级管理员也只能读取自己的收件箱;原有「最近操作」继续显示操作记录。
2051
+ 超级管理员可搜索有效 staff 接收人,一次向 1–50 人发送纯文本通知;普通 staff 无发送或接收人目录权限。
2052
+
2053
+ ```python
2054
+ SIMPLEUI_ANTD_NOTIFICATIONS = {
2055
+ 'enabled': True, # 默认开启;False 时管理端点返回 404、界面隐藏入口
2056
+ 'poll_seconds': 60, # 未读计数间隔,15–3600 整数秒
2057
+ 'send_per_minute': 10, # 每发送人每分钟请求限额,1–1000
2058
+ }
2059
+ ```
2060
+
2061
+ 计数请求不重叠,浏览器隐藏时取消并暂停,恢复时立即读取;关闭收件箱会取消未完成的列表请求。
2062
+ UI 使用普通文本渲染。自助改密、TOTP 开启/关闭及公开数据授权变更会和业务操作同事务写入通知,写入失败时回滚该操作。
2063
+ 这些自动通知不复制密码、动态码、密钥、分享令牌、查询参数值、页面名称或业务结果。
2064
+ 通知链接只记录站内资源引用,每次读取按当前所有权重新判断是否提供跳转;链接不授予数据权限。
2065
+
2066
+ 宿主可在自己的业务事务中创建通知:
2067
+
2068
+ ```python
2069
+ from django.db import transaction
2070
+ from simpleui_antd.notifications import notify
2071
+
2072
+ with transaction.atomic():
2073
+ # 宿主业务保存……
2074
+ notice = notify(staff_user, '任务处理完成', '请查看当前处理结果。',
2075
+ category='general', level='success', dedupe_key='job:123:finished')
2076
+ ```
2077
+
2078
+ `notify` 对停用或非 staff 接收人返回 `None`;去重键按接收人唯一,相同内容重试返回原记录并保留已读状态,改变内容会报错。
2079
+ 可传 `using=<数据库别名>`,应与宿主事务和接收人所属数据库一致;自动授权通知沿用页面数据库。
2080
+ `target` 可为 `{'kind':'security'}`,或 `{'kind':'page|dataset|report|playlist','id':'非负整数标识'}`(`kind` 取其中一种);兼容历史导入的零主键资源,不接受任意 URL。
2081
+
2082
+ 管理 API(均为当前 Session 与 staff 权限;写入要求 CSRF):
2083
+
2084
+ | 路径 | 行为 |
2085
+ | --- | --- |
2086
+ | `GET /api/notifications/?state=all|read|unread&page=1` | 自己的收件箱;返回 `snapshot` 和未读数 |
2087
+ | `GET /api/notifications/summary/` | 未读数和计数刷新间隔 |
2088
+ | `GET/PUT /api/notifications/<id>/` | 读取或设置自己的 `{"read":true|false}` |
2089
+ | `POST /api/notifications/read-all/` | `{"before_id":"snapshot 值"}`,仅处理快照内记录 |
2090
+ | `GET /api/notifications/recipients/?q=账号&page=1` | 超级管理员搜索有效 staff,每页 20 人 |
2091
+ | `POST /api/notifications/` | 超级管理员发送;`request_id` 为 UUID,`recipients` 为账号标识字符串数组,加 `title`、`message`、可选 `level`、`target` |
2092
+
2093
+ 发送重试应复用相同请求 UUID 与内容;去重按发送人、接收人及 UUID 隔离,冲突返回 409,批量写入全部回滚。
2094
+ 达到发送请求限额返回 429 与 `Retry-After`;多实例使用共享 cache。此处负责站内投递,外发通知见下文模型 Webhook。
2095
+
2096
+ ## 审计留存与分批清理
2097
+
2098
+ 0014 同时提供不可改写的 `AuditRetentionBatch` 摘要表。默认不删除任何历史审计;可以分别配置操作记录与公开授权变更的保留周期:
2099
+
2100
+ ```python
2101
+ SIMPLEUI_ANTD_AUDIT_RETENTION = {
2102
+ 'admin_log_days': None, # None 永久保留,或 1–36500 整数天
2103
+ 'grant_audit_days': None,
2104
+ 'batch_size': 500, # 每批 1–1000 条
2105
+ 'max_rows': 10000, # 每次、每类最多 1–100000 条
2106
+ }
2107
+ ```
2108
+
2109
+ ```bash
2110
+ python manage.py simpleui_antd_prune_audit --json # 只预览候选数量,不改数据库
2111
+ python manage.py simpleui_antd_prune_audit --apply --json # 显式清理
2112
+ # 多数据库工程:加 --database <alias>
2113
+ ```
2114
+
2115
+ 命令以开始时固定的截止时间和主键上界选取严格早于保留周期的记录,不追赶本次之后插入的数据。
2116
+ 每个批次在同一事务中删除并追加摘要,仅包含执行编号、记录类型、截止时间、数量与执行时间。
2117
+ 它不存原记录内容,也不清理自己的摘要、业务表、页面、答卷、数据集或不可变发布版本。
2118
+ 遇到关联业务记录时阻止本批次,不执行连带删除或置空;发生错误时该批次回滚、退出码为 1,之前成功批次仍有匹配摘要,可再次运行。
2119
+ 达到数量上限时 `has_more` 表示仍有候选记录;定时运行可由宿主任务调度器或系统 cron 调用,应用不会默认启动清理任务。
2120
+
2121
+ 超级管理员可在「操作记录 → 审计留存」只读查看当前策略和最近 20 个已提交批次,接口为 `GET /api/audit-retention/`。
2122
+ 普通 staff 不可读取全局策略;网页没有删除入口。`simpleui_antd_check` 会检查留存和通知配置是否合法。
2123
+
2124
+ ## 模型 Webhook(0015)
2125
+
2126
+ 此扩展默认关闭,使用已有 GraphQL 注册模型与字段可见权限,需要启用数据层(Python ≥ 3.10、Django ≥ 5.2),
2127
+ 先执行迁移 `0015_webhooks`。关闭扩展时核心包仍支持 Django 4.2,也不需要外部队列服务。
2128
+
2129
+ ```python
2130
+ SIMPLEUI_ANTD_WEBHOOKS = {
2131
+ 'enabled': True,
2132
+ 'allowed_targets': ['https://receiver.example.com/ordloom/events'],
2133
+ 'allowed_networks': [], # 默认只连接公网单播地址;内网可显式加入 '10.0.0.0/8' 等 CIDR
2134
+ 'allow_http': False, # 默认 HTTPS;内网 HTTP 也需明确允许且地址在完整白名单中
2135
+ 'timeout_seconds': 5, # DNS 完成后的连接、TLS 与响应共用网络时间预算,1–15 秒
2136
+ 'max_attempts': 5, # 一轮最多 1–10 次尝试
2137
+ 'retry_seconds': 30, # 指数退避起始间隔,1–3600 秒,最大间隔 1 小时
2138
+ 'lease_seconds': 300, # 认领租约,60–3600 秒;worker 崩溃后可恢复
2139
+ 'max_endpoints_per_user': 20, # 每创建者未归档端点上限,1–100
2140
+ 'manual_per_minute': 30, # 手动测试/重新投递频控,1–1000;多实例使用共享 cache
2141
+ # 'signing_key': '至少32字符的独立服务端主密钥', # 默认使用 Django SECRET_KEY
2142
+ # 'permission_context': 'myapp.webhooks.permission_request',
2143
+ }
2144
+ ```
2145
+
2146
+ `allowed_targets` 是最多 100 个**完整接收 URL**,限制协议、主机、端口和路径;界面只选择已批准地址。
2147
+ 地址不接受账号、查询参数、片段或未编码的非 ASCII 路径。每次投递重新检查 DNS 的全部 A/AAAA 地址,
2148
+ 任一地址不符合公网/显式网络策略即停止,连接固定到已校验的 IP,同时保留原 HTTPS SNI、证书主机名与 Host 校验。
2149
+ 多播与未指定地址始终拒绝;不使用环境代理、不跟随重定向、不持久保存响应体。DNS 解析时限由宿主解析器管理;
2150
+ 网络预算从解析完成后计算。内网 HTTPS 接收器使用受宿主系统信任的证书。
2151
+
2152
+ 登录后通过账号菜单「模型 Webhook」管理。普通 staff 只管理本人端点与历史,超级管理员可管理全部,
2153
+ 实际投递始终以**端点创建者**当前账号状态和模型/字段可见规则授权,管理员编辑别人的端点不会提升接收权限。
2154
+ 模型目录与现有变更订阅一致,排除 Webhook 自身的控制模型;账号停用、权限撤销、模型撤回或地址不再批准时停止投递。
2155
+ 归档会停用端点并保留历史,恢复后保持停用。修改地址、模型、事件或启用状态、轮换密钥会增加配置代次并取消旧队列;
2156
+ 只修改名称保留队列。记录中保留当时地址和代次。已在网络中的请求无法撤回,迟到 worker 不能覆盖新的投递状态。
2157
+
2158
+ ### 事件、事务与权限上下文
2159
+
2160
+ 事件是模型级失效通知,保存(含创建/更新/软删除)为 `model.saved`,物理删除为 `model.deleted`:
2161
+
2162
+ ```json
2163
+ {"schema_version":1,"id":"事件 UUID","event":"model.saved","model":"sales.order","occurred_at":"2026-11-10T02:00:00+00:00"}
2164
+ ```
2165
+
2166
+ 载荷只有模型、事件类型、时间和事件编号,不复制对象主键、字段值或请求凭据。
2167
+ 接收方可用通知触发授权数据重读;读取依然走现有 QuerySet、软删除、对象和字段权限链。
2168
+ 外部只读服务身份可按「外部只读数据服务(0016)」另行配置;Webhook 不会给接收方自动授予数据接口权限。
2169
+
2170
+ ### 钉钉与飞书机器人
2171
+
2172
+ 可选 `bot_targets` 配置将模型事件发送为钉钉/飞书文本消息,复用 0015,无新增迁移。
2173
+ 完整机器人地址与平台密钥仅由宿主服务端保存;界面选择批准目标,端点和历史保存 `bot:<id>` 引用。
2174
+ 使用各平台签名及业务成功码校验;凭据变化取消旧队列,发送间隔等待不消耗重试次数,当前创建者权限保持生效。
2175
+ 通用 Webhook 的完整地址、Ordloom HMAC 与一次性密钥流程继续兼容。
2176
+ 配置示例、协议来源、状态分类、共享缓存及真实群验收边界见 [机器人接入文档](../docs/webhook-bots.md)。
2177
+
2178
+ 常规 `save()` / `delete()` 产生事件,队列保存在源数据库;同一事务内同一端点、模型、事件和配置代次合并。
2179
+ 回滚包含队列回滚,保存点回滚后可正常再次排队;历史迁移模型不触发实时通知。业务 API 已使用事务,
2180
+ 宿主直接保存模型时,应显式使用 `transaction.atomic(using=...)` 才能保证业务与队列联合回滚;
2181
+ 独立 autocommit 写入已先提交业务 SQL,之后的排队错误无法回滚那次业务写入。
2182
+ `update()` / `bulk_create()` / 原始 SQL 不发模型信号,可信宿主可在同一业务事务内调用:
2183
+
2184
+ ```python
2185
+ from django.db import transaction
2186
+ from simpleui_antd.webhooks.signals import publish
2187
+
2188
+ with transaction.atomic(using='default'):
2189
+ Order.objects.filter(...).update(...)
2190
+ publish('sales.order', event='saved', using='default')
2191
+ ```
2192
+
2193
+ 后台会重新从同库加载创建者,不持久化浏览器 Session 或请求头。默认权限请求带当前账号和空 Session;
2194
+ 若已有规则依赖租户等请求上下文,可配置受信任的 `permission_context(user, using) -> HttpRequest`,
2195
+ 由宿主根据账号与数据库补齐确定的租户信息。接口保存时校验真实请求和后台请求,投递前再次校验;上下文缺失或规则异常均拒绝投递。
2196
+ 端点、队列与账号应在相同数据库,投递命令使用对应 `--database`,连接路由由宿主管理。
2197
+
2198
+ ### 投递任务与重试
2199
+
2200
+ ```bash
2201
+ python manage.py simpleui_antd_deliver_webhooks --json # 只预览待处理数量
2202
+ python manage.py simpleui_antd_deliver_webhooks --apply --limit 100 --json
2203
+ # 多数据库:加 --database <alias>;--limit 为 1–1000,默认 100
2204
+ ```
2205
+
2206
+ 由宿主 cron/任务调度器周期调用,应用不默认启动外发。worker 必须在业务事务外执行,认领事务提交后才进行 HTTP。
2207
+ 每次调用数量有上限,使用认领令牌与租约避免并发覆盖;租约过期标记旧尝试并恢复。网络失败、408/425/429 和 5xx 指数退避重试,
2208
+ 有限度遵从 `Retry-After`;其他 HTTP 非 2xx(包括 3xx)终止当前轮。达到上限后失败,管理界面可在同一配置代次重新投递。
2209
+ 人工重试保留事件 UUID、正文和累计次数,开启新一轮预算。测试通知由 HTTP 管理端排队,实际外发仍由 worker 执行。
2210
+ 命令遇到失败/待重试结果返回退出码 1,数据库记录保留实际状态;成功、取消或无任务时返回 0。
2211
+
2212
+ 投递是**至少一次**:接收方需原子记录 `(端点编号, 验签正文 id)` 去重。
2213
+ 日志按创建者隔离,端点、投递及逐次尝试都分页;只展示状态、HTTP 码、有限错误代码和时间,不展示响应内容或签名密钥。
2214
+
2215
+ ### HMAC 协议与接收方
2216
+
2217
+ 创建/轮换时一次性返回 `secret`,以后读取配置不返回它。它由服务端主密钥、端点 UUID 和随机轮换 nonce 通过 HMAC-SHA256 派生,
2218
+ 签名密钥本身不以明文存入数据库。主密钥变更需要同步接收方密钥;可使用独立 `signing_key` 避免 Django `SECRET_KEY` 轮换影响此协议。
2219
+
2220
+ | 请求头 | 内容 |
2221
+ | --- | --- |
2222
+ | `X-Ordloom-Hook` | 端点公开 UUID,用来选择接收方保存的密钥 |
2223
+ | `X-Ordloom-Id` | 事件 UUID,与正文 `id` 相同,重试不变 |
2224
+ | `X-Ordloom-Timestamp` | 当前尝试的 Unix 秒数,重试重新签名 |
2225
+ | `X-Ordloom-Signature` | `v1=` + HMAC-SHA256 十六进制摘要 |
2226
+
2227
+ 摘要输入是 `timestamp 的 ASCII 字节 + b'.' + 原始请求正文`,使用上面返回的密钥字符串作为 UTF-8 HMAC key。
2228
+ 接收方验签前不要重新序列化 JSON。包内 `verify_signature` 常量时间比较并默认检查 300 秒时效。
2229
+ 下面展示 Django 验签入口;注释位置需接入接收方自己的持久队列,再返回成功确认:
2230
+
2231
+ ```python
2232
+ import json
2233
+ from django.http import HttpResponse
2234
+ from django.views.decorators.csrf import csrf_exempt
2235
+ from django.views.decorators.http import require_POST
2236
+ from simpleui_antd.webhooks.worker import verify_signature
2237
+
2238
+ HOOK_KEYS = {'接收端配置的端点 UUID': '创建或轮换后保存的 secret'}
2239
+
2240
+ @csrf_exempt # 此接收端以 HMAC 验证来源,不使用浏览器 Session 作为认证
2241
+ @require_POST
2242
+ def receive(request):
2243
+ hook = request.headers.get('X-Ordloom-Hook')
2244
+ key = HOOK_KEYS.get(hook)
2245
+ if not key or not verify_signature(key, request.headers.get('X-Ordloom-Timestamp'),
2246
+ request.body, request.headers.get('X-Ordloom-Signature')):
2247
+ return HttpResponse(status=403)
2248
+ try:
2249
+ event = json.loads(request.body)
2250
+ if event['id'] != request.headers.get('X-Ordloom-Id'):
2251
+ return HttpResponse(status=400)
2252
+ except (ValueError, KeyError, TypeError):
2253
+ return HttpResponse(status=400)
2254
+ # 接收方在自己的事务中以 (hook, event['id']) 唯一键持久排队,重复事件直接确认。
2255
+ return HttpResponse(status=204)
2256
+ ```
2257
+
2258
+ 网络限制依据 [OWASP SSRF 指南](https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html),
2259
+ TLS 行为沿用 [Python HTTPSConnection](https://docs.python.org/3/library/http.client.html#http.client.HTTPSConnection) 的默认验证。
2260
+ 事务与保存点行为见 [Django 事务说明](https://docs.djangoproject.com/en/5.2/topics/db/transactions/)。
2261
+
2262
+ ### 管理接口与隔离验收
2263
+
2264
+ 均需当前 staff Session;写入要求 CSRF,列表每页 20 条,接口前缀随应用挂载点变化:
2265
+
2266
+ | 路径 | 用途 |
2267
+ | --- | --- |
2268
+ | `GET/POST /api/webhooks/` | 创建人/超级管理员列表与创建;可用 `archived=true` 查看归档 |
2269
+ | `GET /api/webhooks/catalog/` | 当前模型目录、批准地址和尝试上限 |
2270
+ | `GET/PUT/DELETE /api/webhooks/<id>/` | 详情、修改或归档;写入传当前 `revision` |
2271
+ | `POST /api/webhooks/<id>/rotate/` | `{"revision":1}` 轮换密钥并取消旧队列 |
2272
+ | `POST /api/webhooks/<id>/test/` | `{"revision":1,"request_id":"UUID"}` 幂等排入测试事件 |
2273
+ | `GET /api/webhooks/<id>/deliveries/?page=1` | 投递分页,包含当时地址/配置代次和有限状态信息 |
2274
+ | `GET /api/webhooks/<id>/deliveries/<delivery>/?page=1` | 投递详情和尝试分页 |
2275
+ | `POST /api/webhooks/<id>/deliveries/<delivery>/` | `{"action":"retry"}` 或 `{"action":"cancel"}` |
2276
+
2277
+ 创建内容为 `name`、批准的 `url`、1–20 个 `models`、`events=['saved','deleted']` 和可选 `enabled`(默认 false)。
2278
+ 修改冲突返回 409,不覆盖旧修订;归档恢复传 `{"revision":当前修订,"archived":false}`,保持停用。
2279
+ 所有 HTTP 请求只管理队列,生产路由不提供直接执行投递的端点。
2280
+
2281
+ 仓库脚本 `scripts/dev_fields.py --webhooks --production --port=8660` 在临时 SQLite 和本机临时端口创建隔离接收器,
2282
+ 只向自己的 loopback 接收器发送:首次 503、之后 204,用于真实签名/重试验收。
2283
+ `frontend` 下 `npm run test:webhooks` 使用该夹具(ego-browser,账号 `demo / demo-only`);
2284
+ 夹具中的执行控制路由仅在该脚本存在,不进入安装包生产路由。
2285
+
2286
+ ## 外部只读数据服务(0016)
2287
+
2288
+ 服务身份用于后端集成,不依赖浏览器登录 Cookie。先安装可选 GraphQL 扩展、配置模型字段白名单并应用迁移:
2289
+
2290
+ ```python
2291
+ # settings.py;已有 SIMPLEUI_ANTD_GRAPHQL 配置保持不变
2292
+ SIMPLEUI_ANTD_DATA_SERVICES = {
2293
+ 'enabled': True, # 默认关闭;需要 GraphQL 运行时
2294
+ 'allow_http': False, # 仅隔离本机验收可设 True
2295
+ 'max_services_per_user': 20, # 1–100,包含已撤销身份,身份可编辑复用
2296
+ 'requests_per_minute': 60, # 1–10000,每个服务共享的固定窗口预算
2297
+ 'auth_attempts_per_minute': 120, # 1–10000,每个直连 IP 的总准入预算
2298
+ 'max_lifetime_days': 365, # 1–365,必须明确设置到期时间
2299
+ 'permission_context': None, # 可选宿主可信权限上下文工厂
2300
+ }
2301
+ ```
2302
+
2303
+ ```bash
2304
+ python manage.py migrate
2305
+ python manage.py simpleui_antd_check --json
2306
+ ```
2307
+
2308
+ 启用后在右上角账号菜单进入「外部数据服务」。创建人管理自己的服务,超级管理员管理全部;关联账号固定为创建人,不能通过请求切换账号。
2309
+ 新建/轮换只返回一次 `ordloom_<身份UUID>.<随机密钥>`,数据库保存随机 256 位凭据的 SHA-256 摘要,列表、操作记录和后续详情不返回凭据或摘要。
2310
+ 完整凭据不写入组件、页面文档或浏览器持久存储。请在服务端密钥存储中保存;响应丢失时从列表找到身份后轮换,旧凭据随即失效。
2311
+ 服务可撤销或编辑后重新启用,必须设置未来的到期时间;已过期身份先编辑有效期,再轮换。轮换不自动启用已撤销身份。
2312
+
2313
+ ### 授权范围与现有权限
2314
+
2315
+ 两类范围独立,可选择其中一种或同时配置:
2316
+
2317
+ - `model_fields`:最多 20 个模型,每模型最多 100 个明确字段;授权模型必须在宿主 GraphQL 白名单中,并且当前账号可见。
2318
+ 外部 GraphQL 的输出、筛选、排序、聚合和 introspection 均使用此受限结构,不提供未授权模型或字段。
2319
+ 关联字段仅在目标模型主键也有明确授权、且目标对象和主键当前可见时返回授权 ID。
2320
+ - `dataset_versions`:最多 50 个 `{"id":"12","version":1}`;必须为创建人/超级管理员当前可管理的已发布版本。
2321
+ 授予该固定查询及其完整输出,仍检查所有输出、筛选、排序和聚合依赖的当前权限;不隐含模型直接查询授权,也不自动包含未来发布版本。
2322
+ 运行时可传定义允许的类型化参数及分页,不能替换查询定义、增加输出字段或提高保存的 limit。
2323
+
2324
+ 每次请求从数据库重新读取凭据状态、有效期和关联账号。账号被删除、停用或失去 staff/superuser 资格后拒绝读取;模型/模块、QuerySet、软删除、对象及动态字段规则继续由原数据层执行。
2325
+ 超级管理员修改别人的身份时同时检查自己的范围与原创建人的权限;权限已经撤销时仍可停用服务。
2326
+ 权限按请求执行,已经返回的结果和正在执行的请求无法追溯撤回;不缓存业务结果或服务权限。`last_used_at` 最多每分钟更新一次,不作为逐次访问日志。
2327
+ 凭据独立于密码/TOTP,密码修改不会自动轮换服务凭据;需要停止集成时撤销该身份。
2328
+
2329
+ 默认权限上下文为新构造的只读请求及空 Session,不继承外部 `X-Workspace`、查询参数、Cookie 或浏览器账号。
2330
+ 如果 ModelAdmin 依赖租户等宿主上下文,可配置受信任的 Python callable(或其 dotted path):
2331
+
2332
+ ```python
2333
+ from django.test import RequestFactory
2334
+
2335
+ def data_service_context(user, service):
2336
+ request = RequestFactory().get('/internal/data-service/')
2337
+ request.user = user
2338
+ # 从宿主数据库中的受信任账号/组织关系推导租户;不要使用外部请求的任意头。
2339
+ request.tenant = user.profile.tenant
2340
+ return request
2341
+ ```
2342
+
2343
+ 工厂签名 `(user, service) -> HttpRequest`,应用强制使用刚从数据库读取的关联账号,工厂异常拒绝访问。
2344
+ 原 `SIMPLEUI_ANTD_LOGIN_IP_ALLOWLIST` 同样限制服务入口,只检查 `REMOTE_ADDR`;反向代理需自行正确设置连接来源。
2345
+ 生产入口要求 HTTPS,代理终止 TLS 时按宿主可信代理策略配置 `SECURE_PROXY_SSL_HEADER`;不会直接相信任意 `X-Forwarded-Proto`。
2346
+ 多实例应使用共享 Redis cache,令身份/IP 预算在实例间共享;不把基础频控描述为生产容量保证。
2347
+
2348
+ ### 管理和外部读取 API
2349
+
2350
+ 路径均相对于应用挂载点。管理接口继续要求 staff Session,写操作保留 CSRF;外部凭据不能登录或访问这些管理接口:
2351
+
2352
+ | 接口 | 行为 |
2353
+ | --- | --- |
2354
+ | `GET/POST /api/data-services/` | 按权限分页/创建,返回每页 20 条 |
2355
+ | `GET /api/data-services/catalog/` | 当前可授权字段目录与有效期上限 |
2356
+ | `GET/PUT/DELETE /api/data-services/<id>/` | 查看/编辑/撤销;写入提交当前 `revision`,冲突返回 409 |
2357
+ | `POST /api/data-services/<id>/rotate/` | `{"revision":1}` 轮换凭据;旧凭据立即失效 |
2358
+
2359
+ 创建/编辑字段为 `name`、`enabled`、带时区的 `expires_at`、`model_fields` 和 `dataset_versions`。
2360
+ 创建、修改、撤销及轮换与 Django 操作记录同事务提交,记录不含密钥、查询参数或业务结果。
2361
+
2362
+ 下列入口只使用 `Authorization: Bearer …`,不接受 Session 兜底或 URL 参数凭据,读取响应均为 `no-store`,不设置登录 Cookie:
2363
+
2364
+ | 接口 | 行为 |
2365
+ | --- | --- |
2366
+ | `GET /api/data/catalog/` | 当前受限模型/字段目录及已授权固定版本列表 |
2367
+ | `POST /api/data/graphql/` | 受限 GraphQL 查询;没有写入、订阅和管理操作 |
2368
+ | `POST /api/data/datasets/<id>/versions/<number>/` | `{"params":{},"offset":0,"limit":10}` 执行固定版本,参数可省略 |
2369
+
2370
+ 以服务端环境变量读取凭据,例如应用挂载于 `/admin/`:
2371
+
2372
+ ```python
2373
+ import json, os
2374
+ from urllib.request import Request, urlopen
2375
+
2376
+ request = Request(
2377
+ 'https://admin.example.com/admin/api/data/datasets/12/versions/1/',
2378
+ data=json.dumps({'params': {}, 'limit': 10}).encode(),
2379
+ headers={'Authorization': 'Bearer ' + os.environ['ORDLOOM_DATA_TOKEN'],
2380
+ 'Content-Type': 'application/json'}, method='POST')
2381
+ with urlopen(request, timeout=15) as response:
2382
+ result = json.load(response)
2383
+ ```
2384
+
2385
+ 无效/过期/撤销凭据返回 401 和 `WWW-Authenticate: Bearer`;权限不足返回 403,频控返回 429 与 `Retry-After: 60`。
2386
+ GraphQL 查询错误保留 GraphQL `errors` 结构,错误消息和服务端默认错误日志不会输出宿主权限异常、请求字面量或密钥。
2387
+ 外部请求最多 64 KiB,继续使用数据层的查询深度、token 数、操作数、分页、分组及动态权限扫描上限。此入口不增加跨域放行策略。
2388
+ Session 的 CSRF 策略与服务凭据认证分开处理,依据 [Django CSRF](https://docs.djangoproject.com/en/5.2/ref/csrf/) 和
2389
+ [DRF 非 Session 认证说明](https://www.django-rest-framework.org/api-guide/authentication/#tokenauthentication);受限 schema 的可发现结构遵循
2390
+ [GraphQL introspection](https://graphql.org/learn/introspection/)。
2391
+
2392
+ 隔离验收:`scripts/dev_fields.py --data-services --production --port=8660` 启用本机 HTTP 测试,临时数据库使用合成业务数据。
2393
+ 在 `frontend` 运行 `npm run test:data-services`,覆盖可视化授权、真实无 Session 读取、字段限制、版本隔离、轮换撤销、修订冲突、跨账号与移动界面。
2394
+
2395
+ ## 一次性恢复码与登录设备(0018)
2396
+
2397
+ ```bash
2398
+ python manage.py migrate
2399
+ ```
2400
+
2401
+ 恢复码与设备管理属于核心 Django 能力,不要求 Strawberry/GraphQL、Channels 或外部密钥服务。
2402
+
2403
+ ### TOTP 恢复码
2404
+
2405
+ 「安全设置 → 两步验证与恢复码」首次开启 TOTP 后返回 10 份随机恢复码,每份 128 位随机量,完整内容只显示一次。
2406
+ 数据库只保存与账号绑定的 SHA-256 摘要及使用时间;摘要字段按原 `PasswordField` 规则只写,不会进入字段目录或安全状态响应。
2407
+ 旧的 TOTP 账号升级后不自动生成明文恢复码;使用当前密码和认证器动态码主动生成。
2408
+
2409
+ - 登录必须先通过账号密码与现有验证码/IP 策略,再提供 `totp` 或 `recovery_code` 之一;恢复码不能替代密码。
2410
+ 成功登录时原子消耗一份,使用恢复码不会自动关闭 TOTP。连字符/空白分组和字母大小写可归一化,其他格式拒绝。
2411
+ - 重新生成需要当前密码和动态码,或当前密码和另一份未使用恢复码;旧批次随事务失效。
2412
+ 遗失认证器时可先用密码+恢复码登录,再用密码+另一份恢复码关闭旧 TOTP,随后重新扫码开启。
2413
+ - 原有动态码关闭接口保持兼容;使用恢复码关闭必须同时提供密码,关闭后删除所有剩余码。
2414
+ - 账号密码认证失败继续原限额;MFA 失败另按账号 PK 计数,用户名等价写法不能绕过连续 5 次/15 分钟锁定。
2415
+ 设置和设备操作另有账号级 5 次/5 分钟重验证失败预算,多实例需要共享 cache。
2416
+ - 安全变化、恢复码消耗、操作审计和可选站内通知共同提交,失败回滚;不记录明文密码、动态码、恢复码或认证器密钥。
2417
+ 通知关闭时仍保留内容无关的 Django 操作记录。响应丢失后不能重新读取原批次,应在有验证方式时重新生成。
2418
+ - 本地密码不可用的统一认证账号,敏感操作要求在宿主认证成功后的 5 分钟内进行,启用 TOTP 时仍需第二因素;超时请通过原身份服务重新登录。
2419
+ Ordloom 的 TOTP 登录验证适用于 `/api/login/`;宿主独立登录/SSO 入口应实施自己的 MFA 策略。
2420
+
2421
+ 接口继续使用 staff Session 与 CSRF,均有 `no-store`:
2422
+
2423
+ | 路径 | 用途 |
2424
+ | --- | --- |
2425
+ | `GET /api/security/totp/` | 启用状态、剩余恢复码数、生成时间、设备开关;激活后不返回认证器密钥或恢复码 |
2426
+ | `POST /api/security/totp/` | 原有 `provision/activate/disable`;新增 `regenerate`。开启或重置时一次返回 `recovery_codes` |
2427
+ | `POST /api/login/` | 在原字段之外可提供 `recovery_code`,与 `totp` 互斥;恢复码不能重用 |
2428
+
2429
+ 重置请求示例:`{"action":"regenerate","password":"当前密码","code":"六位动态码"}`;
2430
+ 恢复关闭示例:`{"action":"disable","password":"当前密码","recovery_code":"一份未使用恢复码"}`。
2431
+ 读接口与之后的响应只返回 `recovery_remaining`,不返回完整批次。勿把恢复码放在公开页面、组件配置或仓库中。
2432
+ 原则参考 [OWASP MFA 恢复建议](https://cheatsheetseries.owasp.org/cheatsheets/Multifactor_Authentication_Cheat_Sheet.html)。
2433
+
2434
+ ### 可撤销会话设备
2435
+
2436
+ 默认关闭,原 Session 行为保持不变。启用时将中间件放在 Session 和认证中间件之后:
2437
+
2438
+ ```python
2439
+ SIMPLEUI_ANTD_SESSION_DEVICES = {
2440
+ 'enabled': True,
2441
+ 'max_active': 20, # 1–50,超过后退出最近最少活动的记录
2442
+ 'max_records': 100, # 1–500,且不少于 max_active;只保留最近这些会话记录
2443
+ 'max_age_days': 14, # 1–365,设备记录的固定最长有效期
2444
+ 'touch_seconds': 60, # 1–300,最近活动写入间隔
2445
+ }
2446
+ MIDDLEWARE = [
2447
+ # ...
2448
+ 'django.contrib.sessions.middleware.SessionMiddleware',
2449
+ # CsrfViewMiddleware 等原配置保留
2450
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
2451
+ 'simpleui_antd.middleware.SessionDevicesMiddleware',
2452
+ # ...
2453
+ ]
2454
+ ```
2455
+
2456
+ `simpleui_antd_check --json` 校验配置和顺序。开启后,没有登记标识的旧会话需要重新登录,随后经 Django 认证成功信号登记;
2457
+ 原生 Django/SSO 成功登录也可登记 staff 会话。关闭配置不强制退出现有 Session。
2458
+
2459
+ - 「安全设置 → 登录设备」只显示本人的记录,超级管理员也不跨账号查看或退出设备。
2460
+ 提供登录/活动/有效期、粗略浏览器系统标签、IPv4 `/24` 或 IPv6 `/64` 网段;不保存完整 User-Agent、IP 或 Session key。
2461
+ `REMOTE_ADDR` 为实际连接来源,不直接信任任意转发头;代理部署由宿主设置可信来源。
2462
+ - 每次登录对应一个会话记录,并不识别真实硬件。最近活动不是实时在线,浏览器关闭或 Session 本身过期可能早于记录有效期。
2463
+ 记录有效期固定为登录时 Session 有效期与 `max_age_days` 较短者,不随自动续期延长,重新认证可创建新记录。
2464
+ - 退出指定其他设备,需要当前密码;若有 TOTP,还需动态码或一次性恢复码。当前设备使用原「退出登录」。
2465
+ 「退出其他设备」会同时轮换当前 Cookie/登记标识,保留当前登录,阻止旧 Cookie 副本继续复用。
2466
+ - 改密、TOTP 开关和恢复码重置会退出其他已登记会话并轮换当前会话。所有 HTTP 路由经过中间件复核,
2467
+ 已有事件 WebSocket/GraphQL 订阅在下一次事件/签名权限复核时停止转发;不强制中断已通过检查的在途请求,也不立即关闭空闲 socket。
2468
+ - 不依赖物理删除 Django Session 行,因此签名 Cookie、数据库、缓存和文件等 Session 后端均使用服务端登记状态判断。
2469
+ 签名 Cookie 本身不能靠客户端退出实现服务端撤销,背景见 [Django Cookie Session 说明](https://docs.djangoproject.com/en/5.2/topics/http/sessions/#using-cookie-based-sessions)。
2470
+ - 历史记录达到上限时清理最旧记录,缺失或已撤销标识始终要求重新认证;活跃会话达到上限时先退出最少活动的会话。
2471
+ 元数据数据库不可用时不继续授权。使用凭据的外部数据服务和匿名公开授权独立管理,不受此 Session 开关替代。
2472
+
2473
+ 设备接口(仍需本人的 Session/CSRF):
2474
+
2475
+ | 路径 | 用途 |
2476
+ | --- | --- |
2477
+ | `GET /api/security/devices/?page=1` | 每页 20 条本人的会话记录、当前标记、状态及脱敏网络信息 |
2478
+ | `POST /api/security/devices/<公开UUID>/` | 重验证后退出指定其他会话 |
2479
+ | `POST /api/security/devices/` | `{"action":"revoke_others","password":"…","code":"…"}` 退出其他设备;`recovery_code` 可替代 `code` |
2480
+
2481
+ 隔离验收:构建前端后使用 `scripts/dev_fields.py --security --notifications --production --port=8660`,
2482
+ 再在 `frontend` 运行 `npm run test:security-recovery`。验证只修改临时数据库中的演示账号,结束时关闭其 TOTP,真实浏览器和独立 HTTP 客户端分别验证登录与撤销。
2483
+
2484
+ <a id="未发布分享密码限速"></a>
2485
+
2486
+ ## 分享密码限速
2487
+
2488
+ 1.2.0 新增;无需新增迁移,原分享开关、有效期、令牌、资源权限及表单 CSRF 规则继续生效。
2489
+
2490
+ ```python
2491
+ # 以下均为默认值;enabled=False 可沿用原密码校验行为。
2492
+ SIMPLEUI_ANTD_SHARE_LIMITS = {
2493
+ 'enabled': True,
2494
+ 'window_seconds': 300,
2495
+ 'max_failures': 10,
2496
+ 'per_ip_failures': 100,
2497
+ }
2498
+ ```
2499
+
2500
+ 页面/轮播入口、公开数据读取和表单提交复用同一密码守卫及默认 Django 缓存。
2501
+ 固定时间窗口内,同一来源对同一页面或轮播最多 10 次失败、跨资源最多 100 次失败。
2502
+ 校验前预留额度以限制同时进行的密码哈希,成功后释放本次预留,不抹掉已有失败;持续正确的取数轮询不累计失败。
2503
+ 在途校验也占用额度,达到上限时即使密码正确仍需等待;无密码的入口提示不计数,受保护 POST 的缺失密码视为错误尝试。
2504
+
2505
+ 错误密码仍返回 403;超过额度返回 429、`Retry-After` 秒数及 `context.retry_after`。
2506
+ 前端保留解锁表单,窗口结束后可使用同一密码重试。缓存异常或限速配置无效时,受密码保护的访问返回通用 503;无密码页面不依赖这项缓存。
2507
+ 修改配置请先运行 `simpleui_antd_check`;时间范围 1–86400 秒,资源失败额度 1–10000,来源额度 1–100000,均为整数。
2508
+
2509
+ 多 Web/命令进程须将 Django `CACHES['default']` 指向同一具备原子 `add/incr/decr` 的 Redis 或 Memcached,不能以各进程的 LocMemCache 代替全局额度。
2510
+ 计数键使用 HMAC,不包含密码、原始 IP 或公开 UUID。来源仅取可信 `REMOTE_ADDR`,不会直接信任客户端的 `X-Forwarded-For`;代理部署需在可信边界正确设置来源。
2511
+ 同一 NAT 出口共享来源额度;未知来源归到同一预算。固定窗口、缓存逐出和共享出口的边界需按目标部署核对。
2512
+
2513
+ <a id="未发布后台任务运行诊断"></a>
2514
+
2515
+ ## 后台任务运行诊断
2516
+
2517
+ 1.2.0 新增,默认关闭,复用现有超级管理员的全局管理权限;普通 staff、匿名访问或单独 Bearer 凭据不能读取全局任务记录。
2518
+ 启用后在账号菜单打开「后台任务运行诊断」,或使用 `GET <挂载前缀>/api/task-diagnostics/?database=default`。该入口只读、不缓存,不提供执行/重试/删除任务操作。
2519
+
2520
+ ```python
2521
+ SIMPLEUI_ANTD_TASK_DIAGNOSTICS = {
2522
+ 'enabled': True, # 默认 False
2523
+ 'history_size': 10, # 每任务/数据库最近 10 次,允许 1–30
2524
+ 'retention_seconds': 604800, # 默认 7 天,允许 60–2592000 秒
2525
+ 'running_timeout_seconds': 3600, # 执行超期提示,允许 1–86400 秒
2526
+ 'stale_after_seconds': { # 可省略;按实际调度周期调整
2527
+ 'webhooks': 300,
2528
+ 'report_exports': 900,
2529
+ 'report_cleanup': 86400,
2530
+ 'audit_retention': 86400,
2531
+ 'share_visits': 86400,
2532
+ },
2533
+ }
2534
+ ```
2535
+
2536
+ 观测 `simpleui_antd_deliver_webhooks`、`simpleui_antd_export_reports`、其 `--prune-only` 文件清理模式、`simpleui_antd_prune_audit` 和 `simpleui_antd_prune_share_visits`。
2537
+ 仅显式 `--apply` 写入运行记录;默认预览不写遥测,已有参数、JSON 输出及退出码保持兼容。
2538
+ 仍由宿主 cron/任务平台运行原命令,诊断页不会调度任务。启用观测不会替代 Webhook、导出或留存功能自身的开关。
2539
+
2540
+ 运行记录复用默认共享缓存,按任务/数据库隔离并使用独立执行标识,迟到的旧执行不会覆盖较新的执行。
2541
+ 仅保存开始/结束 UTC 时间、耗时、状态、固定错误码和允许的整数计数,兼容宿主 `USE_TZ=False`。
2542
+ 不保存命令参数、查询结果、文件路径、目标地址、响应正文、凭据或原始异常;诊断配置/缓存故障不会中断原业务命令。
2543
+ 队列摘要从数据库只读取数量,包括待处理、执行中、失败、到期和失效租约。报表导出/清理服务仍限 `default` 数据库;支持数据库参数的其他命令按选定别名观测。
2544
+
2545
+ 缓存历史受数量、TTL 和逐出约束,不能代替持久审计或进程心跳。
2546
+ 「尚无执行记录」「近期未观测到执行」「执行超时或中断待核对」均需结合调度器核对,不推断进程已经停止或实时在线。
2547
+ 非已识别的共享缓存会提示跨进程共享能力未确认;缓存异常显示记录不可用,队列不可用时单独提示。
2548
+ 阈值只用于界面提示,不自动重试或发送告警;`stale_after_seconds` 的值允许 1–2592000 秒。
2549
+
2550
+ 隔离回归见 `test_share_limits.py`、`test_task_diagnostics.py`;实际 Redis 双进程检查见仓库 `scripts/test_batch1_cache.py`。
2551
+ 真实浏览器与发布边界见 [验收矩阵](https://github.com/neekin/simpleui-antd/blob/main/docs/roadmap-acceptance.md)。