25 KiB
手机图片排版功能 - 架构文档
基于源代码实际阅读编写,记录排版引擎的设计原理与数据流。
1. 渲染管线
排版引擎的核心是 PhoneImageEngine(IIFE 闭包,public/static/js/phone-image.js),纯前端运行,后端不参与渲染计算。
1.1 管线总览
用户触发 doRender() 或编辑器 onChange 自动触发
|
v
PhoneImageEngine.render() -- L82: 管线入口,带并发锁
|
+---> editorHtml = window.phoneImageEditor.getHtml() -- L99: 从 wangeditor 读取HTML
+---> preprocessContent(editorHtml) -- L154: 清洗HTML(去script/style/iframe,规范化img)
+---> parseHtmlToBlocks(cleanHtml) -- L201: HTML -> 块级元素数组,<hr> 识别为 page-break
|
+---> generateCoverPage(sizeConfig) -- L619: 生成封面页HTML
|
+---> captureEditorBlocks(editorHtml, blocks, areaHeight, sizeConfig) -- L345: DOM测高核心
| |
| +---> 按 page-break(<hr>) 将 blocks 分组 -- L349-365
| +---> getEditorChildren() -- L323: 穿透 div 包装层获取编辑器DOM子元素
| +---> 创建 500px 测量容器(fixed, visibility:hidden) -- L376-387
| +---> 串行测高每组:DOM克隆到测量容器 -> getBoundingClientRect -- L392-433
| +---> 按比例分配实际高度给组内各 block -- L421-428
| +---> paginateContent(blocks, contentAreaHeight, sizeConfig) -- L445: 按高度累加分页
|
+---> generateSummaryPage(sizeConfig, totalPages) -- L731: 生成尾页HTML
+---> 页码补全 N/M -- L122-129: 二次遍历替换占位页码
+---> renderThumbnails(sizeConfig) -- L968: 截图 -> 显示右侧缩略图
|
+---> doCapturePages({scale:1}) -- L841: staging隐藏区域渲染 + html2canvas串行截图
|
+---> 纯图片页优化: 直接用 Image + Canvas,跳过 html2canvas -- L805-830
1.2 并发控制
render() 函数使用 _locked 标志防止并发渲染(phone-image.js:86-91)。渲染期间如果有新请求,设置 _pending = true,当前渲染完成后自动重试(phone-image.js:134-137)。
1.3 DOM 测高机制
captureEditorBlocks(phone-image.js:345-437)是管线中替代旧版 renderContentFlow + measureBlockHeights 的核心函数。工作流程:
- 按
page-break类型将 blocks 分为若干组 - 通过
getEditorChildren()获取 wangeditor 编辑区的真实 DOM 子元素 - 创建 500px 宽的隐藏测量容器(fixed 定位,
visibility:hidden),与内容区宽度(540 - 20*2 padding)一致 - 对每组:将对应的 DOM 子元素克隆到测量容器,通过
requestAnimationFrame后读取getBoundingClientRect().height获取实际高度 - 按各 block 估算高度的比例,将测量的实际高度分配给组内各 block
- 所有组测高完成后,调用
paginateContent进行分页
1.4 截图机制
截图在 #render-staging 隐藏区域进行(phone-image.html:597)。关键步骤:
- 将所有页面HTML append到 staging 区域的
.phone-image-container容器中 - 临时将
visibility:hidden切为visible(html2canvas 会跳过隐藏元素) - 串行逐页调用 html2canvas 截图(
runCaptureLoop, L881) - 纯图片页优化:直接用
Image + Canvas绘制,跳过 html2canvas(phone-image.js:805-830) - 截图完成后恢复
visibility:hidden
缩略图用 scale:1,保存用 scale:2(输出1080px宽度)。
1.5 getEditorChildren 穿透逻辑
getEditorChildren()(phone-image.js:323-334)处理 wangeditor v5 的 DOM 包装层。wangeditor 的 [data-slate-editor] 下可能有一层 <div> 包装,该函数检测到这种情况时穿透到内层子元素,返回实际的内容节点数组。这对 captureEditorBlocks 的 DOM 克隆测高和 exportLongImage 的长图导出都至关重要。
2. wangeditor 集成
2.1 编辑器初始化
编辑器在 phone_image.html 模板脚本中初始化(L192-288):
var E = window.wangEditor;
var phoneImageEditor = E.createEditor({
selector: '#editor-text-area',
html: postData.contentHtml, // 初始内容,来自服务端渲染
config: editorConfig
});
var toolbar = E.createToolbar({
editor: phoneImageEditor,
selector: '#editor-toolbar',
config: {
excludeKeys: ['fullScreen'],
insertKeys: {
index: 24,
keys: ['divider'] // 分割线按钮,插入位置 index 24
}
}
});
window.phoneImageEditor = phoneImageEditor; // 全局引用
2.2 工具栏配置
- 排除
fullScreen按钮 - 在 index 24 位置插入
divider(分割线)按钮 - 其余使用 wangeditor 默认按钮集
2.3 分割线与分页的关系
wangeditor 的 divider 按钮在编辑器中插入 <hr> 元素(带 data-w-e-type="divider" 属性)。渲染管线中 parseHtmlToBlocks 将 <hr> 识别为 page-break 类型的块(phone-image.js:259-263),paginateContent 遇到 page-break 时强制换页(phone-image.js:455-466)。captureEditorBlocks 也按 page-break 对 blocks 分组测高。
2.4 粘贴处理
editorConfig.customPaste(phone_image.html:220-268)拦截粘贴操作,将外部图片(http 开头的URL图片和 data: 开头的 base64 图片)上传到服务器后替换为本地URL,确保编辑器中的图片资源可控。
2.5 DOM 结构
div.content-flow-area (540px, overflow-y:auto)
+-- div#editor-toolbar (wangeditor 工具栏)
+-- div#editor-text-area (wangeditor 编辑区)
+-- div[data-slate-editor] (wangeditor 内部)
+-- div (可能的包装层)
+-- p / h1 / img / hr(divider) / ... (实际内容节点)
3. 数据流
3.1 数据源模型
重构后,wangeditor 编辑器成为运行时的权威内容源。引擎闭包变量 postData.content_html 仅作为初始化回退。
权威数据源:window.phoneImageEditor.getHtml(),通过 getContentHtml() API 统一访问(phone-image.js:1280-1285):
getContentHtml: function () {
if (window.phoneImageEditor) {
return window.phoneImageEditor.getHtml();
}
return postData.content_html; // 回退
}
引擎公开 API:
| API | 位置 | 用途 |
|---|---|---|
getContentHtml() |
phone-image.js:1280-1285 |
优先从 wangeditor 读取HTML,回退到闭包变量 |
updateConfig(newConfig) |
phone-image.js:1286-1290 |
使用 $.extend(config, newConfig) 更新配置,不重置内容 |
saveConfig(postId, saveConfig, url) |
phone-image.js:1138-1170 |
保存配置到服务端(不生成图片),返回 {output_id} |
saveImages(postId, saveConfig) |
phone-image.js:1079-1129 |
截图并保存所有页面图片到服务端 |
exportLongImage() |
phone-image.js:1192-1257 |
从编辑器DOM导出完整长图 |
DOM #post-content-html 的角色:仅作为初始化数据源(phone_image.html:105)和保存后的显示同步层(phone_image.html:434,459),保存操作不从中读取内容。
3.2 各操作的数据流
保存配置 (#btn-save):
content_html 取自 PhoneImageEngine.getContentHtml() -- L431
调用 PhoneImageEngine.saveConfig() API -- L428-432
保存后同步: $('#post-content-html').html(getContentHtml()) -- L434
生成并保存 (#btn-generate):
content_html 取自 PhoneImageEngine.getContentHtml() -- L454
调用 PhoneImageEngine.saveImages() API -- L451-455
保存后同步: $('#post-content-html').html(getContentHtml()) -- L459
重新排版 (doRender):
用 PhoneImageEngine.updateConfig(newConfig) -- L344
再调 PhoneImageEngine.render() -- L346
自动保存 (onChange):
wangeditor onChange -> 2.6s 防抖 -> doAutoSave() -- phone_image.html:209-216
doAutoSave 调用 PhoneImageEngine.saveConfig() -- phone_image.html:403-413
加载历史 (loadFromHistory):
window.phoneImageEditor.setHtml(res.data.content_html) -- phone_image.html:561 (加载到编辑器)
PhoneImageEngine.updateConfig(historyConfig) -- phone_image.html:578
PhoneImageEngine.render() -- phone_image.html:580
导出长图 (更多菜单 -> exportLong):
PhoneImageEngine.exportLongImage() -- phone_image.html:484
内部: getEditorChildren() -> 克隆非divider元素 -> html2canvas scale=2 -> 下载PNG
3.3 自动保存机制
编辑器内容变更时触发自动保存(phone_image.html:206-413):
editorConfig.onChange触发,设置autoSaveLock = true,显示"等待保存..."状态- 清除之前的防抖定时器,设置新的 2.6 秒定时器
- 定时器到期后调用
doAutoSave() doAutoSave()检查autoSaveLock,通过PhoneImageEngine.saveConfig()发送保存请求- 保存状态通过
#save-state指示器反馈(waiting/saving/saved/error,phone_image.html:388-397)
手动保存按钮点击时会清除自动保存定时器,避免冲突。
3.4 完整数据流图
[数据库 Post.content_html]
|
v
Post.php::phoneImage() (L355)
查询 PostOutput 取已保存的 content_html 副本
|
v
phone_image.html 模板渲染
- $layoutContentHtml (优先用已保存副本)
- $layoutConfig (已保存的配置)
|
v
前端初始化 (phone_image.html:152-294)
1. 构造 postData 局部对象,contentHtml 取自 #post-content-html (L173)
2. 恢复 savedConfig (L181-186)
3. wangeditor 初始化,html=postData.contentHtml (L270-274)
4. window.phoneImageEditor = phoneImageEditor (L288)
5. PhoneImageEngine.init(postData, initConfig) (L290)
6. doRender() 触发初始渲染 (L594)
|
v
渲染管线 (phone-image.js render())
- 读取 window.phoneImageEditor.getHtml() (唯一权威源)
- parseHtmlToBlocks + captureEditorBlocks(测高) + paginateContent(分页)
- staging 渲染 + html2canvas 截图
- 显示缩略图
|
v
用户交互
- 编辑内容 -> wangeditor -> onChange 自动保存(2.6s防抖)
- 分割线 -> wangeditor divider -> <hr> -> page-break 分页
- 保存配置 -> PhoneImageEngine.saveConfig() (content_html 取自 getContentHtml())
- 生成保存 -> PhoneImageEngine.saveImages() (content_html 取自 getContentHtml())
- 重新排版 -> updateConfig() + render()
- 加载历史 -> editor.setHtml() + updateConfig() + render()
- 导出长图 -> exportLongImage() (从编辑器DOM直接截图)
|
v
后端存储
- PostOutput.config (JSON字段,含 content_html 副本)
- PostOutputFile (每页图片文件记录)
4. 存储模型
4.1 数据库表
ul_post_output (模型: app/model/PostOutput.php)
| 字段 | 说明 |
|---|---|
id |
主键 |
post_id |
关联文章ID |
output_type |
输出类型,固定为 phone_image |
config |
JSON字段,存储完整配置 + content_html 副本 |
status |
0=生成中, 1=已完成, 2=失败 (常量定义 L20-22) |
page_count |
页数 |
admin_id |
操作管理员ID |
create_time |
创建时间 (int) |
delete_time |
软删除标记 (默认0,trait: SoftDelete) |
模型使用 $json = ['config'] 自动序列化/反序列化配置(PostOutput.php:18)。
ul_post_output_file (模型: app/model/PostOutputFile.php)
| 字段 | 说明 |
|---|---|
id |
主键 |
output_id |
关联 post_output.id |
page |
页码 (从1开始) |
file_path |
相对路径,如 /upload/post_output/20260511/5_1.jpg |
file_url |
访问URL (同 file_path) |
file_size |
文件字节数 |
width |
图片宽度 (px) |
height |
图片高度 (px) |
关系: PostOutput hasMany PostOutputFile,PostOutput belongsTo Post。
4.2 config JSON 结构
{
"size": "xiaohongshu",
"watermark": "",
"pageAlignments": { "1": "top", "2": "center" },
"content_html": "<p>文章HTML副本</p><hr><p>第二页内容</p>"
}
content_html 作为配置的一部分保存,使每次排版记录都携带当时的内容快照。含 <hr> 分页标记的 HTML 被完整保存,parseHtmlToBlocks 可以正确解析。
向后兼容:旧版 PostOutput 记录中保存的 <hr> 分页标记仍可被 parseHtmlToBlocks 正确识别为 page-break(phone-image.js:259-263),不影响历史记录加载和重新排版。
4.3 loadFromHistory 加载机制
loadFromHistory(outputId)
-> GET loadPostOutputConfig?id=XXX
-> window.phoneImageEditor.setHtml(res.data.content_html) -- 加载到编辑器
-> PhoneImageEngine.updateConfig(historyConfig) -- 更新配置
-> PhoneImageEngine.render() -- 重新渲染
不再使用 init() 全量重置,而是通过 editor.setHtml() 直接将历史内容加载到 wangeditor,然后 updateConfig() 更新配置后重新渲染。
4.4 文件存储
图片保存到 public/upload/post_output/{Ymd}/{outputId}_{page}.jpg(PhoneImage.php:87-92)。ZIP 临时文件生成到 runtime/temp/ 目录,请求结束后通过 register_shutdown_function 清理(Post.php:570-575)。
5. wangeditor 配置与工具栏
5.1 编辑器配置
editorConfig = {
MENU_CONF: {},
placeholder: '请输入排版内容,使用分割线标记分页位置',
scroll: false,
MENU_CONF: {
'uploadImage': {
server: '{:url("File/wangEditorSave")}',
fieldName: 'file',
meta: { type: 'editor' }
}
},
onChange: function(editor) { /* 自动保存 2.6s 防抖 */ },
customPaste: function(editor, event) { /* 外部图片上传处理 */ }
}
5.2 工具栏配置
toolbar config = {
excludeKeys: ['fullScreen'],
insertKeys: {
index: 24, // 在第24个按钮位置后插入
keys: ['divider'] // 分割线按钮
}
}
保留 wangeditor 所有默认按钮(粗体、斜体、标题、列表、引用、图片、链接等),额外添加分割线按钮用于手动标记分页位置。
6. 配置体系
6.1 PHP 端配置字段定义
PhoneImage::getConfigFields() 定义了4个字段:
| 字段 | 类型 | 选项/范围 | 默认值 |
|---|---|---|---|
size |
select | xiaohongshu/douyin | xiaohongshu |
fontSize |
number | 10-24 | 14 |
watermark |
text | - | '' |
pageAlignments |
json | - | '{}' |
6.2 JS 端实际使用的配置
phone-image.js 闭包内的 config 对象 (L14-23):
config = {
size: 'xiaohongshu',
watermark: '',
pageAlignments: {}, // 逐页对齐
sizes: { ... }, // 尺寸预设
contentPadding: 20
}
6.3 设置弹框
通过 layui layer 弹框实现(phone_image.html:293-330),提供:
- 尺寸选择:小红书 (1080x1440) / 抖音 (1080x1920)
- 水印输入:可选水印文字
确认后自动触发 doRender() 重新排版。
6.4 自动保存状态指示器
<span id="save-state"> 元素(phone_image.html:417)动态显示保存状态:
waiting:黄色,"等待保存..."saving:蓝色,"保存中..."saved:绿色,"已保存"error:红色,"保存失败"
7. UI 布局
7.1 整体结构
两栏布局,适配宽屏操作:
+----------------------------------------------------------+
| page-header (顶部操作栏) |
| [返回列表] 标题 [设置] [保存] [生成并保存] [更多v] 已保存 |
+------------------------------+---------------------------+
| content-flow-area (540px) | paginated-preview-area |
| +--------------------------+ | (flex:1, 横向滚动) |
| | #editor-toolbar | | |
| | (wangeditor工具栏) | | [缩略图1] [缩略图2] ... |
| +--------------------------+ | 每张带页码和对齐按钮 |
| | #editor-text-area | | |
| | (wangeditor编辑区) | | |
| | min-height:300px | | |
| +--------------------------+ | |
+------------------------------+---------------------------+
7.2 顶部操作栏
按钮直接显示:设置、保存、生成并保存。"更多"下拉菜单(纯 JS 实现,phone_image.html:358-385)包含:历史记录、重新生成、打包下载、导出长图。
layui 2.x 没有 dropdown 模块,更多菜单用纯 JS 实现的 toggle + stopPropagation + 外部点击关闭。
7.3 缩略图区域
右侧缩略图横向排列,每个缩略图带页码标签和对齐切换按钮(仅内容页显示,phone-image.js:1017-1025)。对齐按钮切换 top/center 垂直对齐。
8. 已删除的函数/组件
wangeditor 重构中移除的旧版函数和组件:
8.1 渲染管线相关
| 已删除函数 | 说明 |
|---|---|
renderContentFlow |
旧版将块渲染到中间栏 #content-flow,已由 wangeditor 编辑器替代 |
convertFlowBlocksToImages |
旧版表格/代码块预转换(html2canvas截图替换为img),新版不再需要 |
measureBlockHeights |
旧版从中间栏DOM读取实测高度,已由 captureEditorBlocks 的测量容器方案替代 |
insertPageBreak |
旧版中间栏"+"按钮插入分页标记,已由 wangeditor divider 替代 |
removePageBreak |
旧版中间栏"-"按钮移除分页标记,已由编辑器直接删除 divider 替代 |
8.2 高度估算相关
| 已删除函数 | 说明 |
|---|---|
estimateTextHeight |
旧版纯计算估算段落高度,已由 DOM 测量替代 |
estimateHeadingHeight |
旧版估算标题高度 |
estimateListHeight |
旧版估算列表高度 |
estimateBlockquoteHeight |
旧版估算引用块高度 |
8.3 工具函数
| 已删除函数 | 说明 |
|---|---|
simpleHash |
旧版 djb2 哈希,用于 convertedBlockCache 缓存key |
getContentWidth |
旧版获取内容区宽度 |
8.4 UI 组件
| 已删除组件 | 说明 |
|---|---|
#content-flow 中间栏 |
旧版内容渲染和交互区域,已由 wangeditor 编辑器替代 |
| 左侧工具栏 | 旧版编辑操作面板(分页标记按钮等) |
convertedBlockCache |
旧版表格/代码块截图缓存,新版不再预转换 |
9. 已知限制
9.1 html2canvas 兼容性
html2canvas 截图不支持以下 CSS 特性:
transformfilterclip-pathbackdrop-filter- CSS grid
object-fit
在 wangeditor 编辑器和自定义样式中应避免使用这些属性,否则截图中可能显示异常。
9.2 wangeditor divider 特性
wangeditor 的 divider 是 void 元素,::after 伪元素可能不生效。编辑器中 divider 的视觉样式依赖 wangeditor 内部 CSS。
9.3 layui dropdown 缺失
layui 2.x 没有 dropdown 模块,"更多"菜单用纯 JS 实现(phone_image.html:358-385),包括 toggle 显示/隐藏、stopPropagation 阻止冒泡、全局 click 事件关闭。
9.4 保存数据量限制
saveImages() 有16MB的客户端检查(phone-image.js:1096-1100),但服务端没有对应的分块接收机制。大文章(超过约30页)的base64数据可能超出 PHP post_max_size 配置。
9.5 目录权限 0777
PhoneImage.php:95 和 :140 中 mkdir 使用 0777 权限,与项目其他位置的 anti-pattern 一致。
9.6 接口 PostOutputManagerInterface 未被充分利用
PhoneImage 实现了 process() 和 getPreview() 方法,但两者都返回空值(PhoneImage.php:56-67)。实际业务完全在控制器中直接调用 PhoneImage 的其他方法,绕过了接口定义的流程。接口仅作为类型约束存在。
9.7 saveConfigOnly 是静态方法但 createOutput 是实例方法
PhoneImage::saveConfigOnly() 是 static 方法(L223),而同类的 createOutput() 是实例方法(L193)。两者的实现几乎相同(都是 PostOutput::create()),但调用方式不同。控制器中 savePostOutput 通过实例调用 createOutput,savePostOutputConfig 通过静态调用 saveConfigOnly,风格不统一。
10. API 端点清单
所有端点位于 app/admin/controller/Post.php,通过 ThinkPHP 自动路由访问,URL前缀为 /index.php/admin/post/。
| 端点 | 方法 | 说明 | 前端调用方 |
|---|---|---|---|
phoneImage/{id} |
GET | 排版操作页(渲染模板) | 页面跳转 |
savePostOutput |
POST (JSON) | 保存输出记录+图片 | PhoneImageEngine.saveImages() (phone-image.js:1103) |
savePostOutputConfig |
POST (JSON) | 仅保存配置(不生成图片) | PhoneImageEngine.saveConfig() (phone-image.js:1152) |
loadPostOutputConfig |
GET | 加载历史记录配置 | loadFromHistory() (phone_image.html:550) |
getOutputListJson |
GET | 获取输出记录列表 | 历史弹窗 (phone_image.html:504) |
deletePostOutput |
POST/GET | 删除输出记录及文件 | 未在前端模板中直接调用 |
regeneratePostOutput |
POST/GET | 删除旧记录,返回配置 | 未在前端模板中直接调用 |
downloadPostOutputZip/{id} |
GET | 打包下载ZIP | 更多菜单 -> 打包下载 |
postOutputList/{id} |
GET | 输出管理列表页 | 独立页面入口 |
请求/响应格式
savePostOutput 请求体:
{
"post_id": 123,
"output_type": "phone_image",
"config": { "size": "...", "watermark": "...", ... },
"content_html": "<p>...</p>",
"pages": ["data:image/jpeg;base64,..."]
}
savePostOutputConfig 请求体:
{
"post_id": 123,
"output_type": "phone_image",
"config": { "size": "...", "watermark": "...", "pageAlignments": {} },
"content_html": "<p>...</p>"
}
通用响应格式:
{ "code": 0, "msg": "", "data": { "output_id": 456 } }
失败时 code 为 500。
11. 修复记录
11.1 数据源统一修复(wangeditor 重构前)
修复日期:2026-05-11
核心原理:将三个独立的数据源(DOM、闭包变量、模板局部变量)统一为单一权威数据源,通过新增的引擎 API 对外暴露。
变更清单:
| 变更 | 文件 | 说明 |
|---|---|---|
新增 getContentHtml() |
phone-image.js:1280-1285 |
优先从 wangeditor 读取,回退到闭包变量 |
新增 updateConfig(newConfig) |
phone-image.js:1286-1290 |
使用 $.extend 更新配置,不重置内容 |
新增 saveConfig() API |
phone-image.js:1138-1170 |
引擎级保存配置方法 |
新增 exportLongImage() |
phone-image.js:1192-1257 |
从编辑器DOM导出长图 |
保存按钮改用 getContentHtml() |
phone_image.html:431 |
content_html 取自引擎 API |
生成按钮改用 getContentHtml() |
phone_image.html:454 |
content_html 取自引擎 API |
| 自动保存机制 | phone_image.html:206-413 |
onChange 2.6s 防抖自动保存 |
doRender 改用 updateConfig() |
phone_image.html:344 |
替代 init() 调用 |
loadFromHistory 用 editor.setHtml() |
phone_image.html:561 |
加载到编辑器而非重置引擎 |
11.2 wangeditor 重构
重构日期:2026-05-11
核心变更:将内容编辑从旧版中间栏(#content-flow + 手动分页标记)迁移到 wangeditor 富文本编辑器。
主要变更:
| 变更 | 说明 |
|---|---|
| 引入 wangeditor v5 | 替代旧版中间栏内容展示和编辑 |
新增 captureEditorBlocks |
DOM 测量方案替代旧版高度估算 |
新增 getEditorChildren |
穿透 wangeditor DOM 包装层 |
新增 exportLongImage |
从编辑器DOM导出完整长图 |
| 分割线替代分页标记按钮 | wangeditor divider (<hr>) = page-break |
| 自动保存 | 编辑器 onChange 触发 2.6s 防抖保存 |
| 更多菜单纯 JS 实现 | 替代 layui dropdown(不存在) |
| 删除旧版渲染管线 | renderContentFlow / convertFlowBlocksToImages / measureBlockHeights 等 |
| 删除旧版高度估算 | estimateTextHeight 等,由 DOM 测量替代 |
| 删除旧版 UI | 左侧工具栏、中间栏交互 |
重构效果:
- 内容编辑体验从"预览+标记"模式变为"所见即所得"编辑模式
- 分页标记通过分割线直观可见和可编辑
- 自动保存减少手动操作
- DOM 测量替代估算,分页精度提升
文档基于以下源文件的完整阅读:
public/static/js/phone-image.js(1292行)view/admin/post/phone_image.html(600行)app/admin/controller/Post.php(L350-579)app/common/tools/PhoneImage.php(229行)app/common/tools/PostOutputManagerInterface.php(28行)app/model/PostOutput.php(62行)app/model/PostOutputFile.php(26行)