@oj-module/cli 0.2.54 → 0.2.56

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 (289) hide show
  1. package/devkit/SKILL.md +3 -3
  2. package/devkit/manual.md +59 -52
  3. package/package.json +2 -2
  4. package/shell-dist/assets/{index-e_Mp2mb5.js → index-CImwskAz.js} +24 -11
  5. package/shell-dist/assets/runtime.js +2 -2
  6. package/shell-dist/index.html +3 -3
  7. package/shell-dist/versions.json +3 -3
  8. package/src/__tests__/layout.test.ts +30 -1
  9. package/src/__tests__/templates-schema-columns.test.ts +99 -0
  10. package/src/alias-paths.ts +11 -12
  11. package/src/builtin.ts +10 -3
  12. package/src/contract/__tests__/emit-client.test.ts +4 -4
  13. package/src/contract/__tests__/market-client.integration.test.ts +2 -2
  14. package/src/contract/__tests__/run-api.test.ts +2 -2
  15. package/src/contract/check.ts +6 -2
  16. package/src/contract/emit-client.ts +10 -3
  17. package/src/contract/run.ts +10 -2
  18. package/src/init.ts +19 -11
  19. package/src/layout.ts +72 -16
  20. package/src/lifecycle/__tests__/manifest-contract.test.ts +2 -2
  21. package/src/market/__tests__/bootstrap.test.ts +1 -1
  22. package/src/site.ts +1 -1
  23. package/templates/README.md +30 -20
  24. package/templates/init.include.yaml +19 -11
  25. package/templates/openapi.json +95 -86
  26. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/domain/users.ts +27 -0
  27. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/infra/oplog.ts +4 -2
  28. package/templates/src/app/api/_platform/migrations/0017__users_employee_no.mysql.sql +7 -0
  29. package/templates/src/app/api/_platform/migrations/0017__users_employee_no.pg.sql +4 -0
  30. package/templates/src/app/api/_platform/migrations/0017__users_employee_no.sql +8 -0
  31. package/templates/src/{_platform/api → app/api/_platform}/schema.yaml +5 -0
  32. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/replay.ts +9 -3
  33. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/tenant-admin.ts +18 -27
  34. package/templates/src/{system/api → app/api/system}/oplog/api.ts +5 -3
  35. package/templates/src/{_platform/web/__client/api.ts → app/web/_platform/__client/index.ts} +5 -2
  36. package/templates/src/{auth/web/__client/api.ts → app/web/auth/__client/index.ts} +5 -2
  37. package/templates/src/app/web/notification/pages/center/index.tsx +1 -1
  38. package/templates/src/app/web/notification/routes.ts +1 -1
  39. package/templates/src/app/web/personal-center/pages/my-profile/index.tsx +1 -1
  40. package/templates/src/app/web/personal-center/routes.ts +1 -1
  41. package/templates/src/app/web/system/__client/api.schemas.ts +1 -0
  42. package/templates/src/app/web/system/locales/en-US.json +2 -3
  43. package/templates/src/app/web/system/locales/zh-CN.json +2 -3
  44. package/templates/src/app/web/system/pages/departments/index.tsx +2 -2
  45. package/templates/src/app/web/system/pages/login-methods/index.tsx +1 -1
  46. package/templates/src/app/web/system/pages/oplog/index.tsx +44 -19
  47. package/templates/src/app/web/system/pages/roles/index.tsx +5 -3
  48. package/templates/src/app/web/system/pages/roles/visible-roles.ts +14 -0
  49. package/templates/src/app/web/system/pages/settings/index.tsx +2 -2
  50. package/templates/src/app/web/system/pages/users/can-manage.ts +21 -0
  51. package/templates/src/app/web/system/pages/users/index.tsx +25 -17
  52. package/templates/src/app/web/system/pages/users/roles.tsx +1 -1
  53. package/templates/src/app/web/system/routes.ts +1 -1
  54. package/templates/src/{web/web/__client/api.ts → app/web/web/__client/index.ts} +5 -2
  55. package/templates/src/oj-market/web/api/defaultClient.ts +1 -1
  56. package/templates/tests/auth-ldap.spec.ts +2 -2
  57. package/templates/tests/auth-session.spec.ts +3 -3
  58. package/templates/tests/auth-throttle.spec.ts +1 -1
  59. package/templates/tests/fixture.ts +6 -4
  60. package/templates/tests/guard.spec.ts +1 -1
  61. package/templates/tests/lifecycle/atomic-state.spec.ts +3 -3
  62. package/templates/tests/lifecycle/bootstrap.spec.ts +3 -3
  63. package/templates/tests/lifecycle/deploy-smoke.spec.ts +8 -8
  64. package/templates/tests/lifecycle/installer.spec.ts +10 -10
  65. package/templates/tests/lifecycle/market-api.spec.ts +14 -14
  66. package/templates/tests/lifecycle/migrate.spec.ts +6 -6
  67. package/templates/tests/lifecycle/preflight.spec.ts +3 -3
  68. package/templates/tests/lifecycle/register.spec.ts +1 -1
  69. package/templates/tests/lifecycle/semver.spec.ts +1 -1
  70. package/templates/tests/lifecycle/sha384.spec.ts +1 -1
  71. package/templates/tests/lifecycle/solutions-api.spec.ts +1 -1
  72. package/templates/tests/lifecycle/source-seq.spec.ts +1 -1
  73. package/templates/tests/lifecycle/steps-fs.spec.ts +9 -9
  74. package/templates/tests/lifecycle/text-edit.spec.ts +1 -1
  75. package/templates/tests/lifecycle/tgz-guard.spec.ts +2 -2
  76. package/templates/tests/lifecycle/trust.spec.ts +5 -5
  77. package/templates/tests/lifecycle/uninstall.spec.ts +7 -7
  78. package/templates/tests/lifecycle/verify-checks.spec.ts +5 -5
  79. package/templates/tests/lifecycle/verify-report.spec.ts +4 -4
  80. package/templates/tests/org-departments.spec.ts +10 -6
  81. package/templates/tests/org-read.spec.ts +7 -7
  82. package/templates/tests/query-utils.spec.ts +1 -1
  83. package/templates/tests/rbac-audit.spec.ts +1 -1
  84. package/templates/tests/rbac-contacts.spec.ts +2 -2
  85. package/templates/tests/rbac-guard.spec.ts +1 -1
  86. package/templates/tests/rbac-roles.spec.ts +10 -2
  87. package/templates/tests/rbac-seed.spec.ts +1 -1
  88. package/templates/tests/rbac-settings.spec.ts +77 -7
  89. package/templates/tests/rbac-solution-perms.spec.ts +1 -1
  90. package/templates/tests/rbac-tables.spec.ts +29 -0
  91. package/templates/tests/rbac-users.spec.ts +55 -3
  92. package/templates/tests/roles-page.spec.ts +40 -0
  93. package/templates/tests/seed-role-accounts.spec.ts +1 -1
  94. package/templates/tests/tenant-access-cache.spec.ts +1 -1
  95. package/templates/tests/tenant-forgery.spec.ts +5 -5
  96. package/templates/tests/tenant-isolation.spec.ts +5 -4
  97. package/templates/tests/username-namespace.spec.ts +3 -3
  98. package/templates/tests/users-page.spec.ts +39 -0
  99. package/templates/tests/wecom-admin.spec.ts +6 -6
  100. package/templates/tests/wecom-client-timeout.spec.ts +1 -1
  101. package/templates/tests/wecom-connector.spec.ts +9 -9
  102. package/templates/tests/wecom-corp.spec.ts +9 -9
  103. package/templates/tests/wecom-crypt.spec.ts +1 -1
  104. package/templates/tests/wecom-endpoints.spec.ts +2 -2
  105. package/templates/tests/wecom-landing.spec.ts +1 -1
  106. package/templates/tests/wecom-org-sync.spec.ts +7 -7
  107. package/templates/tests/wecom-sync.spec.ts +3 -3
  108. package/templates/tsconfig.paths.json +11 -11
  109. package/templates/vitest.config.ts +2 -2
  110. package/templates/web.config.ts +5 -2
  111. package/vendor/host-versions.json +3 -3
  112. package/templates/src/{_platform/api → app/api/_platform}/README.md +0 -0
  113. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/atomic.ts +0 -0
  114. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/history.ts +0 -0
  115. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/installer.ts +0 -0
  116. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/limits.ts +0 -0
  117. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/lock.ts +0 -0
  118. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/semver.ts +0 -0
  119. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/sha384.ts +0 -0
  120. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/source-seq.ts +0 -0
  121. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/state.ts +0 -0
  122. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/deploy-web.ts +0 -0
  123. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/download.ts +0 -0
  124. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/extract.ts +0 -0
  125. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/migrate.ts +0 -0
  126. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/preflight.ts +0 -0
  127. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/register.ts +0 -0
  128. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/reload.ts +0 -0
  129. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/smoke.ts +0 -0
  130. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/verify.ts +0 -0
  131. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/tasks.ts +0 -0
  132. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/text-edit.ts +0 -0
  133. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/tgz-guard.ts +0 -0
  134. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/trust.ts +0 -0
  135. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/types.ts +0 -0
  136. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/uninstall.ts +0 -0
  137. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/verify-checks.ts +0 -0
  138. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/verify-report.ts +0 -0
  139. package/templates/src/{_platform/api → app/api/_platform}/_shared/respond.ts +0 -0
  140. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/domain/contacts.ts +0 -0
  141. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/domain/org-read.ts +0 -0
  142. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/domain/org.ts +0 -0
  143. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/domain/roles.ts +0 -0
  144. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/domain/settings.ts +0 -0
  145. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/domain/solution-perms.ts +0 -0
  146. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/infra/access.ts +0 -0
  147. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/infra/audit.ts +0 -0
  148. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/infra/guard.ts +0 -0
  149. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/infra/query.ts +0 -0
  150. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/infra/rate.ts +0 -0
  151. package/templates/src/{_platform/api → app/api/_platform}/_shared/site-facade.ts +0 -0
  152. package/templates/src/{_platform/api → app/api/_platform}/contacts/api.ts +0 -0
  153. package/templates/src/{_platform/api → app/api/_platform}/contacts/detail/api.ts +0 -0
  154. package/templates/src/{_platform/api → app/api/_platform}/fixtures/l1-test-user.sql +0 -0
  155. package/templates/src/{_platform/api → app/api/_platform}/manifest.yaml +0 -0
  156. package/templates/src/{_platform/api → app/api/_platform}/market/api.ts +0 -0
  157. package/templates/src/{_platform/api → app/api/_platform}/market/bootstrap.ts +0 -0
  158. package/templates/src/{_platform/api → app/api/_platform}/market/index-source.ts +0 -0
  159. package/templates/src/{_platform/api → app/api/_platform}/migrations/0001__create_users.mysql.sql +0 -0
  160. package/templates/src/{_platform/api → app/api/_platform}/migrations/0001__create_users.pg.sql +0 -0
  161. package/templates/src/{_platform/api → app/api/_platform}/migrations/0001__create_users.sql +0 -0
  162. package/templates/src/{_platform/api → app/api/_platform}/migrations/0002__add_users_avatar_base64.mysql.sql +0 -0
  163. package/templates/src/{_platform/api → app/api/_platform}/migrations/0002__add_users_avatar_base64.sql +0 -0
  164. package/templates/src/{_platform/api → app/api/_platform}/migrations/0003__users_source_display_name.mysql.sql +0 -0
  165. package/templates/src/{_platform/api → app/api/_platform}/migrations/0003__users_source_display_name.sql +0 -0
  166. package/templates/src/{_platform/api → app/api/_platform}/migrations/0004__sys_rbac.mysql.sql +0 -0
  167. package/templates/src/{_platform/api → app/api/_platform}/migrations/0004__sys_rbac.pg.sql +0 -0
  168. package/templates/src/{_platform/api → app/api/_platform}/migrations/0004__sys_rbac.sql +0 -0
  169. package/templates/src/{_platform/api → app/api/_platform}/migrations/0005__users_must_change.mysql.sql +0 -0
  170. package/templates/src/{_platform/api → app/api/_platform}/migrations/0005__users_must_change.sql +0 -0
  171. package/templates/src/{_platform/api → app/api/_platform}/migrations/0006__users_is_forbidden.mysql.sql +0 -0
  172. package/templates/src/{_platform/api → app/api/_platform}/migrations/0006__users_is_forbidden.sql +0 -0
  173. package/templates/src/{_platform/api → app/api/_platform}/migrations/0007__users_email.sql +0 -0
  174. package/templates/src/{_platform/api → app/api/_platform}/migrations/0008__sys_refresh_token.mysql.sql +0 -0
  175. package/templates/src/{_platform/api → app/api/_platform}/migrations/0008__sys_refresh_token.pg.sql +0 -0
  176. package/templates/src/{_platform/api → app/api/_platform}/migrations/0008__sys_refresh_token.sql +0 -0
  177. package/templates/src/{_platform/api → app/api/_platform}/migrations/0009__sys_throttle.mysql.sql +0 -0
  178. package/templates/src/{_platform/api → app/api/_platform}/migrations/0009__sys_throttle.pg.sql +0 -0
  179. package/templates/src/{_platform/api → app/api/_platform}/migrations/0009__sys_throttle.sql +0 -0
  180. package/templates/src/{_platform/api → app/api/_platform}/migrations/0010__sys_solution_route.mysql.sql +0 -0
  181. package/templates/src/{_platform/api → app/api/_platform}/migrations/0010__sys_solution_route.pg.sql +0 -0
  182. package/templates/src/{_platform/api → app/api/_platform}/migrations/0010__sys_solution_route.sql +0 -0
  183. package/templates/src/{_platform/api → app/api/_platform}/migrations/0011__sys_setting_group_label.mysql.sql +0 -0
  184. package/templates/src/{_platform/api → app/api/_platform}/migrations/0011__sys_setting_group_label.sql +0 -0
  185. package/templates/src/{_platform/api → app/api/_platform}/migrations/0012__market_history.mysql.sql +0 -0
  186. package/templates/src/{_platform/api → app/api/_platform}/migrations/0012__market_history.pg.sql +0 -0
  187. package/templates/src/{_platform/api → app/api/_platform}/migrations/0012__market_history.sql +0 -0
  188. package/templates/src/{_platform/api → app/api/_platform}/migrations/0013__users_profile.sql +0 -0
  189. package/templates/src/{_platform/api → app/api/_platform}/migrations/0014__reg_source_seq.mysql.sql +0 -0
  190. package/templates/src/{_platform/api → app/api/_platform}/migrations/0014__reg_source_seq.pg.sql +0 -0
  191. package/templates/src/{_platform/api → app/api/_platform}/migrations/0014__reg_source_seq.sql +0 -0
  192. package/templates/src/{_platform/api → app/api/_platform}/migrations/0015__tenant.mysql.sql +0 -0
  193. package/templates/src/{_platform/api → app/api/_platform}/migrations/0015__tenant.pg.sql +0 -0
  194. package/templates/src/{_platform/api → app/api/_platform}/migrations/0015__tenant.sql +0 -0
  195. package/templates/src/{_platform/api → app/api/_platform}/migrations/0016__org.mysql.sql +0 -0
  196. package/templates/src/{_platform/api → app/api/_platform}/migrations/0016__org.pg.sql +0 -0
  197. package/templates/src/{_platform/api → app/api/_platform}/migrations/0016__org.sql +0 -0
  198. package/templates/src/{_platform/api → app/api/_platform}/org/departments/api.ts +0 -0
  199. package/templates/src/{_platform/api → app/api/_platform}/org/is-member/api.ts +0 -0
  200. package/templates/src/{_platform/api → app/api/_platform}/org/members/api.ts +0 -0
  201. package/templates/src/{_platform/api → app/api/_platform}/org/picker/api.ts +0 -0
  202. package/templates/src/{_platform/api → app/api/_platform}/org/resolve/api.ts +0 -0
  203. package/templates/src/{_platform/api → app/api/_platform}/org/tree/api.ts +0 -0
  204. package/templates/src/{_platform/api → app/api/_platform}/org/user/api.ts +0 -0
  205. package/templates/src/{_platform/api → app/api/_platform}/seed.mysql.sql +0 -0
  206. package/templates/src/{_platform/api → app/api/_platform}/seed.pg.sql +0 -0
  207. package/templates/src/{_platform/api → app/api/_platform}/seed.sql +0 -0
  208. package/templates/src/{_platform/api → app/api/_platform}/solutions/api.ts +0 -0
  209. package/templates/src/{auth/api → app/api/auth}/_shared/ldap-login.ts +0 -0
  210. package/templates/src/{auth/api → app/api/auth}/_shared/ldap.ts +0 -0
  211. package/templates/src/{auth/api → app/api/auth}/_shared/login-throttle.ts +0 -0
  212. package/templates/src/{auth/api → app/api/auth}/_shared/session.ts +0 -0
  213. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/admin.ts +1 -1
  214. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/client.ts +0 -0
  215. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/config.ts +0 -0
  216. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/contacts.ts +0 -0
  217. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/crypt.ts +0 -0
  218. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/events.ts +1 -1
  219. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/login.ts +0 -0
  220. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/payload.ts +0 -0
  221. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/store.ts +0 -0
  222. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/sync.ts +0 -0
  223. package/templates/src/{auth/api → app/api/auth}/api.ts +0 -0
  224. package/templates/src/{auth/api → app/api/auth}/ldap-login/api.ts +0 -0
  225. package/templates/src/{auth/api → app/api/auth}/manifest.yaml +0 -0
  226. package/templates/src/{auth/api → app/api/auth}/migrations/0001__wecom.mysql.sql +0 -0
  227. package/templates/src/{auth/api → app/api/auth}/migrations/0001__wecom.pg.sql +0 -0
  228. package/templates/src/{auth/api → app/api/auth}/migrations/0001__wecom.sql +0 -0
  229. package/templates/src/{auth/api → app/api/auth}/migrations/0002__org_sync.mysql.sql +0 -0
  230. package/templates/src/{auth/api → app/api/auth}/migrations/0002__org_sync.pg.sql +0 -0
  231. package/templates/src/{auth/api → app/api/auth}/migrations/0002__org_sync.sql +0 -0
  232. package/templates/src/{auth/api → app/api/auth}/migrations/0003__credential_kind.mysql.sql +0 -0
  233. package/templates/src/{auth/api → app/api/auth}/migrations/0003__credential_kind.pg.sql +0 -0
  234. package/templates/src/{auth/api → app/api/auth}/migrations/0003__credential_kind.sql +0 -0
  235. package/templates/src/{auth/api → app/api/auth}/schema.yaml +0 -0
  236. package/templates/src/{auth/api → app/api/auth}/wecom/admin/api.ts +0 -0
  237. package/templates/src/{auth/api → app/api/auth}/wecom/admin/tenant-admin/api.ts +1 -1
  238. package/templates/src/{auth/api → app/api/auth}/wecom/admin/tenant-admin/password/api.ts +1 -1
  239. /package/templates/src/{auth/api → app/api/auth}/wecom/api.ts +0 -0
  240. /package/templates/src/{auth/api → app/api/auth}/wecom/sync/api.ts +0 -0
  241. /package/templates/src/{home/api → app/api/home}/line/api.ts +0 -0
  242. /package/templates/src/{home/api → app/api/home}/manifest.yaml +0 -0
  243. /package/templates/src/{home/api → app/api/home}/migrations/0001__create_home_tables.sql +0 -0
  244. /package/templates/src/{home/api → app/api/home}/migrations/0002__tenant.mysql.sql +0 -0
  245. /package/templates/src/{home/api → app/api/home}/migrations/0002__tenant.sql +0 -0
  246. /package/templates/src/{home/api → app/api/home}/pie/api.ts +0 -0
  247. /package/templates/src/{home/api → app/api/home}/schema.yaml +0 -0
  248. /package/templates/src/{home/api → app/api/home}/seed.sql +0 -0
  249. /package/templates/src/{notification/api → app/api/notification}/_shared/notify.ts +0 -0
  250. /package/templates/src/{notification/api → app/api/notification}/api.ts +0 -0
  251. /package/templates/src/{notification/api → app/api/notification}/del/api.ts +0 -0
  252. /package/templates/src/{notification/api → app/api/notification}/manifest.yaml +0 -0
  253. /package/templates/src/{notification/api → app/api/notification}/migrations/0001__create_notifications.sql +0 -0
  254. /package/templates/src/{notification/api → app/api/notification}/migrations/0002__notifications_user.sql +0 -0
  255. /package/templates/src/{notification/api → app/api/notification}/migrations/0003__notifications_broadcast.sql +0 -0
  256. /package/templates/src/{notification/api → app/api/notification}/migrations/0004__notifications_broadcast_hidden.sql +0 -0
  257. /package/templates/src/{notification/api → app/api/notification}/migrations/0005__tenant.mysql.sql +0 -0
  258. /package/templates/src/{notification/api → app/api/notification}/migrations/0005__tenant.sql +0 -0
  259. /package/templates/src/{notification/api → app/api/notification}/schema.yaml +0 -0
  260. /package/templates/src/{notification/api → app/api/notification}/seed.sql +0 -0
  261. /package/templates/src/{personal-center/api → app/api/personal-center}/manifest.yaml +0 -0
  262. /package/templates/src/{personal-center/api → app/api/personal-center}/profile/api.ts +0 -0
  263. /package/templates/src/{personal-center/api → app/api/personal-center}/upload/api.ts +0 -0
  264. /package/templates/src/{system/api → app/api/system}/departments/api.ts +0 -0
  265. /package/templates/src/{system/api → app/api/system}/departments/detach/api.ts +0 -0
  266. /package/templates/src/{system/api → app/api/system}/departments/members/api.ts +0 -0
  267. /package/templates/src/{system/api → app/api/system}/departments/move/api.ts +0 -0
  268. /package/templates/src/{system/api → app/api/system}/login-methods/api.ts +0 -0
  269. /package/templates/src/{system/api → app/api/system}/manifest.yaml +0 -0
  270. /package/templates/src/{system/api → app/api/system}/permissions/api.ts +0 -0
  271. /package/templates/src/{system/api → app/api/system}/roles/api.ts +0 -0
  272. /package/templates/src/{system/api → app/api/system}/roles/bindings/api.ts +0 -0
  273. /package/templates/src/{system/api → app/api/system}/settings/api.ts +0 -0
  274. /package/templates/src/{system/api → app/api/system}/users/api.ts +0 -0
  275. /package/templates/src/{system/api → app/api/system}/users/forbidden/api.ts +0 -0
  276. /package/templates/src/{system/api → app/api/system}/users/password/api.ts +0 -0
  277. /package/templates/src/{system/api → app/api/system}/users/profile/api.ts +0 -0
  278. /package/templates/src/{system/api → app/api/system}/users/roles/api.ts +0 -0
  279. /package/templates/src/{web/api → app/api/web}/get-async-routes/api.ts +0 -0
  280. /package/templates/src/{web/api → app/api/web}/hello/api.ts +0 -0
  281. /package/templates/src/{web/api → app/api/web}/manifest.yaml +0 -0
  282. /package/templates/src/{web/api → app/api/web}/user-info/api.ts +0 -0
  283. /package/templates/src/{_platform/web → app/web/_platform}/__client/api.schemas.ts +0 -0
  284. /package/templates/src/{auth/web → app/web/auth}/__client/api.schemas.ts +0 -0
  285. /package/templates/src/app/web/home/__client/{api.ts → index.ts} +0 -0
  286. /package/templates/src/app/web/notification/__client/{api.ts → index.ts} +0 -0
  287. /package/templates/src/app/web/personal-center/__client/{api.ts → index.ts} +0 -0
  288. /package/templates/src/app/web/system/__client/{api.ts → index.ts} +0 -0
  289. /package/templates/src/{web → app/web}/web/__client/api.schemas.ts +0 -0
