@stackonward/cli 0.0.1 → 1.0.1

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.
Files changed (638) hide show
  1. package/dist/index.js +4588 -205
  2. package/dist/index.js.map +1 -1
  3. package/dist/resources/apple/.claude/docs/architecture-template.md +54 -0
  4. package/dist/resources/apple/.claude/docs/architecture.md +52 -0
  5. package/dist/resources/apple/.claude/docs/index.md +13 -0
  6. package/dist/resources/apple/.claude/docs/security-lifecycle.md +57 -0
  7. package/dist/resources/apple/.claude/docs/stack.md +93 -0
  8. package/dist/resources/apple/.claude/docs/verification.md +63 -0
  9. package/dist/resources/apple/AGENTS.md +397 -0
  10. package/dist/resources/apple/CLAUDE.md +397 -0
  11. package/dist/resources/apple/Gemfile +2 -0
  12. package/dist/resources/apple/Gemfile.lock +365 -0
  13. package/dist/resources/apple/_gitignore +14 -0
  14. package/dist/resources/apple/fastlane/Fastfile +123 -0
  15. package/dist/resources/components/nuxt/site-source/app/app.vue +42 -0
  16. package/dist/resources/components/nuxt/site-source/app/assets/css/site-shell.css +1035 -0
  17. package/dist/resources/components/nuxt/site-source/app/assets/icons/brand/.gitkeep +0 -0
  18. package/dist/resources/components/nuxt/site-source/app/assets/icons/common/.gitkeep +0 -0
  19. package/dist/resources/components/nuxt/site-source/app/components/site/ApplicationHeader.vue +35 -0
  20. package/dist/resources/components/nuxt/site-source/app/components/site/ApplicationShell.vue +56 -0
  21. package/dist/resources/components/nuxt/site-source/app/components/site/ApplicationSidebar.vue +40 -0
  22. package/dist/resources/components/nuxt/site-source/app/components/site/BackToTop.vue +42 -0
  23. package/dist/resources/components/nuxt/site-source/app/components/site/ContentArticlePage.vue +40 -0
  24. package/dist/resources/components/nuxt/site-source/app/components/site/ContentListPage.vue +30 -0
  25. package/dist/resources/components/nuxt/site-source/app/components/site/CookieConsent.vue +18 -0
  26. package/dist/resources/components/nuxt/site-source/app/components/site/MarketingFooter.vue +52 -0
  27. package/dist/resources/components/nuxt/site-source/app/components/site/MarketingHeader.vue +106 -0
  28. package/dist/resources/components/nuxt/site-source/app/components/site/MarketingShell.vue +35 -0
  29. package/dist/resources/components/nuxt/site-source/app/components/site/SiteBrand.vue +10 -0
  30. package/dist/resources/components/nuxt/site-source/app/components/site/StaticPage.vue +21 -0
  31. package/dist/resources/components/nuxt/site-source/app/components/site/ToastViewport.vue +25 -0
  32. package/dist/resources/components/nuxt/site-source/app/components/ui/BaseButton.vue +86 -0
  33. package/dist/resources/components/nuxt/site-source/app/components/ui/BaseGradientBorder.vue +59 -0
  34. package/dist/resources/components/nuxt/site-source/app/components/ui/BaseModal.vue +131 -0
  35. package/dist/resources/components/nuxt/site-source/app/components/ui/BasePopover.vue +150 -0
  36. package/dist/resources/components/nuxt/site-source/app/components/ui/BaseText.vue +138 -0
  37. package/dist/resources/components/nuxt/site-source/app/components/ui/CheckoutStatus.vue +112 -0
  38. package/dist/resources/components/nuxt/site-source/app/components/ui/CheckoutSummary.vue +172 -0
  39. package/dist/resources/components/nuxt/site-source/app/components/ui/GoogleSignInButton.vue +113 -0
  40. package/dist/resources/components/nuxt/site-source/app/components/ui/InfiniteScroll.vue +123 -0
  41. package/dist/resources/components/nuxt/site-source/app/components/ui/YoutubeEmbed.vue +126 -0
  42. package/dist/resources/components/nuxt/site-source/app/composables/useAnalytics.ts +24 -0
  43. package/dist/resources/components/nuxt/site-source/app/composables/useCheckout.ts +121 -0
  44. package/dist/resources/components/nuxt/site-source/app/composables/useConsent.ts +31 -0
  45. package/dist/resources/components/nuxt/site-source/app/composables/useGoogleIdentity.ts +163 -0
  46. package/dist/resources/components/nuxt/site-source/app/composables/useNavigation.ts +11 -0
  47. package/dist/resources/components/nuxt/site-source/app/composables/useSeo.ts +33 -0
  48. package/dist/resources/components/nuxt/site-source/app/composables/useSiteShell.ts +58 -0
  49. package/dist/resources/components/nuxt/site-source/app/composables/useToasts.ts +48 -0
  50. package/dist/resources/components/nuxt/site-source/app/layouts/app.vue +5 -0
  51. package/dist/resources/components/nuxt/site-source/app/layouts/default.vue +5 -0
  52. package/dist/resources/components/nuxt/site-source/app/plugins/analytics.client.ts +121 -0
  53. package/dist/resources/components/nuxt/site-source/app/plugins/google-identity.client.ts +24 -0
  54. package/dist/resources/components/nuxt/site-source/app/theme/accordion.ts +9 -0
  55. package/dist/resources/components/nuxt/site-source/app/theme/breadcrumb.ts +8 -0
  56. package/dist/resources/components/nuxt/site-source/app/theme/button.ts +92 -0
  57. package/dist/resources/components/nuxt/site-source/app/theme/carousel.ts +11 -0
  58. package/dist/resources/components/nuxt/site-source/app/theme/checkbox.ts +12 -0
  59. package/dist/resources/components/nuxt/site-source/app/theme/index.ts +30 -0
  60. package/dist/resources/components/nuxt/site-source/app/theme/input.ts +47 -0
  61. package/dist/resources/components/nuxt/site-source/app/theme/modal.ts +9 -0
  62. package/dist/resources/components/nuxt/site-source/app/theme/pagination.ts +6 -0
  63. package/dist/resources/components/nuxt/site-source/app/theme/select.ts +23 -0
  64. package/dist/resources/components/nuxt/site-source/app/theme/stepper.ts +10 -0
  65. package/dist/resources/components/nuxt/site-source/app/theme/toast.ts +8 -0
  66. package/dist/resources/components/nuxt/site-source/app/theme/tooltip.ts +6 -0
  67. package/dist/resources/components/nuxt/site-source/app/types/site-shell.ts +85 -0
  68. package/dist/resources/components/react/ui/Button.tsx +73 -0
  69. package/dist/resources/components/react/ui/Input.tsx +44 -0
  70. package/dist/resources/components/registry.json +355 -0
  71. package/dist/resources/components/vue/feedback/Toast.vue +120 -0
  72. package/dist/resources/components/vue/feedback/useToast.ts +70 -0
  73. package/dist/resources/components/vue/media/FileUpload.vue +229 -0
  74. package/dist/resources/components/vue/media/VideoPlayer.vue +157 -0
  75. package/dist/resources/components/vue/ui/AppImage.vue +147 -0
  76. package/dist/resources/components/vue/ui/BottomSheet.vue +217 -0
  77. package/dist/resources/components/vue/ui/Button.vue +154 -0
  78. package/dist/resources/components/vue/ui/Card.vue +111 -0
  79. package/dist/resources/components/vue/ui/Input.vue +113 -0
  80. package/dist/resources/components/vue/ui/Modal.vue +193 -0
  81. package/dist/resources/components/vue/ui/ScrollArea.vue +163 -0
  82. package/dist/resources/components/vue/ui/Tooltip.vue +115 -0
  83. package/dist/resources/registry.json +158 -0
  84. package/dist/resources/snippets/node/cache.ts +116 -0
  85. package/dist/resources/snippets/node/date.ts +126 -0
  86. package/dist/resources/snippets/node/logger.ts +104 -0
  87. package/dist/resources/snippets/node/validation.ts +132 -0
  88. package/dist/resources/snippets/react/useDebounce.ts +90 -0
  89. package/dist/resources/snippets/registry.json +65 -0
  90. package/dist/resources/snippets/vue/useAsync.ts +96 -0
  91. package/dist/resources/snippets/vue/usePagination.ts +102 -0
  92. package/dist/resources/templates/base/.claude/docs/architecture-template.md +459 -0
  93. package/dist/resources/templates/base/.claude/docs/architecture.md +137 -0
  94. package/dist/resources/templates/base/.claude/docs/index.md +42 -0
  95. package/dist/resources/templates/base/.claude/docs/security-lifecycle.md +129 -0
  96. package/dist/resources/templates/base/.claude/docs/verification.md +121 -0
  97. package/dist/resources/templates/base/AGENTS.md +415 -0
  98. package/dist/resources/templates/base/CLAUDE.md +415 -0
  99. package/dist/resources/templates/go-backend/.claude/docs/api-design.md +486 -0
  100. package/dist/resources/templates/go-backend/.claude/docs/architecture-template.md +449 -0
  101. package/dist/resources/templates/go-backend/.claude/docs/check-list.md +155 -0
  102. package/dist/resources/templates/go-backend/.claude/docs/database.md +369 -0
  103. package/dist/resources/templates/go-backend/.claude/docs/e2e-standards.md +161 -0
  104. package/dist/resources/templates/go-backend/.claude/docs/error-handling.md +558 -0
  105. package/dist/resources/templates/go-backend/.claude/docs/event-runtime-boundary.md +124 -0
  106. package/dist/resources/templates/go-backend/.claude/docs/identifier-contract.md +195 -0
  107. package/dist/resources/templates/go-backend/.claude/docs/index.md +22 -0
  108. package/dist/resources/templates/go-backend/.claude/docs/logging.md +314 -0
  109. package/dist/resources/templates/go-backend/.claude/docs/mediagen-provider-onboarding-sop.md +917 -0
  110. package/dist/resources/templates/go-backend/.claude/docs/mediagen-provider-quick-reference.md +195 -0
  111. package/dist/resources/templates/go-backend/.claude/docs/module-standards.md +309 -0
  112. package/dist/resources/templates/go-backend/.claude/docs/openapi-contract.md +141 -0
  113. package/dist/resources/templates/go-backend/.claude/docs/payment-gateway-onboarding.md +283 -0
  114. package/dist/resources/templates/go-backend/.claude/docs/platform-boundary.md +191 -0
  115. package/dist/resources/templates/go-backend/.claude/docs/quality-gates.md +112 -0
  116. package/dist/resources/templates/go-backend/.dockerignore +6 -0
  117. package/dist/resources/templates/go-backend/.env.example +13 -0
  118. package/dist/resources/templates/go-backend/.gitattributes +2 -0
  119. package/dist/resources/templates/go-backend/AGENTS.md +1139 -0
  120. package/dist/resources/templates/go-backend/CLAUDE.md +1139 -0
  121. package/dist/resources/templates/go-backend/Dockerfile +14 -0
  122. package/dist/resources/templates/go-backend/Makefile +71 -0
  123. package/dist/resources/templates/go-backend/README.md +66 -0
  124. package/dist/resources/templates/go-backend/_gitignore +10 -0
  125. package/dist/resources/templates/go-backend/api/openapi/README.md +29 -0
  126. package/dist/resources/templates/go-backend/api/openapi/dist/artifact-manifest.json +19 -0
  127. package/dist/resources/templates/go-backend/api/openapi/dist/conformance-matrix.json +276 -0
  128. package/dist/resources/templates/go-backend/api/openapi/document.go +30 -0
  129. package/dist/resources/templates/go-backend/api/openapi/package.json +28 -0
  130. package/dist/resources/templates/go-backend/api/openapi/pnpm-lock.yaml +3052 -0
  131. package/dist/resources/templates/go-backend/api/openapi/redocly-source.yaml +18 -0
  132. package/dist/resources/templates/go-backend/api/openapi/redocly.yaml +8 -0
  133. package/dist/resources/templates/go-backend/api/openapi/sdk-generators.json +16 -0
  134. package/dist/resources/templates/go-backend/api/openapi/service-registry.json +39 -0
  135. package/dist/resources/templates/go-backend/api/openapi/services/product-api/conformance/runtime.arazzo.yaml +65 -0
  136. package/dist/resources/templates/go-backend/api/openapi/services/product-api/dist/internal.openapi.json +327 -0
  137. package/dist/resources/templates/go-backend/api/openapi/services/product-api/dist/runtime-contract.json +56 -0
  138. package/dist/resources/templates/go-backend/api/openapi/services/product-api/openapi.yaml +222 -0
  139. package/dist/resources/templates/go-backend/api/openapi/services/worker/conformance/runtime.arazzo.yaml +65 -0
  140. package/dist/resources/templates/go-backend/api/openapi/services/worker/dist/internal.openapi.json +327 -0
  141. package/dist/resources/templates/go-backend/api/openapi/services/worker/dist/runtime-contract.json +56 -0
  142. package/dist/resources/templates/go-backend/api/openapi/services/worker/openapi.yaml +222 -0
  143. package/dist/resources/templates/go-backend/api/openapi/tooling/breaking.mjs +416 -0
  144. package/dist/resources/templates/go-backend/api/openapi/tooling/build.mjs +103 -0
  145. package/dist/resources/templates/go-backend/api/openapi/tooling/conformance_coverage.mjs +111 -0
  146. package/dist/resources/templates/go-backend/api/openapi/tooling/contract_compiler.mjs +62 -0
  147. package/dist/resources/templates/go-backend/api/openapi/tooling/contract_policy.mjs +208 -0
  148. package/dist/resources/templates/go-backend/api/openapi/tooling/delegated_contract.mjs +302 -0
  149. package/dist/resources/templates/go-backend/api/openapi/tooling/deterministic_archive.mjs +69 -0
  150. package/dist/resources/templates/go-backend/api/openapi/tooling/documentation_example_privacy.mjs +93 -0
  151. package/dist/resources/templates/go-backend/api/openapi/tooling/documentation_projection.mjs +498 -0
  152. package/dist/resources/templates/go-backend/api/openapi/tooling/documentation_quality.mjs +346 -0
  153. package/dist/resources/templates/go-backend/api/openapi/tooling/git_artifact_history.mjs +188 -0
  154. package/dist/resources/templates/go-backend/api/openapi/tooling/manifest.mjs +170 -0
  155. package/dist/resources/templates/go-backend/api/openapi/tooling/overlay.mjs +98 -0
  156. package/dist/resources/templates/go-backend/api/openapi/tooling/project.mjs +549 -0
  157. package/dist/resources/templates/go-backend/api/openapi/tooling/route_contract.mjs +156 -0
  158. package/dist/resources/templates/go-backend/api/openapi/tooling/runtime_conformance.mjs +193 -0
  159. package/dist/resources/templates/go-backend/api/openapi/tooling/scalar_asset_localization.mjs +164 -0
  160. package/dist/resources/templates/go-backend/api/openapi/tooling/sdk_generator.mjs +229 -0
  161. package/dist/resources/templates/go-backend/api/openapi/tooling/semantic_version.mjs +64 -0
  162. package/dist/resources/templates/go-backend/api/openapi/tooling/service_contract_inputs.mjs +16 -0
  163. package/dist/resources/templates/go-backend/api/openapi/tooling/test/breaking.test.mjs +365 -0
  164. package/dist/resources/templates/go-backend/api/openapi/tooling/test/build.test.mjs +42 -0
  165. package/dist/resources/templates/go-backend/api/openapi/tooling/test/conformance_coverage.test.mjs +88 -0
  166. package/dist/resources/templates/go-backend/api/openapi/tooling/test/contract_policy.test.mjs +257 -0
  167. package/dist/resources/templates/go-backend/api/openapi/tooling/test/delegated_contract.test.mjs +205 -0
  168. package/dist/resources/templates/go-backend/api/openapi/tooling/test/documentation_example_privacy.test.mjs +51 -0
  169. package/dist/resources/templates/go-backend/api/openapi/tooling/test/documentation_projection.test.mjs +368 -0
  170. package/dist/resources/templates/go-backend/api/openapi/tooling/test/documentation_quality.test.mjs +151 -0
  171. package/dist/resources/templates/go-backend/api/openapi/tooling/test/git_artifact_history.test.mjs +60 -0
  172. package/dist/resources/templates/go-backend/api/openapi/tooling/test/head_policy.test.mjs +8 -0
  173. package/dist/resources/templates/go-backend/api/openapi/tooling/test/overlay.test.mjs +44 -0
  174. package/dist/resources/templates/go-backend/api/openapi/tooling/test/projection.test.mjs +270 -0
  175. package/dist/resources/templates/go-backend/api/openapi/tooling/test/redocly_documentation_rules.test.mjs +146 -0
  176. package/dist/resources/templates/go-backend/api/openapi/tooling/test/release_manifest.test.mjs +76 -0
  177. package/dist/resources/templates/go-backend/api/openapi/tooling/test/route_contract.test.mjs +119 -0
  178. package/dist/resources/templates/go-backend/api/openapi/tooling/test/runtime_conformance.test.mjs +75 -0
  179. package/dist/resources/templates/go-backend/api/openapi/tooling/test/scalar_asset_localization.test.mjs +50 -0
  180. package/dist/resources/templates/go-backend/api/openapi/tooling/test/sdk_generator.test.mjs +31 -0
  181. package/dist/resources/templates/go-backend/api/openapi/tooling/test/semantic_version.test.mjs +60 -0
  182. package/dist/resources/templates/go-backend/api/openapi/tooling/test/service_contract_inputs.test.mjs +27 -0
  183. package/dist/resources/templates/go-backend/cmd/internal/runtime/resources.go +125 -0
  184. package/dist/resources/templates/go-backend/cmd/internal/runtime/runtime.go +102 -0
  185. package/dist/resources/templates/go-backend/cmd/internal/runtime/runtime_test.go +70 -0
  186. package/dist/resources/templates/go-backend/cmd/product-api/lifecycle_test.go +58 -0
  187. package/dist/resources/templates/go-backend/cmd/product-api/main.go +32 -0
  188. package/dist/resources/templates/go-backend/cmd/product-api/wire.go +13 -0
  189. package/dist/resources/templates/go-backend/cmd/product-api/wire_gen.go +94 -0
  190. package/dist/resources/templates/go-backend/cmd/worker/main.go +47 -0
  191. package/dist/resources/templates/go-backend/cmd/worker/wire.go +14 -0
  192. package/dist/resources/templates/go-backend/cmd/worker/wire_gen.go +111 -0
  193. package/dist/resources/templates/go-backend/configs/config.yaml +37 -0
  194. package/dist/resources/templates/go-backend/deployments/compose.yaml +71 -0
  195. package/dist/resources/templates/go-backend/docs/openapi-provenance.json +489 -0
  196. package/dist/resources/templates/go-backend/docs/runtime-provenance.json +175 -0
  197. package/dist/resources/templates/go-backend/docs/source-provenance.json +3568 -0
  198. package/dist/resources/templates/go-backend/docs/standards-provenance.json +1624 -0
  199. package/dist/resources/templates/go-backend/go.mod +256 -0
  200. package/dist/resources/templates/go-backend/go.sum +805 -0
  201. package/dist/resources/templates/go-backend/internal/common/canonicaljson/canonical.go +370 -0
  202. package/dist/resources/templates/go-backend/internal/common/canonicaljson/canonical_test.go +223 -0
  203. package/dist/resources/templates/go-backend/internal/common/checkoutbff/contracts.go +125 -0
  204. package/dist/resources/templates/go-backend/internal/common/checkoutbff/platform_client.go +212 -0
  205. package/dist/resources/templates/go-backend/internal/common/checkoutbff/platform_client_test.go +319 -0
  206. package/dist/resources/templates/go-backend/internal/common/contentaccess/contentaccess.go +50 -0
  207. package/dist/resources/templates/go-backend/internal/common/contracts/contact.go +85 -0
  208. package/dist/resources/templates/go-backend/internal/common/ctxutil/identity.go +20 -0
  209. package/dist/resources/templates/go-backend/internal/common/ctxutil/product.go +53 -0
  210. package/dist/resources/templates/go-backend/internal/common/enumdomain/payment.go +12 -0
  211. package/dist/resources/templates/go-backend/internal/common/enums/dto_admin.go +9 -0
  212. package/dist/resources/templates/go-backend/internal/common/enums/enum.go +1247 -0
  213. package/dist/resources/templates/go-backend/internal/common/enums/handler_admin.go +66 -0
  214. package/dist/resources/templates/go-backend/internal/common/enums/module.go +46 -0
  215. package/dist/resources/templates/go-backend/internal/common/enums/provider.go +10 -0
  216. package/dist/resources/templates/go-backend/internal/common/enums/register.go +11 -0
  217. package/dist/resources/templates/go-backend/internal/common/enums/register_audit.go +274 -0
  218. package/dist/resources/templates/go-backend/internal/common/enums/register_command_replay.go +12 -0
  219. package/dist/resources/templates/go-backend/internal/common/enums/register_config.go +27 -0
  220. package/dist/resources/templates/go-backend/internal/common/enums/register_locale.go +37 -0
  221. package/dist/resources/templates/go-backend/internal/common/enums/register_ops.go +14 -0
  222. package/dist/resources/templates/go-backend/internal/common/enums/register_outbox.go +15 -0
  223. package/dist/resources/templates/go-backend/internal/common/enums/register_rule_expression.go +63 -0
  224. package/dist/resources/templates/go-backend/internal/common/enums/registry.go +77 -0
  225. package/dist/resources/templates/go-backend/internal/common/httporigin/origin.go +62 -0
  226. package/dist/resources/templates/go-backend/internal/common/httporigin/origin_test.go +38 -0
  227. package/dist/resources/templates/go-backend/internal/common/iamcredential/plaintext.go +35 -0
  228. package/dist/resources/templates/go-backend/internal/common/iamcredential/plaintext_test.go +29 -0
  229. package/dist/resources/templates/go-backend/internal/common/iamscope/scope.go +212 -0
  230. package/dist/resources/templates/go-backend/internal/common/iamscope/scope_binding.go +26 -0
  231. package/dist/resources/templates/go-backend/internal/common/iamscope/scope_filter.go +173 -0
  232. package/dist/resources/templates/go-backend/internal/common/iamscope/scope_filter_test.go +279 -0
  233. package/dist/resources/templates/go-backend/internal/common/internalapi/authorization_details.go +10 -0
  234. package/dist/resources/templates/go-backend/internal/common/internalapi/route_permission_policies.go +660 -0
  235. package/dist/resources/templates/go-backend/internal/common/internalapi/route_permission_policies_test.go +331 -0
  236. package/dist/resources/templates/go-backend/internal/common/internalservice/audience_token_source.go +166 -0
  237. package/dist/resources/templates/go-backend/internal/common/internalservice/bootstrap_token_exchanger.go +183 -0
  238. package/dist/resources/templates/go-backend/internal/common/internalservice/bound_service_token_signer.go +52 -0
  239. package/dist/resources/templates/go-backend/internal/common/internalservice/delegated_access_token_exchanger.go +189 -0
  240. package/dist/resources/templates/go-backend/internal/common/internalservice/delegated_access_token_exchanger_test.go +159 -0
  241. package/dist/resources/templates/go-backend/internal/common/internalservice/product_scoped_service_token_signer.go +92 -0
  242. package/dist/resources/templates/go-backend/internal/common/internalservice/provider.go +25 -0
  243. package/dist/resources/templates/go-backend/internal/common/internalservice/request_signer.go +86 -0
  244. package/dist/resources/templates/go-backend/internal/common/internalservice/resolver.go +69 -0
  245. package/dist/resources/templates/go-backend/internal/common/internalservice/resolver_test.go +42 -0
  246. package/dist/resources/templates/go-backend/internal/common/internalservice/route_scope.go +95 -0
  247. package/dist/resources/templates/go-backend/internal/common/internalservice/route_scope_test.go +78 -0
  248. package/dist/resources/templates/go-backend/internal/common/internalservice/service_identity_test.go +387 -0
  249. package/dist/resources/templates/go-backend/internal/common/jsonschema/compiler.go +100 -0
  250. package/dist/resources/templates/go-backend/internal/common/jsonschema/compiler_test.go +41 -0
  251. package/dist/resources/templates/go-backend/internal/common/jwksapi/handler.go +83 -0
  252. package/dist/resources/templates/go-backend/internal/common/jwksapi/handler_test.go +104 -0
  253. package/dist/resources/templates/go-backend/internal/common/locale/locale.go +145 -0
  254. package/dist/resources/templates/go-backend/internal/common/locale/locale_test.go +75 -0
  255. package/dist/resources/templates/go-backend/internal/common/mediaref/mediaref.go +54 -0
  256. package/dist/resources/templates/go-backend/internal/common/mediaref/mediaref_test.go +77 -0
  257. package/dist/resources/templates/go-backend/internal/common/middleware/apikey_auth.go +320 -0
  258. package/dist/resources/templates/go-backend/internal/common/middleware/apikey_throttle.go +158 -0
  259. package/dist/resources/templates/go-backend/internal/common/middleware/auth.go +40 -0
  260. package/dist/resources/templates/go-backend/internal/common/middleware/auth_test.go +50 -0
  261. package/dist/resources/templates/go-backend/internal/common/middleware/client_context.go +86 -0
  262. package/dist/resources/templates/go-backend/internal/common/middleware/cors.go +68 -0
  263. package/dist/resources/templates/go-backend/internal/common/middleware/event_context.go +18 -0
  264. package/dist/resources/templates/go-backend/internal/common/middleware/gateway_identity.go +279 -0
  265. package/dist/resources/templates/go-backend/internal/common/middleware/helpers.go +159 -0
  266. package/dist/resources/templates/go-backend/internal/common/middleware/iam_delegated_route_permission.go +171 -0
  267. package/dist/resources/templates/go-backend/internal/common/middleware/iam_delegated_token_auth.go +151 -0
  268. package/dist/resources/templates/go-backend/internal/common/middleware/iam_delegated_token_auth_test.go +99 -0
  269. package/dist/resources/templates/go-backend/internal/common/middleware/iam_service_identity.go +138 -0
  270. package/dist/resources/templates/go-backend/internal/common/middleware/iam_service_token_auth.go +74 -0
  271. package/dist/resources/templates/go-backend/internal/common/middleware/idempotent.go +273 -0
  272. package/dist/resources/templates/go-backend/internal/common/middleware/idempotent_test.go +83 -0
  273. package/dist/resources/templates/go-backend/internal/common/middleware/internal_route_permission.go +45 -0
  274. package/dist/resources/templates/go-backend/internal/common/middleware/internal_route_permission_test.go +186 -0
  275. package/dist/resources/templates/go-backend/internal/common/middleware/logging.go +191 -0
  276. package/dist/resources/templates/go-backend/internal/common/middleware/metrics.go +45 -0
  277. package/dist/resources/templates/go-backend/internal/common/middleware/middleware_test.go +1516 -0
  278. package/dist/resources/templates/go-backend/internal/common/middleware/principal_context.go +160 -0
  279. package/dist/resources/templates/go-backend/internal/common/middleware/principal_context_test.go +355 -0
  280. package/dist/resources/templates/go-backend/internal/common/middleware/product_context.go +100 -0
  281. package/dist/resources/templates/go-backend/internal/common/middleware/recovery.go +40 -0
  282. package/dist/resources/templates/go-backend/internal/common/middleware/referral_attribution.go +178 -0
  283. package/dist/resources/templates/go-backend/internal/common/middleware/requestid.go +48 -0
  284. package/dist/resources/templates/go-backend/internal/common/middleware/resource_permission.go +790 -0
  285. package/dist/resources/templates/go-backend/internal/common/middleware/resource_permission_test.go +440 -0
  286. package/dist/resources/templates/go-backend/internal/common/middleware/service_credential_auth.go +67 -0
  287. package/dist/resources/templates/go-backend/internal/common/middleware/tenant.go +148 -0
  288. package/dist/resources/templates/go-backend/internal/common/middleware/throttle.go +310 -0
  289. package/dist/resources/templates/go-backend/internal/common/middleware/trusted_client_ip.go +23 -0
  290. package/dist/resources/templates/go-backend/internal/common/middleware/trusted_client_ip_test.go +60 -0
  291. package/dist/resources/templates/go-backend/internal/common/money/currency.go +25 -0
  292. package/dist/resources/templates/go-backend/internal/common/money/errors.go +23 -0
  293. package/dist/resources/templates/go-backend/internal/common/money/format.go +123 -0
  294. package/dist/resources/templates/go-backend/internal/common/money/format_test.go +204 -0
  295. package/dist/resources/templates/go-backend/internal/common/money/gateway.go +144 -0
  296. package/dist/resources/templates/go-backend/internal/common/money/gateway_test.go +165 -0
  297. package/dist/resources/templates/go-backend/internal/common/money/rounding.go +56 -0
  298. package/dist/resources/templates/go-backend/internal/common/money/rounding_test.go +51 -0
  299. package/dist/resources/templates/go-backend/internal/common/principal/context.go +56 -0
  300. package/dist/resources/templates/go-backend/internal/common/principal/principal.go +49 -0
  301. package/dist/resources/templates/go-backend/internal/common/productcode/code.go +24 -0
  302. package/dist/resources/templates/go-backend/internal/common/productcode/code_test.go +35 -0
  303. package/dist/resources/templates/go-backend/internal/common/providerconfig/schema.go +577 -0
  304. package/dist/resources/templates/go-backend/internal/common/providerconfig/schema_test.go +47 -0
  305. package/dist/resources/templates/go-backend/internal/common/requestprivacy/request.go +246 -0
  306. package/dist/resources/templates/go-backend/internal/common/requestprivacy/request_test.go +170 -0
  307. package/dist/resources/templates/go-backend/internal/common/response/pagination.go +60 -0
  308. package/dist/resources/templates/go-backend/internal/common/response/response.go +110 -0
  309. package/dist/resources/templates/go-backend/internal/common/response/response_test.go +463 -0
  310. package/dist/resources/templates/go-backend/internal/common/ruleexpression/ast.go +141 -0
  311. package/dist/resources/templates/go-backend/internal/common/ruleexpression/ast_test.go +81 -0
  312. package/dist/resources/templates/go-backend/internal/common/ruleexpression/canonical.go +184 -0
  313. package/dist/resources/templates/go-backend/internal/common/ruleexpression/canonical_test.go +319 -0
  314. package/dist/resources/templates/go-backend/internal/common/ruleexpression/decoder.go +555 -0
  315. package/dist/resources/templates/go-backend/internal/common/ruleexpression/decoder_test.go +260 -0
  316. package/dist/resources/templates/go-backend/internal/common/ruleexpression/errors.go +55 -0
  317. package/dist/resources/templates/go-backend/internal/common/ruleexpression/evaluator.go +96 -0
  318. package/dist/resources/templates/go-backend/internal/common/ruleexpression/evaluator_test.go +220 -0
  319. package/dist/resources/templates/go-backend/internal/common/ruleexpression/fuzz_test.go +116 -0
  320. package/dist/resources/templates/go-backend/internal/common/ruleexpression/limits.go +331 -0
  321. package/dist/resources/templates/go-backend/internal/common/ruleexpression/schema.go +412 -0
  322. package/dist/resources/templates/go-backend/internal/common/ruleexpression/schema_test.go +164 -0
  323. package/dist/resources/templates/go-backend/internal/common/runtimeinput/runtime_input.go +627 -0
  324. package/dist/resources/templates/go-backend/internal/common/runtimeinput/runtime_input_test.go +245 -0
  325. package/dist/resources/templates/go-backend/internal/common/semver/errors.go +6 -0
  326. package/dist/resources/templates/go-backend/internal/common/semver/semver.go +125 -0
  327. package/dist/resources/templates/go-backend/internal/common/semver/semver_test.go +205 -0
  328. package/dist/resources/templates/go-backend/internal/common/serviceidentity/audience.go +48 -0
  329. package/dist/resources/templates/go-backend/internal/common/serviceidentity/audience_test.go +35 -0
  330. package/dist/resources/templates/go-backend/internal/common/serviceidentity/resource_identifier.go +33 -0
  331. package/dist/resources/templates/go-backend/internal/common/serviceidentity/resource_identifier_test.go +32 -0
  332. package/dist/resources/templates/go-backend/internal/common/tradeno/generator.go +99 -0
  333. package/dist/resources/templates/go-backend/internal/common/tradeno/generator_test.go +396 -0
  334. package/dist/resources/templates/go-backend/internal/common/tradeno/prefix.go +247 -0
  335. package/dist/resources/templates/go-backend/internal/common/types/asset_url.go +11 -0
  336. package/dist/resources/templates/go-backend/internal/common/types/billing.go +89 -0
  337. package/dist/resources/templates/go-backend/internal/common/types/billing_test.go +77 -0
  338. package/dist/resources/templates/go-backend/internal/common/types/country.go +74 -0
  339. package/dist/resources/templates/go-backend/internal/common/types/country_test.go +28 -0
  340. package/dist/resources/templates/go-backend/internal/common/types/currency.go +60 -0
  341. package/dist/resources/templates/go-backend/internal/common/types/identifiers.go +41 -0
  342. package/dist/resources/templates/go-backend/internal/common/types/operator.go +28 -0
  343. package/dist/resources/templates/go-backend/internal/common/types/subscription_entitlement.go +24 -0
  344. package/dist/resources/templates/go-backend/internal/common/types/subscription_entitlement_test.go +35 -0
  345. package/dist/resources/templates/go-backend/internal/common/types/tier.go +72 -0
  346. package/dist/resources/templates/go-backend/internal/common/types/types.go +411 -0
  347. package/dist/resources/templates/go-backend/internal/common/types/types_test.go +295 -0
  348. package/dist/resources/templates/go-backend/internal/common/utils/constants.go +100 -0
  349. package/dist/resources/templates/go-backend/internal/common/utils/crypto.go +158 -0
  350. package/dist/resources/templates/go-backend/internal/common/utils/crypto_test.go +345 -0
  351. package/dist/resources/templates/go-backend/internal/common/utils/duration.go +25 -0
  352. package/dist/resources/templates/go-backend/internal/common/utils/helpers.go +18 -0
  353. package/dist/resources/templates/go-backend/internal/common/utils/json.go +62 -0
  354. package/dist/resources/templates/go-backend/internal/common/utils/json_test.go +76 -0
  355. package/dist/resources/templates/go-backend/internal/common/utils/pagination.go +51 -0
  356. package/dist/resources/templates/go-backend/internal/common/utils/pagination_test.go +45 -0
  357. package/dist/resources/templates/go-backend/internal/common/utils/pointer.go +107 -0
  358. package/dist/resources/templates/go-backend/internal/common/utils/pointer_test.go +15 -0
  359. package/dist/resources/templates/go-backend/internal/common/utils/slice.go +24 -0
  360. package/dist/resources/templates/go-backend/internal/common/utils/snowflake.go +110 -0
  361. package/dist/resources/templates/go-backend/internal/common/utils/snowflake_test.go +316 -0
  362. package/dist/resources/templates/go-backend/internal/common/utils/string.go +16 -0
  363. package/dist/resources/templates/go-backend/internal/common/utils/timefmt.go +42 -0
  364. package/dist/resources/templates/go-backend/internal/common/validator/validator.go +136 -0
  365. package/dist/resources/templates/go-backend/internal/common/validator/validator_test.go +240 -0
  366. package/dist/resources/templates/go-backend/internal/common/youtubeapi/client.go +224 -0
  367. package/dist/resources/templates/go-backend/internal/core/apicontract/document.go +269 -0
  368. package/dist/resources/templates/go-backend/internal/core/apicontract/document_test.go +104 -0
  369. package/dist/resources/templates/go-backend/internal/core/apicontract/route_validator.go +78 -0
  370. package/dist/resources/templates/go-backend/internal/core/apicontract/route_validator_test.go +108 -0
  371. package/dist/resources/templates/go-backend/internal/core/apidocs/assets/scalar-api-reference.js +2385 -0
  372. package/dist/resources/templates/go-backend/internal/core/apidocs/assets/scalar-api-reference.metadata.json +10 -0
  373. package/dist/resources/templates/go-backend/internal/core/apidocs/route.go +107 -0
  374. package/dist/resources/templates/go-backend/internal/core/apidocs/route_test.go +200 -0
  375. package/dist/resources/templates/go-backend/internal/core/apidocs/scalar.go +117 -0
  376. package/dist/resources/templates/go-backend/internal/core/apidocs/scalar_test.go +99 -0
  377. package/dist/resources/templates/go-backend/internal/core/audit/access_log_policy.go +91 -0
  378. package/dist/resources/templates/go-backend/internal/core/audit/access_log_policy_test.go +51 -0
  379. package/dist/resources/templates/go-backend/internal/core/audit/actor_resolver.go +36 -0
  380. package/dist/resources/templates/go-backend/internal/core/audit/constants.go +30 -0
  381. package/dist/resources/templates/go-backend/internal/core/audit/context.go +119 -0
  382. package/dist/resources/templates/go-backend/internal/core/audit/context_test.go +310 -0
  383. package/dist/resources/templates/go-backend/internal/core/audit/dto.go +468 -0
  384. package/dist/resources/templates/go-backend/internal/core/audit/dto_admin.go +172 -0
  385. package/dist/resources/templates/go-backend/internal/core/audit/enum.go +460 -0
  386. package/dist/resources/templates/go-backend/internal/core/audit/enum_test.go +314 -0
  387. package/dist/resources/templates/go-backend/internal/core/audit/errors.go +9 -0
  388. package/dist/resources/templates/go-backend/internal/core/audit/handler.go +568 -0
  389. package/dist/resources/templates/go-backend/internal/core/audit/listener.go +319 -0
  390. package/dist/resources/templates/go-backend/internal/core/audit/listener_test.go +98 -0
  391. package/dist/resources/templates/go-backend/internal/core/audit/middleware.go +669 -0
  392. package/dist/resources/templates/go-backend/internal/core/audit/middleware_test.go +268 -0
  393. package/dist/resources/templates/go-backend/internal/core/audit/model.go +306 -0
  394. package/dist/resources/templates/go-backend/internal/core/audit/partition_job.go +316 -0
  395. package/dist/resources/templates/go-backend/internal/core/audit/ports.go +9 -0
  396. package/dist/resources/templates/go-backend/internal/core/audit/provider.go +30 -0
  397. package/dist/resources/templates/go-backend/internal/core/audit/repository.go +1007 -0
  398. package/dist/resources/templates/go-backend/internal/core/audit/repository_test.go +934 -0
  399. package/dist/resources/templates/go-backend/internal/core/audit/router.go +85 -0
  400. package/dist/resources/templates/go-backend/internal/core/audit/service.go +770 -0
  401. package/dist/resources/templates/go-backend/internal/core/audit/service_test.go +331 -0
  402. package/dist/resources/templates/go-backend/internal/core/audit/vo_admin.go +475 -0
  403. package/dist/resources/templates/go-backend/internal/core/cache/config.go +30 -0
  404. package/dist/resources/templates/go-backend/internal/core/cache/key.go +41 -0
  405. package/dist/resources/templates/go-backend/internal/core/cache/key_test.go +62 -0
  406. package/dist/resources/templates/go-backend/internal/core/cache/l1.go +153 -0
  407. package/dist/resources/templates/go-backend/internal/core/cache/l1_test.go +212 -0
  408. package/dist/resources/templates/go-backend/internal/core/cache/manager.go +278 -0
  409. package/dist/resources/templates/go-backend/internal/core/cache/manager_provider.go +24 -0
  410. package/dist/resources/templates/go-backend/internal/core/cache/metrics.go +126 -0
  411. package/dist/resources/templates/go-backend/internal/core/cache/namespace.go +46 -0
  412. package/dist/resources/templates/go-backend/internal/core/cache/namespace_test.go +90 -0
  413. package/dist/resources/templates/go-backend/internal/core/cache/options.go +39 -0
  414. package/dist/resources/templates/go-backend/internal/core/cache/options_test.go +96 -0
  415. package/dist/resources/templates/go-backend/internal/core/cache/redis.go +311 -0
  416. package/dist/resources/templates/go-backend/internal/core/cache/redis_test.go +51 -0
  417. package/dist/resources/templates/go-backend/internal/core/cache/serializer.go +31 -0
  418. package/dist/resources/templates/go-backend/internal/core/cache/serializer_test.go +139 -0
  419. package/dist/resources/templates/go-backend/internal/core/cache/singleflight.go +53 -0
  420. package/dist/resources/templates/go-backend/internal/core/cache/singleflight_test.go +211 -0
  421. package/dist/resources/templates/go-backend/internal/core/cache/warming.go +86 -0
  422. package/dist/resources/templates/go-backend/internal/core/clock/clock.go +66 -0
  423. package/dist/resources/templates/go-backend/internal/core/clock/clock_test.go +26 -0
  424. package/dist/resources/templates/go-backend/internal/core/clock/runtime_clock.go +283 -0
  425. package/dist/resources/templates/go-backend/internal/core/clock/runtime_clock_test.go +98 -0
  426. package/dist/resources/templates/go-backend/internal/core/commandreplay/errors.go +6 -0
  427. package/dist/resources/templates/go-backend/internal/core/commandreplay/model.go +90 -0
  428. package/dist/resources/templates/go-backend/internal/core/commandreplay/model_test.go +79 -0
  429. package/dist/resources/templates/go-backend/internal/core/commandreplay/namespace.go +7 -0
  430. package/dist/resources/templates/go-backend/internal/core/commandreplay/persistence.go +269 -0
  431. package/dist/resources/templates/go-backend/internal/core/commandreplay/persistence_test.go +185 -0
  432. package/dist/resources/templates/go-backend/internal/core/commandreplay/provider.go +9 -0
  433. package/dist/resources/templates/go-backend/internal/core/commandreplay/repository.go +22 -0
  434. package/dist/resources/templates/go-backend/internal/core/commandreplay/service.go +853 -0
  435. package/dist/resources/templates/go-backend/internal/core/commandreplay/service_persistence_test.go +432 -0
  436. package/dist/resources/templates/go-backend/internal/core/commandreplay/service_test.go +891 -0
  437. package/dist/resources/templates/go-backend/internal/core/commandreplay/types.go +121 -0
  438. package/dist/resources/templates/go-backend/internal/core/config/config.go +2069 -0
  439. package/dist/resources/templates/go-backend/internal/core/config/config_test.go +1122 -0
  440. package/dist/resources/templates/go-backend/internal/core/config/contract/contract_test.go +388 -0
  441. package/dist/resources/templates/go-backend/internal/core/config/contract/docs.go +198 -0
  442. package/dist/resources/templates/go-backend/internal/core/config/contract/verify.go +424 -0
  443. package/dist/resources/templates/go-backend/internal/core/config/enum.go +46 -0
  444. package/dist/resources/templates/go-backend/internal/core/config/metadata.go +717 -0
  445. package/dist/resources/templates/go-backend/internal/core/config/metadata_test.go +228 -0
  446. package/dist/resources/templates/go-backend/internal/core/config/product_admin_config_test.go +210 -0
  447. package/dist/resources/templates/go-backend/internal/core/crypto/aes_gcm.go +111 -0
  448. package/dist/resources/templates/go-backend/internal/core/crypto/aes_gcm_test.go +364 -0
  449. package/dist/resources/templates/go-backend/internal/core/crypto/encryptor.go +12 -0
  450. package/dist/resources/templates/go-backend/internal/core/crypto/envelope_encryptor.go +134 -0
  451. package/dist/resources/templates/go-backend/internal/core/crypto/envelope_encryptor_test.go +118 -0
  452. package/dist/resources/templates/go-backend/internal/core/crypto/pem.go +112 -0
  453. package/dist/resources/templates/go-backend/internal/core/crypto/pem_test.go +44 -0
  454. package/dist/resources/templates/go-backend/internal/core/crypto/provider.go +130 -0
  455. package/dist/resources/templates/go-backend/internal/core/crypto/provider_test.go +92 -0
  456. package/dist/resources/templates/go-backend/internal/core/database/base_model.go +24 -0
  457. package/dist/resources/templates/go-backend/internal/core/database/clickhouse.go +121 -0
  458. package/dist/resources/templates/go-backend/internal/core/database/database.go +112 -0
  459. package/dist/resources/templates/go-backend/internal/core/database/database_logger_test.go +95 -0
  460. package/dist/resources/templates/go-backend/internal/core/database/errors.go +25 -0
  461. package/dist/resources/templates/go-backend/internal/core/database/metrics_plugin.go +105 -0
  462. package/dist/resources/templates/go-backend/internal/core/database/pool_collector.go +71 -0
  463. package/dist/resources/templates/go-backend/internal/core/database/read_replica.go +520 -0
  464. package/dist/resources/templates/go-backend/internal/core/database/transaction.go +170 -0
  465. package/dist/resources/templates/go-backend/internal/core/database/transaction_test.go +282 -0
  466. package/dist/resources/templates/go-backend/internal/core/distlock/lock.go +353 -0
  467. package/dist/resources/templates/go-backend/internal/core/distlock/lock_test.go +106 -0
  468. package/dist/resources/templates/go-backend/internal/core/errors/handler.go +189 -0
  469. package/dist/resources/templates/go-backend/internal/core/errors/handler_test.go +515 -0
  470. package/dist/resources/templates/go-backend/internal/core/eventbus/eventbus.go +667 -0
  471. package/dist/resources/templates/go-backend/internal/core/eventbus/eventbus_test.go +792 -0
  472. package/dist/resources/templates/go-backend/internal/core/eventstream/noop.go +22 -0
  473. package/dist/resources/templates/go-backend/internal/core/eventstream/provider.go +20 -0
  474. package/dist/resources/templates/go-backend/internal/core/eventstream/publisher.go +38 -0
  475. package/dist/resources/templates/go-backend/internal/core/eventstream/redis.go +109 -0
  476. package/dist/resources/templates/go-backend/internal/core/eventstream/routing.go +104 -0
  477. package/dist/resources/templates/go-backend/internal/core/eventstream/routing_test.go +71 -0
  478. package/dist/resources/templates/go-backend/internal/core/geoip/model.go +19 -0
  479. package/dist/resources/templates/go-backend/internal/core/geoip/service.go +88 -0
  480. package/dist/resources/templates/go-backend/internal/core/health/health.go +333 -0
  481. package/dist/resources/templates/go-backend/internal/core/health/health_test.go +27 -0
  482. package/dist/resources/templates/go-backend/internal/core/httpclient/client.go +772 -0
  483. package/dist/resources/templates/go-backend/internal/core/httpclient/client_test.go +287 -0
  484. package/dist/resources/templates/go-backend/internal/core/httpclient/download.go +578 -0
  485. package/dist/resources/templates/go-backend/internal/core/httpclient/download_test.go +374 -0
  486. package/dist/resources/templates/go-backend/internal/core/jwtkeyring/jwk.go +38 -0
  487. package/dist/resources/templates/go-backend/internal/core/jwtkeyring/keyring.go +144 -0
  488. package/dist/resources/templates/go-backend/internal/core/jwtkeyring/keyring_test.go +128 -0
  489. package/dist/resources/templates/go-backend/internal/core/jwtkeyring/registry.go +60 -0
  490. package/dist/resources/templates/go-backend/internal/core/logger/logger.go +53 -0
  491. package/dist/resources/templates/go-backend/internal/core/logger/logger_test.go +194 -0
  492. package/dist/resources/templates/go-backend/internal/core/observability/metrics.go +358 -0
  493. package/dist/resources/templates/go-backend/internal/core/observability/sentry.go +445 -0
  494. package/dist/resources/templates/go-backend/internal/core/observability/sentry_privacy_test.go +175 -0
  495. package/dist/resources/templates/go-backend/internal/core/observability/tracing.go +187 -0
  496. package/dist/resources/templates/go-backend/internal/core/outbox/model.go +314 -0
  497. package/dist/resources/templates/go-backend/internal/core/outbox/model_test.go +77 -0
  498. package/dist/resources/templates/go-backend/internal/core/outbox/persistence.go +51 -0
  499. package/dist/resources/templates/go-backend/internal/core/outbox/provider.go +23 -0
  500. package/dist/resources/templates/go-backend/internal/core/outbox/relay.go +1138 -0
  501. package/dist/resources/templates/go-backend/internal/core/outbox/relay_postgres_test.go +257 -0
  502. package/dist/resources/templates/go-backend/internal/core/outbox/relay_test.go +690 -0
  503. package/dist/resources/templates/go-backend/internal/core/outbox/rescheduler.go +164 -0
  504. package/dist/resources/templates/go-backend/internal/core/outbox/rescheduler_test.go +209 -0
  505. package/dist/resources/templates/go-backend/internal/core/outbox/writer.go +249 -0
  506. package/dist/resources/templates/go-backend/internal/core/outbox/writer_postgres_test.go +170 -0
  507. package/dist/resources/templates/go-backend/internal/core/outbox/writer_test.go +123 -0
  508. package/dist/resources/templates/go-backend/internal/core/provider.go +198 -0
  509. package/dist/resources/templates/go-backend/internal/core/queue/metrics_middleware.go +38 -0
  510. package/dist/resources/templates/go-backend/internal/core/queue/metrics_middleware_test.go +12 -0
  511. package/dist/resources/templates/go-backend/internal/core/queue/queue.go +288 -0
  512. package/dist/resources/templates/go-backend/internal/core/queue/queue_test.go +159 -0
  513. package/dist/resources/templates/go-backend/internal/core/resilience/bulkhead.go +514 -0
  514. package/dist/resources/templates/go-backend/internal/core/resilience/bulkhead_test.go +984 -0
  515. package/dist/resources/templates/go-backend/internal/core/resilience/circuitbreaker.go +358 -0
  516. package/dist/resources/templates/go-backend/internal/core/resilience/circuitbreaker_test.go +788 -0
  517. package/dist/resources/templates/go-backend/internal/core/resilience/retry.go +318 -0
  518. package/dist/resources/templates/go-backend/internal/core/resilience/retry_test.go +735 -0
  519. package/dist/resources/templates/go-backend/internal/core/schedule/recorder.go +305 -0
  520. package/dist/resources/templates/go-backend/internal/core/schedule/run_context.go +124 -0
  521. package/dist/resources/templates/go-backend/internal/core/schedule/run_context_test.go +85 -0
  522. package/dist/resources/templates/go-backend/internal/core/schedule/scheduler.go +1589 -0
  523. package/dist/resources/templates/go-backend/internal/core/schedule/scheduler_control_test.go +808 -0
  524. package/dist/resources/templates/go-backend/internal/core/storage/oss_provider.go +398 -0
  525. package/dist/resources/templates/go-backend/internal/core/storage/oss_provider_test.go +89 -0
  526. package/dist/resources/templates/go-backend/internal/core/storage/path.go +110 -0
  527. package/dist/resources/templates/go-backend/internal/core/storage/path_test.go +301 -0
  528. package/dist/resources/templates/go-backend/internal/core/storage/s3_provider.go +489 -0
  529. package/dist/resources/templates/go-backend/internal/core/storage/s3_provider_test.go +60 -0
  530. package/dist/resources/templates/go-backend/internal/core/storage/storage.go +268 -0
  531. package/dist/resources/templates/go-backend/internal/core/storage/validation.go +225 -0
  532. package/dist/resources/templates/go-backend/internal/core/storage/validation_test.go +495 -0
  533. package/dist/resources/templates/go-backend/internal/core/temporal/logger.go +38 -0
  534. package/dist/resources/templates/go-backend/internal/core/temporal/provider.go +126 -0
  535. package/dist/resources/templates/go-backend/internal/core/temporal/provider_test.go +67 -0
  536. package/dist/resources/templates/go-backend/internal/core/websocket/client.go +282 -0
  537. package/dist/resources/templates/go-backend/internal/core/websocket/client_test.go +81 -0
  538. package/dist/resources/templates/go-backend/internal/core/websocket/config.go +58 -0
  539. package/dist/resources/templates/go-backend/internal/core/websocket/handler.go +53 -0
  540. package/dist/resources/templates/go-backend/internal/core/websocket/hub.go +269 -0
  541. package/dist/resources/templates/go-backend/internal/core/websocket/message.go +273 -0
  542. package/dist/resources/templates/go-backend/internal/core/websocket/notifier.go +92 -0
  543. package/dist/resources/templates/go-backend/internal/core/websocket/provider.go +33 -0
  544. package/dist/resources/templates/go-backend/internal/integrations/onex/iam/action.go +77 -0
  545. package/dist/resources/templates/go-backend/internal/integrations/onex/iam/action_test.go +238 -0
  546. package/dist/resources/templates/go-backend/internal/integrations/onex/iam/condition.go +107 -0
  547. package/dist/resources/templates/go-backend/internal/integrations/onex/iam/decision.go +256 -0
  548. package/dist/resources/templates/go-backend/internal/integrations/onex/iam/enum.go +643 -0
  549. package/dist/resources/templates/go-backend/internal/integrations/onex/iam/errors.go +102 -0
  550. package/dist/resources/templates/go-backend/internal/integrations/onex/iam/principal_record.go +21 -0
  551. package/dist/resources/templates/go-backend/internal/integrations/onex/iam/product_context.go +486 -0
  552. package/dist/resources/templates/go-backend/internal/integrations/onex/iam/resource.go +215 -0
  553. package/dist/resources/templates/go-backend/internal/integrations/onex/iam/resource_test.go +242 -0
  554. package/dist/resources/templates/go-backend/internal/products/README.md +5 -0
  555. package/dist/resources/templates/go-backend/internal/sharedevents/README.md +5 -0
  556. package/dist/resources/templates/go-backend/migrations/000001_infrastructure.down.sql +13 -0
  557. package/dist/resources/templates/go-backend/migrations/000001_infrastructure.up.sql +886 -0
  558. package/dist/resources/templates/go-backend/pkg/apierror/error.go +327 -0
  559. package/dist/resources/templates/go-backend/pkg/apierror/error_code.go +65 -0
  560. package/dist/resources/templates/go-backend/pkg/apierror/error_test.go +41 -0
  561. package/dist/resources/templates/go-backend/pkg/apierror/error_type.go +14 -0
  562. package/dist/resources/templates/go-backend/pkg/apierror/http_response.go +216 -0
  563. package/dist/resources/templates/go-backend/pkg/apierror/http_response_test.go +216 -0
  564. package/dist/resources/templates/go-backend/scripts/check-openapi-clean-deletion.sh +54 -0
  565. package/dist/resources/templates/go-backend/scripts/test-migrations.sh +32 -0
  566. package/dist/resources/templates/go-backend/tests/infrastructure.sql +69 -0
  567. package/dist/resources/templates/mcp-server/.claude/docs/stack.md +338 -0
  568. package/dist/resources/templates/mcp-server/.editorconfig +13 -0
  569. package/dist/resources/templates/mcp-server/.env.example +8 -0
  570. package/dist/resources/templates/mcp-server/.github/ISSUE_TEMPLATE/bug_report.yml +104 -0
  571. package/dist/resources/templates/mcp-server/.github/ISSUE_TEMPLATE/feature_request.yml +54 -0
  572. package/dist/resources/templates/mcp-server/.github/PULL_REQUEST_TEMPLATE.md +36 -0
  573. package/dist/resources/templates/mcp-server/.github/workflows/ci.yml +78 -0
  574. package/dist/resources/templates/mcp-server/.husky/commit-msg +1 -0
  575. package/dist/resources/templates/mcp-server/.husky/pre-commit +1 -0
  576. package/dist/resources/templates/mcp-server/.lintstagedrc.json +4 -0
  577. package/dist/resources/templates/mcp-server/.nvmrc +1 -0
  578. package/dist/resources/templates/mcp-server/.prettierrc +8 -0
  579. package/dist/resources/templates/mcp-server/CONTRIBUTING.md +184 -0
  580. package/dist/resources/templates/mcp-server/Dockerfile +23 -0
  581. package/dist/resources/templates/mcp-server/LICENSE +21 -0
  582. package/dist/resources/templates/mcp-server/README.md +123 -0
  583. package/dist/resources/templates/mcp-server/_gitignore +26 -0
  584. package/dist/resources/templates/mcp-server/commitlint.config.js +24 -0
  585. package/dist/resources/templates/mcp-server/eslint.config.js +30 -0
  586. package/dist/resources/templates/mcp-server/package.json +72 -0
  587. package/dist/resources/templates/mcp-server/src/index.ts +27 -0
  588. package/dist/resources/templates/mcp-server/src/resources.ts +33 -0
  589. package/dist/resources/templates/mcp-server/src/services/.gitkeep +0 -0
  590. package/dist/resources/templates/mcp-server/src/tools.ts +43 -0
  591. package/dist/resources/templates/mcp-server/src/types/index.ts +19 -0
  592. package/dist/resources/templates/mcp-server/src/utils/index.ts +31 -0
  593. package/dist/resources/templates/mcp-server/tsconfig.json +22 -0
  594. package/dist/resources/templates/mcp-server/tsup.config.ts +14 -0
  595. package/dist/resources/templates/mcp-server/vitest.config.ts +15 -0
  596. package/dist/resources/templates/nuxt/.claude/docs/stack.md +666 -0
  597. package/dist/resources/templates/nuxt/.editorconfig +15 -0
  598. package/dist/resources/templates/nuxt/.env.example +22 -0
  599. package/dist/resources/templates/nuxt/.github/workflows/ci.yml +34 -0
  600. package/dist/resources/templates/nuxt/.husky/commit-msg +1 -0
  601. package/dist/resources/templates/nuxt/.husky/pre-commit +1 -0
  602. package/dist/resources/templates/nuxt/.nvmrc +1 -0
  603. package/dist/resources/templates/nuxt/.prettierignore +9 -0
  604. package/dist/resources/templates/nuxt/.prettierrc +1 -0
  605. package/dist/resources/templates/nuxt/README.md +23 -0
  606. package/dist/resources/templates/nuxt/_gitignore +7 -0
  607. package/dist/resources/templates/nuxt/app/app.config.ts +105 -0
  608. package/dist/resources/templates/nuxt/app/i18n/locales/en.json +5 -0
  609. package/dist/resources/templates/nuxt/app/pages/app/index.vue +224 -0
  610. package/dist/resources/templates/nuxt/app/pages/authors/[slug].vue +37 -0
  611. package/dist/resources/templates/nuxt/app/pages/blog/[slug].vue +55 -0
  612. package/dist/resources/templates/nuxt/app/pages/blog/category/[category].vue +38 -0
  613. package/dist/resources/templates/nuxt/app/pages/blog/index.vue +46 -0
  614. package/dist/resources/templates/nuxt/app/pages/index.vue +626 -0
  615. package/dist/resources/templates/nuxt/app/pages/legal/privacy.vue +21 -0
  616. package/dist/resources/templates/nuxt/app/pages/legal/terms.vue +21 -0
  617. package/dist/resources/templates/nuxt/app/pages/ui/index.vue +397 -0
  618. package/dist/resources/templates/nuxt/commitlint.config.mjs +1 -0
  619. package/dist/resources/templates/nuxt/eslint.config.mjs +4 -0
  620. package/dist/resources/templates/nuxt/lint-staged.config.mjs +8 -0
  621. package/dist/resources/templates/nuxt/nuxt.config.ts +71 -0
  622. package/dist/resources/templates/nuxt/package.json +67 -0
  623. package/dist/resources/templates/nuxt/public/apple-touch-icon.png +0 -0
  624. package/dist/resources/templates/nuxt/public/favicon.ico +0 -0
  625. package/dist/resources/templates/nuxt/public/favicon.svg +5 -0
  626. package/dist/resources/templates/nuxt/public/icon-16x16.png +0 -0
  627. package/dist/resources/templates/nuxt/public/icon-192x192.png +0 -0
  628. package/dist/resources/templates/nuxt/public/icon-32x32.png +0 -0
  629. package/dist/resources/templates/nuxt/public/icon-512x512.png +0 -0
  630. package/dist/resources/templates/nuxt/public/logo.svg +6 -0
  631. package/dist/resources/templates/nuxt/public/media/hero-product-gallery.png +0 -0
  632. package/dist/resources/templates/nuxt/server/api/content/sitemap-urls.get.ts +12 -0
  633. package/dist/resources/templates/nuxt/server/api/preview.get.ts +9 -0
  634. package/dist/resources/templates/nuxt/stylelint.config.mjs +3 -0
  635. package/dist/resources/templates/nuxt/test/e2e/rendering.test.ts +13 -0
  636. package/dist/resources/templates/nuxt/tsconfig.json +3 -0
  637. package/dist/resources/templates/nuxt/vitest.config.ts +8 -0
  638. package/package.json +8 -5
