feat(aihr): add M1 broadcast targeting and mandatory reads

This commit is contained in:
2026-07-23 12:11:41 +08:00
parent cd94f4fe9f
commit f6609808df
14 changed files with 925 additions and 69 deletions
+7 -1
View File
@@ -39,7 +39,13 @@ const pending: PendingBroadcastPublish = {
tenantId: 'tenant-a',
requestId: 'request-1',
title: '夏季服务标准通知',
content: '请按最新标准执行。'
content: '请按最新标准执行。',
requiredRead: true,
targets: {
projectCodes: ['PRJ-FCW'],
positionNames: ['物业管家'],
positionLevels: []
}
};
describe('pending broadcast publish storage', () => {
+36 -1
View File
@@ -1,3 +1,5 @@
import type { BroadcastTargetRequest } from '@/api/aihr/broadcast';
export const pendingBroadcastStoragePrefix = 'aihr.broadcast.pending.v2';
export interface PendingBroadcastPublish {
@@ -5,6 +7,8 @@ export interface PendingBroadcastPublish {
requestId: string;
title: string;
content: string;
requiredRead: boolean;
targets: BroadcastTargetRequest;
}
export interface BroadcastPendingStorageOptions {
@@ -14,6 +18,31 @@ export interface BroadcastPendingStorageOptions {
const accountKey = (userId: string | number) => String(userId).trim();
const emptyTargets = (): BroadcastTargetRequest => ({
projectCodes: [],
positionNames: [],
positionLevels: []
});
const parseTargetValues = (value: unknown, maximumCount: number, maximumLength: number) => {
if (!Array.isArray(value) || value.length > maximumCount) return undefined;
const normalized = value.map((entry) => typeof entry === 'string' ? entry.trim() : '');
if (normalized.some((entry) => !entry || entry.length > maximumLength)) return undefined;
return [...new Set(normalized)];
};
const parseTargets = (value: unknown): BroadcastTargetRequest | undefined => {
if (value === undefined) return emptyTargets();
if (!value || typeof value !== 'object') return undefined;
const candidate = value as Partial<BroadcastTargetRequest>;
const projectCodes = parseTargetValues(candidate.projectCodes, 50, 50);
const positionNames = parseTargetValues(candidate.positionNames, 50, 100);
const positionLevels = parseTargetValues(candidate.positionLevels, 20, 30);
return projectCodes && positionNames && positionLevels
? { projectCodes, positionNames, positionLevels }
: undefined;
};
export const broadcastPendingStorageKey = (tenantId: string, userId: string | number) => {
const normalizedTenantId = String(tenantId).trim();
const normalizedUserId = accountKey(userId);
@@ -26,11 +55,15 @@ const parsePendingPublish = (raw: string | null): PendingBroadcastPublish | unde
if (!raw) return undefined;
try {
const candidate = JSON.parse(raw) as Partial<PendingBroadcastPublish>;
const targets = parseTargets(candidate.targets);
const requiredRead = candidate.requiredRead === undefined ? false : candidate.requiredRead;
if (
typeof candidate.tenantId !== 'string'
|| typeof candidate.requestId !== 'string'
|| typeof candidate.title !== 'string'
|| typeof candidate.content !== 'string'
|| typeof requiredRead !== 'boolean'
|| !targets
) {
return undefined;
}
@@ -38,7 +71,9 @@ const parsePendingPublish = (raw: string | null): PendingBroadcastPublish | unde
tenantId: candidate.tenantId,
requestId: candidate.requestId,
title: candidate.title,
content: candidate.content
content: candidate.content,
requiredRead,
targets
};
} catch {
return undefined;