Files
MES_Manage_View_V20/work/generate-mes-takeover-report.js

546 lines
38 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;')
}
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 &quot;1-3&quot; \\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 指向测试 10101B1/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 }))