feat(ops): v4.0.7 HQ 活动图快链与勾选导出

HQ 指定一张活动图为勾选主合伙人合成 PNG/zip;城市合伙人页增加快链与单下/导出。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-01 15:12:10 +08:00
parent eeee55e215
commit cdc4b82931
21 changed files with 1041 additions and 28 deletions
@@ -1,5 +1,9 @@
import { describe, expect, it } from 'vitest';
import { activityPosterQrSlotPx } from './activity-poster';
import {
activityPosterPackFileName,
activityPosterQrSlotPx,
activityPosterTemplateTooLarge,
} from './activity-poster';
describe('activityPosterQrSlotPx', () => {
it('uses width for square size', () => {
@@ -18,3 +22,45 @@ describe('activityPosterQrSlotPx', () => {
});
});
});
describe('activityPosterTemplateTooLarge', () => {
it('allows 1080x1920', () => {
expect(activityPosterTemplateTooLarge(1080, 1920)).toBe(false);
});
it('rejects edge over 2500', () => {
expect(activityPosterTemplateTooLarge(2501, 1000)).toBe(true);
});
it('rejects pixel count over 4e6', () => {
expect(activityPosterTemplateTooLarge(2500, 2000)).toBe(true);
});
});
describe('activityPosterPackFileName', () => {
it('uses city, company, id', () => {
expect(activityPosterPackFileName({
cityName: '郑州',
companyName: '杜康合伙',
partnerName: '张三',
partnerId: '12',
})).toBe('郑州_杜康合伙_12.png');
});
it('falls back to partner name when company empty', () => {
expect(activityPosterPackFileName({
cityName: '洛阳',
companyName: ' ',
partnerName: '李四',
partnerId: '9',
})).toBe('洛阳_李四_9.png');
});
it('strips path separators', () => {
expect(activityPosterPackFileName({
cityName: 'a/b',
companyName: 'c:d',
partnerId: '1',
})).toBe('a-b_c-d_1.png');
});
});
@@ -55,6 +55,39 @@ export type ActivityPosterSelectionRequest = {
posterId?: string | null;
};
export type ActivityPosterPackRequest = {
partnerIds: string[];
};
/** 底图最长边(px);超出则拒绝合成,避免单进程 Sharp 撑爆内存 */
export const ACTIVITY_POSTER_MAX_EDGE = 2500;
/** 底图像素上限(宽×高) */
export const ACTIVITY_POSTER_MAX_PIXELS = 4_000_000;
/** 单次 zip 最多合伙人数 */
export const ACTIVITY_POSTER_PACK_MAX_PARTNERS = 200;
export function activityPosterTemplateTooLarge(width: number, height: number): boolean {
const w = Math.max(0, Math.round(width));
const h = Math.max(0, Math.round(height));
return w > ACTIVITY_POSTER_MAX_EDGE || h > ACTIVITY_POSTER_MAX_EDGE || w * h > ACTIVITY_POSTER_MAX_PIXELS;
}
/** zip / 单张下载文件名:`{城市}_{公司或姓名}_{id}.png` */
export function activityPosterPackFileName(input: {
cityName?: string | null;
companyName?: string | null;
partnerName?: string | null;
partnerId: string;
}): string {
const label = (input.companyName?.trim() || input.partnerName?.trim() || '').trim();
const raw = [input.cityName?.trim(), label, input.partnerId.trim()]
.filter(Boolean)
.join('_')
.replace(/[\\/:*?"<>|]+/g, '-')
.replace(/\s+/g, '');
return `${raw || input.partnerId}.png`;
}
/** 码栏百分比 → 像素。size 相对图宽,保证正方形;落点夹在画布内。 */
export function activityPosterQrSlotPx(
width: number,