@@ -1,5 +1,6 @@
1
1
  // 账号管理应用服务(管理域 /api/system/users,AC1-7~10 + B6 建号加固)。
2
2
  // 红线:admin 不碰 superadmin/admin 成员资格(AC1-8,service 按 actor 角色自查);
3
+ // 只有 superadmin 能改 superadmin 账号(口令/启停/资料/删除/角色,见 assertCanManageTarget);
3
4
  // 禁止操作自己(AC1-10 注);最后 superadmin 兜底(服务端二次防线);
4
5
  // 角色唯一权威源 sys_user_role + 写时失效(AC1-7/D10);禁用/重置口令级联删 refresh(§3.1)。
5
6
  // bcrypt 经注入(Node 侧全局不可 stub,同 session.ts 约定)。
@@ -59,6 +60,27 @@ function assertNotSelf(actorId: number, targetUid: number): void {
59
60
  fail(400, "cannot operate on yourself");
60
61
  }
61
62
 
63
+ /**
64
+ * 红线(RBAC 保护红线):只有 superadmin 能改 superadmin。
65
+ *
66
+ * 与 `assertMembershipAllowed` 的分工:那条按**被授予的角色**管提权方向(谁能任命 admin /
67
+ * superadmin),这条按**目标账号当前的角色**管反向——谁能动一个已经是 superadmin 的号。
68
+ * 缺了它,同一租户的 admin 可以:重置 superadmin 口令(知道口令即接管账号,而 superadmin
69
+ * 是平台级、能跨租户)/把人禁用/把角色整集清空——后者连 `assertMembershipAllowed`
70
+ * 都拦不住(`next` 里没有 admin/superadmin 时它直接放行)。故口令/启停/资料/删除/角色
71
+ * 五条写路径统一走这里。
72
+ *
73
+ * 读角色走 `rolesOf()`,与写入同受请求租户注入约束:只能影响本租户的人。
74
+ */
75
+ async function assertCanManageTarget(actorId: number, targetUid: number): Promise<void> {
76
+ const targetRoles = await rolesOf(Number(targetUid));
77
+ if (!targetRoles.includes("superadmin"))
78
+ return;
79
+ if ((await rolesOf(Number(actorId))).includes("superadmin"))
80
+ return;
81
+ fail(403, "modifying a superadmin requires superadmin");
82
+ }
83
+
62
84
  /**
63
85
  * AC1-8(已修订):成员资格提权守卫——按**被授予的角色**分层,不再一刀切。
64
86
  *
@@ -200,6 +222,7 @@ export async function updateUserRoles(actorId: number, targetUid: number, roles:
200
222
  const target = await db.table("users").select(["id"]).where({ field: "id", op: "eq", value: targetUid }).all();
201
223
  if (!target.length)
202
224
  fail(404, "user not found");
225
+ await assertCanManageTarget(actorId, targetUid);
203
226
  const next = [...new Set(roles.map(String))];
204
227
  if (next.length) {
205
228
  const known = await db.table("sys_role").select(["name"]).where({ field: "name", op: "in", value: next }).all();
@@ -258,6 +281,7 @@ export async function changePasswordByAdmin(actorId: number, targetUid: number,
258
281
  const rows = await db.table("users").select(["id"]).where({ field: "id", op: "eq", value: targetUid }).all();
259
282
  if (!rows.length)
260
283
  fail(404, "user not found");
284
+ await assertCanManageTarget(actorId, targetUid);
261
285
  const hash = await deps.bcryptHash(String(newPassword));
262
286
  await db.tx(async (t) => {
263
287
  await t.table("users").update({ password_hash: hash, must_change: 1 }).where({ field: "id", op: "eq", value: targetUid }).run();
@@ -273,6 +297,7 @@ export async function setForbidden(actorId: number, targetUid: number, forbidden
273
297
  const rows = await db.table("users").select(["id"]).where({ field: "id", op: "eq", value: targetUid }).all();
274
298
  if (!rows.length)
275
299
  fail(404, "user not found");
300
+ await assertCanManageTarget(actorId, targetUid);
276
301
  await db.tx(async (t) => {
277
302
  await t.table("users").update({ is_forbidden: forbidden ? 1 : 0 }).where({ field: "id", op: "eq", value: targetUid }).run();
278
303
  if (forbidden)
@@ -294,6 +319,7 @@ export async function updateUserProfile(actorId: number, targetUid: number, patc
294
319
  const rows = await db.table("users").select(["id"]).where({ field: "id", op: "eq", value: targetUid }).all();
295
320
  if (!rows.length)
296
321
  fail(404, "user not found");
322
+ await assertCanManageTarget(actorId, targetUid);
297
323
  const cols: Record<string, unknown> = {};
298
324
  if (patch.displayName !== undefined)
299
325
  cols.display_name = patch.displayName == null ? null : String(patch.displayName).trim() || null;
@@ -320,6 +346,7 @@ export async function deleteUser(actorId: number, targetUid: number): Promise<vo
320
346
  const rows = await db.table("users").select(["id", "username"]).where({ field: "id", op: "eq", value: targetUid }).all() as Array<{ id: number, username: string }>;
321
347
  if (!rows.length)
322
348
  fail(404, "user not found");
349
+ await assertCanManageTarget(actorId, targetUid);
323
350
  const roles = await rolesOf(Number(targetUid));
324
351
  if (roles.includes("superadmin")) {
325
352
  const supers = await db.table("sys_user_role").select([{ fn: "count", as: "c" }]).where({ field: "role_name", op: "eq", value: "superadmin" }).all() as Array<{ c: number }>;
@@ -21,7 +21,7 @@ export interface OplogPage {
21
21
  truncated: boolean
22
22
  }
23
23
 
24
- export async function searchOplog(filter: { action?: unknown, username?: unknown, module?: unknown, limit?: unknown }): Promise<OplogPage> {
24
+ export async function searchOplog(filter: { action?: unknown, username?: unknown, module?: unknown, page?: unknown, limit?: unknown }): Promise<OplogPage> {
25
25
  // 构造器(v0.1.82):可选过滤走 whereIf,条件写一份(mk 工厂),count + list 复用;
26
26
  // username 走语义 op contains(引擎转义 %/_/!,等价旧 LIKE ? ESCAPE '!' 的 %kw%)。
27
27
  // 照抄口径:devkit scenarios 场景 42 ④。
@@ -35,6 +35,8 @@ export async function searchOplog(filter: { action?: unknown, username?: unknown
35
35
  .whereIf(action, { field: "action", op: "eq", value: action })
36
36
  .whereIf(module, { field: "module", op: "eq", value: module })
37
37
  .whereIf(username, { field: "username", op: "contains", value: username });
38
- const r = await pagedFactory<OplogRow>(mk, { size: filter.limit });
38
+ // page/limit 成对:limit 是每页条数(pageArgs 钳 200),缺省第 1 页 / 20 条。
39
+ // total 由 count 查询单独给出,页面按 total 算页数——不再靠 truncated 提示「结果被截断」。
40
+ const r = await pagedFactory<OplogRow>(mk, { page: filter.page, size: filter.limit });
39
41
  return { list: r.list, total: r.total, truncated: r.truncated };
40
42
  }
@@ -0,0 +1,7 @@
1
+ -- 0017__users_employee_no(mysql 方言):工号列 + 租户内唯一。列语义同 0017__users_employee_no.sql 头注释。
2
+ -- 差异(沿用 0015__tenant.mysql.sql 惯例):
3
+ -- · 键列不能用 TEXT 落唯一键 → 按业务长度收窄为 VARCHAR(64);
4
+ -- · 无部分索引 → 普通唯一键,NULL 在 MySQL 里可重复出现,效果等同「有值才唯一」;
5
+ -- · (255 + 64) × 4B = 1276B < InnoDB 3072B 上限。
6
+ ALTER TABLE users ADD COLUMN employee_no VARCHAR(64) NULL;
7
+ ALTER TABLE users ADD UNIQUE KEY uk_users_employee_no (tenant_id, employee_no);
@@ -0,0 +1,4 @@
1
+ -- 0017__users_employee_no(postgres 方言):工号列 + 租户内唯一。列语义同 0017__users_employee_no.sql 头注释。
2
+ -- 差异(沿用 0016__org.pg.sql 惯例):TEXT 在 pg 合法且可用于部分唯一索引,无需收窄类型。
3
+ ALTER TABLE users ADD COLUMN employee_no TEXT;
4
+ CREATE UNIQUE INDEX IF NOT EXISTS uk_users_employee_no ON users (tenant_id, employee_no) WHERE employee_no IS NOT NULL;
@@ -0,0 +1,8 @@
1
+ -- 0017__users_employee_no(sqlite 方言):工号列 + 租户内唯一
2
+ --
3
+ -- 为什么单开一列而不是复用 display_name / 外部 origin_id:用户 CSV 导入导出要与源系统
4
+ -- 9 列表头完全互通,「工号」必须是独立可查字段。
5
+ -- 可空是必需的:存量本地账号没有工号,外部同步号也可能不带;唯一性只在有值时有意义,
6
+ -- 故用部分索引(sqlite/pg 都支持 WHERE),mysql 侧靠「NULL 可重复」的唯一键等效。
7
+ ALTER TABLE users ADD COLUMN employee_no TEXT;
8
+ CREATE UNIQUE INDEX IF NOT EXISTS uk_users_employee_no ON users (tenant_id, employee_no) WHERE employee_no IS NOT NULL;
@@ -37,6 +37,11 @@ tables:
37
37
  activated: {type: integer, null: false}
38
38
  # 职务(与来源无关的通用字段)
39
39
  position: {type: text}
40
+ # 工号(0017,可空):与姓名/用户名分开的独立工号列,租户内有值时唯一。
41
+ # 必须在这里声明——列只落库而不进白名单时,任何模块 select 它都会被构造器拒为
42
+ # `unknown column 'employee_no'`(/system/departments 依赖 /system/users 的选人列表,
43
+ # 曾因此在打开部门页时整页报错)。
44
+ employee_no: {type: text}
40
45
  # 外部源侧的状态快照(企微 1/2/4/5)。**绝不**自动改写 is_forbidden——那会和
41
46
  # 本站管理员的手动启停互相覆盖(方案 §4.3)
42
47
  external_status: {type: integer}
@@ -43,7 +43,13 @@ export function runtimeReplayGate(): ReplayGate {
43
43
  incr: key => kv.incr(key),
44
44
  expire: (key, ttlSecs) => kv.expire(key, ttlSecs),
45
45
  get: key => kv.get(key),
46
- set: async (key, ttlSecs) => { await kv.set(key, "1"); if (!(await kv.expire(key, ttlSecs))) { log.warn("wecom applied marker ttl not set", "key", key); await kv.del(key); } },
46
+ set: async (key, ttlSecs) => {
47
+ await kv.set(key, "1");
48
+ if (!(await kv.expire(key, ttlSecs))) {
49
+ log.warn("wecom applied marker ttl not set", "key", key);
50
+ await kv.del(key);
51
+ }
52
+ },
47
53
  del: key => kv.del(key),
48
54
  now: () => Math.floor(Date.now() / 1000),
49
55
  };
@@ -61,7 +67,7 @@ export class ReplayAlreadyApplied extends Error {
61
67
 
62
68
  /** 处理成功后调用:标记该 nonce 已落库成功,供重推幂等回 success(避免首次偏慢被企微判超时、重推又被重放守卫挡成 400)。 */
