shenjicore 0.6.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. shenjicore-0.6.4/.claude/skills/shenjicore/SKILL.md +113 -0
  2. shenjicore-0.6.4/.claude/skills/shenjicore/examples/your_project/.env.example +34 -0
  3. shenjicore-0.6.4/.claude/skills/shenjicore/examples/your_project/README.md +53 -0
  4. shenjicore-0.6.4/.claude/skills/shenjicore/examples/your_project/action/user.py +42 -0
  5. shenjicore-0.6.4/.claude/skills/shenjicore/examples/your_project/common/config.py +15 -0
  6. shenjicore-0.6.4/.claude/skills/shenjicore/examples/your_project/common/errors.py +9 -0
  7. shenjicore-0.6.4/.claude/skills/shenjicore/examples/your_project/dao/user.py +34 -0
  8. shenjicore-0.6.4/.claude/skills/shenjicore/examples/your_project/domain/user.py +76 -0
  9. shenjicore-0.6.4/.claude/skills/shenjicore/examples/your_project/main.py +19 -0
  10. shenjicore-0.6.4/.claude/skills/shenjicore/examples/your_project/schema/user.py +39 -0
  11. shenjicore-0.6.4/.claude/skills/shenjicore/examples/your_project/tests/conftest.py +8 -0
  12. shenjicore-0.6.4/.claude/skills/shenjicore/examples/your_project/tests/test_layered.py +178 -0
  13. shenjicore-0.6.4/.claude/skills/shenjicore/references/CONVENTIONS.md +749 -0
  14. shenjicore-0.6.4/.claude/skills/shenjicore/references/layering.md +99 -0
  15. shenjicore-0.6.4/.claude/skills/shenjicore/references/plugins.md +397 -0
  16. shenjicore-0.6.4/.env.example +115 -0
  17. shenjicore-0.6.4/.env.prod.example +8 -0
  18. shenjicore-0.6.4/.github/workflows/ci.yml +73 -0
  19. shenjicore-0.6.4/.gitignore +25 -0
  20. shenjicore-0.6.4/CHANGELOG.md +529 -0
  21. shenjicore-0.6.4/LICENSE +21 -0
  22. shenjicore-0.6.4/PKG-INFO +204 -0
  23. shenjicore-0.6.4/README.md +147 -0
  24. shenjicore-0.6.4/docs/04-/344/273/243/347/240/201/345/256/241/350/256/241/minio/346/217/222/344/273/266/346/265/213/350/257/225-/345/256/241/350/256/241/346/212/245/345/221/212.md +64 -0
  25. shenjicore-0.6.4/docs/04-/344/273/243/347/240/201/345/256/241/350/256/241/mysql/346/217/222/344/273/266/346/265/213/350/257/225-/345/256/241/350/256/241/346/212/245/345/221/212.md +66 -0
  26. shenjicore-0.6.4/docs/CONVENTIONS.md +753 -0
  27. shenjicore-0.6.4/docs/PLANS/v0.3-upgrade-plan.md +566 -0
  28. shenjicore-0.6.4/docs/PUBLISHING.md +46 -0
  29. shenjicore-0.6.4/docs/ROADMAP.md +195 -0
  30. shenjicore-0.6.4/docs/USER_GUIDE.md +1844 -0
  31. shenjicore-0.6.4/examples/your_project/.env.example +34 -0
  32. shenjicore-0.6.4/examples/your_project/README.md +53 -0
  33. shenjicore-0.6.4/examples/your_project/action/user.py +42 -0
  34. shenjicore-0.6.4/examples/your_project/common/config.py +15 -0
  35. shenjicore-0.6.4/examples/your_project/common/errors.py +9 -0
  36. shenjicore-0.6.4/examples/your_project/dao/user.py +34 -0
  37. shenjicore-0.6.4/examples/your_project/domain/user.py +76 -0
  38. shenjicore-0.6.4/examples/your_project/main.py +19 -0
  39. shenjicore-0.6.4/examples/your_project/schema/user.py +39 -0
  40. shenjicore-0.6.4/examples/your_project/tests/conftest.py +8 -0
  41. shenjicore-0.6.4/examples/your_project/tests/test_layered.py +178 -0
  42. shenjicore-0.6.4/pyproject.toml +134 -0
  43. shenjicore-0.6.4/shenjicore/__init__.py +144 -0
  44. shenjicore-0.6.4/shenjicore/__main__.py +6 -0
  45. shenjicore-0.6.4/shenjicore/cli.py +322 -0
  46. shenjicore-0.6.4/shenjicore/core/app.py +259 -0
  47. shenjicore-0.6.4/shenjicore/core/config.py +228 -0
  48. shenjicore-0.6.4/shenjicore/core/errors.py +108 -0
  49. shenjicore-0.6.4/shenjicore/core/log.py +180 -0
  50. shenjicore-0.6.4/shenjicore/core/middleware.py +153 -0
  51. shenjicore-0.6.4/shenjicore/core/request_id.py +38 -0
  52. shenjicore-0.6.4/shenjicore/core/response.py +61 -0
  53. shenjicore-0.6.4/shenjicore/core/time.py +40 -0
  54. shenjicore-0.6.4/shenjicore/plugin.py +224 -0
  55. shenjicore-0.6.4/shenjicore/plugins/__init__.py +12 -0
  56. shenjicore-0.6.4/shenjicore/plugins/audit.py +430 -0
  57. shenjicore-0.6.4/shenjicore/plugins/auth/__init__.py +227 -0
  58. shenjicore-0.6.4/shenjicore/plugins/auth/api_key.py +123 -0
  59. shenjicore-0.6.4/shenjicore/plugins/auth/deps.py +233 -0
  60. shenjicore-0.6.4/shenjicore/plugins/auth/security.py +197 -0
  61. shenjicore-0.6.4/shenjicore/plugins/auth/store.py +207 -0
  62. shenjicore-0.6.4/shenjicore/plugins/auth/tokens.py +169 -0
  63. shenjicore-0.6.4/shenjicore/plugins/file/__init__.py +210 -0
  64. shenjicore-0.6.4/shenjicore/plugins/file/connection.py +1200 -0
  65. shenjicore-0.6.4/shenjicore/plugins/file/errors.py +55 -0
  66. shenjicore-0.6.4/shenjicore/plugins/file/export.py +244 -0
  67. shenjicore-0.6.4/shenjicore/plugins/file/imports.py +137 -0
  68. shenjicore-0.6.4/shenjicore/plugins/file/ops.py +713 -0
  69. shenjicore-0.6.4/shenjicore/plugins/file/router.py +435 -0
  70. shenjicore-0.6.4/shenjicore/plugins/help.py +63 -0
  71. shenjicore-0.6.4/shenjicore/plugins/hikvision/__init__.py +145 -0
  72. shenjicore-0.6.4/shenjicore/plugins/hikvision/connection.py +274 -0
  73. shenjicore-0.6.4/shenjicore/plugins/hikvision/errors.py +89 -0
  74. shenjicore-0.6.4/shenjicore/plugins/hikvision/meter.py +108 -0
  75. shenjicore-0.6.4/shenjicore/plugins/hikvision/ops.py +342 -0
  76. shenjicore-0.6.4/shenjicore/plugins/hikvision/scale.py +655 -0
  77. shenjicore-0.6.4/shenjicore/plugins/hikvision/stream.py +740 -0
  78. shenjicore-0.6.4/shenjicore/plugins/httpclient/__init__.py +124 -0
  79. shenjicore-0.6.4/shenjicore/plugins/httpclient/api.py +303 -0
  80. shenjicore-0.6.4/shenjicore/plugins/httpclient/connection.py +178 -0
  81. shenjicore-0.6.4/shenjicore/plugins/httpclient/errors.py +84 -0
  82. shenjicore-0.6.4/shenjicore/plugins/minio/__init__.py +182 -0
  83. shenjicore-0.6.4/shenjicore/plugins/minio/connection.py +196 -0
  84. shenjicore-0.6.4/shenjicore/plugins/minio/errors.py +70 -0
  85. shenjicore-0.6.4/shenjicore/plugins/minio/ops.py +764 -0
  86. shenjicore-0.6.4/shenjicore/plugins/mysql/__init__.py +152 -0
  87. shenjicore-0.6.4/shenjicore/plugins/mysql/cipher.py +183 -0
  88. shenjicore-0.6.4/shenjicore/plugins/mysql/connection.py +444 -0
  89. shenjicore-0.6.4/shenjicore/plugins/mysql/dao.py +214 -0
  90. shenjicore-0.6.4/shenjicore/plugins/mysql/errors.py +76 -0
  91. shenjicore-0.6.4/shenjicore/plugins/mysql/query.py +456 -0
  92. shenjicore-0.6.4/shenjicore/plugins/mysql/sql.py +323 -0
  93. shenjicore-0.6.4/shenjicore/plugins/rbac/__init__.py +83 -0
  94. shenjicore-0.6.4/shenjicore/plugins/rbac/deps.py +103 -0
  95. shenjicore-0.6.4/shenjicore/plugins/rbac/resolver.py +102 -0
  96. shenjicore-0.6.4/shenjicore/plugins/redis/__init__.py +187 -0
  97. shenjicore-0.6.4/shenjicore/plugins/redis/cache.py +89 -0
  98. shenjicore-0.6.4/shenjicore/plugins/redis/connection.py +166 -0
  99. shenjicore-0.6.4/shenjicore/plugins/redis/errors.py +39 -0
  100. shenjicore-0.6.4/shenjicore/plugins/redis/lock.py +194 -0
  101. shenjicore-0.6.4/shenjicore/plugins/redis/queue.py +726 -0
  102. shenjicore-0.6.4/shenjicore/plugins/redis/rate_limit.py +113 -0
  103. shenjicore-0.6.4/shenjicore/plugins/redis/semaphore.py +250 -0
  104. shenjicore-0.6.4/shenjicore/plugins/scheduler.py +528 -0
  105. shenjicore-0.6.4/shenjicore/plugins/tasks/__init__.py +209 -0
  106. shenjicore-0.6.4/shenjicore/plugins/tasks/events.py +118 -0
  107. shenjicore-0.6.4/shenjicore/plugins/tasks/manage.py +712 -0
  108. shenjicore-0.6.4/shenjicore/plugins/tasks/model.py +135 -0
  109. shenjicore-0.6.4/shenjicore/plugins/tasks/record.py +297 -0
  110. shenjicore-0.6.4/shenjicore/plugins/tasks/router.py +176 -0
  111. shenjicore-0.6.4/shenjicore/plugins/tasks/store.py +260 -0
  112. shenjicore-0.6.4/shenjicore/plugins/tasks/worker.py +468 -0
  113. shenjicore-0.6.4/shenjicore/plugins/yolo_vision/__init__.py +134 -0
  114. shenjicore-0.6.4/shenjicore/plugins/yolo_vision/engine.py +266 -0
  115. shenjicore-0.6.4/shenjicore/plugins/yolo_vision/watch.py +157 -0
  116. shenjicore-0.6.4/shenjicore/router.py +118 -0
  117. shenjicore-0.6.4/shenjicore/run.py +132 -0
  118. shenjicore-0.6.4/tests/file_fakes.py +149 -0
  119. shenjicore-0.6.4/tests/hikvision_fakes.py +268 -0
  120. shenjicore-0.6.4/tests/httpclient_fakes.py +68 -0
  121. shenjicore-0.6.4/tests/minio_fakes.py +417 -0
  122. shenjicore-0.6.4/tests/redis_fakes.py +442 -0
  123. shenjicore-0.6.4/tests/test_app.py +280 -0
  124. shenjicore-0.6.4/tests/test_audit_plugin.py +397 -0
  125. shenjicore-0.6.4/tests/test_auth_api_key.py +240 -0
  126. shenjicore-0.6.4/tests/test_auth_plugin.py +230 -0
  127. shenjicore-0.6.4/tests/test_auth_security.py +265 -0
  128. shenjicore-0.6.4/tests/test_auth_store.py +102 -0
  129. shenjicore-0.6.4/tests/test_auth_tokens.py +227 -0
  130. shenjicore-0.6.4/tests/test_config.py +230 -0
  131. shenjicore-0.6.4/tests/test_errors.py +102 -0
  132. shenjicore-0.6.4/tests/test_file_chunked.py +384 -0
  133. shenjicore-0.6.4/tests/test_file_direct_upload.py +336 -0
  134. shenjicore-0.6.4/tests/test_file_export.py +338 -0
  135. shenjicore-0.6.4/tests/test_file_import.py +204 -0
  136. shenjicore-0.6.4/tests/test_file_integration.py +193 -0
  137. shenjicore-0.6.4/tests/test_file_local_backend.py +562 -0
  138. shenjicore-0.6.4/tests/test_file_minio_backend.py +444 -0
  139. shenjicore-0.6.4/tests/test_file_ops.py +199 -0
  140. shenjicore-0.6.4/tests/test_file_plugin.py +222 -0
  141. shenjicore-0.6.4/tests/test_file_router.py +507 -0
  142. shenjicore-0.6.4/tests/test_help.py +103 -0
  143. shenjicore-0.6.4/tests/test_hikvision_connection.py +204 -0
  144. shenjicore-0.6.4/tests/test_hikvision_integration.py +153 -0
  145. shenjicore-0.6.4/tests/test_hikvision_meter.py +98 -0
  146. shenjicore-0.6.4/tests/test_hikvision_ops.py +552 -0
  147. shenjicore-0.6.4/tests/test_hikvision_plugin.py +187 -0
  148. shenjicore-0.6.4/tests/test_hikvision_scale.py +902 -0
  149. shenjicore-0.6.4/tests/test_hikvision_stream.py +523 -0
  150. shenjicore-0.6.4/tests/test_httpclient_connection.py +146 -0
  151. shenjicore-0.6.4/tests/test_httpclient_errors.py +82 -0
  152. shenjicore-0.6.4/tests/test_httpclient_plugin.py +86 -0
  153. shenjicore-0.6.4/tests/test_httpclient_retry.py +348 -0
  154. shenjicore-0.6.4/tests/test_log.py +197 -0
  155. shenjicore-0.6.4/tests/test_middleware.py +333 -0
  156. shenjicore-0.6.4/tests/test_minio_connection.py +151 -0
  157. shenjicore-0.6.4/tests/test_minio_integration.py +206 -0
  158. shenjicore-0.6.4/tests/test_minio_multipart.py +657 -0
  159. shenjicore-0.6.4/tests/test_minio_ops.py +322 -0
  160. shenjicore-0.6.4/tests/test_minio_plugin.py +172 -0
  161. shenjicore-0.6.4/tests/test_mysql_cipher.py +248 -0
  162. shenjicore-0.6.4/tests/test_mysql_connection.py +501 -0
  163. shenjicore-0.6.4/tests/test_mysql_integration.py +518 -0
  164. shenjicore-0.6.4/tests/test_mysql_plugin.py +519 -0
  165. shenjicore-0.6.4/tests/test_mysql_query.py +427 -0
  166. shenjicore-0.6.4/tests/test_plugin.py +249 -0
  167. shenjicore-0.6.4/tests/test_plugin_meta.py +310 -0
  168. shenjicore-0.6.4/tests/test_rbac_plugin.py +468 -0
  169. shenjicore-0.6.4/tests/test_redis_connection.py +104 -0
  170. shenjicore-0.6.4/tests/test_redis_integration.py +272 -0
  171. shenjicore-0.6.4/tests/test_redis_ops.py +343 -0
  172. shenjicore-0.6.4/tests/test_redis_plugin.py +75 -0
  173. shenjicore-0.6.4/tests/test_redis_queue.py +921 -0
  174. shenjicore-0.6.4/tests/test_redis_semaphore.py +212 -0
  175. shenjicore-0.6.4/tests/test_response.py +49 -0
  176. shenjicore-0.6.4/tests/test_router.py +144 -0
  177. shenjicore-0.6.4/tests/test_run.py +266 -0
  178. shenjicore-0.6.4/tests/test_scheduler.py +411 -0
  179. shenjicore-0.6.4/tests/test_tasks_plugin.py +590 -0
  180. shenjicore-0.6.4/tests/test_time.py +38 -0
