generate_qm_word.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. # -*- coding: utf-8 -*-
  2. """Generate Word doc from qm-api device integration manual."""
  3. from docx import Document
  4. from docx.shared import Pt, Cm, RGBColor
  5. from docx.enum.text import WD_ALIGN_PARAGRAPH
  6. from docx.enum.table import WD_TABLE_ALIGNMENT
  7. from docx.oxml.ns import qn
  8. import os
  9. OUTPUT = os.path.join(os.path.dirname(__file__), "气密工位MES对接说明(设备方).docx")
  10. def set_cell_shading(cell, color="D9E2F3"):
  11. from docx.oxml import OxmlElement
  12. shading = OxmlElement("w:shd")
  13. shading.set(qn("w:fill"), color)
  14. shading.set(qn("w:val"), "clear")
  15. cell._tc.get_or_add_tcPr().append(shading)
  16. def add_heading(doc, text, level=1):
  17. h = doc.add_heading(text, level=level)
  18. for run in h.runs:
  19. run.font.name = "微软雅黑"
  20. run._element.rPr.rFonts.set(qn("w:eastAsia"), "微软雅黑")
  21. return h
  22. def add_para(doc, text, bold=False, size=11):
  23. p = doc.add_paragraph()
  24. run = p.add_run(text)
  25. run.font.name = "微软雅黑"
  26. run._element.rPr.rFonts.set(qn("w:eastAsia"), "微软雅黑")
  27. run.font.size = Pt(size)
  28. run.bold = bold
  29. p.paragraph_format.space_after = Pt(6)
  30. return p
  31. def add_code(doc, text):
  32. p = doc.add_paragraph()
  33. p.paragraph_format.left_indent = Cm(0.5)
  34. run = p.add_run(text)
  35. run.font.name = "Consolas"
  36. run._element.rPr.rFonts.set(qn("w:eastAsia"), "微软雅黑")
  37. run.font.size = Pt(9)
  38. run.font.color.rgb = RGBColor(0x33, 0x33, 0x33)
  39. p.paragraph_format.space_after = Pt(8)
  40. return p
  41. def add_table(doc, headers, rows, col_widths=None):
  42. table = doc.add_table(rows=1 + len(rows), cols=len(headers))
  43. table.style = "Table Grid"
  44. table.alignment = WD_TABLE_ALIGNMENT.CENTER
  45. hdr = table.rows[0].cells
  46. for i, h in enumerate(headers):
  47. hdr[i].text = h
  48. set_cell_shading(hdr[i])
  49. for p in hdr[i].paragraphs:
  50. for run in p.runs:
  51. run.bold = True
  52. run.font.name = "微软雅黑"
  53. run._element.rPr.rFonts.set(qn("w:eastAsia"), "微软雅黑")
  54. run.font.size = Pt(10)
  55. for ri, row in enumerate(rows):
  56. cells = table.rows[ri + 1].cells
  57. for ci, val in enumerate(row):
  58. cells[ci].text = str(val)
  59. for p in cells[ci].paragraphs:
  60. for run in p.runs:
  61. run.font.name = "微软雅黑"
  62. run._element.rPr.rFonts.set(qn("w:eastAsia"), "微软雅黑")
  63. run.font.size = Pt(10)
  64. if col_widths:
  65. for i, w in enumerate(col_widths):
  66. for row in table.rows:
  67. row.cells[i].width = Cm(w)
  68. doc.add_paragraph()
  69. return table
  70. def build():
  71. doc = Document()
  72. sec = doc.sections[0]
  73. sec.top_margin = Cm(2.5)
  74. sec.bottom_margin = Cm(2.5)
  75. sec.left_margin = Cm(2.8)
  76. sec.right_margin = Cm(2.8)
  77. # Title
  78. title = doc.add_paragraph()
  79. title.alignment = WD_ALIGN_PARAGRAPH.CENTER
  80. tr = title.add_run("气密工位 MES 对接说明(设备方)")
  81. tr.bold = True
  82. tr.font.size = Pt(18)
  83. tr.font.name = "微软雅黑"
  84. tr._element.rPr.rFonts.set(qn("w:eastAsia"), "微软雅黑")
  85. sub = doc.add_paragraph()
  86. sub.alignment = WD_ALIGN_PARAGRAPH.CENTER
  87. sr = sub.add_run("文档版本:v1.0 更新日期:2026-06-08")
  88. sr.font.size = Pt(10)
  89. sr.font.color.rgb = RGBColor(0x66, 0x66, 0x66)
  90. sr.font.name = "微软雅黑"
  91. sr._element.rPr.rFonts.set(qn("w:eastAsia"), "微软雅黑")
  92. doc.add_paragraph()
  93. add_para(doc, "文档用途:气密检测设备与 MES 系统对接,完成本工位进站校验与结果上传。")
  94. add_para(doc, "对接方:气密设备软件 / 上位机开发")
  95. add_para(doc, "MES 地址:http://192.168.16.99:8980", bold=True)
  96. doc.add_paragraph()
  97. # Section 1
  98. add_heading(doc, "一、先看这个(1 分钟读懂)", 1)
  99. add_para(doc, "您的设备软件只需要做 2 件事:")
  100. add_table(doc, ["时机", "调用哪个接口", "作用"], [
  101. ["扫码后、开始测试前", "接口 1:qmcheck", "问 MES:这个工件能不能测?"],
  102. ["测试结束后", "接口 2:qmresult", "告诉 MES:测完了,结果 OK 还是 NG"],
  103. ], [4, 4.5, 6])
  104. add_para(doc, "不需要登录、不需要 Token,设备能访问 MES 服务器 IP 即可。")
  105. add_para(doc, "MES 会提前提供给您的固定参数(请填入下表,对接前向 MES 方确认):")
  106. add_table(doc, ["参数", "含义", "示例(以 MES 方确认为准)"], [
  107. ["oprno", "工位号", "OP450"],
  108. ["lineSn", "产线编号", "XT"],
  109. ], [3, 4, 6])
  110. # Section 2
  111. add_heading(doc, "二、整体流程(设备侧逻辑)", 1)
  112. add_code(doc, """操作员扫码(得到 sn)
  113. 调用 qmcheck(进站校验)
  114. result=true 且 data=UD ?
  115. 否 ──┴── 是
  116. │ │
  117. ▼ ▼
  118. 屏幕提示 开始气密测试
  119. 禁止测试 (设备自行测试)
  120. 调用 qmresult(上传结果)
  121. result=true ?
  122. 否 ────┴──── 是
  123. │ │
  124. ▼ ▼
  125. 提示失败 显示上传成功""")
  126. # Section 3
  127. add_heading(doc, "三、通信方式(统一约定)", 1)
  128. add_table(doc, ["项目", "值"], [
  129. ["协议", "HTTP"],
  130. ["方法", "POST"],
  131. ["参数格式", "表单:application/x-www-form-urlencoded"],
  132. ["编码", "UTF-8"],
  133. ["返回格式", "JSON 字符串"],
  134. ], [4, 10])
  135. add_para(doc, "请求示例格式(所有接口相同):")
  136. add_code(doc, """POST http://192.168.16.99:8980/js/a/mes/mesProductRecord/接口名
  137. Content-Type: application/x-www-form-urlencoded
  138. 参数1=值1&参数2=值2&...""")
  139. add_para(doc, "建议每个请求都带上:__ajax=json(固定值,便于返回 JSON)。")
  140. # Section 4
  141. add_heading(doc, "四、接口 1:进站校验 qmcheck", 1)
  142. add_heading(doc, "4.1 什么时候调", 2)
  143. add_para(doc, "• 操作员扫完条码、设备尚未开始充气/测试之前")
  144. add_para(doc, "• 每个工件测一次(换件后重新调)")
  145. add_heading(doc, "4.2 地址", 2)
  146. add_code(doc, "POST http://192.168.16.99:8980/js/a/mes/mesProductRecord/qmcheck")
  147. add_heading(doc, "4.3 要传哪些参数", 2)
  148. add_table(doc, ["参数", "必填", "从哪里来", "说明"], [
  149. ["sn", "是", "扫码枪 / 人工输入", "工件条码"],
  150. ["oprno", "是", "MES 提供,写死在配置里", "工位号"],
  151. ["lineSn", "是", "MES 提供,写死在配置里", "产线号"],
  152. ["workNum", "建议", "登录工号,没有就传 system", "操作员"],
  153. ["__ajax", "建议", "固定 json", "—"],
  154. ], [2.5, 1.5, 4.5, 4.5])
  155. add_heading(doc, "4.4 请求示例", 2)
  156. add_para(doc, "表单内容:")
  157. add_code(doc, "__ajax=json&sn=501901660045119990G4JB100071&oprno=OP450&lineSn=XT&workNum=system")
  158. add_para(doc, "curl(Windows):")
  159. add_code(doc, 'curl -X POST "http://192.168.16.99:8980/js/a/mes/mesProductRecord/qmcheck" ^\n -H "Content-Type: application/x-www-form-urlencoded" ^\n -d "__ajax=json&sn=501901660045119990G4JB100071&oprno=OP450&lineSn=XT&workNum=system"')
  160. add_heading(doc, "4.5 返回怎么判断(设备程序重点)", 2)
  161. add_para(doc, "返回示例:")
  162. add_code(doc, '{\n "result": "true",\n "message": "工件可以加工",\n "data": "UD"\n}')
  163. add_para(doc, "判断规则(写进设备程序):")
  164. add_code(doc, 'IF result == "true" AND data == "UD"\n → 允许开始测试\nELSE\n → 不允许测试,屏幕显示 message 原文')
  165. add_table(doc, ["result", "data", "设备怎么处理"], [
  166. ["true", "UD", "允许测试"],
  167. ["false", "任意", "禁止测试,显示 message"],
  168. ], [2.5, 2.5, 8])
  169. add_para(doc, "常见 message 含义:")
  170. add_table(doc, ["message 示例", "含义"], [
  171. ["工件可以加工", "正常,可以测"],
  172. ["该工件OP180未加工", "前面工位没做,不能测"],
  173. ["该工件本工位已加工,结果:OK", "已经测过且合格,不必再测"],
  174. ["该工件未录入系统", "条码错误或 MES 无此件"],
  175. ["两次气密必须间隔15分钟", "同一工件 15 分钟内不能重复测"],
  176. ["参数错误1", "sn / oprno / lineSn 有缺失"],
  177. ], [6, 7])
  178. # Section 5
  179. add_heading(doc, "五、接口 2:结果上传 qmresult", 1)
  180. add_heading(doc, "5.1 什么时候调", 2)
  181. add_para(doc, "• 气密测试结束后(无论 OK 还是 NG 都要调)")
  182. add_para(doc, "• 必须先 qmcheck 通过再测;测完再调本接口")
  183. add_heading(doc, "5.2 地址", 2)
  184. add_code(doc, "POST http://192.168.16.99:8980/js/a/mes/mesProductRecord/qmresult")
  185. add_heading(doc, "5.3 要传哪些参数", 2)
  186. add_para(doc, "必填(少了会报「参数错误1」):")
  187. add_table(doc, ["参数", "说明", "示例"], [
  188. ["sn", "工件条码(与 qmcheck 相同)", "扫码值"],
  189. ["oprno", "工位号", "OP450"],
  190. ["lineSn", "产线号", "XT"],
  191. ["result", "测试结果", "OK 或 NG(必须大写)"],
  192. ["craft", "工艺号", "固定传 100000"],
  193. ["__ajax", "建议", "json"],
  194. ], [2.5, 5, 5.5])
  195. add_para(doc, "建议传:workNum(操作员工号,没有传 system)")
  196. add_para(doc, "可选(有就传,便于 MES 存过程数据):")
  197. add_table(doc, ["参数", "是否编码", "说明"], [
  198. ["testPressure", "Base64", "测试压力"],
  199. ["testPressureUnit", "明文", "单位,如 Pa"],
  200. ["leakVal", "Base64", "泄漏值"],
  201. ["leakValUnit", "明文", "单位"],
  202. ["testTime", "Base64", "测试时间,格式 yyyy-MM-dd HH:mm:ss"],
  203. ["cxm", "明文", "设备程序号"],
  204. ["deviceType", "明文", "设备编号"],
  205. ["remark", "明文", "备注"],
  206. ], [3.5, 2, 8])
  207. add_para(doc, "关于 Base64:testPressure、leakVal、testTime 需先 Base64 编码再提交。若暂时不做 Base64,最少只传必填项也能完成工位对接。")
  208. add_para(doc, "Base64 对照(方便联调):")
  209. add_table(doc, ["原始值", "传参值"], [
  210. ["200", "MjAw"],
  211. ["5", "NQ=="],
  212. ["2024-05-16 12:12:00", "MjAyNC0wNS0xNiAxMjoxMjowMA=="],
  213. ], [5, 8])
  214. add_heading(doc, "5.4 请求示例", 2)
  215. add_para(doc, "最简版(能完成工位对接):")
  216. add_code(doc, "__ajax=json&sn=501901660045119990G4JB100071&oprno=OP450&lineSn=XT&craft=100000&result=OK&workNum=system")
  217. add_para(doc, "完整版(含过程数据):")
  218. add_code(doc, "__ajax=json&sn=501901660045119990G4JB100071&oprno=OP450&lineSn=XT&craft=100000&result=OK&workNum=system&testPressure=MjAw&testPressureUnit=Pa&leakVal=NQ==&leakValUnit=Pa&testTime=MjAyNC0wNS0xNiAxMjoxMjowMA==&cxm=PROGRAM_01")
  219. add_heading(doc, "5.5 返回怎么判断", 2)
  220. add_para(doc, "成功:")
  221. add_code(doc, '{\n "result": "true",\n "message": "操作成功!"\n}')
  222. add_para(doc, "失败:")
  223. add_code(doc, '{\n "result": "false",\n "message": "该工件本工位已加工,结果:OK"\n}')
  224. add_code(doc, 'IF result == "true"\n → 上传成功,MES 工位结果已更新\nELSE\n → 上传失败,显示 message,建议本地保存待人工处理')
  225. add_table(doc, ["常见 message", "原因", "建议"], [
  226. ["操作成功!", "正常", "—"],
  227. ["参数错误1!", "缺 sn/oprno/lineSn/result", "检查必填参数"],
  228. ["子工位未配置!", "MES 未配置该工位", "联系 MES 管理员"],
  229. ["该工件本工位已加工…", "重复上传", "提示用户,勿重复提交"],
  230. ["该工件OPxxx未加工", "未做 qmcheck 或前序工位未完成", "先校验流程"],
  231. ], [4.5, 4.5, 4])
  232. # Section 6
  233. add_heading(doc, "六、设备程序伪代码", 1)
  234. add_code(doc, """// 配置项(MES 方提供,写死在设备软件里)
  235. MES_HOST = "192.168.16.99"
  236. MES_PORT = 8980
  237. OPRNO = "OP450" // 工位号,向 MES 确认
  238. LINE_SN = "XT" // 产线号,向 MES 确认
  239. // 1. 扫码后
  240. sn = 扫码结果
  241. body = "__ajax=json&sn=" + urlEncode(sn)
  242. + "&oprno=" + OPRNO + "&lineSn=" + LINE_SN
  243. + "&workNum=" + urlEncode(当前工号或"system")
  244. resp = HTTP_POST("http://" + MES_HOST + ":8980/js/a/mes/mesProductRecord/qmcheck", body)
  245. if resp.result != "true" OR resp.data != "UD":
  246. 显示(resp.message); 停止,不允许测试
  247. // 2. 设备自行做气密测试
  248. testResult = 设备判定结果 // "OK" 或 "NG"
  249. // 3. 测试结束后上传
  250. body = "__ajax=json&sn=" + urlEncode(sn)
  251. + "&oprno=" + OPRNO + "&lineSn=" + LINE_SN
  252. + "&craft=100000" + "&result=" + testResult
  253. + "&workNum=" + urlEncode(当前工号或"system")
  254. resp = HTTP_POST("http://" + MES_HOST + ":8980/js/a/mes/mesProductRecord/qmresult", body)
  255. if resp.result != "true":
  256. 显示("MES上传失败:" + resp.message); 本地记录,供补传
  257. else
  258. 显示("MES上传成功")""")
  259. # Section 7
  260. add_heading(doc, "七、联调步骤(双方配合)", 1)
  261. add_heading(doc, "第 1 步:网络", 2)
  262. add_para(doc, "□ 气密设备电脑能 ping 192.168.16.99")
  263. add_para(doc, "□ 浏览器或 Postman 能访问 http://192.168.16.99:8980")
  264. add_heading(doc, "第 2 步:用 Postman 测通两个接口", 2)
  265. add_para(doc, "1. 向 MES 方要一个真实可测的工件码 sn")
  266. add_para(doc, "2. 调 qmcheck,确认返回 result=true, data=UD")
  267. add_para(doc, "3. 调 qmresult,result=OK,确认返回「操作成功!」")
  268. add_para(doc, "4. MES 方在后台确认工位结果已更新")
  269. add_heading(doc, "第 3 步:设备软件接入", 2)
  270. add_para(doc, "按第六节伪代码接入;先实现最简参数,过程数据稳定后再补。")
  271. add_heading(doc, "第 4 步:现场验证", 2)
  272. add_para(doc, "□ 正常件:check 通过 → 测试 → result 上传成功")
  273. add_para(doc, "□ NG 件:result=NG 上传成功,MES 显示 NG")
  274. add_para(doc, "□ 重复扫码:check 提示已加工,不允许再测")
  275. add_para(doc, "□ 错误条码:check 提示未录入系统")
  276. # Section 8 FAQ
  277. add_heading(doc, "八、常见问题 FAQ", 1)
  278. faqs = [
  279. ("Q1:要不要登录 MES?", "不需要。两个接口均已开放匿名访问。"),
  280. ("Q2:只调 qmresult 不调 qmcheck 可以吗?", "不建议。可能因前序工位、重复加工等原因上传失败。"),
  281. ("Q3:craft 为什么固定 100000?", "MES 约定 100000 表示工位总结果(OK/NG),必须传。"),
  282. ("Q4:result 用小写 ok/ng 可以吗?", "不可以,必须大写 OK / NG。"),
  283. ("Q5:qmcheck 和 qmresult 的 sn 必须一致吗?", "必须一致,为同一工件条码。"),
  284. ("Q6:上传失败要不要重试?", "看 message。若是「已加工」等业务拦截,不要盲重试;网络超时可在本地缓存后定时重试 qmresult。"),
  285. ("Q7:工件码有特殊字符怎么办?", "做 URL 编码(如空格、&、= 等)。"),
  286. ]
  287. for q, a in faqs:
  288. add_para(doc, q, bold=True)
  289. add_para(doc, "A:" + a)
  290. # Section 9
  291. add_heading(doc, "九、对接确认单(请 MES 方填写后发给设备方)", 1)
  292. add_table(doc, ["项", "值"], [
  293. ["MES 服务器 IP", "192.168.16.99"],
  294. ["端口", "8980"],
  295. ["工位号 oprno", "__________"],
  296. ["产线号 lineSn", "__________"],
  297. ["测试用工件码 sn", "__________"],
  298. ["是否需要传过程数据(压力/泄漏值)", "是 / 否"],
  299. ["MES 联系人", "__________"],
  300. ["联系电话 / 微信", "__________"],
  301. ], [5.5, 8.5])
  302. doc.save(OUTPUT)
  303. print("Generated:", OUTPUT)
  304. if __name__ == "__main__":
  305. build()