63
69
  export async function markApplied(nonce: string, gate: ReplayGate): Promise<void> {
64
- await gate.set("wecom:applied:" + nonce, NONCE_TTL_SECS);
70
+ await gate.set(`wecom:applied:${nonce}`, NONCE_TTL_SECS);
65
71
  }
66
72
 
67
73
  /**
@@ -85,7 +91,7 @@ export async function guardReplay(timestampRaw: string, nonce: string, gate: Rep
85
91
  if (seen !== 1) {
86
92
  // 重推场景:若首次处理已成功落库(标了 applied 标记),直接幂等回 success,
87
93
  // 不让企微卡在「应用安装失败」。未标记者说明首推仍在进行或已失败(失败会 release 让重推重放),维持 400。
88
- const applied = await gate.get("wecom:applied:" + nonce);
94
+ const applied = await gate.get(`wecom:applied:${nonce}`);
89
95
  if (applied)
90
96
  throw new ReplayAlreadyApplied();
91
97
  fail(400, "回调 nonce 重复:同一条指令回调已被处理过(重放请求已拒绝)");
@@ -61,15 +61,12 @@ function randomPassword(): string {
61
61
 
62
62
  /** 跨租户读:指定租户下的管理员行(asSystem 例外,见文件头) */
63
63
  async function adminRow(tenantId: string): Promise<{ id: number, must_change: number } | null> {
64
- const rows = await db.asSystem().table("users")
65
- .select(["id", "must_change"])
66
- .where({
67
- and: [
68
- { field: "tenant_id", op: "eq", value: tenantId },
69
- { field: "username", op: "eq", value: TENANT_ADMIN_USERNAME },
70
- ],
71
- })
72
- .all() as Array<{ id: number, must_change: number | null }>;
64
+ const rows = await db.asSystem().table("users").select(["id", "must_change"]).where({
65
+ and: [
66
+ { field: "tenant_id", op: "eq", value: tenantId },
67
+ { field: "username", op: "eq", value: TENANT_ADMIN_USERNAME },
68
+ ],
69
+ }).all() as Array<{ id: number, must_change: number | null }>;
73
70
  if (!rows.length)
74
71
  return null;
75
72
  return { id: Number(rows[0]!.id), must_change: Number(rows[0]!.must_change ?? 0) };
@@ -77,15 +74,12 @@ async function adminRow(tenantId: string): Promise<{ id: number, must_change: nu
77
74
 
78
75
  /** 跨租户读:该用户在本租户是否握着 admin 角色(账号存在 ≠ 有角色,可能被收回过) */
79
76
  async function hasAdminRole(tenantId: string, uid: number): Promise<boolean> {
80
- const roles = await db.asSystem().table("sys_user_role")
81
- .select(["role_name"])
82
- .where({
83
- and: [
84
- { field: "tenant_id", op: "eq", value: tenantId },
85
- { field: "user_id", op: "eq", value: uid },
86
- ],
87
- })
88
- .all() as Array<{ role_name: string }>;
77
+ const roles = await db.asSystem().table("sys_user_role").select(["role_name"]).where({
78
+ and: [
79
+ { field: "tenant_id", op: "eq", value: tenantId },
80
+ { field: "user_id", op: "eq", value: uid },
81
+ ],
82
+ }).all() as Array<{ role_name: string }>;
89
83
  return roles.some(r => String(r.role_name) === TENANT_ADMIN_ROLE);
90
84
  }
91
85
 
@@ -176,15 +170,12 @@ export async function setTenantAdminPassword(tenantId: string, password: string)
176
170
  if (!row)
177
171
  throw Object.assign(new Error("租户管理员建档失败"), { status: 500 });
178
172
 
179
- await db.asSystem().table("users")
180
- .update({ password_hash: await bcrypt.hash(password), must_change: 1 })
181
- .where({
182
- and: [
183
- { field: "tenant_id", op: "eq", value: tenantId },
184
- { field: "username", op: "eq", value: TENANT_ADMIN_USERNAME },
185
- ],
186
- })
187
- .run();
173
+ await db.asSystem().table("users").update({ password_hash: await bcrypt.hash(password), must_change: 1 }).where({
174
+ and: [
175
+ { field: "tenant_id", op: "eq", value: tenantId },
176
+ { field: "username", op: "eq", value: TENANT_ADMIN_USERNAME },
177
+ ],
178
+ }).run();
188
179
 
189
180
  // 改密后旧会话必须失效。跨租户删 refresh 走带 tenant_id 条件的裸 SQL
190
181
  // (先例见 store.ts::revokeTenantSessions:守卫是 check-on-miss,带了租户条件即放行)
@@ -1,5 +1,5 @@
1
1
  import { respond } from "#/_platform/_shared/respond";
2
- // GET /api/system/oplog?action=&username=&module=&limit= —— 操作日志查询(AC1-29;D13 admin 可查)。
2
+ // GET /api/system/oplog?action=&username=&module=&page=&limit= —— 操作日志查询(AC1-29;D13 admin 可查)。
3
3
  // POST /api/system/oplog —— 操作日志写入(C3;登录态即可,解决方案模块记录自身操作)。
4
4
  import { assertSystemAdmin, currentAccess } from "#/_platform/_shared/service/infra/access";
5
5
  import { recordOp } from "#/_platform/_shared/service/infra/audit";
@@ -16,10 +16,12 @@ get.schema = {
16
16
  type: "object",
17
17
  properties: {
18
18
  action: { type: "string" },
19
- // username 前缀匹配(service likePattern),非精确
19
+ // username 包含匹配(子串,service 走 contains),非精确
20
20
  username: { type: "string" },
21
21
  module: { type: "string" },
22
- // 分页大小,service paged 钳 200
22
+ // 页码(1 起);service 的 pageArgs 非法回落 1
23
+ page: { type: "number", minimum: 1 },
24
+ // 每页条数 1~200(schema 兜住;service 的 pageArgs 再钳一次)
23
25
  limit: { type: "number", minimum: 1, maximum: 200 },
24
26
  },
25
27
  },
@@ -17,6 +17,9 @@ export const API_PREFIX = "/_platform";
17
17
  /** 认领身份前缀(契约 apiPrefix,字面等于模块目录名):跨模块认领护栏对比基准 */
18
18
  export const MODULE_PREFIX = "/_platform";
19
19
 
20
+ /** 认领方模块(web.config clientOwners 指定):跨模块认领护栏对比基准 */
21
+ export const MODULE_OWNER = "/app";
22
+
20
23
  /**
21
24
  * 模块 entry 的 onInit 里一行创建(构造即 install):完成 apiPrefix 登记 +
22
25
  * scoped request 绑定,返回绑好前缀的端点函数集合。一模块一 client;
@@ -28,8 +31,8 @@ export function create_platformClient(ctx: ModuleContext) {
28
31
  // 单槽、卸载互相耦合;跨模块需求应由本模块创建后经 provider 暴露。
29
32
  // 注意对比基准是 MODULE_PREFIX(目录名)而非 API_PREFIX——D-M8 映射时线上前缀
30
33
  // 属于别的命名空间,认领身份仍按目录名。
31
- if (import.meta.env.DEV && ctx.module.name !== MODULE_PREFIX.slice(1)) {
32
- console.warn(`[ojm-api] 模块 "${ctx.module.name}" 正在认领 ${MODULE_PREFIX} 的 client——跨模块认领会共享 request 单槽、卸载互相耦合;应由 "${MODULE_PREFIX.slice(1)}" 模块在自身 onInit 创建,跨模块需求经 provider 暴露。`);
34
+ if (import.meta.env.DEV && ctx.module.name !== MODULE_OWNER.slice(1)) {
35
+ console.warn(`[ojm-api] 模块 "${ctx.module.name}" 正在认领 ${MODULE_PREFIX} 的 client——跨模块认领会共享 request 单槽、卸载互相耦合;应由 "${MODULE_OWNER.slice(1)}" 模块在自身 onInit 创建,跨模块需求经 provider 暴露。`);
33
36
  }
34
37
  ctx.register.apiPrefix(API_PREFIX);
35
38
  bindRequest(ctx.utils.request);
@@ -17,6 +17,9 @@ export const API_PREFIX = "/auth";
17
17
  /** 认领身份前缀(契约 apiPrefix,字面等于模块目录名):跨模块认领护栏对比基准 */
18
18
  export const MODULE_PREFIX = "/auth";
19
19
 
20
+ /** 认领方模块(web.config clientOwners 指定):跨模块认领护栏对比基准 */
21
+ export const MODULE_OWNER = "/app";
22
+
20
23
  /**
21
24
  * 模块 entry 的 onInit 里一行创建(构造即 install):完成 apiPrefix 登记 +
22
25
  * scoped request 绑定,返回绑好前缀的端点函数集合。一模块一 client;
@@ -28,8 +31,8 @@ export function createAuthClient(ctx: ModuleContext) {
28
31
  // 单槽、卸载互相耦合;跨模块需求应由本模块创建后经 provider 暴露。
29
32
  // 注意对比基准是 MODULE_PREFIX(目录名)而非 API_PREFIX——D-M8 映射时线上前缀
30
33
  // 属于别的命名空间,认领身份仍按目录名。
31
- if (import.meta.env.DEV && ctx.module.name !== MODULE_PREFIX.slice(1)) {
32
- console.warn(`[ojm-api] 模块 "${ctx.module.name}" 正在认领 ${MODULE_PREFIX} 的 client——跨模块认领会共享 request 单槽、卸载互相耦合;应由 "${MODULE_PREFIX.slice(1)}" 模块在自身 onInit 创建,跨模块需求经 provider 暴露。`);
34
+ if (import.meta.env.DEV && ctx.module.name !== MODULE_OWNER.slice(1)) {
35
+ console.warn(`[ojm-api] 模块 "${ctx.module.name}" 正在认领 ${MODULE_PREFIX} 的 client——跨模块认领会共享 request 单槽、卸载互相耦合;应由 "${MODULE_OWNER.slice(1)}" 模块在自身 onInit 创建,跨模块需求经 provider 暴露。`);
33
36
  }
34
37
  ctx.register.apiPrefix(API_PREFIX);
35
38
  bindRequest(ctx.utils.request);
@@ -11,7 +11,7 @@ import {
11
11
  notificationNotificationsGet,
12
12
  notificationNotificationsReadAllPost,
13
13
  notificationNotificationsReadPost,
14
- } from "../../__client/api";
14
+ } from "../../__client";
15
15
 
16
16
  type Item = Awaited<ReturnType<typeof notificationNotificationsGet>>[number];
17
17
  type CatFilter = "all" | NoticeCategory;
@@ -4,7 +4,7 @@ import { BellOutlined } from "@ant-design/icons";
4
4
 
5
5
  import { createElement, lazy } from "react";
6
6
 
7
- import { createNotificationClient } from "./__client/api";
7
+ import { createNotificationClient } from "./__client";
8
8
 
9
9
  const NotificationCenter = lazy(() => import("./pages/center"));
10
10
 
@@ -7,7 +7,7 @@ import {
7
7
  import { BasicContent, FormAvatarItem, useUserStore } from "@oj-module/runtime";
8
8
  import { App, Form, Input } from "antd";
9
9
 
10
- import { personalCenterProfilePost } from "../../__client/api";
10
+ import { personalCenterProfilePost } from "../../__client";
11
11
 
12
12
  export default function Profile() {
13
13
  const currentUser = useUserStore();
@@ -3,7 +3,7 @@ import { ProfileCardIcon, RiAccountCircleLine, useAuthStore } from "@oj-module/r
3
3
 
4
4
  import { createElement, lazy } from "react";
5
5
 
6
- import { createPersonalCenterClient } from "./__client/api";
6
+ import { createPersonalCenterClient } from "./__client";
7
7
 
8
8
  const MyProfile = lazy(() => import("./pages/my-profile"));
9
9
 
@@ -130,6 +130,7 @@ export const schemas = {
130
130
  "action": z.string().optional(),
131
131
  "username": z.string().optional(),
132
132
  "module": z.string().optional(),
133
+ "page": z.number().min(1).optional(),
133
134
  "limit": z.number().min(1).max(200).optional()
134
135
  }),
135
136
  data: z.object({
@@ -99,10 +99,9 @@
99
99
  },
100
100
  "oplog": {
101
101
  "filterAction": "Action",
102
- "filterUsername": "Username (prefix)",
102
+ "filterUsername": "Username (contains)",
103
103
  "filterModule": "Module",
104
- "filterLimit": "Limit",
105
- "truncated": "Results truncated by limit; narrow your filters",
104
+ "total": "{{total}} total",
106
105
  "columns": {
107
106
  "time": "Time",
108
107
  "username": "Username",
@@ -99,10 +99,9 @@
99
99
  },
100
100
  "oplog": {
101
101
  "filterAction": "操作 (action)",
102
- "filterUsername": "用户名(前缀匹配)",
102
+ "filterUsername": "用户名(包含匹配)",
103
103
  "filterModule": "模块",
104
- "filterLimit": "条数上限",
105
- "truncated": "结果已按上限截断,请缩小过滤范围",
104
+ "total": "共 {{total}} 条",
106
105
  "columns": {
107
106
  "time": "时间",
108
107
  "username": "用户名",
@@ -1,4 +1,4 @@
1
- import type { SystemDepartmentsGetData, SystemDepartmentsMembersGetData } from "../../__client/api";
1
+ import type { SystemDepartmentsGetData, SystemDepartmentsMembersGetData } from "../../__client";
2
2
 
3
3
  import { PlusOutlined } from "@ant-design/icons";
4
4
  import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
@@ -32,7 +32,7 @@ import {
32
32
  systemDepartmentsPost,
33
33
  systemDepartmentsPut,
34
34
  systemUsersGet,
35
- } from "../../__client/api";
35
+ } from "../../__client";
36
36
  import { explainError, isForbidden } from "../errors";
37
37
  import { ForbiddenPanel } from "../forbidden";
38
38
  import { WecomSyncBar } from "./sync-bar";
@@ -2,7 +2,7 @@ import { useQuery } from "@tanstack/react-query";
2
2
  import { Card, Descriptions, Tag } from "antd";
3
3
  import { useTranslation } from "react-i18next";
4
4
 
5
- import { systemLoginMethodsGet } from "../../__client/api";
5
+ import { systemLoginMethodsGet } from "../../__client";
6
6
 
7
7
  const METHOD_KEY: Record<string, string> = {
8
8
  local: "system:loginMethodsPage.methodLocal",
@@ -1,10 +1,10 @@
1
1
  import { useQuery } from "@tanstack/react-query";
2
- import { Alert, Button, Form, Input, InputNumber, Space, Table, Tooltip } from "antd";
2
+ import { Button, Form, Input, Space, Table, Tooltip } from "antd";
3
3
  import dayjs from "dayjs";
4
4
  import { useState } from "react";
5
5
  import { useTranslation } from "react-i18next";
6
6
 
7
- import { systemOplogGet } from "../../__client/api";
7
+ import { systemOplogGet } from "../../__client";
8
8
  import { isForbidden } from "../errors";
9
9
  import { ForbiddenPanel } from "../forbidden";
10
10
 
@@ -12,20 +12,26 @@ interface OplogFilters {
12
12
  action?: string
13
13
  username?: string
14
14
  module?: string
15
- limit?: number
16
15
  }
17
16
 
17
+ const DEFAULT_PAGE_SIZE = 20;
18
+ const PAGE_SIZE_OPTIONS = [20, 50, 100];
19
+
18
20
  /**
19
- * 操作日志(AC1-29/D13):action 精确 / username 前缀 / module 精确,limit 钳 200。
20
- * truncated=true 表示结果被截断(服务端 paged),顶部提示缩小过滤范围。
21
+ * 操作日志(AC1-29/D13):action 精确 / username 包含 / module 精确。
22
+ *
23
+ * 分页交给服务端:`page` + `limit`(每页条数,钳 200),响应带 `total`,页面按 total 出页码。
24
+ * 不再有「结果被截断」的横幅——记录再多也能通过页码翻到,不必靠缩小过滤范围。
21
25
  */
22
26
  export default function OplogPage() {
23
27
  const { t } = useTranslation();
24
28
  const [filters, setFilters] = useState<OplogFilters>({});
29
+ const [page, setPage] = useState(1);
30
+ const [size, setSize] = useState(DEFAULT_PAGE_SIZE);
25
31
 
26
32
  const query = useQuery({
27
- queryKey: ["system", "oplog", filters],
28
- queryFn: () => systemOplogGet(filters),
33
+ queryKey: ["system", "oplog", filters, page, size],
34
+ queryFn: () => systemOplogGet({ ...filters, page, limit: size }),
29
35
  });
30
36
 
31
37
  if (query.error && isForbidden(query.error))
@@ -50,12 +56,14 @@ export default function OplogPage() {
50
56
  <Space direction="vertical" style={{ width: "100%", paddingTop: 16 }}>
51
57
  <Form
52
58
  layout="inline"
53
- onFinish={values => setFilters({
54
- action: values.action || undefined,
55
- username: values.username || undefined,
56
- module: values.module || undefined,
57
- limit: values.limit || undefined,
58
- })}
59
+ onFinish={(values) => {
60
+ setFilters({
61
+ action: values.action || undefined,
62
+ username: values.username || undefined,
63
+ module: values.module || undefined,
64
+ });
65
+ setPage(1);
66
+ }}
59
67
  >
60
68
  <Form.Item name="action" label={t("system:oplog.filterAction")}>
61
69
  <Input allowClear />
@@ -66,25 +74,42 @@ export default function OplogPage() {
66
74
  <Form.Item name="module" label={t("system:oplog.filterModule")}>
67
75
  <Input allowClear />
68
76
  </Form.Item>
69
- <Form.Item name="limit" label={t("system:oplog.filterLimit")} initialValue={50}>
70
- <InputNumber min={1} max={200} />
71
- </Form.Item>
72
77
  <Form.Item>
73
78
  <Space>
74
79
  <Button type="primary" htmlType="submit">{t("system:common.search")}</Button>
75
- <Button onClick={() => { setFilters({}); }}>
80
+ <Button
81
+ onClick={() => {
82
+ setFilters({});
83
+ setPage(1);
84
+ }}
85
+ >
76
86
  {t("system:common.reset")}
77
87
  </Button>
78
88
  </Space>
79
89
  </Form.Item>
80
90
  </Form>
81
- {query.data?.truncated ? <Alert type="warning" showIcon message={t("system:oplog.truncated")} /> : null}
82
91
  <Table
83
92
  rowKey="id"
84
93
  loading={query.isLoading}
85
94
  dataSource={query.data?.list ?? []}
86
95
  columns={columns}
87
- pagination={{ hideOnSinglePage: true, pageSize: 50 }}
96
+ pagination={{
97
+ current: page,
98
+ pageSize: size,
99
+ total: query.data?.total ?? 0,
100
+ showSizeChanger: true,
101
+ pageSizeOptions: PAGE_SIZE_OPTIONS,
102
+ showTotal: total => t("system:oplog.total", { total }),
103
+ onChange: (nextPage, nextSize) => {
104
+ // 改每页条数后原页码可能越界:统一回第 1 页
105
+ if (nextSize !== size) {
106
+ setSize(nextSize);
107
+ setPage(1);
108
+ return;
109
+ }
110
+ setPage(nextPage);
111
+ },
112
+ }}
88
113
  />
89
114
  </Space>
90
115
  );
@@ -1,12 +1,13 @@
1
- import type { SystemPermissionsGetData, SystemRolesGetData } from "../../__client/api";
1
+ import type { SystemPermissionsGetData, SystemRolesGetData } from "../../__client";
2
2
 
3
3
  import { useQuery } from "@tanstack/react-query";
4
4
  import { Alert, Space, Table, Tabs, Tag } from "antd";
5
5
  import { useTranslation } from "react-i18next";
6
6
 
7
- import { systemPermissionsGet, systemRolesGet } from "../../__client/api";
7
+ import { systemPermissionsGet, systemRolesGet } from "../../__client";
8
8
  import { isForbidden } from "../errors";
9
9
  import { ForbiddenPanel } from "../forbidden";
10
+ import { visibleRoles } from "./visible-roles";
10
11
 
11
12
  type RoleRow = SystemRolesGetData[number];
12
13
  type PermRow = SystemPermissionsGetData[number];
@@ -19,6 +20,7 @@ export default function RolesPage() {
19
20
  const { t } = useTranslation();
20
21
  const rolesQuery = useQuery({ queryKey: ["system", "roles"], queryFn: systemRolesGet });
21
22
  const permsQuery = useQuery({ queryKey: ["system", "permissions"], queryFn: systemPermissionsGet });
23
+ const rows = visibleRoles(rolesQuery.data ?? []);
22
24
 
23
25
  if ((rolesQuery.error && isForbidden(rolesQuery.error)) || (permsQuery.error && isForbidden(permsQuery.error)))
24
26
  return <ForbiddenPanel t={t} error={rolesQuery.error ?? permsQuery.error} />;
@@ -75,7 +77,7 @@ export default function RolesPage() {
75
77
  children: (
76
78
  <>
77
79
  <Alert style={{ marginBottom: 12 }} type="info" showIcon message={t("system:rolesPage.readonlyHint")} />
78
- <Table rowKey="name" loading={rolesQuery.isLoading} dataSource={rolesQuery.data ?? []} columns={roleColumns} pagination={{ hideOnSinglePage: true }} />
80
+ <Table rowKey="name" loading={rolesQuery.isLoading} dataSource={rows} columns={roleColumns} pagination={{ hideOnSinglePage: true }} />
79
81
  </>
80
82
  ),
81
83
  },
@@ -0,0 +1,14 @@
1
+ /**
2
+ * 角色展示页的可见范围(纯函数,便于用例直接钉住,不必起 React)。
3
+ *
4
+ * superadmin 是平台级角色(跨租户),不属于「站点可分配的角色目录」;列在角色页只会让人
5
+ * 误以为能改它的绑定,故展示层隐去。
6
+ *
7
+ * **只在这一层过滤**:后端 listRoles() 必须原样返回 superadmin——成员资格页
8
+ * (users/roles.tsx)还要拿它作为可授予项(仅 superadmin 可勾选,见那里的 disabled 逻辑)。
9
+ */
10
+ export const HIDDEN_ROLE = "superadmin";
11
+
12
+ export function visibleRoles<T extends { name: string }>(roles: readonly T[]): T[] {
13
+ return roles.filter(r => r.name !== HIDDEN_ROLE);
14
+ }
@@ -1,13 +1,13 @@
1
1
  import type { TFunction } from "i18next";
2
2
 
3
- import type { SystemSettingsGetData } from "../../__client/api";
3
+ import type { SystemSettingsGetData } from "../../__client";
4
4
 
5
5
  import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
6
6
  import { App, Button, Card, Input, Space, Switch, Tooltip } from "antd";
7
7
  import { useState } from "react";
8
8
  import { useTranslation } from "react-i18next";
9
9
 
10
- import { systemSettingsGet, systemSettingsPut } from "../../__client/api";
10
+ import { systemSettingsGet, systemSettingsPut } from "../../__client";
11
11
  import { explainError, isForbidden } from "../errors";
12
12
  import { ForbiddenPanel } from "../forbidden";
13
13
 
@@ -0,0 +1,21 @@
1
+ /**
2
+ * /system/users 行级操作门禁:只有 superadmin 能改 superadmin 账号。
3
+ *
4
+ * 与后端 `domain/users.ts::assertCanManageTarget` 同一条红线(只有 superadmin 能改
5
+ * superadmin 的口令/状态/资料/角色)。前端这层只为「不给会 403 的按钮」——灰色可点却不
6
+ * 生效比报错更糟——真正的防线在服务端:admin 直打接口同样会被 403 挡住。
7
+ *
8
+ * 单独成文件是为了可测:vitest 跑在 node 环境,没有 jsdom,整页 render 不了。
9
+ */
10
+ export const PLATFORM_ROLE = "superadmin";
11
+
12
+ /**
13
+ * @param myRoles 当前登录者的角色
14
+ * @param targetRoles 行上用户的角色
15
+ * @returns 是否允许对目标账号执行管理操作(改密/启停/改资料/成员资格)
16
+ */
17
+ export function canManageUser(myRoles: readonly string[], targetRoles: readonly string[]): boolean {
18
+ if (!targetRoles.includes(PLATFORM_ROLE))
19
+ return true;
20
+ return myRoles.includes(PLATFORM_ROLE);
21
+ }