@@ -0,0 +1,1589 @@
1
+ package schedule
2
+
3
+ import (
4
+ "context"
5
+ "errors"
6
+ "fmt"
7
+ "regexp"
8
+ "strings"
9
+ "sync"
10
+ "time"
11
+ "unicode/utf8"
12
+
13
+ "github.com/robfig/cron/v3"
14
+ "go.opentelemetry.io/otel/attribute"
15
+ "go.opentelemetry.io/otel/codes"
16
+ "go.opentelemetry.io/otel/trace"
17
+ "go.uber.org/zap"
18
+ )
19
+
20
+ // ═══════════════════════════════════════════════════════════════
21
+ // 任务注册信息
22
+ // ═══════════════════════════════════════════════════════════════
23
+
24
+ // TaskType 任务类型
25
+ type TaskType string
26
+
27
+ const (
28
+ TaskTypeCron TaskType = "cron"
29
+ TaskTypeInterval TaskType = "interval"
30
+ TaskTypeManual TaskType = "manual"
31
+ )
32
+
33
+ // TaskHandler 是调度器执行任务的统一处理器契约。
34
+ type TaskHandler func(context.Context) error
35
+
36
+ // RegisteredTask 已注册任务的信息
37
+ type RegisteredTask struct {
38
+ // 任务名称
39
+ Name string
40
+ // 任务类型
41
+ Type TaskType
42
+ // 默认 Cron 表达式
43
+ DefaultPattern string
44
+ // Cron 表达式(仅 cron 类型)
45
+ Pattern string
46
+ // 上次运行时间
47
+ LastRun *time.Time
48
+ // 下次运行时间
49
+ NextRun *time.Time
50
+ // 运行次数
51
+ RunCount int64
52
+ // 错误次数
53
+ ErrorCount int64
54
+ // 上次错误信息
55
+ LastError string
56
+ // 是否正在运行
57
+ IsRunning bool
58
+ // 注册时间
59
+ RegisteredAt time.Time
60
+ // cron 入口 ID
61
+ entryID cron.EntryID
62
+ // 当前控制面定义版本
63
+ controlRevision int64
64
+ // 任务处理器
65
+ handler TaskHandler
66
+ }
67
+
68
+ // ExecutionResult 任务执行结果
69
+ type ExecutionResult struct {
70
+ // 任务名称
71
+ TaskName string
72
+ // 开始时间
73
+ StartTime time.Time
74
+ // 结束时间
75
+ EndTime time.Time
76
+ // 执行时长(毫秒)
77
+ Duration int64
78
+ // 是否成功
79
+ Success bool
80
+ // 错误信息
81
+ Error string
82
+ }
83
+
84
+ const taskRunMetadataSchemaVersion = "scheduler.run.v1"
85
+
86
+ const dispatchHeartbeatInterval = 10 * time.Second
87
+
88
+ const taskErrorMaxLength = 512
89
+
90
+ const (
91
+ taskDisplayNameMaxRunes = 191
92
+ taskDescriptionMaxRunes = 1000
93
+ taskMaxRuntimeSecondsLimit = 86400
94
+ taskRetryMaxAttemptsLimit = 10
95
+ taskRetryBackoffSecondsLimit = 86400
96
+ )
97
+
98
+ type taskHandlerDisposition uint8
99
+
100
+ const (
101
+ taskHandlerSucceeded taskHandlerDisposition = iota
102
+ taskHandlerFailed
103
+ taskHandlerSkipped
104
+ )
105
+
106
+ type taskHandlerOutcome struct {
107
+ disposition taskHandlerDisposition
108
+ message string
109
+ }
110
+
111
+ var taskErrorSensitivePairPattern = regexp.MustCompile(`(?i)\b(password|passwd|pwd|secret|token|api[_-]?key|access[_-]?token|refresh[_-]?token|authorization|credential)\b\s*[:=]\s*("[^"]*"|'[^']*'|[^\s,;]+)`)
112
+
113
+ var taskErrorBearerPattern = regexp.MustCompile(`(?i)\bbearer\s+[A-Za-z0-9._~+/\-=]+`)
114
+
115
+ // ═══════════════════════════════════════════════════════════════
116
+ // 调度器
117
+ // ═══════════════════════════════════════════════════════════════
118
+
119
+ // Scheduler 定时任务调度器
120
+ type Scheduler struct {
121
+ cron *cron.Cron
122
+ logger *zap.Logger
123
+
124
+ mu sync.RWMutex
125
+ tasks map[string]*RegisteredTask
126
+ executionHistory map[string][]ExecutionResult
127
+ maxHistorySize int
128
+ // executionCtx 是 cron 执行的根生命周期,Stop 通过它向运行中任务广播退出信号。
129
+ executionCtx context.Context
130
+ executionCancel context.CancelFunc
131
+
132
+ // 可观测性钩子在启动期注入,保持 schedule 包不反向依赖 observability。
133
+ tracer trace.Tracer
134
+ metrics jobObserver
135
+ recorder RunRecorder
136
+ controls ControlStore
137
+ }
138
+
139
+ // jobObserver 是 Scheduler 对 metrics 的最小依赖。
140
+ // 抽出 interface 避免 schedule 包反向 import observability(A4 防循环)。
141
+ type jobObserver interface {
142
+ JobSucceeded(name string, ts time.Time)
143
+ JobFailed(name string)
144
+ }
145
+
146
+ // New 创建调度器
147
+ func New(logger *zap.Logger) *Scheduler {
148
+ ctx, cancel := context.WithCancel(context.Background())
149
+ return &Scheduler{
150
+ cron: cron.New(cron.WithSeconds()),
151
+ logger: logger.Named("Scheduler"),
152
+ tasks: make(map[string]*RegisteredTask),
153
+ executionHistory: make(map[string][]ExecutionResult),
154
+ maxHistorySize: 100,
155
+ executionCtx: ctx,
156
+ executionCancel: cancel,
157
+ }
158
+ }
159
+
160
+ // SetTracer 注入 OTel tracer,使所有 cron 任务自动获得 root span。
161
+ // 应在 cmd 启动期 wire 阶段调一次;运行时不应反复变更。
162
+ func (s *Scheduler) SetTracer(tracer trace.Tracer) {
163
+ s.mu.Lock()
164
+ defer s.mu.Unlock()
165
+ s.tracer = tracer
166
+ }
167
+
168
+ // SetObserver 注入 metrics observer,使所有 cron 任务写入成功时间和连续失败计数。
169
+ // 同 SetTracer 仅启动期注入。
170
+ func (s *Scheduler) SetObserver(obs jobObserver) {
171
+ s.mu.Lock()
172
+ defer s.mu.Unlock()
173
+ s.metrics = obs
174
+ }
175
+
176
+ // SetRunRecorder 注入运行持久化 recorder。
177
+ // recorder 失败只影响运维观测,不阻断业务任务执行。
178
+ func (s *Scheduler) SetRunRecorder(recorder RunRecorder) {
179
+ s.mu.Lock()
180
+ defer s.mu.Unlock()
181
+ s.recorder = recorder
182
+ }
183
+
184
+ // SetControlStore 注入调度控制面存储。
185
+ func (s *Scheduler) SetControlStore(store ControlStore) {
186
+ s.mu.Lock()
187
+ defer s.mu.Unlock()
188
+ s.controls = store
189
+ }
190
+
191
+ // Start 启动调度器
192
+ func (s *Scheduler) Start() {
193
+ s.mu.Lock()
194
+ if s.executionCtx == nil || s.executionCtx.Err() != nil {
195
+ s.executionCtx, s.executionCancel = context.WithCancel(context.Background())
196
+ }
197
+ s.mu.Unlock()
198
+ s.cron.Start()
199
+ s.logger.Info("scheduler started")
200
+ }
201
+
202
+ // Stop 停止调度器
203
+ func (s *Scheduler) Stop() {
204
+ s.mu.RLock()
205
+ cancel := s.executionCancel
206
+ s.mu.RUnlock()
207
+ if cancel != nil {
208
+ cancel()
209
+ }
210
+ ctx := s.cron.Stop()
211
+ <-ctx.Done()
212
+ s.logger.Info("scheduler stopped")
213
+ }
214
+
215
+ // ═══════════════════════════════════════════════════════════════
216
+ // 任务注册
217
+ // ═══════════════════════════════════════════════════════════════
218
+
219
+ // RegisterCron 注册 cron 定时任务
220
+ func (s *Scheduler) RegisterCron(name, pattern string, fn TaskHandler) error {
221
+ return s.RegisterCronWithOptions(name, pattern, TaskRegistrationOptions{
222
+ DisplayName: name,
223
+ ConcurrencyPolicy: ConcurrencyPolicyForbid,
224
+ ManualRunEnabled: true,
225
+ MaxRuntimeSeconds: 3600,
226
+ RetryMaxAttempts: 0,
227
+ RetryBackoffSeconds: 60,
228
+ }, fn)
229
+ }
230
+
231
+ // RegisterCronWithOptions 注册带首次控制面默认策略的 cron 定时任务。
232
+ func (s *Scheduler) RegisterCronWithOptions(name, pattern string, options TaskRegistrationOptions, fn TaskHandler) error {
233
+ name = strings.TrimSpace(name)
234
+ pattern = strings.TrimSpace(pattern)
235
+ options.DisplayName = strings.TrimSpace(options.DisplayName)
236
+ options.Description = strings.TrimSpace(options.Description)
237
+ if err := validateTaskRegistration(name, pattern, options, fn); err != nil {
238
+ return err
239
+ }
240
+ registeredAt := time.Now()
241
+ effectivePattern := pattern
242
+ effectiveTimezone := "UTC"
243
+ var controlRevision int64
244
+
245
+ s.mu.RLock()
246
+ controlStore := s.controls
247
+ s.mu.RUnlock()
248
+
249
+ if controlStore != nil {
250
+ control, err := controlStore.RegisterTaskControl(context.Background(), TaskControlRegistration{
251
+ TaskName: name,
252
+ TaskType: TaskTypeCron,
253
+ DisplayName: options.DisplayName,
254
+ Description: options.Description,
255
+ DefaultPattern: pattern,
256
+ Pattern: pattern,
257
+ SourceRuntime: "worker",
258
+ ConcurrencyPolicy: options.ConcurrencyPolicy,
259
+ ManualRunEnabled: options.ManualRunEnabled,
260
+ MaxRuntimeSeconds: options.MaxRuntimeSeconds,
261
+ RetryMaxAttempts: options.RetryMaxAttempts,
262
+ RetryBackoffSeconds: options.RetryBackoffSeconds,
263
+ RegisteredAt: registeredAt,
264
+ })
265
+ if err != nil {
266
+ return fmt.Errorf("register scheduler task control '%s': %w", name, err)
267
+ }
268
+ if control != nil && control.Pattern != "" {
269
+ effectivePattern = control.Pattern
270
+ effectiveTimezone = control.Timezone
271
+ controlRevision = control.DefinitionRevision
272
+ }
273
+ }
274
+
275
+ s.mu.Lock()
276
+ if _, exists := s.tasks[name]; exists {
277
+ s.logger.Warn("task already registered, replacing", zap.String("name", name))
278
+ s.removeLocked(name)
279
+ }
280
+
281
+ wrappedFn := s.wrapTask(name, fn)
282
+
283
+ entryID, err := s.cron.AddFunc(schedulerCronSpec(effectivePattern, effectiveTimezone), wrappedFn)
284
+ if err != nil {
285
+ s.mu.Unlock()
286
+ return fmt.Errorf("register cron task '%s': %w", name, err)
287
+ }
288
+
289
+ // 计算下次运行时间
290
+ entry := s.cron.Entry(entryID)
291
+ var nextRun *time.Time
292
+ if !entry.Next.IsZero() {
293
+ t := entry.Next
294
+ nextRun = &t
295
+ }
296
+
297
+ task := &RegisteredTask{
298
+ Name: name,
299
+ Type: TaskTypeCron,
300
+ DefaultPattern: pattern,
301
+ Pattern: effectivePattern,
302
+ NextRun: nextRun,
303
+ RegisteredAt: registeredAt,
304
+ entryID: entryID,
305
+ controlRevision: controlRevision,
306
+ handler: fn,
307
+ }
308
+
309
+ s.tasks[name] = task
310
+ s.executionHistory[name] = []ExecutionResult{}
311
+ recorder := s.recorder
312
+ taskSnapshot := *task
313
+ s.mu.Unlock()
314
+
315
+ if recorder != nil {
316
+ if err := recorder.UpsertTask(context.Background(), taskSnapshot); err != nil {
317
+ s.logger.Error("record scheduler task registration failed",
318
+ zap.String("name", name),
319
+ zap.Error(err),
320
+ )
321
+ }
322
+ }
323
+ s.logger.Info("cron task registered", zap.String("name", name), zap.String("pattern", effectivePattern))
324
+
325
+ return nil
326
+ }
327
+
328
+ func validateTaskRegistration(name, pattern string, options TaskRegistrationOptions, fn TaskHandler) error {
329
+ if name == "" || utf8.RuneCountInString(name) > taskDisplayNameMaxRunes {
330
+ return fmt.Errorf("scheduler task name must contain 1-%d characters", taskDisplayNameMaxRunes)
331
+ }
332
+ if pattern == "" {
333
+ return fmt.Errorf("scheduler task %q cron pattern is required", name)
334
+ }
335
+ if fn == nil {
336
+ return fmt.Errorf("scheduler task %q handler is required", name)
337
+ }
338
+ if options.DisplayName == "" || utf8.RuneCountInString(options.DisplayName) > taskDisplayNameMaxRunes {
339
+ return fmt.Errorf("scheduler task %q display name must contain 1-%d characters", name, taskDisplayNameMaxRunes)
340
+ }
341
+ if utf8.RuneCountInString(options.Description) > taskDescriptionMaxRunes {
342
+ return fmt.Errorf("scheduler task %q description exceeds %d characters", name, taskDescriptionMaxRunes)
343
+ }
344
+ switch options.ConcurrencyPolicy {
345
+ case ConcurrencyPolicyForbid, ConcurrencyPolicyAllow:
346
+ default:
347
+ return fmt.Errorf("scheduler task %q concurrency policy %q is invalid", name, options.ConcurrencyPolicy)
348
+ }
349
+ if options.MaxRuntimeSeconds < 1 || options.MaxRuntimeSeconds > taskMaxRuntimeSecondsLimit {
350
+ return fmt.Errorf("scheduler task %q max runtime seconds must be between 1 and %d", name, taskMaxRuntimeSecondsLimit)
351
+ }
352
+ if options.RetryMaxAttempts < 0 || options.RetryMaxAttempts > taskRetryMaxAttemptsLimit {
353
+ return fmt.Errorf("scheduler task %q retry max attempts must be between 0 and %d", name, taskRetryMaxAttemptsLimit)
354
+ }
355
+ if options.RetryBackoffSeconds < 1 || options.RetryBackoffSeconds > taskRetryBackoffSecondsLimit {
356
+ return fmt.Errorf("scheduler task %q retry backoff seconds must be between 1 and %d", name, taskRetryBackoffSecondsLimit)
357
+ }
358
+ return nil
359
+ }
360
+
361
+ // RegisterInterval 注册固定间隔任务
362
+ func (s *Scheduler) RegisterInterval(name string, interval time.Duration, fn TaskHandler) error {
363
+ // 将间隔转为 cron 表达式(秒级)
364
+ seconds := int(interval.Seconds())
365
+ if seconds < 1 {
366
+ return fmt.Errorf("interval must be at least 1 second")
367
+ }
368
+
369
+ pattern := fmt.Sprintf("@every %ds", seconds)
370
+ return s.RegisterCron(name, pattern, fn)
371
+ }
372
+
373
+ // Unregister 注销任务
374
+ func (s *Scheduler) Unregister(name string) bool {
375
+ s.mu.Lock()
376
+ defer s.mu.Unlock()
377
+ return s.removeLocked(name)
378
+ }
379
+
380
+ func (s *Scheduler) removeLocked(name string) bool {
381
+ task, exists := s.tasks[name]
382
+ if !exists {
383
+ return false
384
+ }
385
+
386
+ s.cron.Remove(task.entryID)
387
+ delete(s.tasks, name)
388
+ delete(s.executionHistory, name)
389
+ s.logger.Info("task unregistered", zap.String("name", name))
390
+ return true
391
+ }
392
+
393
+ // ═══════════════════════════════════════════════════════════════
394
+ // 任务包装(自动记录执行)
395
+ // ═══════════════════════════════════════════════════════════════
396
+
397
+ func (s *Scheduler) wrapTask(name string, fn TaskHandler) func() {
398
+ return func() {
399
+ ctx, cancel := s.scheduledRunContext()
400
+ defer cancel()
401
+ _, _ = s.executeTaskRun(ctx, name, fn, TaskRunOptions{
402
+ Trigger: RunTriggerCron,
403
+ Runtime: "worker",
404
+ })
405
+ }
406
+ }
407
+
408
+ func (s *Scheduler) scheduledRunContext() (context.Context, context.CancelFunc) {
409
+ s.mu.RLock()
410
+ parent := s.executionCtx
411
+ s.mu.RUnlock()
412
+ if parent == nil {
413
+ parent = context.Background()
414
+ }
415
+ return context.WithCancel(parent)
416
+ }
417
+
418
+ // ExecuteTask 由控制面命令触发一次已注册任务。
419
+ func (s *Scheduler) ExecuteTask(ctx context.Context, name string, opts TaskRunOptions) (TaskRunResult, error) {
420
+ s.mu.RLock()
421
+ task, ok := s.tasks[name]
422
+ var handler TaskHandler
423
+ if ok {
424
+ handler = task.handler
425
+ }
426
+ s.mu.RUnlock()
427
+ if !ok || handler == nil {
428
+ return TaskRunResult{Status: RunStatusFailed, Error: "task is not registered"}, fmt.Errorf("task %q is not registered", name)
429
+ }
430
+ if opts.Trigger == "" {
431
+ opts.Trigger = RunTriggerManualRun
432
+ }
433
+ if opts.Runtime == "" {
434
+ opts.Runtime = "worker"
435
+ }
436
+ return s.executeTaskRun(ctx, name, handler, opts)
437
+ }
438
+
439
+ // ProcessDispatches 认领后台写入的手动调度命令并执行。
440
+ func (s *Scheduler) ProcessDispatches(ctx context.Context, store DispatchStore, runtime string, limit int) error {
441
+ if store == nil {
442
+ return nil
443
+ }
444
+ if runtime == "" {
445
+ runtime = "worker"
446
+ }
447
+ if limit <= 0 {
448
+ limit = 10
449
+ }
450
+ dispatches, err := store.ClaimPendingDispatches(ctx, runtime, limit, time.Now().UTC())
451
+ if err != nil {
452
+ return fmt.Errorf("claim scheduler dispatches: %w", err)
453
+ }
454
+ for _, dispatch := range dispatches {
455
+ runCtx, stopHeartbeat, err := s.startDispatchHeartbeat(ctx, store, dispatch.ID, dispatch.ClaimToken, runtime)
456
+ if err != nil {
457
+ return fmt.Errorf("heartbeat scheduler dispatch %d: %w", dispatch.ID, err)
458
+ }
459
+ result, runErr := s.ExecuteTask(runCtx, dispatch.TaskName, TaskRunOptions{
460
+ Trigger: RunTriggerManualRun,
461
+ DispatchID: dispatch.ID,
462
+ CorrelationID: dispatch.CorrelationID,
463
+ OperatorUserID: dispatch.OperatorUserID,
464
+ Reason: dispatch.Reason,
465
+ Runtime: runtime,
466
+ Metadata: dispatch.Payload,
467
+ })
468
+ stopHeartbeat(result.RunID)
469
+ finishedAt := time.Now().UTC()
470
+ if runErr != nil && result.Error == "" {
471
+ result.Error = sanitizeTaskError(runErr.Error())
472
+ result.Status = RunStatusFailed
473
+ }
474
+ if err := store.CompleteDispatch(ctx, dispatch.ID, result.RunID, dispatch.ClaimToken, result, finishedAt); err != nil {
475
+ return fmt.Errorf("complete scheduler dispatch %d: %w", dispatch.ID, err)
476
+ }
477
+ }
478
+ return nil
479
+ }
480
+
481
+ func (s *Scheduler) startDispatchHeartbeat(ctx context.Context, store DispatchStore, dispatchID int64, claimToken string, runtime string) (context.Context, func(int64), error) {
482
+ runCtx, cancel := context.WithCancelCause(ctx)
483
+ state, err := store.HeartbeatDispatch(ctx, dispatchID, 0, runtime, claimToken, time.Now().UTC())
484
+ if err != nil {
485
+ cancel(err)
486
+ return runCtx, func(int64) {}, err
487
+ }
488
+ if state != nil && state.CancelRequested {
489
+ cancel(dispatchCancellationError(state))
490
+ }
491
+ done := make(chan int64, 1)
492
+ finished := make(chan struct{})
493
+ go func() {
494
+ defer close(finished)
495
+ ticker := time.NewTicker(dispatchHeartbeatInterval)
496
+ defer ticker.Stop()
497
+ runID := int64(0)
498
+ for {
499
+ select {
500
+ case id := <-done:
501
+ runID = id
502
+ if state, err := store.HeartbeatDispatch(ctx, dispatchID, runID, runtime, claimToken, time.Now().UTC()); err != nil {
503
+ s.logger.Error("scheduler dispatch heartbeat failed",
504
+ zap.Int64("dispatchID", dispatchID),
505
+ zap.Error(err),
506
+ )
507
+ } else if state != nil && state.CancelRequested {
508
+ cancel(dispatchCancellationError(state))
509
+ }
510
+ return
511
+ case <-ctx.Done():
512
+ cancel(ctx.Err())
513
+ return
514
+ case <-ticker.C:
515
+ state, err := store.HeartbeatDispatch(ctx, dispatchID, runID, runtime, claimToken, time.Now().UTC())
516
+ if err != nil {
517
+ s.logger.Error("scheduler dispatch heartbeat failed",
518
+ zap.Int64("dispatchID", dispatchID),
519
+ zap.Error(err),
520
+ )
521
+ continue
522
+ }
523
+ if state != nil && state.CancelRequested {
524
+ cancel(dispatchCancellationError(state))
525
+ }
526
+ }
527
+ }
528
+ }()
529
+ return runCtx, func(runID int64) {
530
+ select {
531
+ case done <- runID:
532
+ case <-finished:
533
+ }
534
+ <-finished
535
+ cancel(context.Canceled)
536
+ }, nil
537
+ }
538
+
539
+ type dispatchCancellationCauseError struct {
540
+ requestedAt time.Time
541
+ reason string
542
+ }
543
+
544
+ func (e *dispatchCancellationCauseError) Error() string {
545
+ if e == nil || e.reason == "" {
546
+ return context.Canceled.Error()
547
+ }
548
+ return e.reason
549
+ }
550
+
551
+ func dispatchCancellationError(state *DispatchLeaseState) error {
552
+ if state == nil {
553
+ return context.Canceled
554
+ }
555
+ requestedAt := time.Now().UTC()
556
+ if state.CancelRequestedAt != nil && !state.CancelRequestedAt.IsZero() {
557
+ requestedAt = state.CancelRequestedAt.UTC()
558
+ }
559
+ reason := sanitizeTaskError(state.CancelReason)
560
+ if reason == "" {
561
+ reason = context.Canceled.Error()
562
+ }
563
+ return &dispatchCancellationCauseError{
564
+ requestedAt: requestedAt,
565
+ reason: reason,
566
+ }
567
+ }
568
+
569
+ func schedulerContextError(ctx context.Context) string {
570
+ reason, _ := schedulerCancellationDetails(ctx)
571
+ return reason
572
+ }
573
+
574
+ func schedulerCancellationDetails(ctx context.Context) (string, *time.Time) {
575
+ if cause := context.Cause(ctx); cause != nil {
576
+ var cancelCause *dispatchCancellationCauseError
577
+ if errors.As(cause, &cancelCause) {
578
+ cancelRequestedAt := cancelCause.requestedAt
579
+ return cancelCause.Error(), &cancelRequestedAt
580
+ }
581
+ return sanitizeTaskError(cause.Error()), nil
582
+ }
583
+ if err := ctx.Err(); err != nil {
584
+ return sanitizeTaskError(err.Error()), nil
585
+ }
586
+ return "", nil
587
+ }
588
+
589
+ // ReconcileControls 让运行中 cron entry 跟随控制面表达式变更。
590
+ func (s *Scheduler) ReconcileControls(ctx context.Context) error {
591
+ s.mu.RLock()
592
+ store := s.controls
593
+ s.mu.RUnlock()
594
+ if store == nil {
595
+ return nil
596
+ }
597
+ controls, err := store.ListTaskControls(ctx)
598
+ if err != nil {
599
+ return fmt.Errorf("list scheduler controls: %w", err)
600
+ }
601
+ for _, control := range controls {
602
+ if control.Pattern == "" {
603
+ continue
604
+ }
605
+ if err := s.reconcileControl(control); err != nil {
606
+ return err
607
+ }
608
+ }
609
+ return nil
610
+ }
611
+
612
+ func (s *Scheduler) reconcileControl(control TaskControl) error {
613
+ s.mu.Lock()
614
+ task, exists := s.tasks[control.TaskName]
615
+ if !exists || task.Pattern == control.Pattern && task.controlRevision == control.DefinitionRevision {
616
+ s.mu.Unlock()
617
+ return nil
618
+ }
619
+ handler := task.handler
620
+ s.cron.Remove(task.entryID)
621
+ wrappedFn := s.wrapTask(task.Name, handler)
622
+ entryID, err := s.cron.AddFunc(schedulerCronSpec(control.Pattern, control.Timezone), wrappedFn)
623
+ if err != nil {
624
+ s.mu.Unlock()
625
+ return fmt.Errorf("reconcile scheduler task %q: %w", control.TaskName, err)
626
+ }
627
+ entry := s.cron.Entry(entryID)
628
+ var nextRun *time.Time
629
+ if !entry.Next.IsZero() {
630
+ t := entry.Next
631
+ nextRun = &t
632
+ }
633
+ task.Pattern = control.Pattern
634
+ task.NextRun = nextRun
635
+ task.entryID = entryID
636
+ task.controlRevision = control.DefinitionRevision
637
+ taskSnapshot := *task
638
+ recorder := s.recorder
639
+ s.mu.Unlock()
640
+
641
+ if recorder != nil {
642
+ if err := recorder.UpsertTask(context.Background(), taskSnapshot); err != nil {
643
+ s.logger.Error("record reconciled scheduler task failed",
644
+ zap.String("name", control.TaskName),
645
+ zap.Error(err),
646
+ )
647
+ }
648
+ }
649
+ return nil
650
+ }
651
+
652
+ func schedulerCronSpec(pattern, timezone string) string {
653
+ if timezone == "" || timezone == "UTC" || len(pattern) > 0 && pattern[0] == '@' {
654
+ return pattern
655
+ }
656
+ return "CRON_TZ=" + timezone + " " + pattern
657
+ }
658
+
659
+ // ExpireRuntimeExceededRuns 关闭进程异常退出后遗留的超时运行记录。
660
+ func (s *Scheduler) ExpireRuntimeExceededRuns(ctx context.Context) (int64, error) {
661
+ s.mu.RLock()
662
+ recorder := s.recorder
663
+ s.mu.RUnlock()
664
+ expirer, ok := recorder.(RuntimeExceededRunExpirer)
665
+ if !ok || expirer == nil {
666
+ return 0, nil
667
+ }
668
+ return expirer.ExpireRuntimeExceededRuns(ctx, time.Now().UTC())
669
+ }
670
+
671
+ func (s *Scheduler) executeTaskRun(ctx context.Context, name string, fn TaskHandler, opts TaskRunOptions) (TaskRunResult, error) {
672
+ startTime := time.Now()
673
+ if opts.Trigger == "" {
674
+ opts.Trigger = RunTriggerCron
675
+ }
676
+ if opts.Runtime == "" {
677
+ opts.Runtime = "worker"
678
+ }
679
+
680
+ control, controlErr := s.getControl(ctx, name)
681
+ if controlErr != nil {
682
+ return TaskRunResult{Status: RunStatusFailed, Error: controlErr.Error()}, controlErr
683
+ }
684
+ taskSnapshot, recorder, skipReason := s.markStartedWithControl(name, startTime, control)
685
+ if taskSnapshot == nil {
686
+ return TaskRunResult{Status: RunStatusFailed, Error: "task is not registered"}, fmt.Errorf("task %q is not registered", name)
687
+ }
688
+ runMetadata := buildTaskRunMetadata(*taskSnapshot, control, opts, startTime)
689
+ if skipReason != "" {
690
+ runMetadata["skip"] = map[string]any{"reason": skipReason}
691
+ opts.Metadata = runMetadata
692
+ result := s.recordSkipped(ctx, taskSnapshot, recorder, opts, startTime, skipReason)
693
+ return result, nil
694
+ }
695
+
696
+ s.mu.RLock()
697
+ tracer := s.tracer
698
+ obs := s.metrics
699
+ s.mu.RUnlock()
700
+ var span trace.Span
701
+ if tracer != nil {
702
+ ctx, span = tracer.Start(ctx, "cron."+name,
703
+ trace.WithSpanKind(trace.SpanKindInternal),
704
+ trace.WithAttributes(attribute.String("job.name", name)),
705
+ )
706
+ }
707
+
708
+ s.logger.Info("task started", zap.String("name", name))
709
+
710
+ retryMaxAttempts, retryBackoff, maxRuntime := resolveTaskRuntimePolicy(control)
711
+ runID := int64(0)
712
+ startRecordCtx := ctx
713
+ if ctx.Err() != nil {
714
+ startRecordCtx = context.WithoutCancel(ctx)
715
+ }
716
+ if recorder != nil {
717
+ var err error
718
+ runID, err = recorder.StartRun(startRecordCtx, TaskRunStart{
719
+ TaskName: name,
720
+ TaskType: taskSnapshot.Type,
721
+ Pattern: taskSnapshot.Pattern,
722
+ Trigger: opts.Trigger,
723
+ DispatchID: opts.DispatchID,
724
+ CorrelationID: opts.CorrelationID,
725
+ OperatorUserID: opts.OperatorUserID,
726
+ Reason: opts.Reason,
727
+ Runtime: opts.Runtime,
728
+ StartedAt: startTime,
729
+ Metadata: runMetadata,
730
+ })
731
+ if err != nil {
732
+ if errors.Is(err, ErrTaskAlreadyRunning) {
733
+ finishedAt := time.Now()
734
+ reason := ErrTaskAlreadyRunning.Error()
735
+ s.markCompleted(name, startTime, finishedAt, RunStatusSkipped, reason)
736
+ if span != nil {
737
+ span.SetAttributes(attribute.Float64("job.duration_seconds", finishedAt.Sub(startTime).Seconds()))
738
+ span.End()
739
+ }
740
+ s.logger.Info("task skipped", zap.String("name", name), zap.String("reason", reason))
741
+ return TaskRunResult{
742
+ Status: RunStatusSkipped,
743
+ Error: reason,
744
+ Skipped: true,
745
+ SkipReason: reason,
746
+ }, nil
747
+ }
748
+ s.logger.Error("record scheduler task start failed",
749
+ zap.String("name", name),
750
+ zap.Error(err),
751
+ )
752
+ }
753
+ }
754
+ if recorder != nil && runID > 0 {
755
+ s.recordRunEvent(startRecordCtx, recorder, TaskRunEventRecord{
756
+ RunID: runID,
757
+ EventType: RunEventPolicyEvaluated,
758
+ Level: "info",
759
+ Stage: "policy",
760
+ Message: "Task policy evaluated",
761
+ Payload: buildTaskRunPolicyPayload(control, opts, retryMaxAttempts, retryBackoff, maxRuntime),
762
+ CreatedAt: time.Now(),
763
+ })
764
+ }
765
+ if ctx.Err() != nil {
766
+ taskErr, cancelRequestedAt := schedulerCancellationDetails(ctx)
767
+ recordCtx := context.WithoutCancel(ctx)
768
+ finishedAt := time.Now()
769
+ duration := finishedAt.Sub(startTime)
770
+ if recorder != nil && runID > 0 {
771
+ payload := map[string]any{
772
+ "dispatch_id": opts.DispatchID,
773
+ "reason": taskErr,
774
+ }
775
+ if cancelRequestedAt != nil {
776
+ payload["cancel_requested_at"] = cancelRequestedAt.UTC()
777
+ }
778
+ s.recordRunEvent(recordCtx, recorder, TaskRunEventRecord{
779
+ RunID: runID,
780
+ EventType: RunEventCancelRequested,
781
+ Level: "warning",
782
+ Stage: "control",
783
+ Message: "Task cancellation requested",
784
+ Payload: payload,
785
+ CreatedAt: finishedAt,
786
+ })
787
+ }
788
+ s.markCompleted(name, startTime, finishedAt, RunStatusCanceled, taskErr)
789
+ if recorder != nil {
790
+ if err := recorder.CompleteRun(recordCtx, TaskRunCompletion{
791
+ RunID: runID,
792
+ TaskName: name,
793
+ TaskType: taskSnapshot.Type,
794
+ Pattern: taskSnapshot.Pattern,
795
+ Trigger: opts.Trigger,
796
+ DispatchID: opts.DispatchID,
797
+ CorrelationID: opts.CorrelationID,
798
+ OperatorUserID: opts.OperatorUserID,
799
+ Reason: opts.Reason,
800
+ Runtime: opts.Runtime,
801
+ StartedAt: startTime,
802
+ FinishedAt: finishedAt,
803
+ DurationMs: duration.Milliseconds(),
804
+ Status: RunStatusCanceled,
805
+ Error: taskErr,
806
+ CancelRequestedAt: cancelRequestedAt,
807
+ Metadata: runMetadata,
808
+ }); err != nil {
809
+ s.logger.Error("record scheduler task cancellation failed",
810
+ zap.String("name", name),
811
+ zap.Error(err),
812
+ )
813
+ }
814
+ }
815
+ if span != nil {
816
+ span.SetStatus(codes.Error, taskErr)
817
+ span.SetAttributes(attribute.Float64("job.duration_seconds", duration.Seconds()))
818
+ span.End()
819
+ }
820
+ return TaskRunResult{
821
+ RunID: runID,
822
+ Status: RunStatusCanceled,
823
+ Error: taskErr,
824
+ Canceled: true,
825
+ CancelRequestedAt: cancelRequestedAt,
826
+ }, nil
827
+ }
828
+
829
+ handlerCtx := withRunEventReporter(ctx, runID, recorder, s.logger)
830
+ cancelHandlerTimeout := func() {}
831
+ if maxRuntime > 0 {
832
+ handlerCtx, cancelHandlerTimeout = context.WithTimeoutCause(handlerCtx, maxRuntime, ErrTaskRuntimeExceeded)
833
+ }
834
+ taskErr, disposition := s.runTaskHandlerWithRetry(handlerCtx, name, fn, recorder, runID, retryMaxAttempts, retryBackoff)
835
+ cancelHandlerTimeout()
836
+
837
+ finishedAt := time.Now()
838
+ duration := finishedAt.Sub(startTime)
839
+ status := RunStatusSucceeded
840
+ switch disposition {
841
+ case taskHandlerSkipped:
842
+ status = RunStatusSkipped
843
+ case taskHandlerFailed:
844
+ status = RunStatusFailed
845
+ }
846
+ runtimeExceeded := maxRuntime > 0 && (errors.Is(context.Cause(handlerCtx), ErrTaskRuntimeExceeded) || duration > maxRuntime)
847
+ if runtimeExceeded {
848
+ status = RunStatusFailed
849
+ taskErr = ErrTaskRuntimeExceeded.Error()
850
+ }
851
+ var cancelRequestedAt *time.Time
852
+ if ctx.Err() != nil && !runtimeExceeded {
853
+ status = RunStatusCanceled
854
+ taskErr, cancelRequestedAt = schedulerCancellationDetails(ctx)
855
+ recordCtx := context.WithoutCancel(ctx)
856
+ if recorder != nil && runID > 0 {
857
+ payload := map[string]any{
858
+ "dispatch_id": opts.DispatchID,
859
+ "duration_ms": duration.Milliseconds(),
860
+ "reason": taskErr,
861
+ }
862
+ if cancelRequestedAt != nil {
863
+ payload["cancel_requested_at"] = cancelRequestedAt.UTC()
864
+ }
865
+ s.recordRunEvent(recordCtx, recorder, TaskRunEventRecord{
866
+ RunID: runID,
867
+ EventType: RunEventCancelRequested,
868
+ Level: "warning",
869
+ Stage: "control",
870
+ Message: "Task cancellation requested",
871
+ Payload: payload,
872
+ CreatedAt: finishedAt,
873
+ })
874
+ }
875
+ }
876
+ if runtimeExceeded && recorder != nil && runID > 0 {
877
+ s.recordRunEvent(ctx, recorder, TaskRunEventRecord{
878
+ RunID: runID,
879
+ EventType: RunEventRuntimeExceeded,
880
+ Level: "warning",
881
+ Message: "Task runtime exceeded control threshold",
882
+ Payload: map[string]any{
883
+ "duration_ms": duration.Milliseconds(),
884
+ "max_runtime_seconds": int64(maxRuntime / time.Second),
885
+ },
886
+ CreatedAt: finishedAt,
887
+ })
888
+ }
889
+ if status == RunStatusSkipped && recorder != nil && runID > 0 {
890
+ s.recordRunEvent(ctx, recorder, TaskRunEventRecord{
891
+ RunID: runID,
892
+ EventType: RunEventSkipped,
893
+ Level: "info",
894
+ Stage: "handler",
895
+ Message: "Task handler skipped execution",
896
+ Payload: map[string]any{"reason": taskErr},
897
+ CreatedAt: finishedAt,
898
+ })
899
+ }
900
+ s.markCompleted(name, startTime, finishedAt, status, taskErr)
901
+
902
+ if recorder != nil {
903
+ completeCtx := ctx
904
+ if status == RunStatusCanceled {
905
+ completeCtx = context.WithoutCancel(ctx)
906
+ }
907
+ if err := recorder.CompleteRun(completeCtx, TaskRunCompletion{
908
+ RunID: runID,
909
+ TaskName: name,
910
+ TaskType: taskSnapshot.Type,
911
+ Pattern: taskSnapshot.Pattern,
912
+ Trigger: opts.Trigger,
913
+ DispatchID: opts.DispatchID,
914
+ CorrelationID: opts.CorrelationID,
915
+ OperatorUserID: opts.OperatorUserID,
916
+ Reason: opts.Reason,
917
+ Runtime: opts.Runtime,
918
+ StartedAt: startTime,
919
+ FinishedAt: finishedAt,
920
+ DurationMs: duration.Milliseconds(),
921
+ Status: status,
922
+ Error: taskErr,
923
+ CancelRequestedAt: cancelRequestedAt,
924
+ Metadata: runMetadata,
925
+ }); err != nil {
926
+ s.logger.Error("record scheduler task completion failed",
927
+ zap.String("name", name),
928
+ zap.Error(err),
929
+ )
930
+ }
931
+ }
932
+
933
+ if span != nil {
934
+ if status == RunStatusFailed || status == RunStatusCanceled {
935
+ span.SetStatus(codes.Error, taskErr)
936
+ }
937
+ span.SetAttributes(attribute.Float64("job.duration_seconds", duration.Seconds()))
938
+ span.End()
939
+ }
940
+
941
+ if obs != nil {
942
+ switch status {
943
+ case RunStatusSucceeded:
944
+ obs.JobSucceeded(name, time.Now())
945
+ case RunStatusFailed:
946
+ obs.JobFailed(name)
947
+ }
948
+ }
949
+
950
+ if status == RunStatusCanceled {
951
+ s.logger.Warn("task canceled", zap.String("name", name), zap.Duration("duration", duration), zap.String("reason", taskErr))
952
+ } else if status == RunStatusSkipped {
953
+ s.logger.Info("task skipped", zap.String("name", name), zap.Duration("duration", duration), zap.String("reason", taskErr))
954
+ } else if status == RunStatusSucceeded {
955
+ s.logger.Info("task completed", zap.String("name", name), zap.Duration("duration", duration))
956
+ } else {
957
+ s.logger.Error("task failed", zap.String("name", name), zap.Duration("duration", duration), zap.String("error", taskErr))
958
+ }
959
+
960
+ handlerSkipReason := ""
961
+ if status == RunStatusSkipped {
962
+ handlerSkipReason = taskErr
963
+ }
964
+ return TaskRunResult{
965
+ RunID: runID,
966
+ Status: status,
967
+ Error: taskErr,
968
+ Skipped: status == RunStatusSkipped,
969
+ SkipReason: handlerSkipReason,
970
+ Canceled: status == RunStatusCanceled,
971
+ CancelRequestedAt: cancelRequestedAt,
972
+ }, nil
973
+ }
974
+
975
+ func buildTaskRunMetadata(task RegisteredTask, control *TaskControl, opts TaskRunOptions, startedAt time.Time) map[string]any {
976
+ taskSnapshot := map[string]any{
977
+ "name": task.Name,
978
+ "type": string(task.Type),
979
+ "pattern": task.Pattern,
980
+ "default_pattern": task.DefaultPattern,
981
+ "registered_at": task.RegisteredAt.UTC(),
982
+ "run_count_before": task.RunCount,
983
+ "error_count": task.ErrorCount,
984
+ }
985
+ if task.LastRun != nil {
986
+ taskSnapshot["last_run_at"] = task.LastRun.UTC()
987
+ }
988
+ if task.NextRun != nil {
989
+ taskSnapshot["next_run_at"] = task.NextRun.UTC()
990
+ }
991
+ if task.LastError != "" {
992
+ taskSnapshot["last_error"] = task.LastError
993
+ }
994
+
995
+ triggerSnapshot := map[string]any{
996
+ "type": string(opts.Trigger),
997
+ }
998
+ if opts.DispatchID > 0 {
999
+ triggerSnapshot["dispatch_id"] = opts.DispatchID
1000
+ }
1001
+ if opts.CorrelationID != "" {
1002
+ triggerSnapshot["correlation_id"] = opts.CorrelationID
1003
+ }
1004
+ if opts.OperatorUserID != nil {
1005
+ triggerSnapshot["operator_user_id"] = *opts.OperatorUserID
1006
+ }
1007
+ if opts.Reason != "" {
1008
+ triggerSnapshot["reason"] = opts.Reason
1009
+ }
1010
+
1011
+ runtimeSnapshot := map[string]any{
1012
+ "name": opts.Runtime,
1013
+ "started_at": startedAt.UTC(),
1014
+ }
1015
+
1016
+ controlSnapshot := map[string]any{
1017
+ "configured": false,
1018
+ }
1019
+ if control != nil {
1020
+ controlSnapshot = map[string]any{
1021
+ "configured": true,
1022
+ "status": string(control.ControlStatus),
1023
+ "concurrency_policy": string(control.ConcurrencyPolicy),
1024
+ "manual_run_enabled": control.ManualRunEnabled,
1025
+ "max_runtime_seconds": control.MaxRuntimeSeconds,
1026
+ "retry_max_attempts": control.RetryMaxAttempts,
1027
+ "retry_backoff_seconds": control.RetryBackoffSeconds,
1028
+ "definition_revision": control.DefinitionRevision,
1029
+ }
1030
+ if control.Pattern != "" {
1031
+ controlSnapshot["pattern"] = control.Pattern
1032
+ }
1033
+ if control.Timezone != "" {
1034
+ controlSnapshot["timezone"] = control.Timezone
1035
+ }
1036
+ }
1037
+
1038
+ metadata := map[string]any{
1039
+ "schema_version": taskRunMetadataSchemaVersion,
1040
+ "task": taskSnapshot,
1041
+ "trigger": triggerSnapshot,
1042
+ "runtime": runtimeSnapshot,
1043
+ "control": controlSnapshot,
1044
+ }
1045
+ if input := cloneTaskRunMetadata(opts.Metadata); len(input) > 0 {
1046
+ metadata["input"] = input
1047
+ }
1048
+ return metadata
1049
+ }
1050
+
1051
+ func cloneTaskRunMetadata(value map[string]any) map[string]any {
1052
+ if len(value) == 0 {
1053
+ return nil
1054
+ }
1055
+ clone := make(map[string]any, len(value))
1056
+ for key, item := range value {
1057
+ clone[key] = item
1058
+ }
1059
+ return clone
1060
+ }
1061
+
1062
+ func buildTaskRunPolicyPayload(control *TaskControl, opts TaskRunOptions, retryMaxAttempts int, retryBackoff time.Duration, maxRuntime time.Duration) map[string]any {
1063
+ payload := map[string]any{
1064
+ "trigger_type": string(opts.Trigger),
1065
+ "retry_max_attempts": retryMaxAttempts,
1066
+ "retry_backoff_seconds": int64(retryBackoff / time.Second),
1067
+ "max_runtime_seconds": int64(maxRuntime / time.Second),
1068
+ }
1069
+ if opts.DispatchID > 0 {
1070
+ payload["dispatch_id"] = opts.DispatchID
1071
+ }
1072
+ if opts.CorrelationID != "" {
1073
+ payload["correlation_id"] = opts.CorrelationID
1074
+ }
1075
+ if control == nil {
1076
+ payload["control_configured"] = false
1077
+ return payload
1078
+ }
1079
+ payload["control_configured"] = true
1080
+ payload["control_status"] = string(control.ControlStatus)
1081
+ payload["concurrency_policy"] = string(control.ConcurrencyPolicy)
1082
+ payload["manual_run_enabled"] = control.ManualRunEnabled
1083
+ payload["definition_revision"] = control.DefinitionRevision
1084
+ if control.Pattern != "" {
1085
+ payload["pattern"] = control.Pattern
1086
+ }
1087
+ if control.Timezone != "" {
1088
+ payload["timezone"] = control.Timezone
1089
+ }
1090
+ return payload
1091
+ }
1092
+
1093
+ func resolveTaskRuntimePolicy(control *TaskControl) (int, time.Duration, time.Duration) {
1094
+ if control == nil {
1095
+ return 0, 60 * time.Second, 0
1096
+ }
1097
+ retryMaxAttempts := control.RetryMaxAttempts
1098
+ if retryMaxAttempts < 0 {
1099
+ retryMaxAttempts = 0
1100
+ }
1101
+ backoffSeconds := control.RetryBackoffSeconds
1102
+ if backoffSeconds <= 0 {
1103
+ backoffSeconds = 60
1104
+ }
1105
+ var maxRuntime time.Duration
1106
+ if control.MaxRuntimeSeconds > 0 {
1107
+ maxRuntime = time.Duration(control.MaxRuntimeSeconds) * time.Second
1108
+ }
1109
+ return retryMaxAttempts, time.Duration(backoffSeconds) * time.Second, maxRuntime
1110
+ }
1111
+
1112
+ func (s *Scheduler) runTaskHandlerWithRetry(ctx context.Context, name string, fn TaskHandler, recorder RunRecorder, runID int64, retryMaxAttempts int, retryBackoff time.Duration) (string, taskHandlerDisposition) {
1113
+ var taskErr string
1114
+ for attempt := 0; ; attempt++ {
1115
+ if ctx.Err() != nil {
1116
+ return schedulerContextError(ctx), taskHandlerFailed
1117
+ }
1118
+ attemptNumber := attempt + 1
1119
+ attemptStartedAt := time.Now()
1120
+ if recorder != nil && runID > 0 {
1121
+ s.recordRunEvent(ctx, recorder, TaskRunEventRecord{
1122
+ RunID: runID,
1123
+ EventType: RunEventAttemptStarted,
1124
+ Level: "info",
1125
+ Stage: "handler",
1126
+ Message: "Task attempt started",
1127
+ Payload: map[string]any{
1128
+ "attempt": attemptNumber,
1129
+ "max_retries": retryMaxAttempts,
1130
+ },
1131
+ CreatedAt: attemptStartedAt,
1132
+ })
1133
+ }
1134
+ outcome := s.runTaskHandlerOnce(ctx, name, runID, fn)
1135
+ taskErr = outcome.message
1136
+ attemptDuration := time.Since(attemptStartedAt)
1137
+ if outcome.disposition == taskHandlerSucceeded {
1138
+ if recorder != nil && runID > 0 {
1139
+ s.recordRunEvent(ctx, recorder, TaskRunEventRecord{
1140
+ RunID: runID,
1141
+ EventType: RunEventAttemptSucceeded,
1142
+ Level: "info",
1143
+ Stage: "handler",
1144
+ Message: "Task attempt succeeded",
1145
+ Payload: map[string]any{
1146
+ "attempt": attemptNumber,
1147
+ "duration_ms": attemptDuration.Milliseconds(),
1148
+ "max_retries": retryMaxAttempts,
1149
+ },
1150
+ CreatedAt: time.Now(),
1151
+ })
1152
+ }
1153
+ return "", taskHandlerSucceeded
1154
+ }
1155
+ if outcome.disposition == taskHandlerSkipped {
1156
+ return taskErr, taskHandlerSkipped
1157
+ }
1158
+ willRetry := attempt < retryMaxAttempts && ctx.Err() == nil
1159
+ if recorder != nil && runID > 0 {
1160
+ recordCtx := ctx
1161
+ if ctx.Err() != nil {
1162
+ recordCtx = context.WithoutCancel(ctx)
1163
+ }
1164
+ s.recordRunEvent(recordCtx, recorder, TaskRunEventRecord{
1165
+ RunID: runID,
1166
+ EventType: RunEventAttemptFailed,
1167
+ Level: "error",
1168
+ Stage: "handler",
1169
+ Message: "Task attempt failed",
1170
+ Payload: map[string]any{
1171
+ "attempt": attemptNumber,
1172
+ "duration_ms": attemptDuration.Milliseconds(),
1173
+ "max_retries": retryMaxAttempts,
1174
+ "will_retry": willRetry,
1175
+ "backoff_seconds": int64(retryBackoff / time.Second),
1176
+ "error": taskErr,
1177
+ },
1178
+ CreatedAt: time.Now(),
1179
+ })
1180
+ }
1181
+ if ctx.Err() != nil {
1182
+ return schedulerContextError(ctx), taskHandlerFailed
1183
+ }
1184
+ if attempt >= retryMaxAttempts {
1185
+ return taskErr, taskHandlerFailed
1186
+ }
1187
+ s.logger.Warn("task retry scheduled",
1188
+ zap.String("name", name),
1189
+ zap.Int("attempt", attempt+1),
1190
+ zap.Int("nextAttempt", attempt+2),
1191
+ zap.String("error", taskErr),
1192
+ )
1193
+ if recorder != nil && runID > 0 {
1194
+ s.recordRunEvent(ctx, recorder, TaskRunEventRecord{
1195
+ RunID: runID,
1196
+ EventType: RunEventRetrying,
1197
+ Level: "warning",
1198
+ Stage: "handler",
1199
+ Message: "Task retry scheduled",
1200
+ Payload: map[string]any{
1201
+ "attempt": attemptNumber,
1202
+ "next_attempt": attemptNumber + 1,
1203
+ "max_retries": retryMaxAttempts,
1204
+ "backoff_seconds": int64(retryBackoff / time.Second),
1205
+ "error": taskErr,
1206
+ },
1207
+ CreatedAt: time.Now(),
1208
+ })
1209
+ }
1210
+ timer := time.NewTimer(retryBackoff)
1211
+ select {
1212
+ case <-ctx.Done():
1213
+ if !timer.Stop() {
1214
+ select {
1215
+ case <-timer.C:
1216
+ default:
1217
+ }
1218
+ }
1219
+ return schedulerContextError(ctx), taskHandlerFailed
1220
+ case <-timer.C:
1221
+ }
1222
+ }
1223
+ }
1224
+
1225
+ func (s *Scheduler) runTaskHandlerOnce(ctx context.Context, name string, runID int64, fn TaskHandler) (outcome taskHandlerOutcome) {
1226
+ outcome.disposition = taskHandlerSucceeded
1227
+ defer func() {
1228
+ if r := recover(); r != nil {
1229
+ outcome = taskHandlerOutcome{disposition: taskHandlerFailed, message: safeTaskPanicMessage(r)}
1230
+ s.logger.Error("task panic", schedulerErrorLogFields(name, runID, outcome.message)...)
1231
+ }
1232
+ }()
1233
+ if err := fn(ctx); err != nil {
1234
+ if reason, ok := runSkippedErrorReason(err); ok {
1235
+ outcome = taskHandlerOutcome{disposition: taskHandlerSkipped, message: sanitizeTaskError(reason)}
1236
+ if outcome.message == "" {
1237
+ outcome.message = "task skipped"
1238
+ }
1239
+ return outcome
1240
+ }
1241
+ outcome = taskHandlerOutcome{disposition: taskHandlerFailed, message: sanitizeTaskError(err.Error())}
1242
+ s.logger.Error("task returned error", schedulerErrorLogFields(name, runID, outcome.message)...)
1243
+ }
1244
+ return outcome
1245
+ }
1246
+
1247
+ func runSkippedErrorReason(err error) (string, bool) {
1248
+ var value RunSkippedError
1249
+ if errors.As(err, &value) {
1250
+ return value.Reason, true
1251
+ }
1252
+ var pointer *RunSkippedError
1253
+ if errors.As(err, &pointer) && pointer != nil {
1254
+ return pointer.Reason, true
1255
+ }
1256
+ return "", false
1257
+ }
1258
+
1259
+ // recover 返回任意 panic 载荷;这里只保留类型类别,避免秘密值进入日志和持久化。
1260
+ func safeTaskPanicMessage(value any) string {
1261
+ switch value.(type) {
1262
+ case error:
1263
+ return "task panic: error"
1264
+ case string:
1265
+ return "task panic: string"
1266
+ default:
1267
+ return "task panic: value"
1268
+ }
1269
+ }
1270
+
1271
+ func sanitizeTaskError(message string) string {
1272
+ message = strings.TrimSpace(message)
1273
+ if message == "" {
1274
+ return ""
1275
+ }
1276
+ sanitized := taskErrorBearerPattern.ReplaceAllString(message, "Bearer [REDACTED]")
1277
+ sanitized = taskErrorSensitivePairPattern.ReplaceAllString(sanitized, "$1=[REDACTED]")
1278
+ sanitized = strings.Map(func(r rune) rune {
1279
+ if r == '\n' || r == '\r' || r == '\t' {
1280
+ return ' '
1281
+ }
1282
+ if r < 0x20 || r == 0x7f {
1283
+ return -1
1284
+ }
1285
+ return r
1286
+ }, sanitized)
1287
+ sanitized = strings.Join(strings.Fields(sanitized), " ")
1288
+ runes := []rune(sanitized)
1289
+ if len(runes) > taskErrorMaxLength {
1290
+ return string(runes[:taskErrorMaxLength]) + "..."
1291
+ }
1292
+ return sanitized
1293
+ }
1294
+
1295
+ func schedulerErrorLogFields(name string, runID int64, taskErr string) []zap.Field {
1296
+ fields := []zap.Field{
1297
+ zap.String("name", name),
1298
+ zap.String("error", taskErr),
1299
+ }
1300
+ if runID > 0 {
1301
+ fields = append(fields, zap.Int64("runID", runID))
1302
+ }
1303
+ return fields
1304
+ }
1305
+
1306
+ func (s *Scheduler) recordRunEvent(ctx context.Context, recorder RunRecorder, event TaskRunEventRecord) {
1307
+ if err := recorder.RecordRunEvent(ctx, event); err != nil {
1308
+ s.logger.Error("record scheduler task event failed",
1309
+ zap.Int64("runID", event.RunID),
1310
+ zap.String("eventType", string(event.EventType)),
1311
+ zap.Error(err),
1312
+ )
1313
+ }
1314
+ }
1315
+
1316
+ func (s *Scheduler) getControl(ctx context.Context, name string) (*TaskControl, error) {
1317
+ s.mu.RLock()
1318
+ store := s.controls
1319
+ s.mu.RUnlock()
1320
+ if store == nil {
1321
+ return nil, nil
1322
+ }
1323
+ return store.GetTaskControl(ctx, name)
1324
+ }
1325
+
1326
+ func (s *Scheduler) recordSkipped(ctx context.Context, task *RegisteredTask, recorder RunRecorder, opts TaskRunOptions, startedAt time.Time, reason string) TaskRunResult {
1327
+ finishedAt := time.Now()
1328
+ duration := finishedAt.Sub(startedAt)
1329
+ runID := int64(0)
1330
+ if recorder != nil {
1331
+ var err error
1332
+ runID, err = recorder.StartRun(ctx, TaskRunStart{
1333
+ TaskName: task.Name,
1334
+ TaskType: task.Type,
1335
+ Pattern: task.Pattern,
1336
+ Trigger: opts.Trigger,
1337
+ DispatchID: opts.DispatchID,
1338
+ CorrelationID: opts.CorrelationID,
1339
+ OperatorUserID: opts.OperatorUserID,
1340
+ Reason: opts.Reason,
1341
+ Runtime: opts.Runtime,
1342
+ StartedAt: startedAt,
1343
+ Metadata: opts.Metadata,
1344
+ })
1345
+ if err != nil {
1346
+ s.logger.Error("record skipped scheduler task start failed",
1347
+ zap.String("name", task.Name),
1348
+ zap.Error(err),
1349
+ )
1350
+ }
1351
+ if err := recorder.CompleteRun(ctx, TaskRunCompletion{
1352
+ RunID: runID,
1353
+ TaskName: task.Name,
1354
+ TaskType: task.Type,
1355
+ Pattern: task.Pattern,
1356
+ Trigger: opts.Trigger,
1357
+ DispatchID: opts.DispatchID,
1358
+ CorrelationID: opts.CorrelationID,
1359
+ OperatorUserID: opts.OperatorUserID,
1360
+ Reason: opts.Reason,
1361
+ Runtime: opts.Runtime,
1362
+ StartedAt: startedAt,
1363
+ FinishedAt: finishedAt,
1364
+ DurationMs: duration.Milliseconds(),
1365
+ Status: RunStatusSkipped,
1366
+ Error: reason,
1367
+ Metadata: opts.Metadata,
1368
+ }); err != nil {
1369
+ s.logger.Error("record skipped scheduler task completion failed",
1370
+ zap.String("name", task.Name),
1371
+ zap.Error(err),
1372
+ )
1373
+ }
1374
+ }
1375
+ s.markCompleted(task.Name, startedAt, finishedAt, RunStatusSkipped, reason)
1376
+ s.logger.Info("task skipped", zap.String("name", task.Name), zap.String("reason", reason))
1377
+ return TaskRunResult{
1378
+ RunID: runID,
1379
+ Status: RunStatusSkipped,
1380
+ Error: reason,
1381
+ Skipped: true,
1382
+ SkipReason: reason,
1383
+ }
1384
+ }
1385
+
1386
+ func (s *Scheduler) markStartedWithControl(name string, t time.Time, control *TaskControl) (*RegisteredTask, RunRecorder, string) {
1387
+ s.mu.Lock()
1388
+ defer s.mu.Unlock()
1389
+
1390
+ task, exists := s.tasks[name]
1391
+ if !exists {
1392
+ return nil, s.recorder, ""
1393
+ }
1394
+ if control != nil {
1395
+ if control.ControlStatus == ControlStatusPaused {
1396
+ taskCopy := *task
1397
+ return &taskCopy, s.recorder, "task is paused"
1398
+ }
1399
+ if control.ConcurrencyPolicy != ConcurrencyPolicyAllow && task.IsRunning {
1400
+ taskCopy := *task
1401
+ return &taskCopy, s.recorder, "task is already running"
1402
+ }
1403
+ } else if task.IsRunning {
1404
+ taskCopy := *task
1405
+ return &taskCopy, s.recorder, "task is already running"
1406
+ }
1407
+ task.IsRunning = true
1408
+ task.LastRun = &t
1409
+ taskCopy := *task
1410
+ return &taskCopy, s.recorder, ""
1411
+ }
1412
+
1413
+ func (s *Scheduler) markCompleted(name string, startTime, endTime time.Time, status RunStatus, errMsg string) {
1414
+ s.mu.Lock()
1415
+ defer s.mu.Unlock()
1416
+
1417
+ task, exists := s.tasks[name]
1418
+ if !exists {
1419
+ return
1420
+ }
1421
+
1422
+ task.IsRunning = false
1423
+ task.RunCount++
1424
+
1425
+ if status == RunStatusFailed {
1426
+ task.ErrorCount++
1427
+ task.LastError = errMsg
1428
+ }
1429
+
1430
+ // 更新下次运行时间
1431
+ entry := s.cron.Entry(task.entryID)
1432
+ if !entry.Next.IsZero() {
1433
+ t := entry.Next
1434
+ task.NextRun = &t
1435
+ }
1436
+
1437
+ // 记录执行结果
1438
+ result := ExecutionResult{
1439
+ TaskName: name,
1440
+ StartTime: startTime,
1441
+ EndTime: endTime,
1442
+ Duration: endTime.Sub(startTime).Milliseconds(),
1443
+ Success: status == RunStatusSucceeded,
1444
+ Error: errMsg,
1445
+ }
1446
+
1447
+ history := s.executionHistory[name]
1448
+ history = append(history, result)
1449
+ if len(history) > s.maxHistorySize {
1450
+ history = history[1:]
1451
+ }
1452
+ s.executionHistory[name] = history
1453
+ }
1454
+
1455
+ // ═══════════════════════════════════════════════════════════════
1456
+ // 查询
1457
+ // ═══════════════════════════════════════════════════════════════
1458
+
1459
+ // GetAllTasks 获取所有已注册任务
1460
+ func (s *Scheduler) GetAllTasks() []RegisteredTask {
1461
+ s.mu.RLock()
1462
+ defer s.mu.RUnlock()
1463
+
1464
+ tasks := make([]RegisteredTask, 0, len(s.tasks))
1465
+ for _, t := range s.tasks {
1466
+ tasks = append(tasks, *t)
1467
+ }
1468
+ return tasks
1469
+ }
1470
+
1471
+ // GetTask 获取特定任务
1472
+ func (s *Scheduler) GetTask(name string) (*RegisteredTask, bool) {
1473
+ s.mu.RLock()
1474
+ defer s.mu.RUnlock()
1475
+
1476
+ t, ok := s.tasks[name]
1477
+ if !ok {
1478
+ return nil, false
1479
+ }
1480
+ taskCopy := *t
1481
+ return &taskCopy, true
1482
+ }
1483
+
1484
+ // GetTaskHistory 获取任务执行历史
1485
+ func (s *Scheduler) GetTaskHistory(name string, limit int) []ExecutionResult {
1486
+ s.mu.RLock()
1487
+ defer s.mu.RUnlock()
1488
+
1489
+ history, exists := s.executionHistory[name]
1490
+ if !exists {
1491
+ return nil
1492
+ }
1493
+
1494
+ if limit > 0 && limit < len(history) {
1495
+ return history[len(history)-limit:]
1496
+ }
1497
+ result := make([]ExecutionResult, len(history))
1498
+ copy(result, history)
1499
+ return result
1500
+ }
1501
+
1502
+ // GetTaskStatistics 获取任务统计信息
1503
+ func (s *Scheduler) GetTaskStatistics(name string) *TaskStatistics {
1504
+ s.mu.RLock()
1505
+ defer s.mu.RUnlock()
1506
+
1507
+ task, exists := s.tasks[name]
1508
+ if !exists {
1509
+ return nil
1510
+ }
1511
+
1512
+ history := s.executionHistory[name]
1513
+
1514
+ var totalDuration int64
1515
+ var successCount int64
1516
+ for _, h := range history {
1517
+ totalDuration += h.Duration
1518
+ if h.Success {
1519
+ successCount++
1520
+ }
1521
+ }
1522
+
1523
+ var avgDuration float64
1524
+ var successRate float64
1525
+
1526
+ if len(history) > 0 {
1527
+ avgDuration = float64(totalDuration) / float64(len(history))
1528
+ successRate = float64(successCount) / float64(len(history)) * 100
1529
+ } else {
1530
+ successRate = 100
1531
+ }
1532
+
1533
+ // 最近 10 条
1534
+ recent := history
1535
+ if len(recent) > 10 {
1536
+ recent = recent[len(recent)-10:]
1537
+ }
1538
+
1539
+ return &TaskStatistics{
1540
+ Task: *task,
1541
+ AverageDuration: avgDuration,
1542
+ SuccessRate: successRate,
1543
+ RecentExecutions: recent,
1544
+ }
1545
+ }
1546
+
1547
+ // TaskStatistics 任务统计
1548
+ type TaskStatistics struct {
1549
+ Task RegisteredTask
1550
+ AverageDuration float64
1551
+ SuccessRate float64
1552
+ RecentExecutions []ExecutionResult
1553
+ }
1554
+
1555
+ // HealthStatus 调度器健康状态
1556
+ type HealthStatus struct {
1557
+ Healthy bool
1558
+ TotalTasks int
1559
+ RunningTasks int
1560
+ FailedTasks int
1561
+ CronJobs int
1562
+ }
1563
+
1564
+ // GetHealthStatus 获取调度器健康状态
1565
+ func (s *Scheduler) GetHealthStatus() HealthStatus {
1566
+ s.mu.RLock()
1567
+ defer s.mu.RUnlock()
1568
+
1569
+ var running, failed, cronJobs int
1570
+ for _, t := range s.tasks {
1571
+ if t.IsRunning {
1572
+ running++
1573
+ }
1574
+ if t.ErrorCount > 0 {
1575
+ failed++
1576
+ }
1577
+ if t.Type == TaskTypeCron {
1578
+ cronJobs++
1579
+ }
1580
+ }
1581
+
1582
+ return HealthStatus{
1583
+ Healthy: true,
1584
+ TotalTasks: len(s.tasks),
1585
+ RunningTasks: running,
1586
+ FailedTasks: failed,
1587
+ CronJobs: cronJobs,
1588
+ }
1589
+ }