feat(pl): add personnel cert zip packing and shared pack core.
Extract packAndSaveCertZip for reuse; keep vehicle API; name folders with _2 on collisions. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
b4e59ab16b
commit
55abe614f8
2 files changed
+239
-25
No files matched your search
@@ -4,6 +4,9 @@ import {
|
||||
collectVehicleCertZipEntries,
|
||||
buildVehicleCertZipFilename,
|
||||
buildAndSaveVehicleCertZip,
|
||||
collectPersonnelCertZipEntries,
|
||||
buildPersonnelCertZipFilename,
|
||||
buildAndSavePersonnelCertZip,
|
||||
uniqueZipPath,
|
||||
sanitizeZipPathSegment
|
||||
} from './pl-vehicle-cert-zip'
|
||||
@@ -220,3 +223,131 @@ describe('buildAndSaveVehicleCertZip', () => {
|
||||
assert.equal(result.zipName, '')
|
||||
})
|
||||
})
|
||||
|
||||
describe('collectPersonnelCertZipEntries', () => {
|
||||
it('uses personnel name folders; dedupes same names with _2; skips empty', () => {
|
||||
const entries = collectPersonnelCertZipEntries([
|
||||
{
|
||||
id: 'p1',
|
||||
personnelName: '张三',
|
||||
certList: [
|
||||
{
|
||||
id: 'c1',
|
||||
ownerType: 'personnel',
|
||||
ownerId: 'p1',
|
||||
status: '1',
|
||||
certName: '驾驶证',
|
||||
attachmentUrls: 'https://cdn.example/d.jpg'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'p2',
|
||||
personnelName: '张三',
|
||||
certList: [
|
||||
{
|
||||
id: 'c2',
|
||||
ownerType: 'personnel',
|
||||
ownerId: 'p2',
|
||||
status: '1',
|
||||
certName: '从业资格证',
|
||||
attachmentUrls: 'https://cdn.example/q.pdf'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'p3',
|
||||
personnelName: '李四',
|
||||
certList: []
|
||||
}
|
||||
])
|
||||
assert.equal(entries.length, 2)
|
||||
assert.equal(entries[0].folderLabel, '张三')
|
||||
assert.equal(entries[1].folderLabel, '张三_2')
|
||||
assert.equal(entries[0].files[0].baseName, '驾驶证')
|
||||
assert.equal(entries[1].files[0].baseName, '从业资格证')
|
||||
})
|
||||
|
||||
it('does not add 主·/挂· for personnel certs', () => {
|
||||
const entries = collectPersonnelCertZipEntries([
|
||||
{
|
||||
id: 'p1',
|
||||
personnelName: '王五',
|
||||
certList: [
|
||||
{
|
||||
id: 'c1',
|
||||
ownerType: 'personnel',
|
||||
ownerId: 'p1',
|
||||
status: '1',
|
||||
certName: '驾驶证',
|
||||
vehiclePart: 'trailer',
|
||||
attachmentUrls: 'https://cdn.example/a.jpg'
|
||||
}
|
||||
]
|
||||
}
|
||||
])
|
||||
assert.equal(entries[0].files[0].baseName, '驾驶证')
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildPersonnelCertZipFilename', () => {
|
||||
it('names single and multi personnel zips', () => {
|
||||
assert.equal(
|
||||
buildPersonnelCertZipFilename([{ folderLabel: '张三' }], '20260908'),
|
||||
'人员证书_张三.zip'
|
||||
)
|
||||
assert.equal(
|
||||
buildPersonnelCertZipFilename(
|
||||
[{ folderLabel: '张三' }, { folderLabel: '张三_2' }],
|
||||
'20260908'
|
||||
),
|
||||
'人员证书批量_2人_20260908.zip'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildAndSavePersonnelCertZip', () => {
|
||||
it('packs multi personnel into name subfolders', async () => {
|
||||
const paths: string[] = []
|
||||
const result = await buildAndSavePersonnelCertZip(
|
||||
[
|
||||
{
|
||||
id: 'p1',
|
||||
personnelName: '张三',
|
||||
certList: [
|
||||
{
|
||||
id: 'c1',
|
||||
ownerType: 'personnel',
|
||||
ownerId: 'p1',
|
||||
status: '1',
|
||||
certName: '驾驶证',
|
||||
attachmentUrls: 'https://cdn.example/a.jpg'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'p2',
|
||||
personnelName: '李四',
|
||||
certList: [
|
||||
{
|
||||
id: 'c2',
|
||||
ownerType: 'personnel',
|
||||
ownerId: 'p2',
|
||||
status: '1',
|
||||
certName: '身份证',
|
||||
attachmentUrls: 'https://cdn.example/b.png'
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
{
|
||||
yyyymmdd: '20260908',
|
||||
fetchBlob: async () => new Blob(['x']),
|
||||
saveAs: async () => {},
|
||||
onFileAdded: (p) => paths.push(p)
|
||||
}
|
||||
)
|
||||
assert.equal(result.zipName, '人员证书批量_2人_20260908.zip')
|
||||
assert.deepEqual(paths.sort(), ['张三/驾驶证.jpg', '李四/身份证.png'].sort())
|
||||
})
|
||||
})
|
||||
@@ -161,6 +161,9 @@ export type BuildVehicleCertZipResult = {
|
||||
zipName: string
|
||||
}
|
||||
|
||||
export type BuildCertZipOptions = BuildVehicleCertZipOptions
|
||||
export type BuildCertZipResult = BuildVehicleCertZipResult
|
||||
|
||||
function todayYyyymmdd(): string {
|
||||
const d = new Date()
|
||||
const y = d.getFullYear()
|
||||
@@ -200,43 +203,30 @@ async function defaultSaveAs(blob: Blob, filename: string): Promise<void> {
|
||||
FileSaver.saveAs(blob, filename)
|
||||
}
|
||||
|
||||
export async function buildAndSaveVehicleCertZip(
|
||||
vehicles: VehicleCertZipSource[],
|
||||
options: BuildVehicleCertZipOptions = {}
|
||||
): Promise<BuildVehicleCertZipResult> {
|
||||
const entries = collectVehicleCertZipEntries(vehicles)
|
||||
export async function packAndSaveCertZip(
|
||||
entries: Array<{ folderLabel: string; files: VehicleCertZipFile[] }>,
|
||||
zipName: string,
|
||||
options: BuildCertZipOptions = {}
|
||||
): Promise<BuildCertZipResult> {
|
||||
if (!entries.length) {
|
||||
return { okCount: 0, failCount: 0, zipName: '' }
|
||||
}
|
||||
|
||||
const yyyymmdd = options.yyyymmdd || todayYyyymmdd()
|
||||
const zipName = buildVehicleCertZipFilename(entries, yyyymmdd)
|
||||
const multi = entries.length > 1
|
||||
const concurrency = options.concurrency ?? 4
|
||||
|
||||
type Job = {
|
||||
zipPath: string
|
||||
fileUrl: string
|
||||
fileName: string
|
||||
}
|
||||
|
||||
const used = new Set<string>()
|
||||
const jobs: Job[] = []
|
||||
const jobs: Array<{ zipPath: string; fileUrl: string; fileName: string }> = []
|
||||
|
||||
for (const entry of entries) {
|
||||
for (const file of entry.files) {
|
||||
const leaf = `${file.baseName}${file.ext ? `.${file.ext}` : ''}`
|
||||
const rawPath = multi ? `${entry.plateFolder}/${leaf}` : leaf
|
||||
const rawPath = multi ? `${entry.folderLabel}/${leaf}` : leaf
|
||||
const zipPath = uniqueZipPath(used, rawPath)
|
||||
jobs.push({
|
||||
zipPath,
|
||||
fileUrl: file.fileUrl,
|
||||
fileName: leaf
|
||||
})
|
||||
jobs.push({ zipPath, fileUrl: file.fileUrl, fileName: leaf })
|
||||
}
|
||||
}
|
||||
|
||||
const fetchBlob = options.fetchBlob ?? defaultFetchBlob
|
||||
|
||||
const zip = new JSZip()
|
||||
let okCount = 0
|
||||
let failCount = 0
|
||||
@@ -257,9 +247,102 @@ export async function buildAndSaveVehicleCertZip(
|
||||
return { okCount: 0, failCount, zipName: '' }
|
||||
}
|
||||
|
||||
const blob = await zip.generateAsync({ type: 'blob' })
|
||||
const out = await zip.generateAsync({ type: 'blob' })
|
||||
const save = options.saveAs ?? defaultSaveAs
|
||||
await save(blob, zipName)
|
||||
|
||||
await save(out, zipName)
|
||||
return { okCount, failCount, zipName }
|
||||
}
|
||||
|
||||
export async function buildAndSaveVehicleCertZip(
|
||||
vehicles: VehicleCertZipSource[],
|
||||
options: BuildVehicleCertZipOptions = {}
|
||||
): Promise<BuildVehicleCertZipResult> {
|
||||
const collected = collectVehicleCertZipEntries(vehicles)
|
||||
if (!collected.length) {
|
||||
return { okCount: 0, failCount: 0, zipName: '' }
|
||||
}
|
||||
|
||||
const yyyymmdd = options.yyyymmdd || todayYyyymmdd()
|
||||
const zipName = buildVehicleCertZipFilename(collected, yyyymmdd)
|
||||
return packAndSaveCertZip(
|
||||
collected.map((e) => ({ folderLabel: e.plateFolder, files: e.files })),
|
||||
zipName,
|
||||
options
|
||||
)
|
||||
}
|
||||
|
||||
export type PersonnelCertZipSource = {
|
||||
id: string
|
||||
personnelName?: string | null
|
||||
certList?: CertRead[] | null
|
||||
}
|
||||
|
||||
export type PersonnelCertZipEntry = {
|
||||
personnelId: string
|
||||
folderLabel: string
|
||||
files: VehicleCertZipFile[]
|
||||
}
|
||||
|
||||
export function collectPersonnelCertZipEntries(
|
||||
people: PersonnelCertZipSource[]
|
||||
): PersonnelCertZipEntry[] {
|
||||
const result: PersonnelCertZipEntry[] = []
|
||||
const usedFolders = new Set<string>()
|
||||
|
||||
for (const person of people) {
|
||||
const certs = person.certList || []
|
||||
const files: VehicleCertZipFile[] = []
|
||||
|
||||
certs.forEach((cert) => {
|
||||
const certName = String(cert.certName || '').trim() || '证书'
|
||||
parseUrls(cert.attachmentUrls).forEach((fileUrl) => {
|
||||
files.push({
|
||||
fileUrl,
|
||||
baseName: certName,
|
||||
ext: getFileExt(fileUrl)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
if (!files.length) continue
|
||||
|
||||
const rawName = String(person.personnelName || '').trim()
|
||||
const baseFolder = sanitizeZipPathSegment(rawName || person.id)
|
||||
const folderLabel = uniqueZipPath(usedFolders, baseFolder)
|
||||
result.push({
|
||||
personnelId: person.id,
|
||||
folderLabel,
|
||||
files
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export function buildPersonnelCertZipFilename(
|
||||
entries: Array<{ folderLabel: string }>,
|
||||
yyyymmdd: string
|
||||
): string {
|
||||
if (entries.length === 1) {
|
||||
return `人员证书_${entries[0].folderLabel}.zip`
|
||||
}
|
||||
return `人员证书批量_${entries.length}人_${yyyymmdd}.zip`
|
||||
}
|
||||
|
||||
export async function buildAndSavePersonnelCertZip(
|
||||
people: PersonnelCertZipSource[],
|
||||
options: BuildCertZipOptions = {}
|
||||
): Promise<BuildCertZipResult> {
|
||||
const collected = collectPersonnelCertZipEntries(people)
|
||||
if (!collected.length) {
|
||||
return { okCount: 0, failCount: 0, zipName: '' }
|
||||
}
|
||||
|
||||
const yyyymmdd = options.yyyymmdd || todayYyyymmdd()
|
||||
const zipName = buildPersonnelCertZipFilename(collected, yyyymmdd)
|
||||
return packAndSaveCertZip(
|
||||
collected.map((e) => ({ folderLabel: e.folderLabel, files: e.files })),
|
||||
zipName,
|
||||
options
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user