546 lines
38 KiB
JavaScript
546 lines
38 KiB
JavaScript
const fs = require('fs')
|
||
const path = require('path')
|
||
const JSZip = require('jszip')
|
||
|
||
const outputPath = path.resolve(
|
||
__dirname,
|
||
'..',
|
||
'doc',
|
||
'大连元利流体技术有限公司MES测试环境接管报告_V1.1_2026-07-25.docx'
|
||
)
|
||
|
||
const COLORS = {
|
||
navy: '17365D',
|
||
blue: '1F4E78',
|
||
lightBlue: 'D9EAF7',
|
||
paleBlue: 'EAF2F8',
|
||
green: '548235',
|
||
paleGreen: 'E2F0D9',
|
||
amber: 'BF8F00',
|
||
paleAmber: 'FFF2CC',
|
||
red: 'C00000',
|
||
paleRed: 'FCE4D6',
|
||
gray: '666666',
|
||
lightGray: 'F2F2F2',
|
||
border: 'B4C6E7',
|
||
white: 'FFFFFF'
|
||
}
|
||
|
||
function xmlEscape(value) {
|
||
return String(value == null ? '' : value)
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''')
|
||
}
|
||
|
||
function run(text, options = {}) {
|
||
const props = []
|
||
const font = options.font || '宋体'
|
||
const eastAsia = options.eastAsia || font
|
||
props.push(`<w:rFonts w:ascii="${xmlEscape(font)}" w:hAnsi="${xmlEscape(font)}" w:eastAsia="${xmlEscape(eastAsia)}"/>`)
|
||
if (options.bold) props.push('<w:b/>')
|
||
if (options.italic) props.push('<w:i/>')
|
||
if (options.color) props.push(`<w:color w:val="${options.color}"/>`)
|
||
if (options.size) props.push(`<w:sz w:val="${options.size}"/><w:szCs w:val="${options.size}"/>`)
|
||
if (options.highlight) props.push(`<w:shd w:fill="${options.highlight}"/>`)
|
||
return `<w:r><w:rPr>${props.join('')}</w:rPr><w:t xml:space="preserve">${xmlEscape(text)}</w:t></w:r>`
|
||
}
|
||
|
||
function paragraph(content = '', options = {}) {
|
||
const runs = Array.isArray(content) ? content : [run(content, options.run || {})]
|
||
const props = []
|
||
if (options.style) props.push(`<w:pStyle w:val="${options.style}"/>`)
|
||
if (options.align) props.push(`<w:jc w:val="${options.align}"/>`)
|
||
if (options.keepNext) props.push('<w:keepNext/>')
|
||
if (options.keepLines) props.push('<w:keepLines/>')
|
||
if (options.pageBreakBefore) props.push('<w:pageBreakBefore/>')
|
||
if (options.spacingBefore != null || options.spacingAfter != null || options.line != null) {
|
||
props.push(`<w:spacing${options.spacingBefore != null ? ` w:before="${options.spacingBefore}"` : ''}${options.spacingAfter != null ? ` w:after="${options.spacingAfter}"` : ''}${options.line != null ? ` w:line="${options.line}" w:lineRule="auto"` : ''}/>`)
|
||
}
|
||
if (options.indentLeft != null || options.firstLine != null) {
|
||
props.push(`<w:ind${options.indentLeft != null ? ` w:left="${options.indentLeft}"` : ''}${options.firstLine != null ? ` w:firstLine="${options.firstLine}"` : ''}/>`)
|
||
}
|
||
if (options.borderBottom) {
|
||
props.push(`<w:pBdr><w:bottom w:val="single" w:sz="8" w:space="5" w:color="${options.borderBottom}"/></w:pBdr>`)
|
||
}
|
||
return `<w:p><w:pPr>${props.join('')}</w:pPr>${runs.join('')}</w:p>`
|
||
}
|
||
|
||
function heading(text, level, options = {}) {
|
||
return paragraph(text, {
|
||
style: `Heading${level}`,
|
||
keepNext: true,
|
||
pageBreakBefore: Boolean(options.pageBreakBefore)
|
||
})
|
||
}
|
||
|
||
function bullet(text, level = 0) {
|
||
return paragraph([
|
||
run('• ', { font: '微软雅黑', eastAsia: '微软雅黑', color: COLORS.blue, bold: true }),
|
||
run(text, { font: '宋体', eastAsia: '宋体', size: 21 })
|
||
], {
|
||
indentLeft: 360 + level * 360,
|
||
spacingAfter: 80,
|
||
line: 320
|
||
})
|
||
}
|
||
|
||
function numbered(number, text) {
|
||
return paragraph([
|
||
run(`${number}. `, { font: '微软雅黑', eastAsia: '微软雅黑', color: COLORS.blue, bold: true, size: 21 }),
|
||
run(text, { font: '宋体', eastAsia: '宋体', size: 21 })
|
||
], {
|
||
indentLeft: 360,
|
||
spacingAfter: 100,
|
||
line: 320
|
||
})
|
||
}
|
||
|
||
function pageBreak() {
|
||
return '<w:p><w:r><w:br w:type="page"/></w:r></w:p>'
|
||
}
|
||
|
||
function cell(content, width, options = {}) {
|
||
const paragraphs = Array.isArray(content)
|
||
? content
|
||
: [paragraph(String(content), {
|
||
run: {
|
||
font: options.font || '宋体',
|
||
eastAsia: options.font || '宋体',
|
||
size: options.size || 20,
|
||
bold: Boolean(options.bold),
|
||
color: options.color
|
||
},
|
||
align: options.align,
|
||
spacingAfter: 0,
|
||
line: 280
|
||
})]
|
||
const props = [
|
||
`<w:tcW w:w="${width}" w:type="dxa"/>`,
|
||
'<w:tcMar><w:top w:w="90" w:type="dxa"/><w:left w:w="110" w:type="dxa"/><w:bottom w:w="90" w:type="dxa"/><w:right w:w="110" w:type="dxa"/></w:tcMar>',
|
||
`<w:vAlign w:val="${options.vAlign || 'center'}"/>`
|
||
]
|
||
if (options.fill) props.push(`<w:shd w:fill="${options.fill}"/>`)
|
||
if (options.colSpan) props.push(`<w:gridSpan w:val="${options.colSpan}"/>`)
|
||
return `<w:tc><w:tcPr>${props.join('')}</w:tcPr>${paragraphs.join('')}</w:tc>`
|
||
}
|
||
|
||
function table(rows, widths, options = {}) {
|
||
const grid = widths.map(width => `<w:gridCol w:w="${width}"/>`).join('')
|
||
const rowXml = rows.map((row, rowIndex) => {
|
||
const isHeader = options.header && rowIndex === 0
|
||
const cells = row.map((value, index) => {
|
||
const descriptor = value && typeof value === 'object' && !Array.isArray(value)
|
||
? value
|
||
: { text: value }
|
||
return cell(descriptor.paragraphs || descriptor.text, descriptor.width || widths[index], {
|
||
fill: descriptor.fill || (isHeader ? COLORS.blue : (options.band && rowIndex % 2 === 0 ? COLORS.paleBlue : null)),
|
||
bold: descriptor.bold != null ? descriptor.bold : isHeader,
|
||
color: descriptor.color || (isHeader ? COLORS.white : null),
|
||
align: descriptor.align || (isHeader ? 'center' : null),
|
||
vAlign: descriptor.vAlign,
|
||
colSpan: descriptor.colSpan,
|
||
size: descriptor.size,
|
||
font: descriptor.font
|
||
})
|
||
}).join('')
|
||
return `<w:tr><w:trPr>${isHeader ? '<w:tblHeader/>' : ''}<w:cantSplit/></w:trPr>${cells}</w:tr>`
|
||
}).join('')
|
||
return `<w:tbl><w:tblPr><w:tblW w:w="9000" w:type="dxa"/><w:tblLayout w:type="fixed"/><w:tblBorders><w:top w:val="single" w:sz="6" w:color="${COLORS.border}"/><w:left w:val="single" w:sz="6" w:color="${COLORS.border}"/><w:bottom w:val="single" w:sz="6" w:color="${COLORS.border}"/><w:right w:val="single" w:sz="6" w:color="${COLORS.border}"/><w:insideH w:val="single" w:sz="4" w:color="D9E2F3"/><w:insideV w:val="single" w:sz="4" w:color="D9E2F3"/></w:tblBorders><w:tblCellMar><w:top w:w="80" w:type="dxa"/><w:left w:w="90" w:type="dxa"/><w:bottom w:w="80" w:type="dxa"/><w:right w:w="90" w:type="dxa"/></w:tblCellMar></w:tblPr><w:tblGrid>${grid}</w:tblGrid>${rowXml}</w:tbl>`
|
||
}
|
||
|
||
function infoBox(title, text, type = 'info') {
|
||
const palette = {
|
||
info: [COLORS.blue, COLORS.paleBlue],
|
||
success: [COLORS.green, COLORS.paleGreen],
|
||
warning: [COLORS.amber, COLORS.paleAmber],
|
||
danger: [COLORS.red, COLORS.paleRed]
|
||
}[type]
|
||
const paragraphs = [
|
||
paragraph([run(title, { font: '微软雅黑', eastAsia: '微软雅黑', bold: true, color: palette[0], size: 22 })], { spacingAfter: 80 }),
|
||
paragraph([run(text, { font: '宋体', eastAsia: '宋体', size: 21 })], { spacingAfter: 0, line: 320 })
|
||
]
|
||
return table([[{ paragraphs, fill: palette[1] }]], [9000])
|
||
}
|
||
|
||
function tocField() {
|
||
return `<w:p><w:pPr><w:spacing w:after="180"/></w:pPr><w:fldSimple w:instr="TOC \\o "1-3" \\h \\z \\u"><w:r><w:rPr><w:rFonts w:eastAsia="宋体"/><w:color w:val="${COLORS.gray}"/><w:sz w:val="20"/></w:rPr><w:t>目录将在打开文档时自动更新</w:t></w:r></w:fldSimple></w:p>`
|
||
}
|
||
|
||
const body = []
|
||
|
||
// Cover
|
||
body.push(paragraph('大连元利流体技术有限公司', {
|
||
align: 'center', spacingBefore: 900, spacingAfter: 100,
|
||
run: { font: '微软雅黑', eastAsia: '微软雅黑', size: 28, color: COLORS.gray }
|
||
}))
|
||
body.push(paragraph('MES 测试环境接管报告', {
|
||
align: 'center', spacingBefore: 850, spacingAfter: 160,
|
||
run: { font: '微软雅黑', eastAsia: '微软雅黑', size: 56, bold: true, color: COLORS.navy }
|
||
}))
|
||
body.push(paragraph('生产环境接管准备基线', {
|
||
align: 'center', spacingAfter: 900,
|
||
run: { font: '微软雅黑', eastAsia: '微软雅黑', size: 28, color: COLORS.blue }
|
||
}))
|
||
body.push(infoBox('接管结论', '测试环境已完成前端、MES 接口、数据库、只读外部集成、备份恢复与安全基线的受控接管,可作为后续生产环境接管的演练环境。', 'success'))
|
||
body.push(paragraph('', { spacingAfter: 400 }))
|
||
body.push(table([
|
||
['文档编号', 'YL-MES-TAKEOVER-TEST-20260725'],
|
||
['文档版本', 'V1.1'],
|
||
['环境', 'MES 测试环境'],
|
||
['报告日期', '2026-07-25'],
|
||
['文档状态', '正式接管报告'],
|
||
['保密级别', '内部资料']
|
||
], [2200, 6800], { band: true }))
|
||
body.push(paragraph('本报告不包含账号、密码、Token 或生产业务明细。', {
|
||
align: 'center', spacingBefore: 500,
|
||
run: { font: '宋体', eastAsia: '宋体', size: 18, color: COLORS.gray }
|
||
}))
|
||
body.push(pageBreak())
|
||
|
||
// Document control and TOC
|
||
body.push(heading('文档控制', 1))
|
||
body.push(table([
|
||
['版本', '日期', '编制/维护', '变更说明'],
|
||
['V1.0', '2026-07-25', 'MES 接管维护', '首次形成测试环境接管报告'],
|
||
['V1.1', '2026-07-25', 'MES 接管维护', '修复部分富文本 XML 被显示为普通文字的问题']
|
||
], [1200, 1600, 2000, 4200], { header: true, band: true }))
|
||
body.push(heading('审批与分发', 2))
|
||
body.push(table([
|
||
['角色', '姓名/部门', '确认内容', '状态'],
|
||
['业务负责人', '', '业务模块范围及验收口径', '待签字'],
|
||
['MES 运维负责人', '', '环境、发布、备份和回滚', '待签字'],
|
||
['信息安全负责人', '', '账号、网络、证书和审计要求', '待签字'],
|
||
['生产环境负责人', '', '生产接管窗口和授权边界', '待签字']
|
||
], [1800, 2200, 3400, 1600], { header: true, band: true }))
|
||
body.push(heading('目录', 1))
|
||
body.push(tocField())
|
||
body.push(pageBreak())
|
||
|
||
// 1
|
||
body.push(heading('1. 文档目的与接管结论', 1))
|
||
body.push(paragraph('本报告用于固化 MES 测试环境的实际接管状态、验证证据、运维能力和风险边界,并作为后续生产环境接管方案、演练和审批的输入。', {
|
||
firstLine: 420, spacingAfter: 180, line: 360
|
||
}))
|
||
body.push(infoBox('总体判定:受控接管完成', '测试前端已真实部署,MES 接口和数据库可管理,数据库恢复演练已通过,关键安全问题已整改,九个核心模块只读冒烟全部通过。当前环境可以承担日常诊断、受控变更、发布回归和生产接管演练。', 'success'))
|
||
body.push(heading('1.1 接管状态摘要', 2))
|
||
body.push(table([
|
||
['领域', '状态', '结论'],
|
||
['前端代码与部署', '已接管', '构建成功,IIS 10012 已部署并可访问'],
|
||
['MES 通用接口', '已接管', '登录、菜单、权限和只读过程调用通过'],
|
||
['测试数据库', '已接管', '结构诊断、脚本变更、回滚和恢复演练通过'],
|
||
['B1 集成', '只读接管', 'VPN 可达,语义 GET 实测通过,POST/PATCH 强制阻止'],
|
||
['TM 集成', '受限只读', '代理端口可达;当前无安全 GET 验收端点,库存任务写调用全部阻止'],
|
||
['核心后端', '发布物接管', '维护 IIS、配置和现有发布物;当前不修改核心 DLL'],
|
||
['安全基线', '部分完成', '密码响应、Cookie 和目录浏览已整改;证书与服务账号仍待处理']
|
||
], [1900, 1700, 5400], { header: true, band: true }))
|
||
|
||
// 2
|
||
body.push(heading('2. 接管范围与边界', 1, { pageBreakBefore: true }))
|
||
body.push(heading('2.1 已纳入接管范围', 2))
|
||
;[
|
||
'Vue 2 管理前端代码、构建流程、运行配置和 IIS 前端站点。',
|
||
'MESCommonBase.ashx 通用接口的运行状态、配置、日志和已部署处理器。',
|
||
'测试 SQL Server 2019 中 YL_MESDB 的结构、存储过程、索引、备份和恢复。',
|
||
'MES 登录、角色菜单、系统、工艺、计划、生产、质量、设备、异常、销售和看板查询链路。',
|
||
'B1/TM 代理的网络可用性和测试环境只读保护。',
|
||
'WinRM、IIS、部署目录、证书、目录浏览和防火墙等运维基线。'
|
||
].forEach(item => body.push(bullet(item)))
|
||
body.push(heading('2.2 明确边界', 2))
|
||
;[
|
||
'不在测试或生产环境执行未经批准的 MES 业务写入。',
|
||
'B1/TM 的语义 POST 和 PATCH 在测试前端被强制阻止,不进行真实库存、生产订单或条码任务写入。',
|
||
'核心 DLL 没有原始源码,当前按发布物运维;本次不替换、不重建核心 DLL。',
|
||
'本轮全模块验收为只读冒烟,不代表报工、送检、入库、领料和退料等写流程已经验收。',
|
||
'正式环境的任何变更仍需单独授权、变更窗口、备份和回滚方案。'
|
||
].forEach(item => body.push(bullet(item)))
|
||
|
||
// 3
|
||
body.push(heading('3. 环境与资产清单', 1, { pageBreakBefore: true }))
|
||
body.push(heading('3.1 访问与部署地址', 2))
|
||
body.push(table([
|
||
['对象', '地址/位置', '用途与状态'],
|
||
['测试前端', 'http://124.220.15.242:10012/', 'IIS 站点已启动,外部访问 200'],
|
||
['MES 测试接口', 'https://124.220.15.242:10101/submit/MESCommonBase.ashx', '业务接口可用;证书尚待整改'],
|
||
['测试数据库', '124.220.15.242,64722 / YL_MESDB', 'SQL Server 2019,数据库 ONLINE'],
|
||
['测试前端目录', 'C:\\ProjectShow\\元利流体\\MES_Manage_View\\dist', '84 个文件,约 95.69 MB'],
|
||
['后端发布目录', 'C:\\ProjectShow\\元利流体\\MES_ManageV20250916', '现有 10100/10101 IIS 发布物'],
|
||
['B1 代理', 'https://192.168.2.92:9996/b1s', '通过管理机 VPN 访问,仅允许语义 GET'],
|
||
['TM 代理', 'https://192.168.2.92:9996/openapi', '通过管理机 VPN 访问,写请求禁用']
|
||
], [1900, 4300, 2800], { header: true, band: true }))
|
||
body.push(heading('3.2 技术与版本基线', 2))
|
||
body.push(table([
|
||
['层级', '版本/实现'],
|
||
['前端框架', 'Vue 2.7.16 / Vue Router 3.6.5 / Vuex 3.6.2'],
|
||
['UI 与 HTTP', 'Element UI 2.15.14 / Axios 0.21.4'],
|
||
['构建工具', 'Webpack 5.103.0 / Node.js 20'],
|
||
['数据库', 'Microsoft SQL Server 2019,兼容级别 150'],
|
||
['业务接口', 'ASP.NET .ashx 统一存储过程网关'],
|
||
['前端构建入口', 'app.83b8251a.js'],
|
||
['接管基线', 'MES测试环境接管基线_20260725.md / V1.3']
|
||
], [2400, 6600], { band: true }))
|
||
|
||
// 4
|
||
body.push(heading('4. 系统架构与调用链', 1, { pageBreakBefore: true }))
|
||
body.push(table([[
|
||
{ paragraphs: [
|
||
paragraph([run('浏览器 / Vue 前端', { font: 'Consolas', eastAsia: '微软雅黑', bold: true, color: COLORS.navy, size: 22 })], { align: 'center', spacingAfter: 100 }),
|
||
paragraph([run('↓ 运行配置与 Axios', { font: 'Consolas', eastAsia: '微软雅黑', color: COLORS.gray, size: 20 })], { align: 'center', spacingAfter: 100 }),
|
||
paragraph([run('MESCommonBase.ashx', { font: 'Consolas', eastAsia: '微软雅黑', bold: true, color: COLORS.blue, size: 22 })], { align: 'center', spacingAfter: 100 }),
|
||
paragraph([run('↓ 存储过程名 + 参数 + 用户/模块上下文', { font: 'Consolas', eastAsia: '微软雅黑', color: COLORS.gray, size: 20 })], { align: 'center', spacingAfter: 100 }),
|
||
paragraph([run('SQL Server / YL_MESDB', { font: 'Consolas', eastAsia: '微软雅黑', bold: true, color: COLORS.green, size: 22 })], { align: 'center', spacingAfter: 100 }),
|
||
paragraph([run('↘ B1 代理(仅 GET) ↘ TM 代理(写入禁用)', { font: 'Consolas', eastAsia: '微软雅黑', color: COLORS.amber, size: 20 })], { align: 'center' })
|
||
], fill: COLORS.lightGray }
|
||
]], [9000]))
|
||
body.push(heading('4.1 环境隔离原则', 2))
|
||
body.push(paragraph('测试 dist 使用独立运行配置:MES 指向测试 10101;B1/TM 通过 VPN 指向现场代理;externalReadOnly 必须为 true。生产源配置保留 externalReadOnly=false,不将测试限制误带入正式构建。', {
|
||
firstLine: 420, spacingAfter: 120, line: 360
|
||
}))
|
||
body.push(infoBox('重要说明', 'B1/TM 代理封装在传输层统一使用 HTTP POST,但真正操作类型由请求体 func 决定。只读保护按语义 GET/POST/PATCH 判断,语义 GET 仍可通过代理传输。', 'warning'))
|
||
|
||
// 5
|
||
body.push(heading('5. 已完成接管工作', 1, { pageBreakBefore: true }))
|
||
body.push(heading('5.1 前端部署', 2))
|
||
;[
|
||
'完成包含装配任务预算进度、登录安全修复和外部系统只读保护的生产构建。',
|
||
'直接复制 dist 到测试服务器 staging,核对文件数、总字节数及 index.html/config.js SHA-256 后切换为最终目录。',
|
||
'创建独立 IIS 站点和应用池,绑定 *:10012:,站点 Started、端口 Listening。',
|
||
'服务器本机和管理端均返回 HTTP 200,实际加载 app.83b8251a.js。',
|
||
'本次未生成 zip,符合直接部署 dist 的维护约定。'
|
||
].forEach(item => body.push(bullet(item)))
|
||
body.push(heading('5.2 装配任务功能同步', 2))
|
||
;[
|
||
'同步系统类和阀类装配任务合同工时修复、共享核心查询和 5 个查询索引。',
|
||
'增加任务预算装配工时、合同预算测试工时及两个计算进度字段。',
|
||
'测试环境跳过不可达的 SAP 交货日期远程读取,保留返回字段结构和独立回滚过程。',
|
||
'系统类接口约 0.8-1.1 秒,阀类接口约 0.9-1.2 秒,均返回正确分页和预算字段。'
|
||
].forEach(item => body.push(bullet(item)))
|
||
body.push(heading('5.3 安全整改', 2))
|
||
body.push(table([
|
||
['问题', '整改结果', '验证证据'],
|
||
['登录接口返回 password', '登录过程显式返回非密码字段,保留密码校验', '登录成功,响应无 password 属性'],
|
||
['浏览器保存明文密码 Cookie', '只记住账号并删除旧 password Cookie', '源码和构建产物复核通过'],
|
||
['后端目录浏览开启', '根目录和 submit 均设置为 False', '目录返回 403,接口仍返回 200'],
|
||
['仓库历史口令残留', '历史注释和日志文字已脱敏', '已知口令特征扫描文件数为 0'],
|
||
['B1/TM 写污染风险', '测试配置启用语义写保护', '4 个写入口网络请求数为 0']
|
||
], [2500, 3700, 2800], { header: true, band: true }))
|
||
|
||
// 6
|
||
body.push(heading('6. 验证与演练结果', 1, { pageBreakBefore: true }))
|
||
body.push(heading('6.1 核心模块只读冒烟', 2))
|
||
body.push(table([
|
||
['模块', '查询入口', '结果', '耗时'],
|
||
['系统', '人员管理_人员信息查询', '200 / 135 行', '385 ms'],
|
||
['工艺', 'MES_ProcessManagement_ProcessGet', '200 / 合法 JSON', '113 ms'],
|
||
['计划', '计划排产_生产订单_工艺查询', '200 / 9878 行', '2619 ms'],
|
||
['生产', '生产管理_班次管理_班次列表查询', '200 / 4 行', '128 ms'],
|
||
['质量', '质量管理_测试装配任务_查询2', '200 / 0 行', '200 ms'],
|
||
['设备', '设备管理_设备数据_工位信息_查询', '200 / 21 行', '140 ms'],
|
||
['异常', '加急订单_加急明细查询', '200 / 合法 JSON', '117 ms'],
|
||
['销售', '销售管理_销售订单状态', '200 / 200 行', '430 ms'],
|
||
['首页看板', '看板_主页六个关键数据_查询', '200 / 25 行', '1099 ms']
|
||
], [1300, 4200, 2100, 1400], { header: true, band: true }))
|
||
body.push(paragraph('角色菜单查询返回 84 条,覆盖工艺、计划、生产、质量、设备和异常等主模块。质量测试装配任务当前返回 0 行属于有效空结果,不是接口失败。', {
|
||
spacingBefore: 160, spacingAfter: 160, line: 340
|
||
}))
|
||
body.push(heading('6.2 B1/TM 只读验证', 2))
|
||
body.push(table([
|
||
['验证项', '结果'],
|
||
['VPN/代理端口', '192.168.2.92:9996 可达'],
|
||
['B1 ProductionOrders 最小 GET', 'HTTP 200,约 252 ms,包含 value,无 B1 错误'],
|
||
['B1 POST/PATCH', '前端拦截,网络请求 0'],
|
||
['TM POST/PATCH', '前端拦截,网络请求 0'],
|
||
['TM GET', '当前源码无明确安全 GET 端点,未调用库存任务接口']
|
||
], [3000, 6000], { band: true }))
|
||
body.push(heading('6.3 数据库备份恢复演练', 2))
|
||
body.push(table([
|
||
['步骤', '结果', '关键数据'],
|
||
['备份头与文件列表读取', '通过', '完整备份约 1.28 GB,逻辑数据/日志文件完整'],
|
||
['RESTORE VERIFYONLY', '通过', '约 4.3 秒;历史备份未包含 checksum'],
|
||
['隔离恢复', '通过', 'YL_MESDB_RestoreDrill_20260725,约 9.4 秒 ONLINE'],
|
||
['DBCC CHECKDB PHYSICAL_ONLY', '通过', '约 5.9 秒,无物理一致性错误'],
|
||
['对象核对', '通过', '备份 61/27/425;当前 63/27/432'],
|
||
['演练清理', '通过', '演练库、MDF、LDF 均已删除,空间恢复']
|
||
], [2800, 1600, 4600], { header: true, band: true }))
|
||
body.push(infoBox('备份策略改进项', '当前完整备份没有启用 SQL Server checksum。普通 VERIFYONLY 和实际恢复均已通过,但生产备份任务应启用 WITH CHECKSUM,并定期执行独立恢复演练。', 'warning'))
|
||
|
||
// 7
|
||
body.push(heading('7. 当前可开展的工作', 1, { pageBreakBefore: true }))
|
||
;[
|
||
'前端功能开发、生产构建、测试配置注入、IIS 发布和页面回归。',
|
||
'MES 登录、菜单、权限、接口错误和存储过程调用链诊断。',
|
||
'测试数据库结构比对、查询性能分析、索引优化和带事务/回滚的变更。',
|
||
'系统类、阀类装配任务查询、合同工时、预算工时和进度功能维护。',
|
||
'通过 VPN 开展 B1 登录代理与 GET 查询联调,不污染现场业务数据。',
|
||
'数据库备份校验、隔离恢复、DBCC 检查和清理演练。',
|
||
'IIS 站点、应用池、部署目录、日志、证书、WinRM 和防火墙巡检。',
|
||
'为生产接管准备资产清单、变更脚本、回滚路径、验收记录和问题台账。'
|
||
].forEach((item, index) => body.push(numbered(index + 1, item)))
|
||
body.push(infoBox('不能直接开展的工作', '未经专项授权不得执行生产环境变更;测试环境不执行 B1/TM 写入;MES 报工、送检、入库等写流程尚未完成系统性验收;核心 DLL 逻辑暂不修改。', 'danger'))
|
||
|
||
// 8
|
||
body.push(heading('8. 日常运维与变更流程', 1, { pageBreakBefore: true }))
|
||
body.push(heading('8.1 前端发布流程', 2))
|
||
;[
|
||
'确认目标环境和变更范围,核对 Git 差异及运行配置。',
|
||
'执行生产构建,检查构建错误和关键资源入口。',
|
||
'构建后注入测试 config.js,确认 MES 地址和 externalReadOnly=true。',
|
||
'直接复制 dist 到服务器 staging,不生成 zip。',
|
||
'核对文件总数、总字节数和关键文件 SHA-256。',
|
||
'切换 IIS 物理目录或最终 dist,验证首页、config.js、应用 Bundle 和登录。',
|
||
'出现异常时恢复上一个 dist 目录并复测。'
|
||
].forEach((item, index) => body.push(numbered(index + 1, item)))
|
||
body.push(heading('8.2 数据库变更流程', 2))
|
||
;[
|
||
'部署前验证数据库、对象结构、依赖和当前定义哈希。',
|
||
'每项变更必须包含事务、前置校验、验证 SQL 和回滚脚本。',
|
||
'先在测试环境部署,执行接口、性能和数据清理回归。',
|
||
'禁止使用测试库整体覆盖正式库,按对象脚本逐项同步。',
|
||
'记录部署时间、执行人、对象、结果、回滚点和残留风险。'
|
||
].forEach((item, index) => body.push(numbered(index + 1, item)))
|
||
body.push(heading('8.3 故障处理流程', 2))
|
||
body.push(table([
|
||
['故障类型', '首要检查', '回退/处置'],
|
||
['前端无法访问', '10012 监听、站点状态、物理目录、首页文件', '启动站点或恢复上一版 dist'],
|
||
['接口失败', '10101、IIS 日志、证书、ASHX 响应、应用池', '回收应用池前先保留日志;必要时恢复发布物'],
|
||
['数据库慢查询', '执行计划、等待、索引、链接服务器', '终止异常诊断会话,回滚当次 SQL 变更'],
|
||
['B1/TM 异常', 'VPN、9996、func 类型、只读开关', '保持写保护,禁止以真实写入验证连通性'],
|
||
['数据恢复', '备份时间、VERIFYONLY、空间、逻辑文件名', '只恢复到隔离库,检查通过后再制定业务恢复方案']
|
||
], [1800, 3700, 3500], { header: true, band: true }))
|
||
|
||
// 9
|
||
body.push(heading('9. 生产环境接管准备', 1, { pageBreakBefore: true }))
|
||
body.push(heading('9.1 建议接管阶段', 2))
|
||
body.push(table([
|
||
['阶段', '主要工作', '完成标志'],
|
||
['阶段 1:只读盘点', '生产 IIS、数据库、配置、证书、备份、外部依赖和日志', '形成生产资产清单与差异清单'],
|
||
['阶段 2:权限与安全', '建立只读诊断账号、受控发布账号、管理入口白名单', '日常操作不再依赖 sysadmin/Administrator'],
|
||
['阶段 3:发布演练', '前端发布回滚、数据库脚本回滚、接口发布物恢复', '每类发布至少完成一次可重复演练'],
|
||
['阶段 4:业务验收', '只读模块、MES 写流程、B1/TM 受控联调', '关键业务流程有负责人和验收记录'],
|
||
['阶段 5:正式交接', '监控、巡检、值守、故障升级和文档签字', '接管责任、权限和窗口正式生效']
|
||
], [1800, 4300, 2900], { header: true, band: true }))
|
||
body.push(heading('9.2 生产变更前置条件', 2))
|
||
;[
|
||
'用户明确批准生产变更范围和执行窗口。',
|
||
'完成正式备份校验并确认可用回滚点。',
|
||
'测试环境已通过同版本脚本和页面回归。',
|
||
'配置文件不包含测试地址,B1/TM 保护策略符合生产业务要求。',
|
||
'发布账号、数据库账号和远程管理账号遵循最小权限。',
|
||
'发布后有业务负责人完成关键流程验收。'
|
||
].forEach(item => body.push(bullet(item)))
|
||
|
||
// 10
|
||
body.push(heading('10. 风险与整改计划', 1, { pageBreakBefore: true }))
|
||
body.push(table([
|
||
['编号', '级别', '风险', '建议措施', '状态'],
|
||
['SEC-01', '高', '10101 使用名称不匹配的自签名证书', '部署受信任且匹配域名的业务证书', '待整改'],
|
||
['SEC-02', '高', '后端仍使用高权限数据库账号', '建立专用服务账号并完成全部写流程回归后切换', '待整改'],
|
||
['NET-01', '中', '10012 当前使用 HTTP 且来源为 Any', '启用 HTTPS,并按测试人员或网络范围限制入口', '待整改'],
|
||
['TEST-01', '高', 'MES 关键写流程尚未系统验收', '准备隔离测试数据、回滚脚本和业务验收人', '待验收'],
|
||
['INT-01', '中', 'TM 没有明确安全 GET 验收端点', '提供健康检查或只读查询端点', '待补充'],
|
||
['OPS-01', '中', '核心 DLL 没有原始源码', '日常按发布物运维;需要改逻辑时获取源码或专项验证替代构建', '已接受限制'],
|
||
['BAK-01', '中', '历史完整备份未启用 checksum', '调整备份任务并定期执行恢复演练', '待整改'],
|
||
['PERF-01', '低', '计划工艺查询返回量大且约 2.6 秒', '增加分页/条件限制并持续观察执行计划', '持续优化']
|
||
], [1100, 900, 2500, 3400, 1100], { header: true, band: true }))
|
||
|
||
// 11
|
||
body.push(heading('11. 回滚与应急依据', 1, { pageBreakBefore: true }))
|
||
body.push(table([
|
||
['对象', '回滚依据'],
|
||
['登录接口安全修复', 'rollback_secure_test_login_response_20260725.sql 及安全修复前备份过程'],
|
||
['装配任务查询优化', 'rollback_install_task_queries_20260725.sql'],
|
||
['装配任务查询索引', 'rollback_install_task_query_indexes_20260725.sql'],
|
||
['装配任务预算进度', 'rollback_install_task_budget_progress_20260725.sql'],
|
||
['测试 SAP 交货日期适配', 'rollback_test_disable_install_task_sap_delivery_20260725.sql'],
|
||
['后端目录浏览', '服务器同目录 web.config.directorybrowse_before_20260725.bak'],
|
||
['前端发布', '保留上一版 dist 或 staging,通过 IIS 物理路径切回'],
|
||
['数据库恢复', '完整备份 VERIFYONLY + 隔离恢复 + DBCC CHECKDB 流程']
|
||
], [2700, 6300], { band: true }))
|
||
body.push(infoBox('应急原则', '先保留证据和当前状态,再执行最小范围回退;不使用测试数据库整体覆盖正式数据库;不通过关闭只读保护来排查 B1/TM 连通性。', 'danger'))
|
||
|
||
// 12
|
||
body.push(heading('12. 验收与签字', 1, { pageBreakBefore: true }))
|
||
body.push(paragraph('各责任人签字表示已阅读本报告,认可当前测试环境的接管状态、能力范围、限制和生产接管前置条件。', {
|
||
spacingAfter: 240, line: 340
|
||
}))
|
||
body.push(table([
|
||
['角色', '姓名', '意见', '签字', '日期'],
|
||
['业务负责人', '', '', '', ''],
|
||
['MES 运维负责人', '', '', '', ''],
|
||
['信息安全负责人', '', '', '', ''],
|
||
['生产环境负责人', '', '', '', '']
|
||
], [1800, 1500, 2700, 1500, 1500], { header: true }))
|
||
body.push(paragraph('', { spacingAfter: 300 }))
|
||
body.push(infoBox('最终接管意见', '测试环境接管:通过(受控)。生产环境接管:尚未授权,需完成本报告第 9、10 节的前置条件与风险整改,并另行批准。', 'success'))
|
||
|
||
// Appendix
|
||
body.push(heading('附录 A:关键交付物', 1, { pageBreakBefore: true }))
|
||
body.push(table([
|
||
['类型', '交付物'],
|
||
['接管基线', 'doc/MES测试环境接管基线_20260725.md'],
|
||
['接管报告', 'doc/大连元利流体技术有限公司MES测试环境接管报告_V1.1_2026-07-25.docx'],
|
||
['过程日志', 'gptlog-process/gpdlog.md'],
|
||
['前端产物', 'dist/(含测试 static/config.js)'],
|
||
['登录安全脚本', 'db_backups/secure_test_login_response_20260725.sql 及回滚脚本'],
|
||
['装配任务脚本', 'db_backups/ 下 20260725 查询、索引、合同工时、预算进度及回滚脚本'],
|
||
['后端脱敏基线', 'server_baseline/MES_ManageV20250916_20260725/']
|
||
], [2300, 6700], { band: true }))
|
||
body.push(heading('附录 B:验证结论索引', 1))
|
||
;[
|
||
'前端:10012 外部访问 200,配置 externalReadOnly=true。',
|
||
'接口:登录 200,菜单 84 条,登录响应无 password。',
|
||
'模块:9 个核心模块只读查询全部返回 200 和合法 JSON。',
|
||
'外部集成:B1 GET 通过;B1/TM 写请求隔离测试网络调用为 0。',
|
||
'数据库:VERIFYONLY、隔离恢复、DBCC CHECKDB、清理全部通过。',
|
||
'安全:目录浏览关闭、密码 Cookie 移除、仓库已知口令特征扫描为 0。'
|
||
].forEach(item => body.push(bullet(item)))
|
||
|
||
const sectionProperties = `<w:sectPr><w:headerReference w:type="default" r:id="rId1"/><w:footerReference w:type="default" r:id="rId2"/><w:pgSz w:w="11906" w:h="16838"/><w:pgMar w:top="1360" w:right="1360" w:bottom="1360" w:left="1360" w:header="650" w:footer="650" w:gutter="0"/><w:cols w:space="720"/><w:docGrid w:linePitch="312"/></w:sectPr>`
|
||
|
||
const documentXml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><w:body>${body.join('')}${sectionProperties}</w:body></w:document>`
|
||
|
||
const stylesXml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
|
||
<w:docDefaults><w:rPrDefault><w:rPr><w:rFonts w:ascii="宋体" w:hAnsi="宋体" w:eastAsia="宋体"/><w:sz w:val="21"/><w:szCs w:val="21"/><w:lang w:val="zh-CN" w:eastAsia="zh-CN"/></w:rPr></w:rPrDefault><w:pPrDefault><w:pPr><w:spacing w:after="120" w:line="340" w:lineRule="auto"/></w:pPr></w:pPrDefault></w:docDefaults>
|
||
<w:style w:type="paragraph" w:default="1" w:styleId="Normal"><w:name w:val="正文"/><w:qFormat/><w:pPr><w:widowControl/></w:pPr><w:rPr><w:rFonts w:ascii="宋体" w:hAnsi="宋体" w:eastAsia="宋体"/><w:sz w:val="21"/><w:szCs w:val="21"/></w:rPr></w:style>
|
||
<w:style w:type="paragraph" w:styleId="Heading1"><w:name w:val="标题 1"/><w:basedOn w:val="Normal"/><w:next w:val="Normal"/><w:qFormat/><w:pPr><w:keepNext/><w:keepLines/><w:spacing w:before="340" w:after="180"/><w:outlineLvl w:val="0"/><w:pBdr><w:bottom w:val="single" w:sz="12" w:space="7" w:color="${COLORS.blue}"/></w:pBdr></w:pPr><w:rPr><w:rFonts w:ascii="微软雅黑" w:hAnsi="微软雅黑" w:eastAsia="微软雅黑"/><w:b/><w:color w:val="${COLORS.navy}"/><w:sz w:val="32"/><w:szCs w:val="32"/></w:rPr></w:style>
|
||
<w:style w:type="paragraph" w:styleId="Heading2"><w:name w:val="标题 2"/><w:basedOn w:val="Normal"/><w:next w:val="Normal"/><w:qFormat/><w:pPr><w:keepNext/><w:keepLines/><w:spacing w:before="260" w:after="120"/><w:outlineLvl w:val="1"/></w:pPr><w:rPr><w:rFonts w:ascii="微软雅黑" w:hAnsi="微软雅黑" w:eastAsia="微软雅黑"/><w:b/><w:color w:val="${COLORS.blue}"/><w:sz w:val="26"/><w:szCs w:val="26"/></w:rPr></w:style>
|
||
<w:style w:type="paragraph" w:styleId="Heading3"><w:name w:val="标题 3"/><w:basedOn w:val="Normal"/><w:next w:val="Normal"/><w:qFormat/><w:pPr><w:keepNext/><w:keepLines/><w:spacing w:before="180" w:after="100"/><w:outlineLvl w:val="2"/></w:pPr><w:rPr><w:rFonts w:ascii="微软雅黑" w:hAnsi="微软雅黑" w:eastAsia="微软雅黑"/><w:b/><w:color w:val="${COLORS.gray}"/><w:sz w:val="22"/><w:szCs w:val="22"/></w:rPr></w:style>
|
||
</w:styles>`
|
||
|
||
const settingsXml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:settings xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:zoom w:percent="100"/><w:updateFields w:val="true"/><w:defaultTabStop w:val="420"/><w:characterSpacingControl w:val="doNotCompress"/><w:compat><w:compatSetting w:name="compatibilityMode" w:uri="http://schemas.microsoft.com/office/word" w:val="15"/></w:compat></w:settings>`
|
||
|
||
const headerXml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:hdr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:p><w:pPr><w:pBdr><w:bottom w:val="single" w:sz="6" w:space="4" w:color="${COLORS.border}"/></w:pBdr><w:jc w:val="right"/></w:pPr>${run('大连元利流体技术有限公司 | MES 测试环境接管报告', { font: '微软雅黑', eastAsia: '微软雅黑', size: 17, color: COLORS.gray })}</w:p></w:hdr>`
|
||
|
||
const footerXml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:ftr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:p><w:pPr><w:pBdr><w:top w:val="single" w:sz="4" w:space="4" w:color="${COLORS.border}"/></w:pBdr><w:jc w:val="center"/></w:pPr>${run('内部资料 | 第 ', { font: '宋体', eastAsia: '宋体', size: 17, color: COLORS.gray })}<w:fldSimple w:instr="PAGE"><w:r><w:rPr><w:rFonts w:eastAsia="宋体"/><w:sz w:val="17"/><w:color w:val="${COLORS.gray}"/></w:rPr><w:t>1</w:t></w:r></w:fldSimple>${run(' 页', { font: '宋体', eastAsia: '宋体', size: 17, color: COLORS.gray })}</w:p></w:ftr>`
|
||
|
||
const contentTypesXml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/><Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"/><Override PartName="/word/settings.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml"/><Override PartName="/word/header1.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml"/><Override PartName="/word/footer1.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml"/><Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/><Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/></Types>`
|
||
|
||
const packageRelsXml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/><Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/></Relationships>`
|
||
|
||
const documentRelsXml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/header" Target="header1.xml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer" Target="footer1.xml"/><Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/><Relationship Id="rId4" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings" Target="settings.xml"/></Relationships>`
|
||
|
||
const coreXml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:dcmitype="http://purl.org/dc/dcmitype/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><dc:title>大连元利流体技术有限公司MES测试环境接管报告</dc:title><dc:subject>MES 测试环境接管与生产接管准备</dc:subject><dc:creator>MES 接管维护</dc:creator><cp:keywords>MES;测试环境;接管;IIS;数据库;恢复演练;安全</cp:keywords><dc:description>MES 测试环境接管状态、验收证据、能力边界和生产接管准备报告。</dc:description><cp:lastModifiedBy>MES 接管维护</cp:lastModifiedBy><dcterms:created xsi:type="dcterms:W3CDTF">2026-07-25T08:00:00Z</dcterms:created><dcterms:modified xsi:type="dcterms:W3CDTF">2026-07-25T08:00:00Z</dcterms:modified><cp:revision>1</cp:revision></cp:coreProperties>`
|
||
|
||
const appXml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"><Application>Microsoft Office Word</Application><AppVersion>16.0000</AppVersion><Company>大连元利流体技术有限公司</Company><Manager>MES 运维</Manager><DocSecurity>0</DocSecurity><ScaleCrop>false</ScaleCrop><LinksUpToDate>false</LinksUpToDate><SharedDoc>false</SharedDoc><HyperlinksChanged>false</HyperlinksChanged></Properties>`
|
||
|
||
const zip = new JSZip()
|
||
zip.file('[Content_Types].xml', contentTypesXml)
|
||
zip.folder('_rels').file('.rels', packageRelsXml)
|
||
zip.folder('docProps').file('core.xml', coreXml).file('app.xml', appXml)
|
||
const word = zip.folder('word')
|
||
word.file('document.xml', documentXml)
|
||
word.file('styles.xml', stylesXml)
|
||
word.file('settings.xml', settingsXml)
|
||
word.file('header1.xml', headerXml)
|
||
word.file('footer1.xml', footerXml)
|
||
word.folder('_rels').file('document.xml.rels', documentRelsXml)
|
||
|
||
fs.mkdirSync(path.dirname(outputPath), { recursive: true })
|
||
fs.writeFileSync(outputPath, zip.generate({ type: 'nodebuffer', compression: 'DEFLATE' }))
|
||
|
||
const stats = fs.statSync(outputPath)
|
||
console.log(JSON.stringify({ outputPath, bytes: stats.size, sections: 12, appendices: 2 }))
|