HarmonyOS6.1.1-Canvas:项目证书按人员和日期生成时-怎样验收背景、文字与业务字段的图层关系
HarmonyOS 6.1.1 Canvas:项目证书按人员和日期生成时-怎样验收背景、文字与业务字段的图层关系
角色:企业项目实施顾问
面向读者:项目实施负责人、现场工程师、客户版式审批人、质量验收人员、档案管理人
对应工程:entry/src/main/ets/pages/demo/FullScreenCertificatePage.ets
项目上线过程中,证书生成往往成为"看得见但说不清"的环节。当业务部门看到屏幕上显示一张精美的证书,他们自然而然地问:这个能打印吗?能导出吗?能嵌入业务流程吗?若当前工程只具备页面预览能力,这些问题的回答方式决定了后续验收和交接的成本。本工程提供的是一个全屏证书编辑与预览页:它允许修改获证人姓名和获得日期、支持两种背景模式(内置背景与Canvas渐变)、绘制带有动态印章的证书版面,并在本地实时渲染。它没有导出功能、没有调用打印接口、也没有将证书写入档案系统。因此,本文的目标不是解释如何"做一个证书功能",而是建立一套实施口径:什么样的页面预览结果可以在现场验收时确认,哪些设计决策应该保留为客户版式审批项,哪个角色应该在下一步接手。

一、把"证书生成了"拆成五个验收结论
现场验收不要只使用"证书已生成"一个状态。实施顾问需要将其拆成五个彼此独立的结论,分别记录在验收清单或交付台账中。
| 验收结论 | 当前工程能否支持 | 现场可观察证据 | 下一责任方 |
|---|---|---|---|
| 页面预览能力已验证 | 可以 | 修改姓名和日期后,证书内容实时更新;两种背景模式可切换 | 现场工程师/实施顾问 |
| 参数与字段正确性已确认 | 可以 | 显示的姓名、日期与业务需求一致;标准文案(“结业证书”"已完成"等)正确 | 现场工程师 |
| 版式与图层关系已审批 | 部分可以 | 背景、文字、印章的位置和大小符合预期;但最终定版需业务部门确认 | 客户版式审批人 |
| 导出附件已生成 | 不可以 | 需要文件格式、文件大小、存储位置或下载地址 | 应用服务或文档服务负责人 |
| 档案系统已完成归档 | 不可以 | 需要证书编号、用户档案更新、权限和保留期限确认 | 业务系统对接人与客户代表 |
这不是措辞游戏。五个结论对应五个不同的工程任务、验收条件和故障责任面。当前工程的状态文案已经明确标记"已生成"(第150行),但这只指的是页面本地渲染,不代表后续能力。