@@ -0,0 +1,113 @@
1
+ ---
2
+ name: shenjicore
3
+ description: "Use when writing or refactoring business code in a shenjicore project — creating or editing action/domain/dao/schema/common files, adding a new resource, or organizing business logic (imports, layering, service classes). Enforces the layered skeleton: one resource = one file = one namespace class, @classmethod static calls, single-class imports (no function zoos, no scattered exports), one-way dependencies, thin-shell actions, no mutable class state. Also governs plugin usage: auth/rbac/audit, redis cache-lock-rate-limit-queue, mysql/minio/file/scheduler/hikvision — use official plugins instead of reinventing (see references/plugins.md). 在 shenjicore 业务项目中写/改 action、domain、dao、schema 文件前必读本 skill。"
4
+ ---
5
+
6
+ # shenjicore 业务代码组织
7
+
8
+ ## 何时使用
9
+
10
+ - 创建或修改 `action/`、`domain/`、`dao/`、`schema/`、`common/` 任何文件。
11
+ - 设计新资源的业务结构(接口、分层、导入方式)。
12
+ - review 或重构业务模块(函数扎堆、导出遍布、反向依赖、SQL 上移)。
13
+ - 用户问"业务逻辑应该放哪层 / 怎么组织"这类问题。
14
+
15
+ ## 核心规则(必须遵守)
16
+
17
+ 1. **一资源 = 一文件 = 一命名空间类**:`domain/user.py` 一个 `UserDomain`,
18
+ `dao/user.py` 一个 `UserDao(BaseDao)`;方法全 `@classmethod` 静态直调。
19
+ 2. **导入只到类名**:`from domain.user import UserDomain`。
20
+ 禁止函数级导入(`from domain.user import add, update, ...`)、禁止跨层重新导出。
21
+ 3. **依赖单向**:`action → domain → dao`;`schema/common` 可被各层引用;
22
+ dao 不得 import domain/action。
23
+ 4. **action 是薄壳**:端点函数体无 if/for/while/raise,只收参 → `await XxxDomain.xxx()` → `ok()/page()`。
24
+ 5. **类 = 纯命名空间**:类内禁止可变状态;SQL 只在 dao(继承 `BaseDao`,
25
+ 表名用 `cls.TABLE`,参数化);错误码只在 `common/errors.py` 集中登记。
26
+ 6. **分层靠测试守护**:AST 薄壳断言 + fake dao 注入(模式抄 `examples/your_project/tests/`)。
27
+ 7. **有插件用插件,禁止重造轮子**:业务需求先对照插件总览(见下),
28
+ 能用 `shenjicore.plugins.*` 的绝不手写——认证/授权/审计、缓存/锁/限流、
29
+ MySQL/对象存储/文件/定时/海康/对外 HTTP 调用(httpclient),官方插件都齐了。
30
+
31
+ ## 用插件,别重造轮子(写任何业务代码前先查)
32
+
33
+ 遇到以下需求 → 先想插件,详见 **`references/plugins.md`**(完整用法速查):
34
+
35
+ | 需求 | 插件 | 入口 |
36
+ |---|---|---|
37
+ | 登录/令牌/受保护路由 | `auth` | `login()` / `require_access_token` / `get_principal` |
38
+ | 密码哈希 | `auth` | `hash_password` / `verify_password` |
39
+ | 路由权限判定 | `rbac` | `require_permission` / `require_any_permission` |
40
+ | 合规留痕 | `audit` | `@audit("...")` / `add_detail` / `audit_record` |
41
+ | 缓存 / 分布式锁 / 限流 | `redis` | `cache_*` / `lock` / `try_lock` / `rate_limit_raise` |
42
+ | 异步任务队列 | `redis` | `enqueue` / `run_worker`(任务须幂等) |
43
+ | 任务平台(管控/进度/两阶段取消/重跑/归档) | `tasks` | `create_task` / `register_handler` / `run_task_worker` / `cancel_task`(v0.6.0+) |
44
+ | 定时任务 | `scheduler` | `@cron` / `@interval_job`(只保证触发,业务自行入队) |
45
+ | MySQL 数据访问 | `mysql` | `BaseDao` / `transaction()` / `QueryBuilder`;敏感字段 `encrypt_value` |
46
+ | 文件上传/下载/导出 | `file` | `save_bytes` / `read_bytes` / `export_csv` / `make_router` |
47
+ | 大文件直传对象存储(绕过服务器带宽) | `file`(minio 后端) | `create_direct_upload` → PUT put_url → `complete_direct_upload` |
48
+ | 对象存储 | `minio` | `upload_bytes` / `presigned_get_url` / `ensure_bucket` |
49
+ | 海康 NVR | `hikvision` | `get_snapshot` / `live_rtsp_url` / `VideoStream` |
50
+ | 调用外部 HTTP API(推理/第三方) | `httpclient` | `request` / `get` / `post`(超时+重试) |
51
+
52
+ 反例自查:自己写 JWT、手拼 Redis 锁、循环 sleep 当定时、自造文件下载接口
53
+ ……都是重造轮子。插件配置项在 `common/config.py` 的 Settings 子类声明字段
54
+ (类型契约),值写 `.env`;启用方式 `PLUGINS=[...]` 或 `create_app(plugins=[...])`。
55
+ 安全域三段式注册顺序:**auth → rbac → audit**。
56
+
57
+ ## 参考资料(按需加载,先看对应章节再动笔)
58
+
59
+ - **`references/CONVENTIONS.md`** — 完整框架规范:命名约定、REST 方法、响应信封、
60
+ 错误码段位、路由注册。写任何代码前查对应章节;与规范冲突以它为准。
61
+ - **`references/plugins.md`** — 官方插件总览与用法速查:场景触发总表、每插件
62
+ 配置/典型调用/要点、组合示例、反模式。涉及认证授权审计、缓存锁限流队列、
63
+ MySQL/文件/MinIO/海康/定时时必查。
64
+ - **`references/layering.md`** — 分层细节:设计裁决(为何类命名空间而非函数扎堆/DI)、
65
+ 每行代码放哪层的判定表、导入/状态硬规则、测试守护写法、反模式清单、DI 升级边界。
66
+ - **`examples/your_project/`** — 可运行完整骨架(四层 + tests + README)。
67
+ 写对应文件前先照抄其风格;测试守护直接复用。
68
+
69
+ ## 测试防 OOM(打穿内存必检项)
70
+
71
+ **教训(2026-08-31 实证)**:回归测试传 `part_count=10**9`,`complete_upload` 物化
72
+ `list(range(1, part_count+1))` ≈ 8GB → 测试进程 11.6GB RSS → 12GB WSL VM OOM →
73
+ **整个 WSL 崩溃**(Windows 报"灾难性故障 Wsl/Service/E_UNEXPECTED")。这不是测试
74
+ 失败,是环境被打穿;journal 只留一行 `Killed process (python) anon-rss:11.6GB`,
75
+ 错误表象是 dmesg 的 p9io 次生故障。**先查有没有物化点,再跑测试。**
76
+
77
+ **写/改测试后、跑全量前,逐条过(命中即修)**:
78
+
79
+ 1. **物化超大容器**:`list(range(1, n+1))`、推导式/`.extend` 构造 ≥1e6 序列、
80
+ 整文件 `read()`、分页全量攒列表——改惰性(迭代器/生成器;顺序完整性判定用
81
+ `len + 首尾`,不物化全表)。
82
+ 2. **业务硬限制不绕过**:S3 分片 ≤10000、导出 `max_rows`、`rows_per_file`、
83
+ Redis ttl——插件闸门本身要测,但**别在闸门内构造会物化的超大输入**(极端值
84
+ 用例应选闸门边界内侧,如 2MB 分片 + 11GB → 5632 片,而不是 10007 片)。
85
+ 3. **超大字符串/字节**:`"x" * 1e9`、`b"\0" * 8e9`、超长 SQL/JSON 拼接。
86
+ 4. **递归/迭代失控**:无界递归、断言前不截断的无限迭代器、死循环兜底。
87
+
88
+ **验证**:全量测试用 `/usr/bin/time -v pytest ...` 跑,读 `Maximum resident set
89
+ size`——**>1GB 即停手排查**(本仓库基线 ~300MB);疑似泄漏先 `ulimit -v <6G>` 限
90
+ 内存复现(malloc 失败进程自杀,VM 不死),别裸跑把 VM 打崩。
91
+
92
+ ## 完成自检(写完代码逐条过)
93
+
94
+ 1. 新资源四层各就各位,类名即命名空间,方法全 `@classmethod`?
95
+ 2. 导入只到类名一级?无函数级导入、无跨层导出、无深导入插件内部?
96
+ 3. action 端点体零控制流、只收参转发 + `ok()/page()`?
97
+ 4. domain 无 SQL、无魔法错误码、类内无可变状态;事务用 `transaction()`?
98
+ 5. dao 继承 `BaseDao`、SQL 参数化、表名 `cls.TABLE`?
99
+ 6. 错误码已在 `common/errors.py` 登记段位?
100
+ 7. 测试补齐:薄壳 AST 断言 + 关键 domain 业务(fake dao 注入)?
101
+ 8. 需求命中插件场景了吗?用的官方插件 API 而不是手写轮子;插件配置项
102
+ 已在 Settings 声明 + .env 配置?(速查 `references/plugins.md`)
103
+ 9. **测试无 OOM 风险**:无 ≥1e6 级容器/字符串物化、无绕过插件闸门的极端输入;
104
+ 全量跑前过一遍「测试防 OOM」清单?(验证命令:`/usr/bin/time -v pytest`,
105
+ 峰值 RSS >1GB 即排查)
106
+
107
+ ## 分发到业务项目
108
+
109
+ 本 skill 自包含(规范 + 骨架都在目录内),业务项目整目录复制即可:
110
+
111
+ ```bash
112
+ cp -r <shenjicore 路径>/.claude/skills/shenjicore <业务项目>/.claude/skills/
113
+ ```
@@ -0,0 +1,34 @@
1
+ # 复制为 .env 并填写(.env 不进版本库)
2
+ APP_NAME=your_project
3
+ API_PREFIX=/api
4
+ LOG_LEVEL=info
5
+
6
+ # mysql 插件必需
7
+ DB_HOST=127.0.0.1
8
+ DB_PORT=3306
9
+ DB_USER=root
10
+ DB_PASSWORD=123456
11
+ DB_DATABASE=your_project
12
+
13
+ # mysql cipher 主密钥(base64 32 字节;不填则演示加密接口报 1022)
14
+ # 生成: python -c "import base64,os; print(base64.b64encode(os.urandom(32)).decode())"
15
+ # MYSQL_CIPHER_KEY=
16
+
17
+ # ---- Auth 插件(启用 shenjicore.plugins.auth 时必填)----
18
+ # JWT 签名密钥:≥32 字符随机串。生成随机密钥(把输出粘贴到下面):
19
+ # python -c "import secrets; print(secrets.token_hex(32))"
20
+ # AUTH_JWT_SECRET=<上一条命令的输出>
21
+ # AUTH_ACCESS_TTL=1800
22
+ # AUTH_REFRESH_TTL=604800
23
+ # AUTH_TOKEN_STORE=memory
24
+ # Redis 模式必填:
25
+ # AUTH_REDIS_URL=redis://127.0.0.1:6379/0
26
+
27
+ # 建表(示例表):
28
+ # CREATE TABLE user (
29
+ # id INT AUTO_INCREMENT PRIMARY KEY,
30
+ # name VARCHAR(50) NOT NULL,
31
+ # student_no VARCHAR(20) NOT NULL UNIQUE,
32
+ # password VARCHAR(200) NOT NULL,
33
+ # created_at DATETIME DEFAULT CURRENT_TIMESTAMP
34
+ # ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
@@ -0,0 +1,53 @@
1
+ # your_project —— 分层示例(action / domain / dao / schema)
2
+
3
+ **这是 shenjicore 推荐的业务分层骨架**:action 只做壳、业务全在
4
+ domain、数据在 dao、参数在 schema。业务项目复制本目录即得正确分层。
5
+
6
+ ## 结构
7
+
8
+ ```
9
+ your_project/
10
+ action/user.py 路由壳:收参 → await UserDomain.add(body) → ok()/page()
11
+ domain/user.py 业务:校验规则、参数组装、调 dao、结果翻译(全 @classmethod)
12
+ dao/user.py 数据:class UserDao(BaseDao) + TABLE 常量(免传表名)
13
+ schema/user.py 入参/出参(Pydantic,命名约定见 CONVENTIONS §4)
14
+ common/ 业务错误码(集中定义)+ Settings(插件配置字段)
15
+ main.py 组装:create_app(settings, plugins=[MysqlPlugin()])
16
+ .env.example 配置样例(.env 不进版本库)
17
+ tests/ 分层正确性守护:fake dao 注入 domain + 路由全链
18
+ ```
19
+
20
+ ## 三个核心约定
21
+
22
+ 1. **静态直调,无需实例化**:dao/domain 全 `@classmethod`——
23
+ `await UserDao.insert(...)` / `await UserDomain.add(body)`。
24
+ 2. **事务不传 session**:`async with transaction():` 上下文自动感知
25
+ 事务(dao 方法零会话参数,事务内外行为一致)——见 `UserDomain.batch_create`。
26
+ 3. **action 禁止业务逻辑**:无校验/循环/条件分支/SQL;review 时
27
+ action 一眼看完接口做什么,细节进 domain。
28
+
29
+ ## 运行
30
+
31
+ ```bash
32
+ cp .env.example .env # 填写 DB_*(建表 SQL 在 .env.example 注释里)
33
+ python -m uvicorn main:app --reload
34
+ ```
35
+
36
+ 接口(前缀 /api/user):`POST /add`、`POST /batch`(事务演示)、
37
+ `POST /update`、`POST /delete`、`GET /page?keyword=&page=&page_size=`。
38
+
39
+ 敏感字段演示:`UserDomain.add` 落库前用 mysql cipher 加密 password
40
+ (`encrypt_value(data, context="user:login")`),出参永不外泄密文;
41
+ `UserDomain.get_password` 展示解密(context 必须一致)。不配
42
+ `MYSQL_CIPHER_KEY` 时相关接口报 1022(懒校验,其余接口不受影响)。
43
+
44
+ ## 测试
45
+
46
+ ```bash
47
+ cd examples/your_project
48
+ python -m pytest tests/ -q # 无需 MySQL:domain 注入 fake dao
49
+ ```
50
+
51
+ 测试守护三件事:action 薄壳(AST 断言无控制流语句)、domain 业务
52
+ (fake dao 注入,含密码加密与去重冲突)、路由全链(TestClient + 注入
53
+ fake domain,无需真实数据库)。
@@ -0,0 +1,42 @@
1
+ """路由壳:收参 → 调 domain → 统一响应。**不写业务逻辑**。
2
+
3
+ 业务逻辑(校验/循环/条件分支/SQL)一律在 domain 层——action 只做
4
+ 转发,review 时一眼看出每个接口做什么。
5
+ """
6
+
7
+ from domain.user import UserDomain
8
+ from schema.user import UserBatchCreate, UserCreate, UserDelete, UserQuery, UserUpdate
9
+
10
+ from shenjicore import APIRouter, ok, page
11
+
12
+ router = APIRouter()
13
+
14
+
15
+ @router.post("/add")
16
+ async def add(body: UserCreate):
17
+ user_id = await UserDomain.add(body)
18
+ return ok(message="添加成功", data={"id": user_id})
19
+
20
+
21
+ @router.post("/batch")
22
+ async def batch_add(body: UserBatchCreate):
23
+ total = await UserDomain.batch_create(body)
24
+ return ok(message=f"批量创建 {total} 个用户", data={"total": total})
25
+
26
+
27
+ @router.post("/update")
28
+ async def update(body: UserUpdate):
29
+ await UserDomain.update(body)
30
+ return ok(message="更新成功")
31
+
32
+
33
+ @router.post("/delete")
34
+ async def delete(body: UserDelete):
35
+ await UserDomain.delete(body.id)
36
+ return ok(message="删除成功")
37
+
38
+
39
+ @router.get("/page")
40
+ async def page_users(query: UserQuery):
41
+ items, total = await UserDomain.page(query)
42
+ return page(items=items, total=total, page=query.page, page_size=query.page_size)
@@ -0,0 +1,15 @@
1
+ """项目配置:继承 ShenSettings(自动读 .env),插件需要的配置项声明字段。"""
2
+
3
+ from shenjicore import ShenSettings
4
+
5
+
6
+ class Settings(ShenSettings):
7
+ # mysql 插件必需配置(值写 .env,见 .env.example)
8
+ db_host: str
9
+ db_port: int
10
+ db_user: str
11
+ db_password: str
12
+ db_database: str
13
+
14
+ # mysql cipher 主密钥(base64 32 字节;可选——示例演示敏感字段加解密)
15
+ mysql_cipher_key: str | None = None
@@ -0,0 +1,9 @@
1
+ """业务错误码:集中在 common/errors.py 定义(禁止散落各处、禁止魔法数字)。
2
+
3
+ 段位登记:本示例从 20000 起分配(每业务模块一段,每段 100 个)。
4
+ """
5
+
6
+ from shenjicore import ErrorSpec
7
+
8
+ USER_CONFLICT = ErrorSpec(20010, "用户已存在", http_status=409)
9
+ USER_NOT_FOUND = ErrorSpec(20011, "用户不存在", http_status=404)
@@ -0,0 +1,34 @@
1
+ """数据访问层:只做数据访问,不写业务规则。
2
+
3
+ 继承 BaseDao + TABLE 常量即可免传表名;全部 @classmethod 静态直调
4
+ (类名即命名空间,无需实例化)。专属查询加在此层(方法内可写参数化
5
+ SQL——BaseDao.query_one/execute 是唯一允许裸 SQL 的通道)。
6
+ """
7
+
8
+ from shenjicore.plugins.mysql import BaseDao
9
+
10
+
11
+ class UserDao(BaseDao):
12
+ TABLE = "user"
13
+
14
+ @classmethod
15
+ async def find_by_student_no(cls, student_no: str) -> dict | None:
16
+ """按学号查(业务去重/校验用;防注入参数化)。"""
17
+ return await cls.query_one(
18
+ "SELECT id, name, student_no FROM user WHERE student_no = %s",
19
+ (student_no,),
20
+ )
21
+
22
+ @classmethod
23
+ async def page_by_keyword(
24
+ cls, keyword: str | None, page: int, page_size: int
25
+ ) -> tuple[list[dict], int]:
26
+ """关键词分页(模糊查询示例;多表关联用 BaseDao.query() QueryBuilder)。"""
27
+ if keyword:
28
+ where = {"name__like": f"%{keyword}%"}
29
+ else:
30
+ where = None
31
+ return await cls.query_page(
32
+ where, select_cols="id, name, student_no", order_by="id DESC",
33
+ page=page, page_size=page_size,
34
+ )
@@ -0,0 +1,76 @@
1
+ """业务层:业务逻辑的家(参数组装、校验规则、调 dao、结果翻译)。
2
+
3
+ - 全部 @classmethod 静态直调:action 里 `await UserDomain.add(body)`。
4
+ - **不写 SQL**(SQL 在 dao 层);多语句原子性用 `transaction()` 上下文
5
+ (dao 方法零会话参数——事务内自动复用事务连接、事务外即写即生效)。
6
+ - 业务错误码在这里抛(dao 层自动翻译底层错误如唯一键冲突 → 1011)。
7
+ """
8
+
9
+ from common.errors import USER_CONFLICT, USER_NOT_FOUND
10
+ from dao.user import UserDao
11
+ from schema.user import UserBatchCreate, UserCreate, UserUpdate
12
+
13
+ from shenjicore.plugins.mysql import transaction
14
+ from shenjicore.plugins.mysql.cipher import decrypt_value, encrypt_value
15
+
16
+
17
+ class UserDomain:
18
+ """用户业务:所有方法静态直调(类名即命名空间)。"""
19
+
20
+ @classmethod
21
+ async def add(cls, body: UserCreate) -> int:
22
+ """新增:学号去重 → 密码静态加密落库(cipher 演示)。"""
23
+ if await UserDao.find_by_student_no(body.student_no):
24
+ raise USER_CONFLICT.with_detail(f"学号已存在: {body.student_no}")
25
+ data = body.model_dump()
26
+ # context 绑定用途,防跨用途解密;密钥只存 .env 不进库
27
+ data["password"] = encrypt_value(data["password"], context="user:login")
28
+ return await UserDao.insert(data)
29
+
30
+ @classmethod
31
+ async def batch_create(cls, body: UserBatchCreate) -> int:
32
+ """批量创建(**事务演示**):任一条失败整体回滚,不产生半批数据。"""
33
+ async with transaction():
34
+ total = 0
35
+ for user in body.users:
36
+ if await UserDao.find_by_student_no(user.student_no):
37
+ raise USER_CONFLICT.with_detail(f"学号已存在: {user.student_no}")
38
+ data = user.model_dump()
39
+ data["password"] = encrypt_value(data["password"], context="user:login")
40
+ total += await UserDao.insert(data)
41
+ return total
42
+
43
+ @classmethod
44
+ async def update(cls, body: UserUpdate) -> None:
45
+ """更新:不存在报 404(dao 层自动翻译底层冲突错误)。"""
46
+ data = body.model_dump(exclude={"id"}, exclude_none=True)
47
+ affected = await UserDao.update(data, where={"id": body.id})
48
+ if not affected:
49
+ raise USER_NOT_FOUND.with_detail(f"用户不存在: {body.id}")
50
+
51
+ @classmethod
52
+ async def delete(cls, user_id: int) -> None:
53
+ """删除(示例用物理删除;软删用 BaseDao.soft_delete)。"""
54
+ affected = await UserDao.hard_delete(where={"id": user_id})
55
+ if not affected:
56
+ raise USER_NOT_FOUND.with_detail(f"用户不存在: {user_id}")
57
+
58
+ @classmethod
59
+ async def page(cls, query) -> tuple[list[dict], int]:
60
+ """分页:结果翻译(脱敏 password 字段,出参永不外泄密文)。"""
61
+ items, total = await UserDao.page_by_keyword(
62
+ query.keyword, query.page, query.page_size
63
+ )
64
+ for row in items:
65
+ row.pop("password", None)
66
+ return items, total
67
+
68
+ @classmethod
69
+ async def get_password(cls, student_no: str) -> str | None:
70
+ """读取敏感字段演示:解密(context 必须与加密时一致)。"""
71
+ row = await UserDao.query_one(
72
+ "SELECT password FROM user WHERE student_no = %s", (student_no,)
73
+ )
74
+ if row is None:
75
+ return None
76
+ return decrypt_value(row["password"], context="user:login")
@@ -0,0 +1,19 @@
1
+ """分层示例入口:组装配置 + mysql 插件 + 用户路由。
2
+
3
+ 启动(需要 MySQL;配置见 .env.example)::
4
+
5
+ cd examples/your_project
6
+ cp .env.example .env # 填写 DB_* 与可选的 MYSQL_CIPHER_KEY
7
+ python -m uvicorn main:app --reload
8
+ """
9
+
10
+ from action.user import router as user_router
11
+ from common.config import Settings
12
+
13
+ from shenjicore import create_app
14
+ from shenjicore.plugins.mysql import MysqlPlugin
15
+
16
+ settings = Settings() # 自动读 .env
17
+
18
+ app = create_app(settings, plugins=[MysqlPlugin()])
19
+ app.include_router(user_router, prefix="/api/user", tags=["用户"])
@@ -0,0 +1,39 @@
1
+ """入参/出参:schema 层只放 Pydantic 模型,无业务逻辑。"""
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+
6
+ class UserCreate(BaseModel):
7
+ """新增用户请求体(命名约定:XxxCreate)。"""
8
+
9
+ name: str = Field(min_length=1, max_length=50)
10
+ student_no: str = Field(min_length=1, max_length=20)
11
+ # 敏感字段演示:落库前加密(见 domain),出参永不含 password
12
+ password: str = Field(min_length=6)
13
+
14
+
15
+ class UserUpdate(BaseModel):
16
+ """更新用户请求体(命名约定:XxxUpdate)。"""
17
+
18
+ id: int
19
+ name: str | None = None
20
+
21
+
22
+ class UserDelete(BaseModel):
23
+ """删除请求体(命名约定:XxxDelete,含 ids 或 id)。"""
24
+
25
+ id: int
26
+
27
+
28
+ class UserBatchCreate(BaseModel):
29
+ """批量创建(事务演示载体)。"""
30
+
31
+ users: list[UserCreate]
32
+
33
+
34
+ class UserQuery(BaseModel):
35
+ """分页查询参数(命名约定:XxxQuery,含 page/page_size)。"""
36
+
37
+ page: int = 1
38
+ page_size: int = 20
39
+ keyword: str | None = None
@@ -0,0 +1,8 @@
1
+ """示例测试环境:项目根加入 sys.path(直接运行本目录测试用)。"""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ ROOT = Path(__file__).resolve().parent.parent
7
+ if str(ROOT) not in sys.path:
8
+ sys.path.insert(0, str(ROOT))
@@ -0,0 +1,178 @@
1
+ """分层正确性守护:
2
+
3
+ 1. **action 薄壳断言**(AST):端点函数体不含 if/for/while/raise——业务
4
+ 逻辑进 domain,action 只转发。
5
+ 2. **domain 业务测试**(fake dao 注入):去重冲突、密码加密落库、
6
+ 批量事务冲突即中断。
7
+ 3. **路由全链**(TestClient + 注入 fake domain):HTTP 层薄壳转发正确,
8
+ 无需真实数据库。
9
+
10
+ 运行: ``cd examples/your_project && python -m pytest tests/ -q``
11
+ (需要 shenjicore 已安装或可导入;pytest-asyncio auto 模式跑 async 用例)
12
+ """
13
+
14
+ import ast
15
+ from contextlib import asynccontextmanager
16
+ from pathlib import Path
17
+
18
+ import pytest
19
+ from action.user import router as user_router
20
+ from fastapi.testclient import TestClient
21
+
22
+ from shenjicore import create_app
23
+
24
+ # ===========================================================================
25
+ # 1. action 薄壳断言(AST:无控制流 = 无业务逻辑)
26
+ # ===========================================================================
27
+
28
+ def _endpoint_functions(module_path: str) -> dict[str, ast.AST]:
29
+ tree = ast.parse(Path(module_path).read_text(encoding="utf-8"))
30
+ return {
31
+ node.name: node
32
+ for node in ast.walk(tree)
33
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
34
+ and not node.name.startswith("_")
35
+ }
36
+
37
+
38
+ def test_action_is_thin_shell():
39
+ """action 端点函数体不得有 if/for/while/raise(业务逻辑禁止进 action)。"""
40
+ action_dir = Path(__file__).resolve().parent.parent / "action"
41
+ for py in sorted(action_dir.glob("*.py")):
42
+ funcs = _endpoint_functions(str(py))
43
+ assert funcs, f"{py.name} 没有端点函数"
44
+ for name, node in funcs.items():
45
+ for child in ast.walk(node):
46
+ assert not isinstance(
47
+ child, (ast.If, ast.For, ast.While, ast.Raise)
48
+ ), f"{py.name}::{name} 含业务逻辑({type(child).__name__})——业务逻辑应放 domain 层"
49
+
50
+
51
+ # ===========================================================================
52
+ # 2. domain 业务测试(fake dao 注入,无需真实 MySQL)
53
+ # ===========================================================================
54
+
55
+ class FakeUserDao:
56
+ """记录调用 + 预置数据(替换 domain 里的 UserDao 用)。"""
57
+
58
+ inserted: list[dict] = []
59
+ conflict_student_no: str | None = None
60
+
61
+ @classmethod
62
+ async def find_by_student_no(cls, student_no: str) -> dict | None:
63
+ if student_no == cls.conflict_student_no:
64
+ return {"id": 1, "name": "已存在", "student_no": student_no}
65
+ return None
66
+
67
+ @classmethod
68
+ async def insert(cls, data: dict) -> int:
69
+ cls.inserted.append(data)
70
+ return len(cls.inserted)
71
+
72
+
73
+ @pytest.fixture(autouse=True)
74
+ def _reset_fake():
75
+ FakeUserDao.inserted = []
76
+ FakeUserDao.conflict_student_no = None
77
+
78
+
79
+ @pytest.fixture(autouse=True)
80
+ def _cipher_settings():
81
+ """注入 cipher 密钥配置(懒校验读取 _settings_holder,测试无需真实 .env)。"""
82
+ import base64
83
+
84
+ from shenjicore.plugins.mysql import _settings_holder
85
+
86
+ class FakeCipherSettings:
87
+ mysql_cipher_key = base64.b64encode(bytes(range(32))).decode()
88
+ mysql_cipher_key_file = None
89
+
90
+ _settings_holder.set(FakeCipherSettings())
91
+ yield
92
+ _settings_holder.set(None)
93
+
94
+
95
+ def _inject_fake(monkeypatch) -> type:
96
+ """把 domain.user 里的 UserDao 替换为 FakeUserDao(业务方法走真逻辑)。"""
97
+ import domain.user as domain_user
98
+
99
+ monkeypatch.setattr(domain_user, "UserDao", FakeUserDao)
100
+ return FakeUserDao
101
+
102
+
103
+ async def test_domain_add_encrypts_password(monkeypatch):
104
+ """domain 业务:密码落库前经 cipher 加密(v1 前缀),学号正确入库。"""
105
+ import domain.user as domain_user
106
+ from schema.user import UserCreate
107
+
108
+ _inject_fake(monkeypatch)
109
+ body = UserCreate(name="张三", student_no="001", password="secret123")
110
+ await domain_user.UserDomain.add(body)
111
+ assert FakeUserDao.inserted[0]["password"].startswith("v1:"), "密码必须加密落库"
112
+ assert FakeUserDao.inserted[0]["student_no"] == "001"
113
+
114
+
115
+ async def test_domain_add_conflict_raises(monkeypatch):
116
+ """domain 业务:学号已存在 → 业务错误码 20010(409)。"""
117
+ import domain.user as domain_user
118
+ from schema.user import UserCreate
119
+
120
+ _inject_fake(monkeypatch)
121
+ FakeUserDao.conflict_student_no = "001"
122
+ body = UserCreate(name="张三", student_no="001", password="secret123")
123
+ with pytest.raises(Exception) as exc_info:
124
+ await domain_user.UserDomain.add(body)
125
+ assert exc_info.value.code == 20010
126
+
127
+
128
+ async def test_domain_batch_aborts_on_conflict(monkeypatch):
129
+ """domain 业务:批量——中途冲突即抛错中断(事务语义由 transaction() 保证)。"""
130
+ import domain.user as domain_user
131
+ from schema.user import UserBatchCreate, UserCreate
132
+
133
+ _inject_fake(monkeypatch)
134
+ # 事务上下文注入 fake(真实 transaction() 需要 MySQL 连接池)——
135
+ # 单元测试只验证「冲突即中断」,原子性由 shenjicore transaction() 保证
136
+ @asynccontextmanager
137
+ async def _fake_transaction():
138
+ yield None
139
+
140
+ monkeypatch.setattr(domain_user, "transaction", _fake_transaction)
141
+ FakeUserDao.conflict_student_no = "002" # 第二个用户冲突
142
+ body = UserBatchCreate(
143
+ users=[
144
+ UserCreate(name="甲", student_no="001", password="secret123"),
145
+ UserCreate(name="乙", student_no="002", password="secret123"),
146
+ ]
147
+ )
148
+ with pytest.raises(Exception) as exc_info:
149
+ await domain_user.UserDomain.batch_create(body)
150
+ assert exc_info.value.code == 20010
151
+ assert len(FakeUserDao.inserted) == 1, "冲突即中断:第二个用户未插入"
152
+
153
+
154
+ # ===========================================================================
155
+ # 3. 路由全链(注入 fake domain,无需真实数据库)
156
+ # ===========================================================================
157
+
158
+ def test_router_full_chain(monkeypatch):
159
+ """HTTP 全链:薄壳收参 → domain 处理 → 统一信封(code=0)。"""
160
+ import domain.user as domain_user
161
+
162
+ async def fake_add(body):
163
+ assert body.name == "张三" # 收参正确
164
+ return 7
165
+
166
+ monkeypatch.setattr(domain_user.UserDomain, "add", fake_add)
167
+
168
+ app = create_app()
169
+ app.include_router(user_router, prefix="/api/user", tags=["用户"])
170
+ client = TestClient(app)
171
+
172
+ resp = client.post(
173
+ "/api/user/add",
174
+ json={"name": "张三", "student_no": "001", "password": "secret123"},
175
+ )
176
+ assert resp.status_code == 200
177
+ assert resp.json()["code"] == 0
178
+ assert resp.json()["data"]["id"] == 7