ASSET1 统一电子资产与固定资产台账

这个提交包含在:
2026-05-22 22:04:41 +08:00
父节点 c2a3c7a84b
当前提交 9090046a97
共修改 18 个文件,包含 1457 行新增和 199 行删除
@@ -207,4 +207,45 @@ describe("documentSequence service", () => {
expect(config.documentCategoryCode).toBe("ASSET_CARD_DEFAULT");
expect(config.prefix).toBe("ZC");
});
it("can issue asset numbers with independent sequence keys per asset number family", async () => {
const prisma = buildPrisma({
sequenceMap: {
"asset_card:ASSET_EA_DOC": {
document_type: "asset_card:ASSET_EA_DOC",
prefix: "EA-DOC",
date_format: "YYYY",
separator: "-",
number_length: 6,
reset_period: "yearly",
current_number: 8,
last_reset_date: null,
updated_at: new Date("2026-03-07T00:00:00.000Z"),
updated_by: "tester",
},
},
categoryMap: {
ASSET_EA_DOC: {
code: "ASSET_EA_DOC",
entity_type: "asset_card",
sequence_prefix: "EA-DOC",
number_length: 6,
reset_period: "yearly",
enabled: true,
archived_at: null,
},
},
});
const issued = await issueDocumentNumber(prisma, {
documentType: "asset_card",
documentCategoryCode: "ASSET_EA_DOC",
sequenceKey: "asset_card:ASSET_EA_DOC",
actorId: "tester",
timeZone: "Asia/Shanghai",
});
expect(issued.sequence.documentType).toBe("asset_card:ASSET_EA_DOC");
expect(issued.documentNumber).toMatch(/^EA-DOC-\d{4}-000009$/);
});
});
+132 -121
查看文件
@@ -5,9 +5,18 @@ const assetsRoutes = require('../../../src/routes/assets');
function buildAssetRecord({
id = 'asset-1',
assetNo = 'ZC-001',
assetNo = 'FA-2026-000001',
serialNo = 'SN-001',
assetType = 'fixed_asset',
sensitivityLevel = 'internal',
assigneeEmployeeId = 'employee-1',
ownerEmployeeId = null,
keeperEmployeeId = null,
archiveLocation = null,
externalRef = null,
sourceHash = null,
confidentialityNote = null,
metadata = null,
name = '测试笔记本',
} = {}) {
const now = new Date('2026-03-11T00:00:00.000Z');
@@ -15,6 +24,8 @@ function buildAssetRecord({
id,
asset_no: assetNo,
name,
asset_type: assetType,
sensitivity_level: sensitivityLevel,
category_id: 'cat-1',
legal_entity_id: 'legal-1',
cost_center_id: 'cc-1',
@@ -23,6 +34,16 @@ function buildAssetRecord({
location: '深圳办公室',
status: 'in_use',
assignee_employee_id: assigneeEmployeeId,
owner_employee_id: ownerEmployeeId,
keeper_employee_id: keeperEmployeeId,
archive_location: archiveLocation,
external_ref: externalRef,
source_hash: sourceHash,
verification_no: null,
confidentiality_note: confidentialityNote,
metadata,
last_verified_at: null,
expires_at: null,
activated_at: now,
retired_at: null,
created_at: now,
@@ -46,6 +67,8 @@ function buildAssetRecord({
name: '张三',
department: null,
},
owner: null,
keeper: null,
};
}
@@ -109,25 +132,25 @@ async function buildApp({ employee, prisma }) {
}
describe('assets search route', () => {
it('prefers exact assetNo matching before serial or fuzzy lookup', async () => {
it('uses one scoped OR query for assetNo, serialNo, VER/hash/ref/location and name lookup', async () => {
const prisma = buildPrisma({
items: [buildAssetRecord()],
});
const app = await buildApp({
employee: { id: 'manager-1', permissionCodes: ['asset.view_all'] },
employee: { id: 'admin-1', permissionCodes: ['asset.manage'] },
prisma,
});
const response = await app.inject({
method: 'GET',
url: '/api/assets?q=ZC-001',
url: '/api/assets?q=FA-2026-000001',
headers: { authorization: 'Bearer token' },
});
expect(response.statusCode).toBe(200);
expect(response.json().data.items[0]).toEqual(
expect.objectContaining({
assetNo: 'ZC-001',
assetNo: 'FA-2026-000001',
serialNo: 'SN-001',
}),
);
@@ -135,100 +158,18 @@ describe('assets search route', () => {
expect(prisma.assetCard.findMany).toHaveBeenCalledWith(
expect.objectContaining({
where: expect.objectContaining({
asset_no: 'ZC-001',
}),
}),
);
await app.close();
});
it('falls back to serialNo exact lookup when assetNo misses', async () => {
const prisma = {
assetCard: {
findMany: vi
.fn()
.mockResolvedValueOnce([])
.mockResolvedValueOnce([buildAssetRecord({ id: 'asset-2', assetNo: 'ZC-003', serialNo: 'SN-003' })]),
},
};
const app = await buildApp({
employee: { id: 'manager-1', permissionCodes: ['asset.view_all'] },
prisma,
});
const response = await app.inject({
method: 'GET',
url: '/api/assets?q=SN-003',
headers: { authorization: 'Bearer token' },
});
expect(response.statusCode).toBe(200);
expect(prisma.assetCard.findMany).toHaveBeenCalledTimes(2);
expect(prisma.assetCard.findMany).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
where: expect.objectContaining({
asset_no: 'SN-003',
}),
}),
);
expect(prisma.assetCard.findMany).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
where: expect.objectContaining({
serial_no: 'SN-003',
}),
}),
);
await app.close();
});
it('falls back to fuzzy name search only after both exact lookups miss', async () => {
const prisma = {
assetCard: {
findMany: vi
.fn()
.mockResolvedValueOnce([])
.mockResolvedValueOnce([])
.mockResolvedValueOnce([buildAssetRecord({ id: 'asset-3', assetNo: 'ZC-004', serialNo: 'SN-004', name: '研发测试笔记本' })]),
},
};
const app = await buildApp({
employee: { id: 'manager-1', permissionCodes: ['asset.view_all'] },
prisma,
});
const response = await app.inject({
method: 'GET',
url: '/api/assets?q=测试笔记本',
headers: { authorization: 'Bearer token' },
});
expect(response.statusCode).toBe(200);
expect(prisma.assetCard.findMany).toHaveBeenCalledTimes(3);
expect(prisma.assetCard.findMany).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
where: expect.objectContaining({
asset_no: '测试笔记本',
}),
}),
);
expect(prisma.assetCard.findMany).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
where: expect.objectContaining({
serial_no: '测试笔记本',
}),
}),
);
expect(prisma.assetCard.findMany).toHaveBeenNthCalledWith(
3,
expect.objectContaining({
where: expect.objectContaining({
name: { contains: '测试笔记本', mode: 'insensitive' },
AND: expect.arrayContaining([
expect.objectContaining({}),
expect.objectContaining({
OR: expect.arrayContaining([
{ asset_no: 'FA-2026-000001' },
{ serial_no: 'FA-2026-000001' },
{ verification_no: 'FA-2026-000001' },
{ source_hash: 'FA-2026-000001' },
{ name: { contains: 'FA-2026-000001', mode: 'insensitive' } },
]),
}),
]),
}),
}),
);
@@ -237,14 +178,9 @@ describe('assets search route', () => {
});
it('forces own-scope lookup even when caller passes mine=0', async () => {
const prisma = {
assetCard: {
findMany: vi
.fn()
.mockResolvedValueOnce([])
.mockResolvedValueOnce([buildAssetRecord({ assigneeEmployeeId: 'employee-1', assetNo: 'ZC-002', serialNo: 'SN-002' })]),
},
};
const prisma = buildPrisma({
items: [buildAssetRecord({ assigneeEmployeeId: 'employee-1', assetNo: 'FA-2026-000002', serialNo: 'SN-002' })],
});
const app = await buildApp({
employee: { id: 'employee-1', permissionCodes: ['asset.view_own'] },
prisma,
@@ -258,26 +194,101 @@ describe('assets search route', () => {
expect(response.statusCode).toBe(200);
expect(response.json().data.mine).toBe(true);
expect(prisma.assetCard.findMany).toHaveBeenCalledTimes(2);
expect(prisma.assetCard.findMany).toHaveBeenNthCalledWith(
1,
expect(prisma.assetCard.findMany).toHaveBeenCalledTimes(1);
expect(prisma.assetCard.findMany).toHaveBeenCalledWith(
expect.objectContaining({
where: expect.objectContaining({
assignee_employee_id: 'employee-1',
asset_no: 'SN-002',
}),
}),
);
expect(prisma.assetCard.findMany).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
where: expect.objectContaining({
assignee_employee_id: 'employee-1',
serial_no: 'SN-002',
AND: expect.arrayContaining([
expect.objectContaining({
OR: [
{ assignee_employee_id: 'employee-1' },
{ owner_employee_id: 'employee-1' },
{ keeper_employee_id: 'employee-1' },
],
}),
expect.objectContaining({
OR: expect.arrayContaining([{ serial_no: 'SN-002' }]),
}),
]),
}),
}),
);
await app.close();
});
it('redacts sensitive electronic asset fields for asset.view_all callers that are not related employees', async () => {
const prisma = buildPrisma({
items: [
buildAssetRecord({
id: 'asset-3',
assetNo: 'EA-DOC-2026-000001',
assetType: 'electronic_document',
sensitivityLevel: 'confidential',
archiveLocation: '离线档案柜-A',
externalRef: 'DOC-001',
sourceHash: 'sha256:abcdef',
confidentialityNote: '仅负责人可查看',
metadata: { source: 'redacted_index' },
}),
],
});
const app = await buildApp({
employee: { id: 'manager-1', permissionCodes: ['asset.view_all'] },
prisma,
});
const response = await app.inject({
method: 'GET',
url: '/api/assets?q=EA-DOC-2026-000001',
headers: { authorization: 'Bearer token' },
});
expect(response.statusCode).toBe(200);
expect(response.json().data.items[0]).toEqual(
expect.objectContaining({
assetNo: 'EA-DOC-2026-000001',
archiveLocation: null,
externalRef: null,
sourceHash: null,
confidentialityNote: null,
metadata: null,
sensitiveFieldsRedacted: true,
}),
);
await app.close();
});
it('rejects redacted index import items that still contain full local paths', async () => {
const prisma = buildPrisma();
const app = await buildApp({
employee: { id: 'admin-1', permissionCodes: ['asset.manage'] },
prisma,
});
const response = await app.inject({
method: 'POST',
url: '/api/assets/import/redacted-doc-index',
headers: { authorization: 'Bearer token' },
payload: {
dryRun: true,
items: [
{
name: '合同索引',
archiveLocation: '/home/shunli/Projects/faculty-daily-assistant/公司/合同.pdf',
},
],
},
});
expect(response.statusCode).toBe(400);
expect(response.json().error.details).toEqual(
expect.objectContaining({
'items.0.archiveLocation': 'local_absolute_path',
}),
);
await app.close();
});
});