diff --git a/furniture-3d-system/prisma/migrations/202606090001_add_external_intake_business_role/migration.sql b/furniture-3d-system/prisma/migrations/202606090001_add_external_intake_business_role/migration.sql
new file mode 100644
index 00000000..099bfd0b
--- /dev/null
+++ b/furniture-3d-system/prisma/migrations/202606090001_add_external_intake_business_role/migration.sql
@@ -0,0 +1 @@
+ALTER TYPE "BusinessAccountType" ADD VALUE IF NOT EXISTS 'EXTERNAL_INTAKE';
diff --git a/furniture-3d-system/prisma/schema.prisma b/furniture-3d-system/prisma/schema.prisma
index 817dc087..9d446eb7 100644
--- a/furniture-3d-system/prisma/schema.prisma
+++ b/furniture-3d-system/prisma/schema.prisma
@@ -1451,6 +1451,7 @@ enum BusinessAccountType {
PURCHASING
WAREHOUSE
SUPPLIER
+ EXTERNAL_INTAKE
}
enum ApprovalWorkflowStatus {
diff --git a/furniture-3d-system/src/app/api/intake/non-integrated/[id]/jushuitan-product-pricing-push/route.ts b/furniture-3d-system/src/app/api/intake/non-integrated/[id]/jushuitan-product-pricing-push/route.ts
index 9e34e106..593e0016 100644
--- a/furniture-3d-system/src/app/api/intake/non-integrated/[id]/jushuitan-product-pricing-push/route.ts
+++ b/furniture-3d-system/src/app/api/intake/non-integrated/[id]/jushuitan-product-pricing-push/route.ts
@@ -51,6 +51,11 @@ export async function POST(request: NextRequest, context: RouteContext) {
)
}
+ await intakeNonIntegratedProductService.assertCanPushProductPricing(
+ productId,
+ ctx.user,
+ )
+
const result =
await jushuitanProductPricingPushService.pushProductPricing({
product,
diff --git a/furniture-3d-system/src/app/intake/IntakeClient.test.tsx b/furniture-3d-system/src/app/intake/IntakeClient.test.tsx
index 171c616d..9202425a 100644
--- a/furniture-3d-system/src/app/intake/IntakeClient.test.tsx
+++ b/furniture-3d-system/src/app/intake/IntakeClient.test.tsx
@@ -2019,6 +2019,32 @@ describe('IntakeClient', () => {
expect(screen.getByRole('button', { name: '新增产品' })).toBeInTheDocument()
})
+ it('allows external intake users to create normal product development records only', async () => {
+ const actor = userEvent.setup()
+
+ renderSelector({
+ eligibleSkuList: [],
+ userRole: 'USER',
+ userBusinessRoles: ['EXTERNAL_INTAKE'],
+ })
+
+ const rightPanel = screen.getByTestId('right-panel')
+ expect(within(rightPanel).getByRole('button', { name: '产品开发明细' })).toBeInTheDocument()
+ expect(
+ within(rightPanel).queryByRole('button', { name: '模块化产品明细' }),
+ ).not.toBeInTheDocument()
+
+ await actor.click(screen.getByRole('button', { name: '新增产品' }))
+
+ expect(screen.getByRole('tab', { name: '新增产品' })).toHaveAttribute(
+ 'aria-selected',
+ 'true',
+ )
+ expect(screen.getByTestId('embedded-development-kind')).toHaveTextContent(
+ 'product',
+ )
+ })
+
it('opens the series development list when requested from draft groups', () => {
renderSelector({
eligibleSkuList: [],
diff --git a/furniture-3d-system/src/app/intake/IntakeClient.tsx b/furniture-3d-system/src/app/intake/IntakeClient.tsx
index 1cf935ca..5fb50718 100644
--- a/furniture-3d-system/src/app/intake/IntakeClient.tsx
+++ b/furniture-3d-system/src/app/intake/IntakeClient.tsx
@@ -94,6 +94,7 @@ import {
} from "./non-integrated/nonIntegratedApi";
import {
canEditAnyNonIntegratedWorkspace,
+ canCreateNonIntegratedProductFromIntake,
canEditNonIntegratedProductIdentity,
canDeleteNonIntegratedProductByIntakePermission,
isIntakeNonIntegratedDesignDepartmentUser,
@@ -10410,6 +10411,8 @@ export default function IntakeClient(props: IntakeClientProps) {
);
const canUseDesignControlledProductData =
canAccessDesignControlledProductData(props.user);
+ const canCreateNonIntegratedProducts =
+ canCreateNonIntegratedProductFromIntake(props.user);
const shouldPersistWorkspaceState = !isEditorMode(props);
const shouldUseSharedNonIntegratedApi = process.env.NODE_ENV !== "test";
const [nodePermissionOverrides, setNodePermissionOverrides] =
@@ -11116,7 +11119,12 @@ export default function IntakeClient(props: IntakeClientProps) {
productDevelopmentKind: ProductDevelopmentKind = "product",
initialCreateProductKind: NewNonIntegratedCreateKind = "new",
) {
- if (!canUseDesignControlledProductData) {
+ const canCreateTarget =
+ productDevelopmentKind === "product" &&
+ initialCreateProductKind === "new"
+ ? canCreateNonIntegratedProducts
+ : canUseDesignControlledProductData;
+ if (!canCreateTarget) {
return;
}
@@ -11580,7 +11588,7 @@ export default function IntakeClient(props: IntakeClientProps) {
useEffect(() => {
if (
- !canUseDesignControlledProductData ||
+ !canCreateNonIntegratedProducts ||
!props.initialOpenNewNonIntegratedProduct ||
initialOpenNewNonIntegratedProductRef.current
) {
@@ -11590,7 +11598,7 @@ export default function IntakeClient(props: IntakeClientProps) {
initialOpenNewNonIntegratedProductRef.current = true;
openNewNonIntegratedProductWorkspace();
}, [
- canUseDesignControlledProductData,
+ canCreateNonIntegratedProducts,
openNewNonIntegratedProductWorkspace,
props.initialOpenNewNonIntegratedProduct,
]);
@@ -11662,12 +11670,12 @@ export default function IntakeClient(props: IntakeClientProps) {
}
onOpenProductWorkspace={openProductWorkspace}
emptyMessage={
- canUseDesignControlledProductData
+ canCreateNonIntegratedProducts
? undefined
: "当前还没有产品开发明细。"
}
onOpenNewNonIntegratedProductWorkspace={
- canUseDesignControlledProductData
+ canCreateNonIntegratedProducts
? () => openNewNonIntegratedProductWorkspace("product")
: undefined
}
@@ -11850,12 +11858,12 @@ export default function IntakeClient(props: IntakeClientProps) {
}
onOpenProductWorkspace={openProductWorkspace}
emptyMessage={
- canUseDesignControlledProductData
+ canCreateNonIntegratedProducts
? undefined
: "当前还没有产品开发明细。"
}
onOpenNewNonIntegratedProductWorkspace={
- canUseDesignControlledProductData
+ canCreateNonIntegratedProducts
? () => openNewNonIntegratedProductWorkspace("product")
: undefined
}
diff --git a/furniture-3d-system/src/app/intake/non-integrated/NonIntegratedDetailClient.test.tsx b/furniture-3d-system/src/app/intake/non-integrated/NonIntegratedDetailClient.test.tsx
index 2d741b95..0a2e7a9c 100644
--- a/furniture-3d-system/src/app/intake/non-integrated/NonIntegratedDetailClient.test.tsx
+++ b/furniture-3d-system/src/app/intake/non-integrated/NonIntegratedDetailClient.test.tsx
@@ -7411,6 +7411,52 @@ describe("NonIntegratedDetailClient", () => {
expect(costPriceInput).toHaveValue("");
});
+ it("only shows external intake users the data and system-entry workspaces", () => {
+ saveNonIntegratedProduct(
+ createProductRecord({
+ id: "DNZ0001-external-visible-workspaces",
+ createdBy: "external-user-id",
+ }),
+ );
+
+ render(
+ ,
+ );
+
+ expect(screen.getByTestId("document-workspace-trigger-开品参考")).toBeInTheDocument();
+ expect(screen.getByTestId("document-workspace-basic-info-trigger")).toBeInTheDocument();
+ expect(screen.getByTestId("document-workspace-trigger-产品定厂")).toBeInTheDocument();
+ expect(screen.getByTestId("document-workspace-trigger-样包测量")).toBeInTheDocument();
+ expect(screen.getByTestId("document-workspace-trigger-产品核价")).toBeInTheDocument();
+
+ for (const hiddenCategory of [
+ "设计卖点",
+ "立项评审",
+ "产品模型",
+ "评审资料",
+ "企划布局",
+ "专利申请",
+ "产品实拍图",
+ "安装方式",
+ "包装细节",
+ "成品寄样",
+ "产品限价",
+ "工厂质检",
+ "检测报告",
+ ]) {
+ expect(
+ screen.queryByTestId(`document-workspace-trigger-${hiddenCategory}`),
+ ).not.toBeInTheDocument();
+ }
+ });
+
it("lets only admins reopen confirmed product factory rows before sample measurement is approved", async () => {
const actor = userEvent.setup();
const productId = "DNZ0001-product-factory-reopen";
diff --git a/furniture-3d-system/src/app/intake/non-integrated/NonIntegratedDetailClient.tsx b/furniture-3d-system/src/app/intake/non-integrated/NonIntegratedDetailClient.tsx
index 0b626d29..b639d864 100644
--- a/furniture-3d-system/src/app/intake/non-integrated/NonIntegratedDetailClient.tsx
+++ b/furniture-3d-system/src/app/intake/non-integrated/NonIntegratedDetailClient.tsx
@@ -579,6 +579,7 @@ import {
isIntakeNonIntegratedSupplyChainUser,
type IntakeNonIntegratedPermissionUser,
} from "@/lib/non-integrated-permissions";
+import { isExternalIntakeRecordOwner } from "@/lib/non-integrated-external-entry-permissions";
import {
getIntakeNodePermissionTargetKeyByWorkspaceCategory,
resolveIntakeNodePermissionAccess,
@@ -8279,7 +8280,9 @@ export default function NonIntegratedDetailClient({
const isCurrentUserSupplyChainQualityGroup =
isIntakeNonIntegratedQualityGroupUser(currentPermissionUser);
const canUseProductPricingPushControls =
- isCurrentUserAdmin || isCurrentUserSupplyChainPricingGroup;
+ isCurrentUserAdmin ||
+ isCurrentUserSupplyChainPricingGroup ||
+ isExternalIntakeRecordOwner(currentPermissionProduct, currentPermissionUser);
const isCurrentUserPurchaseDownloadRestricted =
hasCurrentUserContext &&
!isCurrentUserFullDetailEditor &&
diff --git a/furniture-3d-system/src/lib/business-account-types.ts b/furniture-3d-system/src/lib/business-account-types.ts
index 6337fa08..c44d3436 100644
--- a/furniture-3d-system/src/lib/business-account-types.ts
+++ b/furniture-3d-system/src/lib/business-account-types.ts
@@ -1,7 +1,12 @@
import type { BusinessAccountType } from '@prisma/client'
+import { EXTERNAL_INTAKE_BUSINESS_ROLE } from './non-integrated-external-entry-permissions'
+
+export type AppBusinessAccountType =
+ | BusinessAccountType
+ | typeof EXTERNAL_INTAKE_BUSINESS_ROLE
export const BUSINESS_ACCOUNT_TYPE_OPTIONS: Array<{
- value: BusinessAccountType
+ value: AppBusinessAccountType
label: string
}> = [
{ value: 'DESIGN', label: '设计' },
@@ -9,14 +14,16 @@ export const BUSINESS_ACCOUNT_TYPE_OPTIONS: Array<{
{ value: 'PURCHASING', label: '采购' },
{ value: 'WAREHOUSE', label: '仓库' },
{ value: 'SUPPLIER', label: '供应商' },
+ { value: EXTERNAL_INTAKE_BUSINESS_ROLE, label: '外部录入员' },
]
-export const BUSINESS_ACCOUNT_TYPE_LABELS: Record = {
+export const BUSINESS_ACCOUNT_TYPE_LABELS: Record = {
DESIGN: '设计',
PLANNING: '企划',
PURCHASING: '采购',
WAREHOUSE: '仓库',
SUPPLIER: '供应商',
+ EXTERNAL_INTAKE: '外部录入员',
}
export function normalizeBusinessRoles(value: unknown): BusinessAccountType[] {
@@ -24,6 +31,6 @@ export function normalizeBusinessRoles(value: unknown): BusinessAccountType[] {
const allowed = new Set(BUSINESS_ACCOUNT_TYPE_OPTIONS.map((option) => option.value))
return value.filter((item): item is BusinessAccountType => {
- return typeof item === 'string' && allowed.has(item as BusinessAccountType)
+ return typeof item === 'string' && allowed.has(item as AppBusinessAccountType)
})
}
diff --git a/furniture-3d-system/src/lib/intake-node-permission-settings.test.ts b/furniture-3d-system/src/lib/intake-node-permission-settings.test.ts
index 388589fd..fbbcfaaf 100644
--- a/furniture-3d-system/src/lib/intake-node-permission-settings.test.ts
+++ b/furniture-3d-system/src/lib/intake-node-permission-settings.test.ts
@@ -149,6 +149,75 @@ describe('intake node permission settings', () => {
})
})
+ it('keeps external intake users on their own product development detail lane', () => {
+ const subjectKeys = resolveIntakeNodePermissionSubjectKeys({
+ role: 'USER',
+ businessRoles: ['EXTERNAL_INTAKE'],
+ departmentPaths: [
+ 'internal company > product research department',
+ 'internal company > supply chain center > purchasing department',
+ ],
+ })
+
+ expect(subjectKeys).toEqual(['externalIntake'])
+ expect(
+ resolveIntakeNodePermissionAccess({}, 'formalProducts', subjectKeys),
+ ).toMatchObject({
+ view: true,
+ edit: true,
+ })
+ expect(
+ resolveIntakeNodePermissionAccess({}, 'productDevelopmentList', subjectKeys),
+ ).toMatchObject({
+ view: true,
+ edit: true,
+ })
+ for (const targetKey of [
+ 'openingReference',
+ 'basicInfo',
+ 'factory',
+ 'measurement',
+ 'pricing',
+ ] as const) {
+ expect(resolveIntakeNodePermissionAccess({}, targetKey, subjectKeys)).toMatchObject({
+ view: true,
+ edit: true,
+ })
+ }
+ for (const targetKey of [
+ 'designSellingPoint',
+ 'projectReview',
+ 'productData',
+ 'review',
+ 'planningLayout',
+ 'patent',
+ 'photo',
+ 'installation',
+ 'packaging',
+ 'finishedSampleShipment',
+ 'limit',
+ 'quality',
+ 'warehouseSampling',
+ ] as const) {
+ expect(resolveIntakeNodePermissionAccess({}, targetKey, subjectKeys)).toMatchObject({
+ view: false,
+ edit: false,
+ })
+ }
+ expect(
+ resolveIntakeNodePermissionAccess({}, 'seriesDevelopmentList', subjectKeys),
+ ).toMatchObject({
+ view: false,
+ edit: false,
+ })
+ expect(
+ resolveIntakeNodePermissionAccess({}, 'productProgressDashboard', subjectKeys),
+ ).toMatchObject({
+ view: false,
+ edit: false,
+ })
+ })
+
it('allows admin settings to explicitly close default permissions', () => {
const overrides = sanitizeIntakeNodePermissionOverrides({
[buildIntakeNodePermissionOverrideKey('planningLayout', 'space', 'edit')]: false,
diff --git a/furniture-3d-system/src/lib/intake-node-permission-settings.ts b/furniture-3d-system/src/lib/intake-node-permission-settings.ts
index 78c5a7ec..4a45e77b 100644
--- a/furniture-3d-system/src/lib/intake-node-permission-settings.ts
+++ b/furniture-3d-system/src/lib/intake-node-permission-settings.ts
@@ -6,6 +6,7 @@ import {
import type {
IntakeNonIntegratedPermissionUser,
} from "@/lib/non-integrated-permissions";
+import { isExternalIntakeUser } from "@/lib/non-integrated-external-entry-permissions";
export type IntakeNodePermissionState = "edit" | "readonly" | "hidden";
export type IntakeNodePermissionAction = "view" | "download" | "edit";
@@ -22,6 +23,7 @@ export type IntakeNodePermissionSubjectKey =
| "warehouse"
| "warehouseQuality"
| "merchandiser"
+ | "externalIntake"
| "viewer";
export type IntakeNodePermissionTargetKey =
@@ -87,6 +89,7 @@ const ALL_SUBJECT_KEYS: IntakeNodePermissionSubjectKey[] = [
"warehouse",
"warehouseQuality",
"merchandiser",
+ "externalIntake",
"viewer",
];
@@ -152,6 +155,7 @@ const NODE_PERMISSION_HIDDEN: Record<
warehouse: "hidden",
warehouseQuality: "hidden",
merchandiser: "hidden",
+ externalIntake: "hidden",
viewer: "hidden",
};
@@ -170,6 +174,7 @@ const NODE_PERMISSION_READONLY: Record<
warehouse: "readonly",
warehouseQuality: "readonly",
merchandiser: "readonly",
+ externalIntake: "hidden",
viewer: "readonly",
};
@@ -290,6 +295,11 @@ export const ORDINARY_NODE_PERMISSION_SUBJECTS: IntakeNodePermissionSubject[] =
name: "跟单组",
department: "供应链中心-品牌采购部-跟单组",
},
+ {
+ key: "externalIntake",
+ name: "外部录入员",
+ department: "仅可维护自己创建的产品开发明细",
+ },
{
key: "viewer",
name: "其他业务用户",
@@ -321,7 +331,10 @@ export const TOP_ENTRY_PERMISSION_TARGETS: IntakeNodePermissionTarget[] = [
{
key: "formalProducts",
label: "产品资料",
- defaults: NODE_PERMISSION_FORMAL_PRODUCTS_DEFAULT,
+ defaults: {
+ ...NODE_PERMISSION_FORMAL_PRODUCTS_DEFAULT,
+ externalIntake: "edit",
+ },
},
{
key: "warehouseManage",
@@ -364,6 +377,7 @@ export const PRODUCT_DEVELOPMENT_PERMISSION_TARGETS: IntakeNodePermissionTarget[
...NODE_PERMISSION_BRAND_READONLY,
design: "edit",
planning: "edit",
+ externalIntake: "edit",
},
},
{
@@ -428,6 +442,7 @@ export const PRODUCT_DEVELOPMENT_PERMISSION_TARGETS: IntakeNodePermissionTarget[
defaults: {
...NODE_PERMISSION_PLAIN_VISIBLE,
design: "edit",
+ externalIntake: "edit",
},
},
{
@@ -483,6 +498,7 @@ export const PRODUCT_DEVELOPMENT_PERMISSION_TARGETS: IntakeNodePermissionTarget[
...NODE_PERMISSION_SUPPLY_VISIBLE,
purchase: "edit",
merchandiser: "edit",
+ externalIntake: "edit",
},
downloads: {
purchase: true,
@@ -495,6 +511,7 @@ export const PRODUCT_DEVELOPMENT_PERMISSION_TARGETS: IntakeNodePermissionTarget[
defaults: {
...NODE_PERMISSION_SUPPLY_VISIBLE,
pricing: "edit",
+ externalIntake: "edit",
},
},
{
@@ -516,6 +533,7 @@ export const PRODUCT_DEVELOPMENT_PERMISSION_TARGETS: IntakeNodePermissionTarget[
defaults: {
...NODE_PERMISSION_SUPPLY_VISIBLE,
pricing: "edit",
+ externalIntake: "edit",
},
},
{
@@ -558,7 +576,10 @@ export const FORMAL_WORKSPACE_PERMISSION_TARGETS: IntakeNodePermissionTarget[] =
{
key: "productDevelopmentList",
label: "产品开发明细",
- defaults: NODE_PERMISSION_FORMAL_PRODUCTS_DEFAULT,
+ defaults: {
+ ...NODE_PERMISSION_FORMAL_PRODUCTS_DEFAULT,
+ externalIntake: "edit",
+ },
},
{
key: "seriesDevelopmentList",
@@ -807,6 +828,10 @@ export function sanitizeIntakeNodePermissionOverrides(
export function resolveIntakeNodePermissionSubjectKeys(
user: IntakeNonIntegratedPermissionUser,
) {
+ if (isExternalIntakeUser(user)) {
+ return ["externalIntake"] satisfies IntakeNodePermissionSubjectKey[];
+ }
+
const keys: IntakeNodePermissionSubjectKey[] = [];
const isBrandPlanningUser = hasDepartmentPath(user, isBrandPlanningDepartmentPath);
const isSpaceVisualUser = hasDepartmentPath(user, isSpaceVisualDepartmentPath);
diff --git a/furniture-3d-system/src/lib/non-integrated-external-entry-permissions.test.ts b/furniture-3d-system/src/lib/non-integrated-external-entry-permissions.test.ts
new file mode 100644
index 00000000..9c6f0199
--- /dev/null
+++ b/furniture-3d-system/src/lib/non-integrated-external-entry-permissions.test.ts
@@ -0,0 +1,67 @@
+import {
+ EXTERNAL_INTAKE_BUSINESS_ROLE,
+ canExternalIntakeAccessWorkspace,
+ canExternalIntakeCreateRecord,
+ isExternalIntakeRecordOwner,
+ isExternalIntakeUser,
+ isExternalIntakeWorkspaceCategory,
+} from './non-integrated-external-entry-permissions'
+
+describe('external intake entry permissions', () => {
+ it('recognizes external intake users by business role', () => {
+ expect(
+ isExternalIntakeUser({
+ id: 'external-user-id',
+ businessRoles: [EXTERNAL_INTAKE_BUSINESS_ROLE],
+ }),
+ ).toBe(true)
+ expect(
+ isExternalIntakeUser({
+ id: 'designer-user-id',
+ businessRoles: ['DESIGN'],
+ }),
+ ).toBe(false)
+ })
+
+ it('allows external intake users to create records', () => {
+ expect(
+ canExternalIntakeCreateRecord({
+ id: 'external-user-id',
+ businessRoles: [EXTERNAL_INTAKE_BUSINESS_ROLE],
+ }),
+ ).toBe(true)
+ })
+
+ it('matches only records created by the external intake user', () => {
+ const user = {
+ id: 'external-user-id',
+ businessRoles: [EXTERNAL_INTAKE_BUSINESS_ROLE],
+ }
+
+ expect(isExternalIntakeRecordOwner({ createdBy: 'external-user-id' }, user)).toBe(true)
+ expect(isExternalIntakeRecordOwner({ createdBy: 'other-user-id' }, user)).toBe(false)
+ expect(isExternalIntakeRecordOwner({ createdBy: '' }, user)).toBe(false)
+ })
+
+ it('limits external intake users to product data and system-entry workspaces', () => {
+ const user = {
+ id: 'external-user-id',
+ businessRoles: [EXTERNAL_INTAKE_BUSINESS_ROLE],
+ }
+ const ownRecord = { createdBy: 'external-user-id' }
+
+ expect(isExternalIntakeWorkspaceCategory('开品参考')).toBe(true)
+ expect(isExternalIntakeWorkspaceCategory('基础信息')).toBe(true)
+ expect(isExternalIntakeWorkspaceCategory('产品定厂')).toBe(true)
+ expect(isExternalIntakeWorkspaceCategory('样包测量')).toBe(true)
+ expect(isExternalIntakeWorkspaceCategory('产品核价')).toBe(true)
+ expect(isExternalIntakeWorkspaceCategory('产品资料')).toBe(false)
+ expect(isExternalIntakeWorkspaceCategory('首批质检')).toBe(false)
+
+ expect(canExternalIntakeAccessWorkspace(ownRecord, user, '产品核价')).toBe(true)
+ expect(canExternalIntakeAccessWorkspace(ownRecord, user, '产品限价')).toBe(false)
+ expect(
+ canExternalIntakeAccessWorkspace({ createdBy: 'other-user-id' }, user, '产品核价'),
+ ).toBe(false)
+ })
+})
diff --git a/furniture-3d-system/src/lib/non-integrated-external-entry-permissions.ts b/furniture-3d-system/src/lib/non-integrated-external-entry-permissions.ts
new file mode 100644
index 00000000..0a5961db
--- /dev/null
+++ b/furniture-3d-system/src/lib/non-integrated-external-entry-permissions.ts
@@ -0,0 +1,62 @@
+export const EXTERNAL_INTAKE_BUSINESS_ROLE = 'EXTERNAL_INTAKE'
+
+export type ExternalIntakePermissionUser = {
+ id?: string | null
+ businessRoles?: readonly string[] | null
+}
+
+export type ExternalIntakePermissionRecord = {
+ createdBy?: string | null
+}
+
+export const EXTERNAL_INTAKE_WORKSPACE_CATEGORIES = [
+ '开品参考',
+ '基础信息',
+ '产品定厂',
+ '样包测量',
+ '产品核价',
+] as const
+
+const EXTERNAL_INTAKE_WORKSPACE_CATEGORY_SET = new Set(
+ EXTERNAL_INTAKE_WORKSPACE_CATEGORIES,
+)
+
+function normalizeText(value: unknown) {
+ return typeof value === 'string' ? value.trim() : ''
+}
+
+export function isExternalIntakeUser(user: ExternalIntakePermissionUser | null | undefined) {
+ return Boolean(
+ user?.businessRoles?.some((role) => role === EXTERNAL_INTAKE_BUSINESS_ROLE),
+ )
+}
+
+export function isExternalIntakeRecordOwner(
+ record: ExternalIntakePermissionRecord,
+ user: ExternalIntakePermissionUser,
+) {
+ if (!isExternalIntakeUser(user)) return false
+
+ const userId = normalizeText(user.id)
+ const createdBy = normalizeText(record.createdBy)
+ return Boolean(userId && createdBy === userId)
+}
+
+export function canExternalIntakeCreateRecord(user: ExternalIntakePermissionUser) {
+ return isExternalIntakeUser(user)
+}
+
+export function isExternalIntakeWorkspaceCategory(category: unknown) {
+ return EXTERNAL_INTAKE_WORKSPACE_CATEGORY_SET.has(normalizeText(category))
+}
+
+export function canExternalIntakeAccessWorkspace(
+ record: ExternalIntakePermissionRecord,
+ user: ExternalIntakePermissionUser,
+ category: unknown,
+) {
+ return (
+ isExternalIntakeRecordOwner(record, user) &&
+ isExternalIntakeWorkspaceCategory(category)
+ )
+}
diff --git a/furniture-3d-system/src/lib/non-integrated-permissions.test.ts b/furniture-3d-system/src/lib/non-integrated-permissions.test.ts
index b8aac417..2ac9af45 100644
--- a/furniture-3d-system/src/lib/non-integrated-permissions.test.ts
+++ b/furniture-3d-system/src/lib/non-integrated-permissions.test.ts
@@ -2,8 +2,12 @@ import {
canCreateNonIntegratedProductFromIntake,
canDownloadNonIntegratedWorkspace,
canEditAnyNonIntegratedWorkspace,
+ canEditNonIntegratedProductIdentity,
canEditNonIntegratedWorkspace,
+ canPushNonIntegratedProductPricingToJushuitan,
+ canUploadNonIntegratedDetachedAsset,
} from './non-integrated-permissions'
+import { NON_INTEGRATED_BASIC_INFO_WORKSPACE_CATEGORY } from '@/app/intake/non-integrated/nonIntegratedStorage'
const baseProduct = {
createdBy: 'creator-id',
@@ -53,6 +57,68 @@ describe('non-integrated intake permissions', () => {
expect(canEditNonIntegratedWorkspace(baseProduct, user, '产品核价')).toBe(false)
})
+ it('lets external intake users maintain and push only their own records', () => {
+ const externalUser = {
+ id: 'creator-id',
+ name: 'ExternalUser',
+ role: 'USER',
+ businessRoles: ['EXTERNAL_INTAKE'],
+ departmentPaths: [],
+ }
+ const otherProduct = {
+ ...baseProduct,
+ createdBy: 'other-creator-id',
+ }
+
+ expect(canCreateNonIntegratedProductFromIntake(externalUser)).toBe(true)
+ expect(canEditAnyNonIntegratedWorkspace(baseProduct, externalUser)).toBe(true)
+ expect(
+ canEditNonIntegratedWorkspace(
+ baseProduct,
+ externalUser,
+ NON_INTEGRATED_BASIC_INFO_WORKSPACE_CATEGORY,
+ ),
+ ).toBe(true)
+ expect(canEditNonIntegratedWorkspace(baseProduct, externalUser, '开品参考')).toBe(true)
+ expect(canEditNonIntegratedWorkspace(baseProduct, externalUser, '产品定厂')).toBe(true)
+ expect(canEditNonIntegratedWorkspace(baseProduct, externalUser, '样包测量')).toBe(true)
+ expect(canEditNonIntegratedWorkspace(baseProduct, externalUser, '产品核价')).toBe(true)
+ expect(canEditNonIntegratedWorkspace(baseProduct, externalUser, '产品资料')).toBe(false)
+ expect(canEditNonIntegratedWorkspace(baseProduct, externalUser, '设计卖点')).toBe(false)
+ expect(canEditNonIntegratedWorkspace(baseProduct, externalUser, '产品限价')).toBe(false)
+ expect(canEditNonIntegratedWorkspace(baseProduct, externalUser, '首批质检')).toBe(false)
+ expect(canEditNonIntegratedProductIdentity(baseProduct, externalUser)).toBe(true)
+ expect(canUploadNonIntegratedDetachedAsset(baseProduct, externalUser)).toBe(true)
+ expect(
+ canDownloadNonIntegratedWorkspace(
+ baseProduct,
+ externalUser,
+ NON_INTEGRATED_BASIC_INFO_WORKSPACE_CATEGORY,
+ ),
+ ).toBe(true)
+ expect(canDownloadNonIntegratedWorkspace(baseProduct, externalUser, '产品资料')).toBe(false)
+ expect(canPushNonIntegratedProductPricingToJushuitan(baseProduct, externalUser)).toBe(true)
+
+ expect(canEditAnyNonIntegratedWorkspace(otherProduct, externalUser)).toBe(false)
+ expect(
+ canEditNonIntegratedWorkspace(
+ otherProduct,
+ externalUser,
+ NON_INTEGRATED_BASIC_INFO_WORKSPACE_CATEGORY,
+ ),
+ ).toBe(false)
+ expect(canEditNonIntegratedProductIdentity(otherProduct, externalUser)).toBe(false)
+ expect(canUploadNonIntegratedDetachedAsset(otherProduct, externalUser)).toBe(false)
+ expect(
+ canDownloadNonIntegratedWorkspace(
+ otherProduct,
+ externalUser,
+ NON_INTEGRATED_BASIC_INFO_WORKSPACE_CATEGORY,
+ ),
+ ).toBe(false)
+ expect(canPushNonIntegratedProductPricingToJushuitan(otherProduct, externalUser)).toBe(false)
+ })
+
it('does not let unrelated designers download another designer product', () => {
const user = {
id: 'designer-other-id',
diff --git a/furniture-3d-system/src/lib/non-integrated-permissions.ts b/furniture-3d-system/src/lib/non-integrated-permissions.ts
index 3fcaeaeb..07e32116 100644
--- a/furniture-3d-system/src/lib/non-integrated-permissions.ts
+++ b/furniture-3d-system/src/lib/non-integrated-permissions.ts
@@ -4,6 +4,12 @@ import {
type NonIntegratedProductRecord,
type NonIntegratedWorkflowNodeCategory,
} from '@/app/intake/non-integrated/nonIntegratedStorage'
+import {
+ canExternalIntakeAccessWorkspace,
+ canExternalIntakeCreateRecord,
+ isExternalIntakeRecordOwner,
+ isExternalIntakeUser,
+} from './non-integrated-external-entry-permissions'
export type IntakeNonIntegratedPermissionUser = {
id?: string | null
@@ -312,7 +318,8 @@ export function canCreateNonIntegratedProductFromIntake(
) {
return (
isIntakeNonIntegratedFullEditor(user) ||
- isIntakeNonIntegratedDesignDepartmentUser(user)
+ isIntakeNonIntegratedDesignDepartmentUser(user) ||
+ canExternalIntakeCreateRecord(user)
)
}
@@ -462,6 +469,9 @@ export function canEditNonIntegratedWorkspace(
const normalizedCategory = normalizeWorkspaceCategory(category)
if (!normalizedCategory) return false
if (isIntakeNonIntegratedFullEditor(user)) return true
+ if (isExternalIntakeUser(user)) {
+ return canExternalIntakeAccessWorkspace(product, user, normalizedCategory)
+ }
if (OPEN_LINK_EDIT_WORKSPACE_CATEGORIES.has(normalizedCategory)) return true
if (
@@ -533,6 +543,9 @@ export function canDownloadNonIntegratedWorkspace(
const normalizedCategory = normalizeWorkspaceCategory(category)
if (!normalizedCategory) return false
if (isIntakeNonIntegratedFullEditor(user)) return true
+ if (isExternalIntakeUser(user)) {
+ return canExternalIntakeAccessWorkspace(product, user, normalizedCategory)
+ }
if (
PRICING_GROUP_WORKSPACE_CATEGORIES.has(normalizedCategory) &&
@@ -563,6 +576,7 @@ export function canEditAnyNonIntegratedWorkspace(
user: IntakeNonIntegratedPermissionUser,
) {
if (isIntakeNonIntegratedFullEditor(user)) return true
+ if (isExternalIntakeRecordOwner(product, user)) return true
return Array.from(EDITABLE_WORKSPACE_CATEGORIES).some((category) =>
!OPEN_LINK_EDIT_WORKSPACE_CATEGORIES.has(category) &&
@@ -576,10 +590,22 @@ export function canEditNonIntegratedProductIdentity(
) {
return (
isIntakeNonIntegratedFullEditor(user) ||
+ isExternalIntakeRecordOwner(product, user) ||
isCurrentUserNonIntegratedProductDesigner(product, user)
)
}
+export function canPushNonIntegratedProductPricingToJushuitan(
+ product: IntakeNonIntegratedPermissionProduct,
+ user: IntakeNonIntegratedPermissionUser,
+) {
+ return (
+ isIntakeNonIntegratedDeleteAdmin(user) ||
+ isIntakeNonIntegratedPricingGroupUser(user) ||
+ isExternalIntakeRecordOwner(product, user)
+ )
+}
+
export function canDeleteNonIntegratedProductByIntakePermission(
user: IntakeNonIntegratedPermissionUser,
) {
diff --git a/furniture-3d-system/src/lib/role-scope.test.ts b/furniture-3d-system/src/lib/role-scope.test.ts
index 3a3cfbea..cfa4efe7 100644
--- a/furniture-3d-system/src/lib/role-scope.test.ts
+++ b/furniture-3d-system/src/lib/role-scope.test.ts
@@ -23,6 +23,17 @@ describe('role-scope business roles', () => {
expect(isReadOnlyUser({ role: 'USER', businessRoles: [] })).toBe(true)
})
+ it('keeps external intake users out of internal content and design workspaces', () => {
+ expect(canCreateContent({ role: 'USER', businessRoles: ['EXTERNAL_INTAKE'] })).toBe(false)
+ expect(
+ canOperateDesignWorkspace({
+ role: 'USER',
+ businessRoles: ['EXTERNAL_INTAKE'],
+ departmentPaths: ['internal company > product research department'],
+ }),
+ ).toBe(false)
+ })
+
it('设计业务类型取代旧设计师权限角色', () => {
expect(hasBusinessRole({ role: 'USER', businessRoles: ['DESIGN'] }, 'DESIGN')).toBe(true)
})
diff --git a/furniture-3d-system/src/lib/role-scope.ts b/furniture-3d-system/src/lib/role-scope.ts
index 3d1ab66a..eeb3ed94 100644
--- a/furniture-3d-system/src/lib/role-scope.ts
+++ b/furniture-3d-system/src/lib/role-scope.ts
@@ -1,5 +1,6 @@
import type { UserRole } from '@/server/core/types'
import type { BusinessAccountType } from '@prisma/client'
+import { EXTERNAL_INTAKE_BUSINESS_ROLE } from './non-integrated-external-entry-permissions'
export type AllowedRole = UserRole | 'SUPERVISOR_DEPARTMENT' | 'SUPERVISOR_TEAM'
@@ -117,6 +118,10 @@ export function isProductResearchDesignUser(user: ScopedRoleUser): boolean {
}
export function canOperateDesignWorkspace(user: ScopedRoleUser): boolean {
+ if (!isAdmin(user) && hasBusinessRoleValue(user, EXTERNAL_INTAKE_BUSINESS_ROLE)) {
+ return false
+ }
+
return isAdmin(user) || isProductResearchDesignUser(user)
}
@@ -125,10 +130,18 @@ export function isReadOnlyUser(user: ScopedRoleUser): boolean {
}
export function canCreateContent(user: ScopedRoleUser): boolean {
+ if (hasBusinessRoleValue(user, EXTERNAL_INTAKE_BUSINESS_ROLE)) {
+ return false
+ }
+
return hasInternalContentBusinessRole(user)
}
export function hasBusinessRole(user: ScopedRoleUser, role: BusinessAccountType): boolean {
+ return hasBusinessRoleValue(user, role)
+}
+
+function hasBusinessRoleValue(user: ScopedRoleUser, role: string): boolean {
return Boolean(user.businessRoles?.includes(role))
}
diff --git a/furniture-3d-system/src/server/services/__tests__/intake-non-integrated-product.service.test.ts b/furniture-3d-system/src/server/services/__tests__/intake-non-integrated-product.service.test.ts
index 4d54f299..c6e0d77d 100644
--- a/furniture-3d-system/src/server/services/__tests__/intake-non-integrated-product.service.test.ts
+++ b/furniture-3d-system/src/server/services/__tests__/intake-non-integrated-product.service.test.ts
@@ -369,6 +369,49 @@ describe('intakeNonIntegratedProductService', () => {
expect(mockedRepository.listActive).toHaveBeenCalledWith(undefined)
})
+ it('limits external intake list pages to records created by the current user', async () => {
+ mockedRepository.listActivePage.mockResolvedValue({
+ records: [],
+ total: 0,
+ } as never)
+
+ await intakeNonIntegratedProductService.listPage(
+ {
+ ...baseUser,
+ id: 'external-user-id',
+ email: 'external@example.com',
+ name: 'ExternalUser',
+ businessRoles: ['EXTERNAL_INTAKE'] as const,
+ },
+ { quickScope: 'created' },
+ )
+
+ expect(mockedRepository.listActivePage).toHaveBeenCalledWith(
+ expect.objectContaining({
+ scopeWhere: { createdBy: 'external-user-id' },
+ filterWhere: { AND: [{ createdBy: 'external-user-id' }] },
+ }),
+ )
+ })
+
+ it('does not let external intake users read another user record by id', async () => {
+ mockedRepository.findActiveById.mockResolvedValue(
+ createRecord({
+ createdBy: 'other-user-id',
+ }) as never,
+ )
+
+ await expect(
+ intakeNonIntegratedProductService.getById('non-integrated-1', {
+ ...baseUser,
+ id: 'external-user-id',
+ email: 'external@example.com',
+ name: 'ExternalUser',
+ businessRoles: ['EXTERNAL_INTAKE'] as const,
+ }),
+ ).resolves.toBeNull()
+ })
+
it('读取列表时用结构化 profile 和 SKU 明细覆盖 payload 主数据', async () => {
mockedRepository.listActive.mockResolvedValue([
createRecord({
@@ -3996,6 +4039,63 @@ describe('intakeNonIntegratedProductService', () => {
})
})
+ it('does not restore another user soft-deleted record for external intake users', async () => {
+ const payload = createPayload({
+ id: 'external-new-record',
+ spu: 'DNZ0999',
+ createdAt: '2026-04-22T10:00:00.000Z',
+ updatedAt: '2026-04-22T10:00:00.000Z',
+ })
+ const externalUser = {
+ ...baseUser,
+ id: 'external-user-id',
+ email: 'external@example.com',
+ name: 'ExternalUser',
+ businessRoles: ['EXTERNAL_INTAKE'] as const,
+ }
+
+ mockedRepository.findBySpu.mockResolvedValue(null as never)
+ mockedRepository.findDeletedBySpu.mockResolvedValue(
+ createRecord({
+ id: 'deleted-other-record',
+ createdBy: 'other-user-id',
+ deletedAt: new Date('2026-04-21T10:00:00.000Z'),
+ payload: createPayload({
+ id: 'deleted-other-record',
+ spu: 'DNZ0999',
+ }),
+ }) as never,
+ )
+ mockedRepository.findById.mockResolvedValue(null as never)
+ mockedRepository.create.mockResolvedValue(
+ createRecord({
+ id: 'external-new-record',
+ createdBy: 'external-user-id',
+ payload,
+ }) as never,
+ )
+ mockedRepository.findActiveById.mockResolvedValue(
+ createRecord({
+ id: 'external-new-record',
+ createdBy: 'external-user-id',
+ payload,
+ }) as never,
+ )
+
+ await intakeNonIntegratedProductService.save(payload, externalUser)
+
+ expect(mockedRepository.findDeletedBySpu).toHaveBeenCalledWith(
+ 'DNZ0999',
+ 'external-new-record',
+ 'Base',
+ )
+ expect(mockedRepository.create).toHaveBeenCalled()
+ expect(mockedRepository.update).not.toHaveBeenCalledWith(
+ 'deleted-other-record',
+ expect.anything(),
+ )
+ })
+
it('reuses a soft-deleted record when recreating the same full SPU', async () => {
const payload = createPayload({
id: 'non-integrated-recreate',
diff --git a/furniture-3d-system/src/server/services/__tests__/intake-non-integrated-scope.service.test.ts b/furniture-3d-system/src/server/services/__tests__/intake-non-integrated-scope.service.test.ts
index 201fe64b..2dcafc34 100644
--- a/furniture-3d-system/src/server/services/__tests__/intake-non-integrated-scope.service.test.ts
+++ b/furniture-3d-system/src/server/services/__tests__/intake-non-integrated-scope.service.test.ts
@@ -1,6 +1,7 @@
import { intakeNodePermissionSettingsService } from '../intake-node-permission-settings.service'
import { IntakeNonIntegratedScopeService } from '../intake-non-integrated-scope.service'
import type { AuthUser } from '@/server/core/types'
+import { NON_INTEGRATED_BASIC_INFO_WORKSPACE_CATEGORY } from '@/app/intake/non-integrated/nonIntegratedStorage'
jest.mock('../intake-node-permission-settings.service', () => ({
intakeNodePermissionSettingsService: {
@@ -62,6 +63,59 @@ describe('IntakeNonIntegratedScopeService node permissions', () => {
expect(service.canUploadDetachedAsset(record, baseUser)).toBe(false)
})
+ it('limits external intake users to records they created', async () => {
+ const externalUser: AuthUser = {
+ id: 'external-user-id',
+ email: 'external@example.local',
+ name: 'ExternalUser',
+ role: 'USER',
+ status: 'ACTIVE',
+ businessRoles: ['EXTERNAL_INTAKE'],
+ departmentPaths: [
+ 'internal company > product research department',
+ ],
+ }
+ const ownRecord = {
+ id: 'own-record',
+ createdBy: 'external-user-id',
+ designer: '',
+ operator: '',
+ purchaser: '',
+ visibility: 'DEPARTMENT',
+ departmentId: null,
+ teamId: null,
+ payload: {},
+ profile: null,
+ }
+ const otherRecord = {
+ ...ownRecord,
+ id: 'other-record',
+ createdBy: 'other-user-id',
+ }
+
+ expect(service.buildReadScopeWhere(externalUser)).toEqual({
+ createdBy: 'external-user-id',
+ })
+ expect(service.canReadRecord(ownRecord, externalUser)).toBe(true)
+ expect(service.canReadRecord(otherRecord, externalUser)).toBe(false)
+ await expect(
+ service.canWriteWorkspaceRecordWithSettings(
+ ownRecord,
+ externalUser,
+ NON_INTEGRATED_BASIC_INFO_WORKSPACE_CATEGORY,
+ ),
+ ).resolves.toBe(true)
+ await expect(
+ service.canWriteWorkspaceRecordWithSettings(
+ otherRecord,
+ externalUser,
+ NON_INTEGRATED_BASIC_INFO_WORKSPACE_CATEGORY,
+ ),
+ ).resolves.toBe(false)
+ expect(() => service.assertCanPushProductPricing(ownRecord, externalUser)).not.toThrow()
+ expect(() => service.assertCanPushProductPricing(otherRecord, externalUser)).toThrow()
+ })
+
it('uses stored payload owner fields when structured profile data is missing', async () => {
const record = {
id: 'payload-backed-owner-record',
diff --git a/furniture-3d-system/src/server/services/intake-non-integrated-product.service.ts b/furniture-3d-system/src/server/services/intake-non-integrated-product.service.ts
index 6e3223d1..82f5cdac 100644
--- a/furniture-3d-system/src/server/services/intake-non-integrated-product.service.ts
+++ b/furniture-3d-system/src/server/services/intake-non-integrated-product.service.ts
@@ -26,6 +26,7 @@ import {
buildAssetVariantBarcode,
DEFAULT_SKU_VERSION_NO,
} from '@/lib/open-product/asset-coding'
+import { isExternalIntakeRecordOwner } from '@/lib/non-integrated-external-entry-permissions'
import { prisma } from '@/lib/prisma'
import { AppError, ErrorCode } from '@/server/core/errors'
import type { AuthUser } from '@/server/core/types'
@@ -2961,6 +2962,18 @@ function isListAdminUser(user: AuthUser) {
return user.role === 'SUPER_ADMIN' || user.role === 'ADMIN'
}
+function filterRestorableDeletedRecordForUser(
+ record: T | null,
+ user: AuthUser,
+) {
+ if (!record) return null
+ if (!intakeNonIntegratedScopeService.isExternalIntakeScopeUser(user)) {
+ return record
+ }
+
+ return isExternalIntakeRecordOwner(record, user) ? record : null
+}
+
function getListDepartmentText(user: AuthUser) {
return [
user.department?.name,
@@ -3073,6 +3086,13 @@ function buildQuickScopeWhere(
quickScope: IntakeNonIntegratedProductListParams['quickScope'],
user: AuthUser,
): Prisma.IntakeNonIntegratedProductWhereInput | null {
+ if (
+ (quickScope === 'created' || quickScope === 'pending') &&
+ intakeNonIntegratedScopeService.isExternalIntakeScopeUser(user)
+ ) {
+ return { createdBy: user.id }
+ }
+
if (quickScope === 'created') {
return buildResponsibleOwnerSearchWhere(user)
}
@@ -4966,6 +4986,19 @@ class IntakeNonIntegratedProductService {
)
}
+ async assertCanPushProductPricing(id: string, user: AuthUser) {
+ const existingRecord = await this.repository.findActiveById(id)
+ if (!existingRecord) {
+ throw new AppError(ErrorCode.PRODUCT_NOT_FOUND, '非集成产品不存在')
+ }
+
+ if (!intakeNonIntegratedScopeService.canReadRecord(existingRecord, user)) {
+ throw new AppError(ErrorCode.PRODUCT_NOT_FOUND, '非集成产品不存在')
+ }
+
+ intakeNonIntegratedScopeService.assertCanPushProductPricing(existingRecord, user)
+ }
+
assertCanCreate(user: AuthUser) {
if (intakeNonIntegratedScopeService.canCreateRecord(user)) {
return
@@ -5070,6 +5103,15 @@ class IntakeNonIntegratedProductService {
existingRecord && !existingRecord.deletedAt
? (existingRecord as IntakeNonIntegratedRecord)
: null
+ if (
+ existingRecord &&
+ !intakeNonIntegratedScopeService.canReadRecord(
+ existingRecord as IntakeNonIntegratedRecord,
+ user,
+ )
+ ) {
+ throw new AppError(ErrorCode.PRODUCT_NOT_FOUND, '非集成产品不存在')
+ }
const reservation = await spuNumberReservationService.reserveInTransaction(tx, {
productCategory,
currentProductId: readText(productInput.id) || null,
@@ -5093,7 +5135,7 @@ class IntakeNonIntegratedProductService {
})
const normalizedSpu = resolveNonIntegratedPersistedSpu(normalizedProduct)
- const deletedRecordToRestore = activeExistingRecord
+ const deletedRecordToRestoreCandidate = activeExistingRecord
? null
: await this.repository.findDeletedBySpuInTransaction(
tx,
@@ -5101,6 +5143,10 @@ class IntakeNonIntegratedProductService {
normalizedProduct.id,
normalizedProduct.brand,
)
+ const deletedRecordToRestore = filterRestorableDeletedRecordForUser(
+ deletedRecordToRestoreCandidate,
+ user,
+ )
if (
!activeExistingRecord &&
@@ -5364,6 +5410,12 @@ class IntakeNonIntegratedProductService {
const existingRecordCandidate = isRecord(productInput) && typeof productInput.id === 'string'
? await this.repository.findById(productInput.id)
: null
+ if (
+ existingRecordCandidate &&
+ !intakeNonIntegratedScopeService.canReadRecord(existingRecordCandidate, user)
+ ) {
+ throw new AppError(ErrorCode.PRODUCT_NOT_FOUND, '非集成产品不存在')
+ }
const normalizedProduct = buildPersistedProductPayload(
productInput,
@@ -5376,13 +5428,17 @@ class IntakeNonIntegratedProductService {
const normalizedSpu = resolveNonIntegratedPersistedSpu(normalizedProduct)
- const deletedRecordToRestore = existingRecordCandidate
+ const deletedRecordToRestoreCandidate = existingRecordCandidate
? null
: await this.repository.findDeletedBySpu(
normalizedSpu,
normalizedProduct.id,
normalizedProduct.brand,
)
+ const deletedRecordToRestore = filterRestorableDeletedRecordForUser(
+ deletedRecordToRestoreCandidate,
+ user,
+ )
const existingRecord = existingRecordCandidate
if (!existingRecord && !intakeNonIntegratedScopeService.canCreateRecord(user)) {
diff --git a/furniture-3d-system/src/server/services/intake-non-integrated-scope.service.ts b/furniture-3d-system/src/server/services/intake-non-integrated-scope.service.ts
index 89c1af36..f8bc3c66 100644
--- a/furniture-3d-system/src/server/services/intake-non-integrated-scope.service.ts
+++ b/furniture-3d-system/src/server/services/intake-non-integrated-scope.service.ts
@@ -8,9 +8,14 @@ import {
canEditAnyNonIntegratedWorkspace,
canEditNonIntegratedWorkspace,
canDownloadNonIntegratedWorkspace,
+ canPushNonIntegratedProductPricingToJushuitan,
isIntakeNonIntegratedFullEditor,
type IntakeNonIntegratedPermissionProduct,
} from '@/lib/non-integrated-permissions'
+import {
+ isExternalIntakeRecordOwner,
+ isExternalIntakeUser,
+} from '@/lib/non-integrated-external-entry-permissions'
import {
getIntakeNodePermissionTargetKeyByWorkspaceCategory,
resolveIntakeNodePermissionAccess,
@@ -64,6 +69,10 @@ class IntakeNonIntegratedScopeService {
return ADMIN_ROLES.has(user.role)
}
+ isExternalIntakeScopeUser(user: AuthUser): boolean {
+ return !this.isAdmin(user) && isExternalIntakeUser(user)
+ }
+
getScopedDepartmentId(user: AuthUser): string | undefined {
return user.managedDepartmentId ?? user.departmentId
}
@@ -73,6 +82,10 @@ class IntakeNonIntegratedScopeService {
}
buildReadScopeWhere(user: AuthUser): Prisma.IntakeNonIntegratedProductWhereInput | undefined {
+ if (this.isExternalIntakeScopeUser(user)) {
+ return { createdBy: user.id }
+ }
+
if (!this.isScopeEnabled()) {
return undefined
}
@@ -108,6 +121,10 @@ class IntakeNonIntegratedScopeService {
}
canReadRecord(record: IntakeNonIntegratedPermissionTarget, user: AuthUser): boolean {
+ if (this.isExternalIntakeScopeUser(user)) {
+ return isExternalIntakeRecordOwner(record, user)
+ }
+
if (!this.isScopeEnabled()) return true
if (this.isAdmin(user)) return true
if (record.createdBy === user.id) return true
@@ -184,6 +201,13 @@ class IntakeNonIntegratedScopeService {
return canUploadNonIntegratedDetachedAsset(this.buildPermissionProduct(record), user)
}
+ canPushProductPricing(record: IntakeNonIntegratedPermissionTarget, user: AuthUser): boolean {
+ return canPushNonIntegratedProductPricingToJushuitan(
+ this.buildPermissionProduct(record),
+ user,
+ )
+ }
+
canWriteWorkspaceRecord(
record: IntakeNonIntegratedPermissionTarget,
user: AuthUser,
@@ -317,6 +341,17 @@ class IntakeNonIntegratedScopeService {
throw new AppError(ErrorCode.AUTH_PERMISSION_DENIED, '没有权限下载这条非集成产品资料')
}
+ assertCanPushProductPricing(
+ record: IntakeNonIntegratedPermissionTarget,
+ user: AuthUser,
+ ): void {
+ if (this.canPushProductPricing(record, user)) {
+ return
+ }
+
+ throw new AppError(ErrorCode.AUTH_PERMISSION_DENIED, '没有权限推送这条非集成产品核价')
+ }
+
assertCanDeleteRecord(record: IntakeNonIntegratedPermissionTarget, user: AuthUser): void {
if (canDeleteNonIntegratedProductByIntakePermission(user)) {
return