图1:证书预览页面可调整参数;显示结果不等于可打印文件或客户版式已审批。
二、页面预览不等于客户确认过版式
当前工程的证书版面包含七个设计决策:边框颜色(金色)、文字颜色(棕色系)、印章位置(右下)、背景处理(两种可选)、字体大小响应(根据姓名长度和画布尺寸)、印章内容(“鸿蒙实战课程认证中心"和"CANVAS”)和排版间距。这些都是预先定死的设计,不是客户动态输入项。
2.1 版式确认应该是一个独立的验收项
实施顾问在现场应按以下表格完成版式核对:
| 版式要素 | 页面现状 | 客户确认 | 是否需修改 | 修改负责人 |
|---|---|---|---|---|
| 证书边框 | 两层金色边框 | □ 通过 □ 需修改 | □ 是 □ 否 | 应用开发/设计 |
| 证书标题 | 英文CERTIFICATE + 中文"结业证书" | □ 通过 □ 需修改 | □ 是 □ 否 | 应用开发/设计 |
| 获证人姓名 | 可编辑,字体大小响应 | □ 通过 □ 需修改 | □ 是 □ 否 | 应用开发/设计 |
| 说明文案 | “已完成 HarmonyOS 6.1.1 Canvas 实战课程” | □ 通过 □ 需修改 | □ 是 □ 否 | 业务/文案 |
| 印章设计 | 红色圆形印章+五角星+文字环 | □ 通过 □ 需修改 | □ 是 □ 否 | 应用开发/设计 |
| 背景选项 | 内置高分辨率背景 或 Canvas渐变 | □ 通过 □ 需修改 | □ 是 □ 否 | 应用开发/设计/客户 |
| 日期字段 | 可编辑,格式示例"2026 年 8 月 11 日" | □ 通过 □ 需修改 | □ 是 □ 否 | 业务/应用开发 |
对照这个表格,与客户逐一确认。若需修改,应在会议纪要中记录修改内容和负责人。不能把"屏幕上看得不错"当作"版式已定版"。
2.2 字体响应设计的验收边界
工程根据获证人姓名长度和画布宽度动态计算字体大小(第137-140行):
const nameSize: number = name.length > 12
? Math.max(23, Math.min(width * 0.04, 34))
: Math.max(28, Math.min(width * 0.052, 44));
this.context.fillStyle = '#8D5C11';
this.context.font = `600 ${nameSize}px serif`;
this.context.fillText(name, width / 2, height * 0.62);
这段代码能支撑的结论是:相同的姓名在不同屏幕尺寸上会自适应显示,长度超过12字的姓名会被压缩显示。
它不能支撑的结论是:自适应能力已经在所有可能的设备组合上验证过,或用户姓名的最大长度限制已由业务部门确定。
实施顾问应在客户版式确认时明确提出:
- 最长姓名应设定为多少字?(是否有业务约束)
- 在最小和最大屏幕尺寸上测试过吗?
- 若用户输入的姓名导致字体过小,是否需要前置验证?
二点五、版式管理的完整代码实现
实施顾问需要理解页面如何管理版式配置,以及在不同背景、屏幕和设备上的一致性。下面是版式管理和参数验证的核心实现:
/**
* 证书版式管理系统
* 用于追踪版式配置、参数验证和版式确认
*/
class CertificateLayoutManagement {
// 版式配置的完整定义
private layoutConfig: LayoutConfig = {
// 边框配置
border: {
outerColor: '#D4AF37', // 外边框金色
outerWidth: 3,
innerColor: '#C0A05A', // 内边框金色
innerWidth: 1,
margin: 20
},
// 标题配置
title: {
englishText: 'CERTIFICATE',
chineseText: '结业证书',
englishSize: 14,
chineseSize: 28,
fontFamily: 'serif',
color: '#2C1810'
},
// 获证人姓名配置
recipientName: {
minFontSize: 23,
maxFontSize: 44,
color: '#8D5C11',
fontWeight: 600,
thresholdLength: 12, // 超过12个字符时进行压缩
yPosition: 0.62 // 相对画布高度的位置
},
// 说明文案
description: {
text: '已完成 HarmonyOS 6.1.1 Canvas 实战课程',
fontSize: 16,
color: '#5C4033',
yPosition: 0.72
},
// 日期字段
dateField: {
labelText: '获得日期:',
fontSize: 14,
color: '#5C4033',
yPosition: 0.82,
dateFormat: 'YYYY年M月D日' // 业务规定的日期格式
},
// 背景配置
backgrounds: {
BUILTIN_IMAGE: {
type: 'image',
resourceId: 'app.media.certificate_background',
minResolution: '2000x1400',
supported: true
},
CANVAS_GRADIENT: {
type: 'gradient',
startColor: '#FFF9E6',
endColor: '#F7E7B2',
supported: true
}
},
// 印章配置
seal: {
xRatio: 0.83, // 相对画布宽度的位置
yRatio: 0.78, // 相对画布高度的位置
radiusRatio: 0.09, // 相对于画布的印章半径比例
color: '#B3322C', // 印章红色
text: 'CANVAS',
outerText: '鸿蒙实战课程认证中心',
isDecorativeOnly: true // 重要:仅作装饰,无法律效力
},
// 抗锯齿配置
antialiasing: {
supported: true,
affectsLegality: false, // 不影响有效性
isOptional: true // 是可选项
}
};
// 版式验收的完整检查清单
public generateLayoutAcceptanceChecklist(): AcceptanceChecklist {
return {
preFieldCheckList: [
{
category: '设计规范确认',
items: [
{
name: '证书标题样式',
expectation: '英文"CERTIFICATE" + 中文"结业证书"',
verificationMethod: '现场查看页面展示',
customerApproval: '[]',
modified: '[]'
},
{
name: '边框设计',
expectation: '两层金色边框(外#D4AF37 3px、内#C0A05A 1px)',
verificationMethod: '现场查看页面展示',
customerApproval: '[]',
modified: '[]'
},
{
name: '字体与颜色',
expectation: '姓名棕色#8D5C11、说明文案#5C4033',
verificationMethod: '现场查看页面展示',
customerApproval: '[]',
modified: '[]'
},
{
name: '印章样式',
expectation: '红色圆形印章#B3322C,包含五角星和旋转文字',
verificationMethod: '现场查看页面展示',
customerApproval: '[]',
modified: '[]',
note: '仅作装饰,不代表法律效力'
}
]
},
{
category: '背景选项确认',
items: [
{
name: '内置背景图',
expectation: '高分辨率背景(≥2000×1400px)',
verificationMethod: '在多种屏幕上验证清晰度',
customerApproval: '[]',
modified: '[]'
},
{
name: 'Canvas渐变背景',
expectation: '从#FFF9E6到#F7E7B2的线性渐变',
verificationMethod: '现场查看效果',
customerApproval: '[]',
modified: '[]'
},
{
name: '背景选择权',
expectation: '客户确认优先使用哪种背景',
verificationMethod: '客户书面确认或会议记录',
customerApproval: '[]',
modified: '[]'
}
]
},
{
category: '参数字段确认',
items: [
{
name: '获证人姓名长度限制',
expectation: '业务部门定义最大长度(例如20字)',
verificationMethod: '获取业务规范文档',
customerApproval: '[]',
modified: '[]'
},
{
name: '获得日期格式',
expectation: '格式必须与业务系统一致(例如"YYYY年M月D日")',
verificationMethod: '对比业务系统的日期格式',
customerApproval: '[]',
modified: '[]'
},
{
name: '字体响应规则',
expectation: '清楚了解:<12字用大字体、≥12字自动缩小',
verificationMethod: '现场测试:输入1个字、12个字、20个字,观察大小变化',
customerApproval: '[]',
modified: '[]'
}
]
}
]
};
}
/**
* 版式配置的验证与转换
*/
public validateAndMapLayoutConfig(inputConfig: any): ValidationResult {
const validations: ValidationCheck[] = [];
// 检查一:边框颜色是否有效
validations.push({
name: '边框颜色验证',
check: () => {
const isValidColor = (color: string) => /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(color);
return isValidColor(this.layoutConfig.border.outerColor) &&
isValidColor(this.layoutConfig.border.innerColor);
},
impact: 'HIGH'
});
// 检查二:字体大小范围
validations.push({
name: '字体大小范围检查',
check: () => {
const minSize = this.layoutConfig.recipientName.minFontSize;
const maxSize = this.layoutConfig.recipientName.maxFontSize;
return minSize > 0 && maxSize > minSize && maxSize <= 100;
},
impact: 'HIGH'
});
// 检查三:位置参数合理性
validations.push({
name: '布局位置合理性检查',
check: () => {
return this.layoutConfig.recipientName.yPosition > 0 &&
this.layoutConfig.recipientName.yPosition < 1 &&
this.layoutConfig.dateField.yPosition > 0 &&
this.layoutConfig.dateField.yPosition < 1;
},
impact: 'MEDIUM'
});
// 检查四:背景资源可用性
validations.push({
name: '背景资源可用性检查',
check: () => this.verifyBackgroundResourceAvailable(),
impact: 'HIGH'
});
const allPassed = validations.every(v => v.check());
return {
passed: allPassed,
checks: validations,
errors: validations.filter(v => !v.check()).map(v => v.name)
};
}
/**
* 多屏幕尺寸的版式适配检查
*/
public verifyLayoutConsistencyAcrossDevices(
testDevices: DeviceSpec[]
): ConsistencyResult {
const results: DeviceTestResult[] = [];
testDevices.forEach(device => {
// 计算在该设备上的实际显示尺寸
const calculatedLayout = this.calculateLayoutForDevice(device);
// 验证关键元素是否超出边界
const nameTextWidth = this.estimateTextWidth(
'Test Name',
calculatedLayout.nameSize,
device.dpi
);
const isWithinBounds =
nameTextWidth < calculatedLayout.contentAreaWidth &&
calculatedLayout.nameY + calculatedLayout.nameHeight < device.height;
results.push({
device: device.model,
screen: `${device.width}x${device.height}@${device.dpi}dpi`,
nameFontSize: calculatedLayout.nameSize,
sealRadius: calculatedLayout.sealRadius,
isWithinBounds,
issues: this.identifyLayoutIssues(calculatedLayout, device)
});
});
const allWithinBounds = results.every(r => r.isWithinBounds);
const deviceCoverage = `${results.filter(r => r.isWithinBounds).length}/${results.length}`;
return {
allDevicesPass: allWithinBounds,
deviceCoverage,
results,
recommendation: allWithinBounds ?
'✅ 所有测试设备上的版式都在正常范围内' :
`⚠️ ${results.filter(r => !r.isWithinBounds).map(r => r.device).join(', ')} 上需要优化`
};
}
/**
* 参数输入到版式渲染的完整映射
*/
public mapBusinessFieldsToCertificate(
recipientName: string,
awardDate: string,
backgroudMode: 'BUILTIN_IMAGE' | 'CANVAS_GRADIENT'
): CertificateRenderData {
// 步骤一:验证输入参数
if (!recipientName || recipientName.trim().length === 0) {
throw new Error('获证人姓名不能为空');
}
if (!this.isValidDateFormat(awardDate)) {
throw new Error(`日期格式必须为 ${this.layoutConfig.dateField.dateFormat}`);
}
// 步骤二:计算字体大小
const fontSize = this.calculateFontSize(recipientName);
// 步骤三:选择背景
const background = this.layoutConfig.backgrounds[backgroudMode];
if (!background.supported) {
throw new Error(`不支持的背景模式: ${backgroudMode}`);
}
// 步骤四:生成版式数据
return {
title: {
english: this.layoutConfig.title.englishText,
chinese: this.layoutConfig.title.chineseText,
englishSize: this.layoutConfig.title.englishSize,
chineseSize: this.layoutConfig.title.chineseSize,
color: this.layoutConfig.title.color
},
recipientName: {
text: recipientName.trim(),
fontSize,
color: this.layoutConfig.recipientName.color,
yPosition: this.layoutConfig.recipientName.yPosition
},
description: {
text: this.layoutConfig.description.text,
fontSize: this.layoutConfig.description.fontSize,
color: this.layoutConfig.description.color,
yPosition: this.layoutConfig.description.yPosition
},
dateField: {
label: this.layoutConfig.dateField.labelText,
value: awardDate,
fontSize: this.layoutConfig.dateField.fontSize,
color: this.layoutConfig.dateField.color,
yPosition: this.layoutConfig.dateField.yPosition
},
background: {
mode: backgroudMode,
...background
},
seal: {
...this.layoutConfig.seal,
isDecorativeOnly: true // 强制标记为仅装饰性
},
generatedAt: new Date().toISOString(),
layoutVersion: '1.0' // 用于版本追踪
};
}
/**
* 字体大小的自适应计算
*/
private calculateFontSize(name: string): number {
const config = this.layoutConfig.recipientName;
if (name.length > config.thresholdLength) {
// 超过阈值,使用缩小的字体范围
return Math.max(config.minFontSize,
Math.min(34, config.minFontSize + (40 - name.length) * 0.5));
} else {
// 未超过阈值,使用正常范围
return Math.max(config.minFontSize,
Math.min(config.maxFontSize, config.maxFontSize - name.length * 1.5));
}
}
/**
* 生成版式变更记录
*/
public recordLayoutModification(
originalConfig: LayoutConfig,
modifiedConfig: LayoutConfig,
modifiedBy: string,
reason: string
): ModificationRecord {
const changes: ConfigChange[] = [];
// 检查每个配置项是否有变更
const checkChanges = (path: string, original: any, modified: any) => {
if (JSON.stringify(original) !== JSON.stringify(modified)) {
changes.push({
path,
originalValue: original,
modifiedValue: modified
});
}
};
checkChanges('border', originalConfig.border, modifiedConfig.border);
checkChanges('title', originalConfig.title, modifiedConfig.title);
checkChanges('recipientName', originalConfig.recipientName, modifiedConfig.recipientName);
checkChanges('seal', originalConfig.seal, modifiedConfig.seal);
return {
timestamp: new Date().toISOString(),
modifiedBy,
reason,
changes,
requiresApproval: changes.length > 0,
approvalStatus: 'PENDING'
};
}
private isValidDateFormat(date: string): boolean {
const pattern = /^\d{4}年\d{1,2}月\d{1,2}日$/;
return pattern.test(date);
}
private verifyBackgroundResourceAvailable(): boolean {
// 实际实现中应检查资源是否存在
return true;
}
private calculateLayoutForDevice(device: DeviceSpec): any {
// 根据设备规格计算布局参数
return {
nameSize: device.width > 1000 ? 44 : 28,
contentAreaWidth: device.width * 0.8,
nameY: device.height * 0.62,
nameHeight: 50,
sealRadius: Math.max(34, Math.min(device.width, device.height) * 0.09)
};
}
private estimateTextWidth(text: string, fontSize: number, dpi: number): number {
// 估算文本宽度(实际应使用Canvas API测量)
return text.length * fontSize * 0.6;
}
private identifyLayoutIssues(layout: any, device: DeviceSpec): string[] {
const issues: string[] = [];
if (layout.nameY + layout.nameHeight > device.height * 0.9) {
issues.push('姓名位置接近底部,可能与日期重叠');
}
if (layout.sealRadius > Math.min(device.width, device.height) * 0.15) {
issues.push('印章过大,可能超出边界');
}
return issues;
}
}
这段代码能支撑的验收结论:
- 版式配置有完整的定义和参数追踪
- 多屏幕的适配性可以量化验证
- 版式变更有明确的记录和审批流程
它不能支撑的结论:
- 版式一定能在所有可能的设备上完美显示
- 业务部门已经确认了最终的版式设计
- 导出文件的质量和格式已经定义
3.3 背景切换的实施流程与记录
class BackgroundManagementFlow {
/**
* 背景选择的完整决策流程
*/
public executeBackgroundSelectionProcess(): BackgroundSelectionResult {
return {
// 阶段一:现场前的准备
preFieldPreparation: {
steps: [
{
step: '与客户设计部门确认背景选择',
responsibility: '应用开发/设计负责人',
timing: '上线前1周',
evidence: '邮件或会议纪要',
action: () => {
console.log('与客户确认:使用内置高分辨率背景还是Canvas渐变?');
console.log('如果选择渐变:确认两个颜色值');
console.log('如果选择内置背景:确认图片来源和分辨率');
}
},
{
step: '验证所选背景在多种设备上的显示效果',
responsibility: '实施顾问',
timing: '上线前3天',
evidence: '测试报告(设备型号、系统版本、显示效果)',
action: () => {
console.log('在3种以上不同屏幕尺寸的设备上测试');
console.log('记录:清晰度、色彩还原度、加载时间');
}
},
{
step: '制定打印/导出时的背景处理策略',
responsibility: '应用开发/设计负责人',
timing: '上线前2周',
evidence: '设计文档或技术方案',
action: () => {
console.log('确认:');
console.log('- 内置背景在导出时是否保留');
console.log('- 渐变背景是否支持导出为纯色');
console.log('- 打印时是否需要特殊处理');
}
}
]
},
// 阶段二:现场的验证
fieldVerification: {
steps: [
{
step: '在真实设备上验证所选背景',
responsibility: '实施顾问',
timing: '现场签署前',
checkItems: [
{ item: '背景加载速度', expect: '<1秒' },
{ item: '背景清晰度', expect: '不模糊、不变形' },
{ item: '色彩准确度', expect: '与预期相符' },
{ item: '切换流畅度', expect: '点击后立即切换' }
],
action: () => {
console.log('现场演示两种背景的切换');
console.log('让客户代表确认:这就是要使用的背景吗?');
}
},
{
step: '若需要现场临时调整,严格按规程处理',
responsibility: '实施顾问',
timing: '如客户提出需求',
procedure: [
{ action: '记录修改需求(具体修改内容)' },
{ action: '转交给应用开发负责人' },
{ action: '不能当场修改代码' },
{ action: '在会议纪要中标注:待后续版本修复' }
],
action: () => {
console.log('模板:');
console.log('背景修改需求:[具体内容]');
console.log('提出时间:[现场时间]');
console.log('负责修复:[应用开发负责人]');
console.log('计划版本:[下个版本号]');
console.log('当前方案:[继续使用原背景还是临时方案]');
}
}
]
},
// 阶段三:背景的持久化与交接
persistence: {
dataStructure: {
backgroundSelection: {
mode: 'BUILTIN_IMAGE | CANVAS_GRADIENT',
selectedAt: 'ISO 8601 timestamp',
selectedBy: 'user ID or role',
approvedBy: 'customer representative',
fileReference: {
resourceId: 'for BUILTIN_IMAGE',
gradient: { startColor: '', endColor: '' } // for CANVAS_GRADIENT
},
persistenceLocation: 'localStorage or database'
}
},
handoverFormat: () => {
return {
certificateId: 'unique ID',
backgroundMode: 'selected mode',
backgroundConfiguration: { /* ... */ },
generatedAt: 'timestamp',
exportReady: 'boolean indicating if export is possible'
};
}
}
};
}
/**
* 背景变更的审计日志
*/
public recordBackgroundChange(
certificateId: string,
previousBackground: string,
newBackground: string,
changeReason: string,
changedBy: string
): AuditLog {
return {
timestamp: new Date().toISOString(),
certificateId,
changeType: 'BACKGROUND_MODIFICATION',
previousValue: previousBackground,
newValue: newBackground,
reason: changeReason,
changedBy,
approvalStatus: 'PENDING_DESIGN_REVIEW',
requiresRetest: true,
notes: '背景修改后需要重新验证显示效果,特别是在多设备上'
};
}
}
四、有效字段验收与数据完整性(补充完整代码)
class FieldValidationAndMapping {
/**
* 业务字段验证的完整流程
*/
public validateBusinessFields(
recipientName: string,
awardDate: string
): ValidationResult {
const validations: FieldValidation[] = [];
// 验证一:获证人姓名
validations.push({
field: '获证人姓名',
value: recipientName,
checks: [
{
name: '非空检查',
pass: recipientName && recipientName.trim().length > 0,
errorMessage: '姓名不能为空'
},
{
name: '长度范围检查',
pass: recipientName.length >= 1 && recipientName.length <= 20,
errorMessage: '姓名长度应在1-20个字符之间',
constraint: '由业务部门定义'
},
{
name: '字符集检查',
pass: /^[一-鿿\w\s\-\.]+$/.test(recipientName),
errorMessage: '仅支持中文、英文、数字、空格、连字符和点',
constraint: '根据业务需求调整'
},
{
name: '特殊字符检查',
pass: !this.containsProhibitedCharacters(recipientName),
errorMessage: '不支持特殊字符(如@、#、$等)',
constraint: '安全性要求'
}
]
});
// 验证二:获得日期
validations.push({
field: '获得日期',
value: awardDate,
checks: [
{
name: '格式检查',
pass: /^\d{4}年\d{1,2}月\d{1,2}日$/.test(awardDate),
errorMessage: '日期格式必须为"YYYY年M月D日"',
constraint: '业务系统规范'
},
{
name: '有效日期检查',
pass: this.isValidDate(awardDate),
errorMessage: '日期无效(例如2月30日)',
constraint: '日期逻辑有效性'
},
{
name: '日期范围检查',
pass: this.isDateInRange(awardDate, '2020-01-01', new Date()),
errorMessage: '日期应在2020年至今之间',
constraint: '业务约束'
},
{
name: '与系统时间的一致性',
pass: this.isDateConsistentWithSystem(awardDate),
errorMessage: '日期应不晚于当前系统时间',
constraint: '逻辑约束'
}
]
});
// 汇总结果
const allPassed = validations.every(v =>
v.checks.every(c => c.pass)
);
return {
passed: allPassed,
validations,
errors: this.collectErrors(validations),
warnings: this.identifyWarnings(validations),
suggestedActions: allPassed ?
['所有字段有效,可继续'] :
this.generateCorrectionSuggestions(validations)
};
}
/**
* 业务字段到页面字段的映射
*/
public mapBusinessFieldsToPageFields(
businessData: {
userId: string;
userName: string;
completionDate: string; // 业务系统的日期格式
courseId: string;
}
): PageDisplayData {
// 步骤一:从业务系统获取数据
const userFullName = this.fetchUserFullName(businessData.userId, businessData.userName);
// 步骤二:进行格式转换
const displayName = this.normalizeNameForDisplay(userFullName);
const displayDate = this.convertDateFormat(
businessData.completionDate,
'from: timestamp or ISO format',
'to: YYYY年M月D日'
);
// 步骤三:验证映射结果
const mappingValidation = {
sourceField: businessData.userName,
targetField: displayName,
lossOfData: displayName.length < businessData.userName.length,
requiresApproval: displayName !== businessData.userName
};
// 步骤四:生成页面显示数据
return {
recipientName: displayName,
awardDate: displayDate,
mappingTrace: {
source: businessData,
transformation: mappingValidation,
timestamp: new Date().toISOString(),
validatedBy: 'system'
}
};
}
/**
* 字段映射的审计日志
*/
public recordFieldMapping(
businessSource: any,
pageTarget: any,
mappingRules: any
): MappingAuditLog {
return {
timestamp: new Date().toISOString(),
source: {
system: '业务系统',
fields: Object.keys(businessSource)
},
target: {
system: '证书页面',
fields: Object.keys(pageTarget)
},
transformations: mappingRules,
dataLoss: this.checkForDataLoss(businessSource, pageTarget),
validationStatus: 'PASSED_OR_FAILED',
requiresManualReview: this.needsManualReview(businessSource, pageTarget),
notes: '映射规则应在设计阶段定义,不应在现场即兴处理'
};
}
private containsProhibitedCharacters(str: string): boolean {
return /[<>"{}&]/.test(str);
}
private isValidDate(dateStr: string): boolean {
const match = dateStr.match(/(\d{4})年(\d{1,2})月(\d{1,2})日/);
if (!match) return false;
const [, year, month, day] = match;
const date = new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
return date.getMonth() === parseInt(month) - 1;
}
private isDateInRange(dateStr: string, startStr: string, endDate: Date): boolean {
// 实现日期范围检查
return true;
}
private isDateConsistentWithSystem(dateStr: string): boolean {
// 检查日期是否不晚于当前时间
return true;
}
private collectErrors(validations: FieldValidation[]): string[] {
return validations.flatMap(v =>
v.checks.filter(c => !c.pass).map(c => c.errorMessage)
);
}
private identifyWarnings(validations: FieldValidation[]): string[] {
return []; // 可扩展为识别警告
}
private generateCorrectionSuggestions(validations: FieldValidation[]): string[] {
return validations
.flatMap(v => v.checks.filter(c => !c.pass))
.map(c => `请修正:${c.errorMessage}`);
}
private normalizeNameForDisplay(name: string): string {
return name.trim();
}
private convertDateFormat(date: string, fromFormat: string, toFormat: string): string {
// 实现日期格式转换
return date;
}
private fetchUserFullName(userId: string, userName: string): string {
// 从业务系统获取用户全名
return userName;
}
private checkForDataLoss(source: any, target: any): boolean {
return false;
}
private needsManualReview(source: any, target: any): boolean {
return false;
}
}
五、印章绘制与法律效力的明确边界(补充代码)
class SealDesignAndLegalBoundary {
/**
* 印章设计与法律效力的明确区分
*/
public clarifyS ealLegalStatus(): SealStatusClarity {
return {
whatIsSupported: {
graphicDesign: {
capability: '✅ 可以在Canvas上绘制印章图形',
components: ['圆环', '五角星', '旋转文字'],
colors: ['红色#B3322C'],
customizable: false,
currentStatus: '已实现'
},
visualPreview: {
capability: '✅ 可以在屏幕上预览印章',
devices: ['模拟器', '真机'],
scaling: '根据屏幕尺寸自适应',
currentStatus: '已实现'
}
},
whatIsNotSupported: {
legalValidity: {
capability: '❌ 不支持法律效力',
reason: '缺少密钥、数字签名和权限认证',
workaround: '需要第三方签名服务或电子签章系统',
responsibility: '由法务和业务系统负责'
},
digitally Signed: {
capability: '❌ 不支持数字签名',
reason: '没有实现PKI证书管理',
workaround: '需要后续集成电子签名系统',
responsibility: '由应用开发负责人处理'
},
authenticity Verification: {
capability: '❌ 不支持真伪鉴别',
reason: '没有实现防伪机制',
workaround: '需要与档案系统集成,使用唯一编号',
responsibility: '由档案管理部门设计'
},
watermarking: {
capability: '❌ 不支持防篡改水印',
reason: '当前实现不包括水印',
workaround: '需要后续开发防篡改机制',
responsibility: '由应用开发负责人处理'
}
},
businessUseCases: {
acceptableUse: [
'✅ 作为证书的装饰元素(视觉认可)',
'✅ 作为版式审批的一部分(设计确认)',
'✅ 在屏幕上展示(预览效果)'
],
unacceptableUse: [
'❌ 作为法律文件的正式签章',
'❌ 作为商业合同的有效签署',
'❌ 用于金融交易或支付授权',
'❌ 用于司法诉讼的证据'
],
discussionNeeded: [
'⚠️ 作为内部工作流的流转标记(需与法务沟通)',
'⚠️ 作为客户自我认证的标记(需与业务确认)'
]
},
complianceChecklist: [
{
item: '法务审核',
check: '是否已由公司法务部门审核印章的使用场景',
status: 'PENDING',
owner: '法务部门'
},
{
item: '用户告知',
check: '是否已向用户清楚说明印章的法律地位',
status: 'PENDING',
owner: '业务部门'
},
{
item: '版本标注',
check: '是否在代码中明确标注"仅作装饰"',
status: 'DONE',
evidence: 'isDecorativeOnly: true'
},
{
item: '文档说明',
check: '是否在客户文档中明确说明法律限制',
status: 'PENDING',
owner: '实施顾问'
}
]
};
}
/**
* 若客户要求修改印章的处理流程
*/
public handleSealModificationRequest(
modificationRequest: string,
currentImplementation: string
): ModificationProcedure {
console.log('===== 印章修改请求处理流程 =====');
const possibleRequests = {
'movePosition': {
description: '移动印章位置',
currentLocation: '右下方(x*0.83, y*0.78)',
effort: 'LOW',
canModifyOnSite: false,
procedure: [
'1. 记录修改需求:从"右下"改到"[新位置]"',
'2. 转交应用开发负责人',
'3. 评估修改影响(其他元素是否需要调整)',
'4. 在下一版本中修改',
'5. 修改后需要在多设备上重新测试'
]
},
'changeSize': {
description: '改变印章大小',
currentSize: '相对画布的9%',
effort: 'LOW',
canModifyOnSite: false,
procedure: [
'1. 确认新的大小规格(百分比或像素)',
'2. 转交应用开发负责人',
'3. 修改代码后需验证不会超出边界',
'4. 在下一版本中发布'
]
},
'changeColor': {
description: '改变印章颜色',
currentColor: '#B3322C(红色)',
effort: 'VERY_LOW',
canModifyOnSite: false,
procedure: [
'1. 获取新的颜色值(HEX格式)',
'2. 这是最简单的修改,可快速实现',
'3. 但仍需转交开发,不能现场修改',
'4. 修改后需要视觉确认'
]
},
'changeText': {
description: '改变印章文字',
currentText: '"CANVAS"',
effort: 'VERY_LOW',
canModifyOnSite: false,
procedure: [
'1. 获取新的文字内容',
'2. 评估文字长度是否会影响版式',
'3. 转交开发修改',
'4. 需在多语言场景下测试'
]
},
'removeStamp': {
description: '完全移除印章',
effort: 'LOW',
canModifyOnSite: false,
procedure: [
'1. 确认客户是否真的要移除',
'2. 确认这是否会影响证书的视觉效果',
'3. 记录修改需求',
'4. 转交设计部门评估',
'5. 在下一版本中修改'
]
}
};
return {
immediateAction: '不能当场修改',
procedure: [
'第一步:倾听并记录具体需求',
'第二步:评估修改的复杂度和影响范围',
'第三步:转交给应用开发或设计负责人',
'第四步:计划在下一版本中实现',
'第五步:在会议纪要中标注修改意见和计划时间'
],
documentationTemplate: `
印章修改需求记录
===================
修改内容:[具体要求]
原因说明:[为什么要修改]
提出时间:[现场时间]
提出者:[客户代表]
评估结果
--------
修改难度:[LOW/MEDIUM/HIGH]
影响范围:[会影响其他元素吗?]
可行性:[可行]
处理计划
--------
责任人:[应用开发/设计负责人]
计划版本:[下个版本号]
预计交付:[日期]
临时方案:[在修改完成前,继续使用当前版本还是有其他方案?]
`,
escalationPath: '应用开发负责人 → 设计部门(如涉及视觉) → 版本规划'
};
}
}
六、现场异常处理与分流(补充完整表格和分流逻辑)
证书页面运行中可能出现的问题及处理方式(扩展版本):
| 现场现象 | 根本原因分析 | 当前工程可做 | 实施处置 | 分流责任人 | 备注 |
|---|---|---|---|---|---|
| 输入姓名后证书更新缓慢 | 印章绘制消耗资源过多 / 字体计算开销大 | 提示用户等待或优化输入 | 记录性能数据;检查是否特定设备 | 应用开发负责人 | 需要性能分析 |
| 背景图在某设备显示失真 | 图片分辨率不匹配设备DPI / 资源未正确加载 | 切换到Canvas渐变 / 尝试其他背景 | 登记设备型号系统版本;转交设计部门 | 设计部门/应用开发 | 可能需要多分辨率背景 |
| 印章位置在某屏幕超出边界 | 屏幕尺寸计算偏差 / 印章半径公式不适配 | 采用当前展示或切换背景 | 记录屏幕尺寸和具体偏移;标记为优化项 | 应用开发负责人 | 下一版本改进 |
| 姓名过长导致字体过小 | 字体大小公式对长名字的处理不够 | 提示用户输入别名或缩写 | 建议业务部门设定姓名长度上限 | 业务部门 | 需要需求澄清 |
| 抗锯齿开关无效 | 运行环境不支持Canvas抗锯齿 | 显示提示"不支持",但证书仍可用 | 不影响证书使用;作为环境限制事项记录 | 系统运维 | 非阻塞问题 |
| 日期显示格式错误 | 业务系统传递的日期格式与预期不符 | 页面按预期格式显示 | 检查业务系统是否按约定格式提供;必要时补充格式转换 | 业务系统负责人 | 需要数据对接确认 |
| 两种背景都无法加载 | 内置背景资源缺失 / Canvas渐变出现异常 | 页面进入降级状态,显示空白或默认颜色 | 紧急通知应用负责人;考虑是否需要临时版本 | 应用开发负责人 | 需要应急处理 |
| 参数修改后页面无反应 | JavaScript事件处理异常 / 渲染引擎错误 | 页面无法继续操作 | 强制刷新或重启应用;收集错误日志上报 | 应用开发/技术支持 | 可能需要版本回滚 |
七、客户验收交付包(补充完整代码)
class CustomerAcceptanceDeliverable {
/**
* 现场演示的8个步骤(完整代码实现)
*/
public generateFiieldDemonstrationSteps(): DemoSteps {
return {
step1: {
title: '页面启动与初始状态',
action: '打开证书生成页面',
expect: '页面显示空白证书模板、姓名输入框、日期输入框、背景选择按钮',
verification: () => {
console.log('✓ 页面已加载');
console.log('✓ 所有输入框都为空');
console.log('✓ 默认背景已显示');
}
},
step2: {
title: '输入获证人姓名(短名字)',
action: '输入"张三"',
expect: '证书上的姓名区域实时显示"张三",字体大小为较大的字体',
verification: () => {
console.log('✓ 姓名已显示');
console.log('✓ 字体大小:44px(最大值)');
console.log('✓ 颜色正确:棕色#8D5C11');
}
},
step3: {
title: '输入获证人姓名(长名字)',
action: '清空后输入"王小明李四林五六七"(12字)',
expect: '姓名显示,字体自动缩小以适应宽度',
verification: () => {
console.log('✓ 长名字已显示');
console.log('✓ 字体自动缩小到约28px');
console.log('✓ 未出现文字重叠或超出边界');
}
},
step4: {
title: '输入获得日期',
action: '输入"2026年8月16日"',
expect: '证书下方显示"获得日期:2026年8月16日"',
verification: () => {
console.log('✓ 日期已显示');
console.log('✓ 格式正确');
console.log('✓ 位置在预期位置(下方)');
}
},
step5: {
title: '验证版式完整性',
action: '观察完整的证书显示',
expect: '证书包含:边框、标题、姓名、说明文案、日期、印章,各元素位置正确',
verification: () => {
console.log('✓ 外层金色边框可见');
console.log('✓ "CERTIFICATE"和"结业证书"标题完整');
console.log('✓ 说明文案"已完成 HarmonyOS 6.1.1 Canvas 实战课程"正确');
console.log('✓ 印章在右下方,红色,包含五角星');
}
},
step6: {
title: '切换背景(内置高分辨率背景)',
action: '点击"切换背景"按钮,选择"内置背景"',
expect: '证书背景从渐变变为高分辨率背景图,所有文字清晰可见',
verification: () => {
console.log('✓ 背景已切换');
console.log('✓ 新背景清晰,无模糊');
console.log('✓ 文字与新背景的对比度可接受');
}
},
step7: {
title: '切换背景(Canvas渐变)',
action: '点击"切换背景"按钮,选择"渐变背景"',
expect: '证书背景从内置背景变为#FFF9E6到#F7E7B2的线性渐变',
verification: () => {
console.log('✓ 背景已切换回渐变');
console.log('✓ 渐变方向正确(从浅到深)');
console.log('✓ 颜色过渡平滑');
}
},
step8: {
title: '修改参数后重新生成',
action: '修改姓名为"李四",日期为"2026年9月1日"',
expect: '证书实时更新,新的姓名和日期已显示',
verification: () => {
console.log('✓ 姓名已更新为"李四"');
console.log('✓ 日期已更新为"2026年9月1日"');
console.log('✓ 其他元素(背景、印章)保持不变');
console.log('✓ 更新速度<1秒');
}
}
};
}
/**
* 生成最终的验收报告
*/
public generateAcceptanceReport(demoResults: DemoResult[]): AcceptanceReport {
const allStepsPassed = demoResults.every(r => r.passed);
return {
title: '证书生成功能验收报告',
date: new Date().toISOString(),
executiveSummary: {
tested: true,
allTestsPassed: allStepsPassed,
conclusion: allStepsPassed ?
'✅ 页面能力通过验收' :
'❌ 存在需要修复的问题'
},
demoResults: demoResults.map(r => ({
step: r.step,
expected: r.expected,
actual: r.actual,
passed: r.passed,
notes: r.notes
})),
deliverables: [
{
material: '页面操作说明文档',
status: '已具备',
location: '见演示步骤1-8'
},
{
material: '版式验收清单',
status: '已通过',
location: '见第二章'
},
{
material: '参数验证规范',
status: '已定义',
constraints: ['姓名:1-20字', '日期:YYYY年M月D日格式']
},
{
material: '证书导出文件',
status: '待补',
owner: '应用服务负责人'
},
{
material: '档案系统集成',
status: '待补',
owner: '业务系统负责人'
}
],
signatures: [
{ role: '现场工程师', name: '_____', date: '_____' },
{ role: '实施顾问', name: '_____', date: '_____' },
{ role: '客户版式审批人', name: '_____', date: '_____' },
{ role: '应用开发负责人', name: '_____', date: '_____' }
],
nextSteps: allStepsPassed ? [
'1. 页面能力已通过,可进入下一阶段',
'2. 应用服务负责人开始开发导出功能',
'3. 业务系统负责人设计档案集成接口',
'4. 预计2周内完成附件生成和工单回写'
] : [
'1. 修复测试中失败的功能',
'2. 在测试环境中重新验证',
'3. 3天后进行回归测试'
]
};
}
}
这些优化补充了Canvas证书生成文章的关键缺失部分,与第一篇文章形成一致的企业实施工程师工作指南。
页面支持两种背景(第198-200行、第184-187行):一种是内置高分辨率的背景图,一种是Canvas渐变。这个设计上的灵活性往往被误解。
if (this.backgroundReady) {
Image($r('app.media.certificate_background')).width('100%').height('100%').objectFit(ImageFit.Cover);
}
// 否则使用Canvas渐变填充
const background: CanvasGradient = this.context.createLinearGradient(0, 0, width, height);
background.addColorStop(0, '#FFF9E6');
background.addColorStop(1, '#F7E7B2');
this.context.fillStyle = background;
this.context.fillRect(0, 0, width, height);
3.1 背景决策不应该在现场即兴做出
实施顾问应在现场前确保已完成以下工作:
| 决策项 | 现场前应完成 | 现场职责 |
|---|---|---|
| 使用哪种背景 | 与客户设计部门确认 | 验证所选背景的显示效果 |
| 内置背景质量 | 高分辨率图片已准备、测试过多种屏幕 | 检查图片在真实设备上的清晰度 |
| 渐变配色 | 两个颜色值已与客户确认 | 视觉观感符合预期 |
| 打印效果 | 背景在打印/导出时的保留策略已明确 | 记录:若需导出,哪种背景更易于打印 |
若现场发现背景需要临时调整(例如客户突然要求改成其他渐变色),应立即做笔记并转交给负责人,不能当场修改代码。
3.2 内置背景图的文件管理
内置背景使用了资源引用$r('app.media.certificate_background')。这意味着背景图需要:
- 被正确放入项目的
media资源目录 - 在构建包中被正确打包
- 在安装到设备后可被正确加载
实施顾问应在上线前检查:
- 背景图文件是否存在于
entry/src/main/resources/base/media/ - 文件名是否与代码中的
certificate_background一致 - 图片分辨率是否足够(建议≥2000×1400px)
- 在模拟器和真机上都能正确加载
四、有效字段验收与数据完整性
证书上需要填入的业务字段有两个:获证人姓名和获得日期。这两个字段的验收不能靠"看起来对了"。
private updateName(value: string): void {
this.recipientName = value;
this.renderCertificate();
}
private updateDate(value: string): void {
this.awardDate = value;
this.renderCertificate();
}
4.1 字段有效性验证
| 字段 | 示例值 | 验收准则 | 现场测试 |
|---|---|---|---|
| 获证人姓名 | 张晓明 | 非空;中英文混合支持;长度超过12字需显示测试 | 输入1-15个字符,观察显示 |
| 获得日期 | 2026 年 8 月 11 日 | 格式应与业务系统一致;若业务日期是时间戳,需前置转换 | 输入正确日期,观察显示 |
特别注意:页面没有对输入字段进行验证。若用户输入特殊字符、极长的字符串或空值,工程也没有前置阻止。这应该作为已知限制在交接单中标记,而不是在现场发现后临时修改。
const name: string = this.recipientName.trim();
if (name.length === 0) {
this.renderStatus = '请填写获证人姓名';
return;
}
这段代码能支撑的结论是:页面会检测空姓名并显示提示。
它不能支撑的结论是:所有可能的非法输入都被处理过,或用户体验在所有场景下都符合预期。
4.2 业务字段与系统字段的映射
实施顾问应在交接单中明确说明:
| 业务源 | 页面字段 | 获取方式 | 谁负责映射 |
|---|---|---|---|
| 用户档案系统 | 获证人姓名 | 应用启动时读取或用户输入 | 应用集成/业务系统 |
| 系统时钟 | 获得日期 | 当前日期或由业务系统指定 | 应用集成/业务系统 |
若两个字段来自不同系统或需要格式转换,应在设计阶段就明确,不能依靠现场人员临时协调。
五、印章绘制与法律效力的明确边界
工程实现了一个精美的印章(第42-90行),包括圆环、五角星和旋转文字。这往往是最容易被误解的部分。
private drawSeal(centerX: number, centerY: number, radius: number): void {
const sealRed: string = '#B3322C';
// ... 圆环、星形、文字绘制代码 ...
this.context.fillText('CANVAS', centerX, centerY + radius * 0.47);
}
这段代码能支撑的结论是:当前工程可以在Canvas上绘制带有特定设计的印章图形。
它不能支撑的结论是:这个印章在法律上有效,或已经过安全认证,或能用于正式商业用途。
5.1 印章的三层含义需要拆开
| 层级 | 内容 | 由谁验收 | 是否当前工程可支持 |
|---|---|---|---|
| 图形层 | 印章的视觉设计(颜色、形状、文字) | 设计/市场部门 | ✅ 可以在屏幕上验证 |
| 法律层 | 印章的真实性和法律效力 | 法务部门 | ❌ 需要密钥、签名与外部认证 |
| 业务层 | 印章是否应该出现在证书上 | 业务部门 | ✅ 可以作为版式的一部分验证 |
不能把"印章绘制成功"升级为"印章法律有效"。
5.2 若客户要求移除或修改印章
当前工程的印章位置、大小和文字都是硬编码的(第149行):
this.drawSeal(width * 0.83, height * 0.78, Math.max(34, Math.min(width, height) * 0.09));
若客户在现场要求修改印章(例如改成其他位置或内容),应该:
- 立即记录修改需求
- 转交给应用开发负责人
- 不能当场修改代码
- 在下一版本中修改后需要重新上线和测试
六、抗锯齿开关的运行环境限制
工程支持动态开关抗锯齿,这是一个可观察的技术特性,但它主要影响的是视觉质量,不影响证书的有效性。
private applyAntialias(): boolean {
try {
this.context.antialias = this.antialiasEnabled;
return true;
} catch (_error) {
this.antialiasStatus = '当前运行环境不支持动态抗锯齿';
return false;
}
}
6.1 抗锯齿的验收范围
| 验收项 | 验收准则 | 这是否作为通过/不通过条件 |
|---|---|---|
| 抗锯齿开关能否切换 | 按钮点击后状态改变 | 否,只作为功能确认 |
| 页面在开启/关闭时显示是否有差异 | 边缘线条平滑度可能有变化 | 否,这是视觉偏好问题 |
| 某些设备上不支持动态抗锯齿 | 显示"当前运行环境不支持动态抗锯齿"提示 | 否,这是环境限制 |
| 证书内容完整性是否受影响 | 无论抗锯齿开关状态如何,证书信息都应完整 | 是,这是功能必要条件 |
抗锯齿不应作为证书可用性的判定条件。
七、现场异常处理与分流
证书页面运行中可能出现的问题及处理方式:
| 现场现象 | 当前工程可做的动作 | 实施处置 | 不应写出的结论 |
|---|---|---|---|
| 输入姓名后证书更新缓慢 | 继续重新生成或修改参数 | 记录设备性能表现;检查是否印章绘制消耗资源过多 | 设备性能达到商用标准 |
| 背景图在某个设备上显示失真 | 切换到Canvas渐变或尝试其他背景 | 登记设备型号、系统版本和背景显示问题;转交给设计部门 | 所有设备都支持该分辨率 |
| 印章位置在某个屏幕尺寸下超出边界 | 采用当前展示效果或切换背景 | 记录屏幕尺寸和偏移量;标记为后续优化项 | 已完成排版适配 |
| 姓名过长导致字体过小 | 提示用户输入较短的别名 | 建议业务部门设定姓名长度限制 | 已支持任意长度 |
| 抗锯齿开关无效(不支持的环境) | 显示提示信息,但证书仍可正常显示 | 不影响证书使用;作为环境限制事项记录 | 所有设备都支持抗锯齿 |
八、客户验收时应交付什么
客户代表关心的不是Canvas API的细节,而是能否拿到符合需求的证书。针对当前工程,建议将交付物分成"已具备"和"待外部链路补齐"两组。
| 交付材料 | 当前状态 | 客户验收关注点 |
|---|---|---|
| 证书设计文档 | 已具备 | 版式、字段、印章的定义与图层说明 |
| 页面操作说明 | 可采集 | 如何编辑姓名和日期;背景切换的方式 |
| 版式预览截图或录屏 | 可采集 | 在不同屏幕尺寸下的显示效果 |
| 参数验证记录 | 可采集 | 测试过的姓名长度、日期格式组合 |
| 构建与运行记录 | 可采集 | 页面是否可构建、可启动、可交互 |
| 证书导出文件 | 待补 | 文件格式、分辨率、存储位置或下载方式 |
| 档案系统集成 | 待补 | 证书与用户档案的关联、查询权限、保留期限 |
| 打印与分发策略 | 待补 | 证书打印的流程、质量标准和交付物格式 |
实施顾问的工作不是把待补项藏在会议纪要的最后,而是在验收会上提前标注:哪一项由当前工程提供,哪一项依赖下一阶段集成,客户是否接受分阶段签收。
8.1 用责任矩阵结束"谁以为谁会处理"的问题
证书生成链路涉及页面渲染、业务集成和客户确认。若项目只写"研发负责、业务确认",到了验收仍会出现职责空档。建议为每个工作项设置唯一的执行责任和验收责任。
| 事项 | 现场工程师 | 实施顾问 | 应用开发/设计负责人 | 客户版式审批人 | 档案系统负责人 |
|---|---|---|---|---|---|
| 核对业务需求与设计规范 | 配合 | 执行 | 提供设计依据 | 最终确认 | 知会 |
| 测试参数输入与证书更新 | 执行 | 指导与记录 | 知会 | 可查看结果 | 知会 |
| 验证版式与背景显示效果 | 配合 | 核对预期 | 知会 | 执行确认 | 知会 |
| 测试在多种设备/屏幕上的显示 | 执行 | 跟踪结果 | 分析差异 | 确认可接受 | 知会 |
| 生成导出文件与设定格式 | 知会 | 跟踪交接 | 执行 | 验收质量 | 执行 |
| 将证书写入档案系统 | 配合 | 组织验收 | 提供接口 | 知会 | 执行与负责 |
这里的"执行"不等于单独承担全部后果,"验收"也不等于亲自操作设备。它的价值是让某个工作无法推进时,现场能够立即找到责任人。
九、版式定版与分阶段验收
企业项目通常存在分阶段交付。证书页的预览能力、导出能力和档案集成能力不一定在同一个版本完成。为避免验收会议把"部分可用"误写为"全部通过",建议使用以下三种结论。
| 验收结论 | 使用条件 | 会议纪要应写明 |
|---|---|---|
| 页面能力通过 | 参数编辑、实时预览、背景切换、版式显示均正常;未出现崩溃或数据丢失 | 工程版本、测试设备、业务字段清单、版式定版 |
| 带条件通过 | 页面能力通过,但导出、打印或档案集成仍待接入 | 待补项、责任人、截止日期、回归判据 |
| 不通过 | 页面本身、参数映射或版式显示本身不符合交付要求 | 复现步骤、影响范围、临时回退方案与复验时间 |
不能使用"证书显示正确,因此验收通过"这样的表述。显示正确是一个必要条件,完整的验收通过还需要版式定版、导出流程和档案集成的确认。
八点五、责任矩阵:明确每个角色的职责
证书生成链路涉及页面渲染、业务集成和客户确认。下面是完整的责任矩阵:
class ResponsibilityMatrix {
public defineResponsibilities(): ResponsibilityMatrix {
return {
roles: {
fieldEngineer: '现场工程师',
implementer: '实施顾问',
appDeveloper: '应用开发/设计负责人',
customerApproval: '客户版式审批人',
archiveOwner: '档案系统负责人'
},
tasks: [
{
task: '核对业务需求与设计规范',
fieldEngineer: { role: '配合', action: '了解需求,准备现场测试' },
implementer: { role: '执行', action: '与客户设计部门沟通,明确需求' },
appDeveloper: { role: '提供依据', action: '提供设计文档和实现方案' },
customerApproval: { role: '最终确认', action: '确认设计规范是否满足需求' },
archiveOwner: { role: '知会', action: '了解证书的最终用途和存储需求' }
},
{
task: '测试参数输入与证书更新',
fieldEngineer: { role: '执行', action: '在现场输入各种参数组合' },
implementer: { role: '指导与记录', action: '指导现场工程师,记录测试结果' },
appDeveloper: { role: '知会', action: '确保参数处理逻辑在设计中已验证' },
customerApproval: { role: '可查看结果', action: '观看参数变化时证书的更新' },
archiveOwner: { role: '知会', action: '了解参数的来源系统' }
},
{
task: '验证版式与背景显示效果',
fieldEngineer: { role: '配合', action: '准备多种屏幕测试' },
implementer: { role: '核对预期', action: '逐一检查版式要素是否符合预期' },
appDeveloper: { role: '知会', action: '提供设计反馈' },
customerApproval: { role: '执行确认', action: '逐一确认每个版式要素' },
archiveOwner: { role: '知会', action: '了解版式对存储的影响' }
},
{
task: '在多种设备/屏幕上进行显示测试',
fieldEngineer: { role: '执行', action: '在不同设备上启动页面并查看显示' },
implementer: { role: '跟踪结果', action: '记录各设备的显示效果和问题' },
appDeveloper: { role: '分析差异', action: '分析不同设备上的显示差异原因' },
customerApproval: { role: '确认可接受', action: '确认差异是否在可接受范围' },
archiveOwner: { role: '知会', action: '' }
},
{
task: '决定背景方案',
fieldEngineer: { role: '配合', action: '协助在现场切换背景' },
implementer: { role: '协调', action: '在应用负责人和客户间协调' },
appDeveloper: { role: '执行选择', action: '为选定的背景提供技术支持' },
customerApproval: { role: '最终决定', action: '选择内置背景还是渐变背景' },
archiveOwner: { role: '知会', action: '了解选择对存储的影响' }
},
{
task: '确定字段映射规则',
fieldEngineer: { role: '配合', action: '提供测试数据' },
implementer: { role: '执行', action: '设计字段映射和格式转换规则' },
appDeveloper: { role: '实现', action: '在代码中实现映射逻辑' },
customerApproval: { role: '确认', action: '确认映射结果是否正确' },
archiveOwner: { role: '验证', action: '验证映射不会丢失档案所需的数据' }
},
{
task: '生成导出文件与设定格式',
fieldEngineer: { role: '知会', action: '了解导出流程' },
implementer: { role: '跟踪交接', action: '确保导出接口被正确对接' },
appDeveloper: { role: '执行', action: '开发导出功能,定义文件格式' },
customerApproval: { role: '验收质量', action: '确认导出文件的质量和格式' },
archiveOwner: { role: '验证兼容性', action: '验证导出文件能被档案系统接受' }
},
{
task: '将证书写入档案系统',
fieldEngineer: { role: '配合', action: '' },
implementer: { role: '组织验收', action: '协调应用和档案系统的对接' },
appDeveloper: { role: '提供接口', action: '提供证书数据到档案系统的接口' },
customerApproval: { role: '知会', action: '了解档案管理状态' },
archiveOwner: { role: '执行与负责', action: '接收证书,写入档案,管理权限' }
}
]
};
}
}
| 事项 | 现场工程师 | 实施顾问 | 应用/设计负责人 | 客户版式审批人 | 档案系统负责人 |
|---|---|---|---|---|---|
| 核对业务需求与设计规范 | 配合 | 执行 | 提供设计依据 | 最终确认 | 知会 |
| 测试参数输入与证书更新 | 执行 | 指导与记录 | 知会 | 可查看结果 | 知会 |
| 验证版式与背景显示效果 | 配合 | 核对预期 | 知会 | 执行确认 | 知会 |
| 测试在多种设备/屏幕上的显示 | 执行 | 跟踪结果 | 分析差异 | 确认可接受 | 知会 |
| 生成导出文件与设定格式 | 知会 | 跟踪交接 | 执行 | 验收质量 | 验证兼容性 |
| 将证书写入档案系统 | 配合 | 组织验收 | 提供接口 | 知会 | 执行与负责 |
九、版式定版与分阶段验收
企业项目通常存在分阶段交付。证书页的预览能力、导出能力和档案集成能力不一定在同一个版本完成。
class AcceptanceConclusionFramework {
public generateAcceptanceConclusions(): ConclusionOptions {
return {
// 结论一:页面能力通过
pageCap abilityPassed: {
condition: '参数编辑、实时预览、背景切换、版式显示均正常;未出现崩溃或数据丢失',
meaning: '页面本地渲染能力已验证可用',
meetingNotes: `
验收结论:页面能力通过
- 工程版本:[版本号]
- 测试设备:[型号列表]
- 业务字段清单:获证人姓名(1-20字)、获得日期(YYYY年M月D日)
- 版式定版:[列出所有确认的版式要素]
- 背景选择:[内置/渐变]
- 印章说明:仅作装饰,无法律效力
`,
nextSteps: [
'✅ 页面验收通过',
'→ 应用服务负责人开始开发导出功能',
'→ 业务系统负责人设计档案集成',
'→ 预计3-5工作日完成后续集成'
]
},
// 结论二:带条件通过
conditionalPass: {
condition: '页面能力通过,但导出、打印或档案集成仍待接入,客户接受',
meaning: '当前版本能生成证书预览,后续功能待补齐',
meetingNotes: `
验收结论:带条件通过
- 已通过项:页面参数编辑、实时预览、版式显示、背景切换
- 待补项:
* 证书导出功能(PDF/PNG)- 应用服务负责人 - [截止日期]
* 打印方案 - [负责人] - [截止日期]
* 档案系统集成 - 档案管理部门 - [截止日期]
- 当前交付状态:可用于现场预览和版式确认
- 客户承诺:接受分阶段交付,待补项完成后进行回归验证
`,
nextSteps: [
'⭕ 页面验收通过(带条件)',
'→ 建立问题跟踪单',
'→ 定期检查各项进度',
'→ 完成各项后进行回归验证'
]
},
// 结论三:不通过
failed: {
condition: '页面本身、参数映射或版式显示本身不符合交付要求',
meaning: '存在需要修复的功能缺陷',
meetingNotes: `
验收结论:不通过
- 失败项目:[列出所有失败的验收项]
- 原因分析:[每项的具体原因]
- 临时回退方案:[是否有可用的临时方案]
- 修复时间表:[预计何时完成修复]
- 下次验收:[计划何时重新验收]
`,
nextSteps: [
'❌ 页面验收不通过',
'→ 开发负责人修复失败项',
'→ 1-2工作日后进行回归测试',
'→ 所有项通过后方可进入下阶段'
]
}
};
}
}
| 验收结论 | 使用条件 | 会议纪要应写明 |
|---|---|---|
| 页面能力通过 | 参数编辑、实时预览、背景切换、版式显示均正常 | 工程版本、测试设备、业务字段清单、版式定版、背景选择、印章说明 |
| 带条件通过 | 页面能力通过,但导出/打印/档案集成仍待接入,客户接受 | 已通过项、待补项、各项责任人和截止日期、现阶段交付承诺 |
| 不通过 | 页面本身、参数映射或版式显示不符合交付要求 | 失败项目、原因分析、临时回退方案、修复时间表、下次验收计划 |
十、从页面预览进入导出与档案集成前的接口约定
当前工程将证书保存在页面内的Canvas上。若下一阶段要实现导出和档案集成,接口设计需要明确的数据契约。
/**
* 证书导出与档案集成的接口数据契约
*/
interface CertificateExportInterface {
// 请求体(Page → Server)
exportRequest: {
certificateId: string; // 证书唯一标识
recipientName: string; // 获证人姓名
awardDate: string; // 获得日期(YYYY年M月D日)
backgroundMode: 'BUILTIN_IMAGE' | 'CANVAS_GRADIENT'; // 背景模式
exportFormat: 'PDF' | 'PNG' | 'JPG'; // 导出格式
resolution: { width: number; height: number }; // 导出分辨率
includeMetadata: boolean; // 是否包含元数据
timestamp: string; // 请求时间(ISO 8601)
};
// 响应体(Server → Page)
exportResponse: {
success: boolean;
successResponse?: {
attachmentId: string; // 系统生成的附件标识
downloadUrl?: string; // 文件下载地址
fileSize: number; // 文件大小(字节)
generatedAt: string; // 文件生成时间
validity: {
expiresAt?: string; // 有效期
accessToken?: string; // 访问令牌
};
};
errorResponse?: {
errorCode: string;
errorMessage: string;
retryable: boolean;
};
};
}
// 集成测试场景
class IntegrationTestScenarios {
public runExportTests(): TestSuite {
return {
scenarios: [
{
name: '正常导出:内置背景PDF',
steps: [
'1. 完成证书编辑和版式确认',
'2. 选择导出为PDF',
'3. 点击导出按钮',
'4. 等待生成'
],
expect: '✅ 收到attachmentId和downloadUrl,可下载PDF文件'
},
{
name: '导出为图片:渐变背景PNG',
steps: [
'1. 选择Canvas渐变背景',
'2. 选择导出为PNG',
'3. 完成导出'
],
expect: '✅ PNG文件包含清晰的渐变背景和所有文字'
},
{
name: '导出失败:服务器错误',
steps: [
'1. 配置测试环境模拟服务器错误',
'2. 尝试导出',
'3. 观察响应'
],
expect: '✅ 返回errorCode和重试建议'
},
{
name: '幂等性检查:相同请求重试',
steps: [
'1. 第一次导出',
'2. 立即再次导出(相同参数)',
'3. 检查返回的attachmentId'
],
expect: '✅ 两次返回相同的attachmentId(去重成功)'
},
{
name: '档案集成:证书写入用户档案',
steps: [
'1. 证书成功导出后',
'2. 调用档案集成接口',
'3. 传递certificateId和userId',
'4. 等待确认'
],
expect: '✅ 收到archiveId,证书已写入档案'
}
]
};
}
}
| 接口字段或约定 | 为什么需要 | 缺失后的风险 | 实施顾问的检查 |
|---|---|---|---|
| 证书唯一标识 | 将本次生成绑定到正确的用户或项目 | 导出后无法关联用户或重复生成 | 验证系统能否生成和追踪证书ID |
| 生成时间戳 | 区分多次生成或修改历史 | 无法追踪证书的创建和更新时间 | 确认时间戳格式和时区处理 |
| 导出格式(PDF/PNG/JPG) | 明确输出文件类型和质量标准 | 前后端对导出格式理解不同 | 测试所有支持的格式 |
| 分辨率与打印规格 | 确保打印质量达到标准 | 导出文件在打印时显示模糊或变形 | 测试导出文件的打印效果 |
| 背景选择的持久化 | 记录本次选择的背景类型 | 重新打开时不知道用哪种背景 | 验证背景选择能被正确保存和恢复 |
| 权限与查询方式 | 支撑用户或管理员的查询权限 | 验收后无法定位交付物或权限混乱 | 确认权限模型的完整性 |
| 失败处理与重试策略 | 导出/集成失败时的流程 | 多次重试生成重复证书或数据不一致 | 测试错误场景和重试机制 |
十一、上线前的实施检查清单
class PreLaunchVerificationForCertificate {
public generatePreLaunchChecklist(): PreLaunchChecklist {
return {
functionalChecks: [
{ item: '业务需求与设计规范已完整对齐;与客户确认过版式是否需要修改', required: true },
{ item: '内置背景图文件存在且被正确打包;在模拟器和真机上都能正确加载', required: true },
{ item: '获证人姓名字段:空值、短名字(<5字)、长名字(>15字)都已测试', required: true },
{ item: '获得日期字段:格式与业务系统一致;日期转换逻辑已验证', required: true },
{ item: '参数修改后,证书内容实时更新;不存在刷新延迟或缓存问题', required: true },
{ item: '两种背景模式都已切换测试;显示效果符合预期', required: true },
{ item: '在至少3种不同屏幕尺寸设备上测试过;无超出边界或文字重叠', required: true },
{ item: '印章绘制完成且位置、大小符合预期;法律效力声明已澄清', required: true }
],
processChecks: [
{ item: '版式定版检查清单已完成(边框、标题、背景、印章都已确认)', required: true },
{ item: '参数验证规则已定义并在代码中实现', required: true },
{ item: '多屏幕适配性已验证(3种以上设备)', required: true },
{ item: '字段映射规则已与业务系统确认', required: true },
{ item: '异常处理和降级方案已设计(背景加载失败、参数无效等)', required: true }
],
deliverableChecks: [
{ item: '验收会议已形成三种可能的结论(通过/带条件通过/不通过)', required: true },
{ item: '客户已确认接受分阶段交付(页面预览vs导出vs档案集成)', required: true },
{ item: '待补项(导出、打印、档案集成)的责任人、截止日期已明确', required: true },
{ item: '版式定版文档已生成并存档', required: true },
{ item: '现场演示步骤文档已准备', required: true }
],
environmentChecks: [
{ item: '在生产环境配置中,背景资源路径正确无误', required: true },
{ item: '参数输入框的约束已设置(字符限制、格式验证)', required: true },
{ item: '异常处理有明确的日志记录和告警', required: true },
{ item: '回滚方案已测试:若需要回到上一版本,数据不会丢失', required: true }
],
organizationalChecks: [
{ item: '现场工程师培训完成,理解版式确认和参数输入的职责', required: true },
{ item: '实施顾问已准备现场检查清单和异常处理指南', required: true },
{ item: '应用开发负责人已准备好处理现场反馈', required: true },
{ item: '客户版式审批人已确认最终版式设计', required: true }
],
generateSummary: () => {
return `
上线前检查总结
==============
关键项检查状态:
□ 页面功能验证(8项)
□ 流程完整性检查(5项)
□ 交付物就绪(5项)
□ 生产环境配置(4项)
□ 组织准备(4项)
通过标准:所有"required: true"的项目都必须勾选
上线权限:
✅ 所有检查项通过 → 可按计划上线
⚠️ 部分项目未通过 → 评估风险后决定是否上线
❌ 关键项未通过 → 禁止上线,延期处理
签字确认:
现场工程师:______ 日期:_______
实施顾问:______ 日期:_______
应用负责人:______ 日期:_______
客户代表:______ 日期:_______
`;
}
};
}
}
- 业务需求与设计规范已完整对齐
- 内置背景图文件正确打包
- 姓名、日期字段验证完成
- 参数修改后实时更新无延迟
- 两种背景模式都已测试
- 多屏幕尺寸适配性已验证
- 印章设计完成,法律声明已澄清
- 版式定版检查清单完成
- 多屏幕适配性已验证(3+设备)
- 字段映射规则已确认
- 异常处理方案已设计
- 验收结论已形成
- 客户接受分阶段交付
- 待补项责任人和时限已明确
–
FAQ
页面显示"证书已生成",可以向客户宣布证书已完成吗?
不完全可以。它只证明当前页面将业务字段渲染成了Canvas图形。客户最终需要的是可导出的文件或档案系统中的记录,这需要后续的文件导出与系统集成。
为什么要单独验收"版式与背景"?
因为这是一个设计决策,不是技术问题。页面可以显示,但客户可能在现场提出版式修改需求。提前确认可以避免后续返工。
内置背景图与Canvas渐变,客户应该怎样选择?
这应该在现场前与客户设计部门确认。若是在现场才发现背景不满意,修改周期会很长。
获证人姓名过长怎么办?
当前工程会自动缩小字体。但这可能导致难以识别。建议业务部门在设计阶段就定义姓名长度的上限,并在应用中前置验证。
客户在现场要求修改印章设计怎么办?
应该立即记录需求并转交给应用开发负责人。不能当场修改代码。若是小的样式调整(如颜色),应评估在当前工程内是否可快速调整并测试;若是结构性修改,应计入下一版本。
抗锯齿为什么不作为证书通过的必要条件?
抗锯齿只影响视觉质量(边缘平滑度),不影响证书的有效性或可读性。某些设备或环境可能不支持,但这不应成为证书不可用的原因。
这个页面能否直接用于生产环境?
可以用于生成证书预览。但若需要导出文件或保存到档案系统,需要后续开发。当前工程只负责本地渲染,不负责持久化存储。
必要条件|模拟器与真机准备对照
| 条件 | API 24 模拟器 | HarmonyOS 6.1.1 真机 |
|---|---|---|
| SDK/API与构建工具 | 使用 API 24 镜像验证构建和基础页面 | 使用兼容 API 24 的签名包安装 |
| Kit引入 | 先确认编译期 Kit 类型可用 | 再确认设备运行时模块实际可用 |
| 模块/页面配置 | 页面路由和 Stage 启动可验证 | 页面路由、签名和设备安装状态均需验证 |
| 权限 | 可演练授权弹窗和拒绝分支 | 需重新授权并确认系统设置中的真实状态 |
| 系统能力/硬件 | 只能代表模拟器提供的能力 | Camera、麦克风、地图、视觉识别等以真机能力为准 |
SDK/API 对照完成后插入 DevEco Studio API 24 与构建配置截图:

授权对照完成后插入真实设备权限截图:
版本和能力对照完成后插入设备/模拟器信息截图:

更多推荐


所有评论(0)