feat: org capacity policy admin surface

ADR-0022 / Spec.System.Capacity pins layered capacity limits: a platform
ceiling per dimension is unbreakable, and each organization may only set a
lower `organizationLimit`. The effective limit is the minimum of the two
(`LayeredLimit.effective`); dimensions with no org override fall back to
the platform ceiling. No dimension may be unlimited (a ceiling must exist
before an org limit can be set, `LayeredLimit.Valid`).

Add the backend: the pinned 23 CapacityDimension set + labels
(src/capacity/dimensions.ts), platform ceilings sourced from existing
runtime env vars plus `HUB_CEILING_<DIMENSION>` (src/capacity/ceilings.ts),
the OrganizationCapacityPolicy prisma model + migration, the
getCapacityPolicy/setCapacityPolicy service enforcing LayeredLimit.Valid,
and org-admin GET/PUT /api/org/:orgSlug/capacity-policy routes wired into
the org route tree.

Add the admin-web surface: CapacityDimension/CapacityPolicyView api client
types, capacityPolicy/setCapacityPolicy methods, a `容量` nav entry, and a
capacity page that lists every dimension with its platform ceiling (or
`未配置` when unset), an org-limit input (disabled until a ceiling exists),
and a live effective-value column. Saving sends the partial limits map;
the service rejects values above the ceiling or for unconfigured dimensions.
This commit is contained in:
2026-07-14 21:23:41 +08:00
parent adce8fb6f5
commit ab9dfad53a
10 changed files with 545 additions and 0 deletions
@@ -0,0 +1,18 @@
-- ADR-0022 layered capacity policy. Org admins may set per-dimension lower
-- `organizationLimit` overrides; platform ceilings come from deployment config
-- (see `src/capacity/ceilings.ts`). `limits` is a JSON map of
-- CapacityDimension → number (only the set dimensions); service enforces
-- `LayeredLimit.Valid` (org limit ≤ platform ceiling).
-- CreateTable
CREATE TABLE "OrganizationCapacityPolicy" (
"organizationId" TEXT NOT NULL,
"limits" JSONB NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "OrganizationCapacityPolicy_pkey" PRIMARY KEY ("organizationId")
);
-- AddForeignKey
ALTER TABLE "OrganizationCapacityPolicy" ADD CONSTRAINT "OrganizationCapacityPolicy_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+15
View File
@@ -37,6 +37,7 @@ model Organization {
memberships OrganizationMembership[]
projectSettings OrganizationProjectSettings?
capacityPolicy OrganizationCapacityPolicy?
folders Folder[]
projects Project[]
teams Team[]
@@ -160,6 +161,20 @@ model OrganizationProjectSettings {
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
}
/// ADR-0022 layered capacity policy. Org admins may set per-dimension lower
/// limits; `limits` is a JSON map of CapacityDimension → number (only the set
/// ones). Each set value must be ≤ the platform ceiling for that dimension
/// (validated in service; spec `LayeredLimit.Valid`). Dimensions with no entry
/// fall back to the platform ceiling (`LayeredLimit.effective`).
model OrganizationCapacityPolicy {
organizationId String @id
limits Json
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
}
/// A person known to the Hub. `feishuOpenId` remains a legacy compatibility
/// key; new customer-app identities live in FeishuUserIdentity and store a
/// connection-scoped opaque USER principal here instead of a raw open_id.