企業サイトバックエンドWord/微信コンテンツインポート機能統合プロジェクト記録
1. プロジェクト要件分析
広西の某グループ会社プロジェクトリーダーとして、企業サイトバックエンド管理システムの記事投稿モジュールに以下の機能を追加する必要がありました:
主要要件:
- Wordクリップボード機能:Wordからコンテンツをコピーしてエディタに貼り付け、画像を自動アップロード
- Wordドキュメントインポート:Word/Excel/PPT/PDFドキュメントのバッチインポート対応
- 微信公众号コンテンツ貼り付け:画像を自動ダウンロードしてアップロード
- 元のスタイル維持:表/図形/数式/GB2312フォント/サイズ/色など
技術スタック要件:
- フロントエンド:Vue2 CLI + TinyMCE
- バックエンド:SpringBoot
- データベース:MySQL
- サーバー:華為クラウドECS + OBS
- 開発ツール:IntelliJ IDEA
特殊要件:
- 信頼性コンピューティング環境対応:複数OS+CPU組み合わせ
- ブラウザ互換:IE8を含む主要ブラウザ
- 予算:58万以内でライセンス購入
- 今後の価格変動リスク回避
2. 市場調査と製品評価
2.1 解決策候補調査
市場調査の結果、以下の解決策が候補となりました:
- TinyMCE公式PowerPasteプラグイン
- 有利点:TinyMCEネイティブサポート、簡単な統合
- 不利点:微信コンテンツインポート非対応、信頼環境での互換性懸念
- KindEditor商用版
- 有利点:国内製品、信頼環境互換性良好
- 不利点:数式や複雑な図形非対応、メンテナンス頻度低
- WangEditor企業版
- 有利点:軽量設計、信頼環境サポート良好
- 不利点:機能が単純、Word/Excel/PPT/PDF/微信インポート非対応
- UEditor百度エディタ
- 有利点:機能豊富、国内環境対応
- 不利点:メンテナンス停止、IE8互換性問題
- カスタム開発方案
- 有利点:完全カスタマイズ
- 不利点:開発周期長、コスト高
2.2 製品機能比較表
| 機能特性 | TinyMCE PowerPaste | KindEditor商用版 | WangEditor企業版 | UEditor | カスタム開発 | WordPasterソースコード版 |
|---|---|---|---|---|---|---|
| Word貼り付け | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| ファイルインポート | ✅(Wordのみ) | ✅ | ❌ | ✅ | ✅ | Word,Excel,PPT,PDF |
| 微信貼り付け | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ |
| 数式サポート | ✅ | ❌ | ❌ | ❌ | ✅ | Latex,MathType |
| 信頼環境互換 | 一部 | ✅ | ✅ | ✅ | ✅ | ✅ |
| IE8互換 | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ |
| ライセンス買断 | ❌ | ✅ | ✅ | ✅ | 不適用 | ✅ |
| 予算内 | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
2.3 最終選択:WordPasterソースコード版採用
決定理由:
- TinyMCEネイティブサポートで統合リスク最小
- 核心Word処理機能が成熟安定
- 拡張性高く、微信機能追加容易
- チームがTinyMCEに精通しメンテコスト低
- 二次開発で信頼環境要件満たせる
商談結果:
- WordPasterソースコードライセンス+PowerPasteプラグイン購入
- 元請業者に微信機能モジュール開発委託
- 総費用55万(ライセンス買断+カスタム開発)
- 3年間サポートとバージョン更新込み
3. 技術実装計画
3.1 システムアーキテクチャ設計
[フロントエンド]
Vue2 CLI → TinyMCEエディタ → カスタムプラグイン(Word/微信処理)
↓
[バックエンドAPI]
SpringBoot → ファイル処理サービス → 華為クラウドOBSストレージ
↑
[データベース]
MySQL(メタデータ保存)
3.2 キー技術実装
3.2.1 フロントエンド(Vue2+TinyMCE)
main.js統合コード:
import tinymce from 'tinymce/tinymce'
import 'tinymce/themes/silver'
import 'tinymce/plugins/powerpaste'
import 'tinymce/plugins/wechatpaste' // カスタム微信プラグイン
Vue.prototype.$tinymce = tinymce
const initTinyMCE = {
language: 'zh_CN',
plugins: 'powerpaste wechatpaste image table code',
toolbar: 'paste wechatpaste | image table',
powerpaste_word_import: 'clean',
powerpaste_html_import: 'clean',
powerpaste_allow_local_images: true,
paste_data_images: true,
images_upload_handler: function (blobInfo, success, failure) {
// バックエンド画像アップロードAPI呼び出し
uploadImage(blobInfo.blob()).then(url => {
success(url)
}).catch(err => {
failure('アップロード失敗: ' + err)
})
}
}
3.2.2 バックエンド(SpringBoot)
画像アップロードController:
@RestController
@RequestMapping("/api/upload")
public class UploadController {
@Autowired
private HuaweiObsService obsService;
@PostMapping("/image")
public ResponseEntity uploadImage(@RequestParam("file") MultipartFile file) {
try {
// 一意ファイル名生成
String filename = UUID.randomUUID() + getFileExtension(file.getOriginalFilename());
// 華為クラウドOBSアップロード
String url = obsService.uploadFile("images/"+filename, file.getInputStream());
// JSON結果返却
return ResponseEntity.ok(Collections.singletonMap("url", url));
} catch (Exception e) {
return ResponseEntity.status(500).body("アップロード失敗: " + e.getMessage());
}
}
private String getFileExtension(String filename) {
return filename.substring(filename.lastIndexOf("."));
}
}
3.2.3 華為クラウドOBSサービス実装
@Service
public class HuaweiObsServiceImpl implements HuaweiObsService {
@Value("${huawei.obs.endpoint}")
private String endpoint;
@Value("${huawei.obs.bucket}")
private String bucketName;
private ObsClient obsClient;
@PostConstruct
public void init() {
// 華為クラウド永続AK/SKで初期化
obsClient = new ObsClient(
"your-access-key",
"your-secret-key",
endpoint
);
}
@Override
public String uploadFile(String objectKey, InputStream inputStream) throws Exception {
PutObjectRequest request = new PutObjectRequest(bucketName, objectKey, inputStream);
obsClient.putObject(request);
return String.format("https://%s.%s/%s", bucketName, endpoint, objectKey);
}
@PreDestroy
public void destroy() {
if (obsClient != null) {
try {
obsClient.close();
} catch (Exception e) {
// ロギング
}
}
}
}
3.3 信頼環境互換性対応
信頼環境向け特別対応:
- フォント互換性方案:
/* GB2312フォントフォールバック設定 */
body {
font-family: "SimSun", "STSong", "NSimSun", "FangSong", "KaiTi", sans-serif;
}
- ブラウザポリフィル対応:
// エントリーファイルにIE8対応コード追加
if (window.attachEvent && !window.addEventListener) {
// es5-shimなどのポリフィル読み込み
import('es5-shim').then(() => {
import('console-polyfill')
import('eventlistener-polyfill')
})
}
- 国内CPUアーキテクチャ対応: Docker構築時、多段階ビルドで対応:
# 多アーキテクチャDockerfile例
FROM --platform=$TARGETPLATFORM openjdk:8-jdk-alpine
COPY target/*.jar app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
4. プロジェクト進行と実施
4.1 実施マイルストーン
- 第1週:製品選定と商談完了
- 第2週:環境準備とアーキテクチャレビュー
- 第3-4週:コア機能開発と統合テスト
- 第5週:信頼環境適応テスト
- 第6週:ユーザーアセプタンステストとリリース準備
4.2 テスト戦略
- ブラウザ互換性テストマトリクス:
- IE8/IE11/Edge/Chrome/Firefox/QQブラウザ
- 国内ブラウザ:360セキュリティ/高速モード、UC、Sogou
- 信頼OSテストリスト:
- 中標麒麟NeoKylin V7
- 銀河麒麟Kylin V10
- 統合UOS 20
- 深度Deepin 20
5. リスク管理
- 信頼環境互換性リスク:
- 対応:事前メーカーよりテストデバイス取得
- 代替案:互換性ない環境向け簡易UI提供
- IE8性能問題:
- 対応:コード分割+遅延ロード
- 監視:リリース後CPU/メモリ使用状況密接監視
- 画像アップロード安定性:
- 対応:断点再開と分割アップロード実装
- 再試行:3回自動再試行+手動介入通知
6. プロジェクト成果
6.1 要件達成状況
| 要件項目 | 実現状況 | 備考 |
|---|---|---|
| Word貼り付け | ✅ | Office 2019形式まで対応 |
| ファイルインポート | ✅ | Word/Excel/PPT/PDF対応 |
| 微信コンテンツ貼り付け | ✅ | 主要微信フォーマット対応 |
| スタイル維持 | ✅ | 表/数式/フォントなど |
| 信頼互換 | ✅ | 麒麟/UOSテスト合格 |
| IE8互換 | ✅ | 性能最適化後基準達成 |
6.2 性能指標
- 画像アップロード平均時間:<1.5秒(2MB以内)
- Wordドキュメントインポート(20ページ):<3秒
- メモリ使用量:<50MB(IE8下)
7. 後続最適化提案
- 長期メンテナンス計画:
- プラグイン自動更新機構構築
- 四半期単位で信頼環境再テスト
- 機能拡張ルートマップ:
- WPS文書専用最適化
- 政府機関文書フォーマット自動検証
- 複数段階承認ワークフロー統合
- 技術アップグレード準備:
- Vue3移行計画立案
- オブジェクトストレージマルチクラウドアダプター層
本プロジェクトは予算内で期日通り完了し、すべての指標が予測を上回る結果を達成。特に信頼環境互換性の優れたパフォーマンスにより、今後の政府案件入札に強力な技術的裏付けを提供しました。ライセンス買断方式により、グループ内の今後プロジェクトで再購入不要となり、約200万円のライセンス費用節約が見込まれます。
プラグイン実装
jQueryインストール
npm install jquery
コンポーネント内インポート
// tinymce-vueインポート
import Editor from '@tinymce/tinymce-vue'
import {WordPaster} from '../../static/WordPaster/js/w'
import {zyOffice} from '../../static/zyOffice/js/o'
import {zyCapture} from '../../static/zyCapture/z'
ツールバー追加
// Excelインポートボタン追加
(function () {
'use strict';
var global = tinymce.util.Tools.resolve('tinymce.PluginManager');
function selectLocalImages(editor) {
WordPaster.getInstance().SetEditor(editor).importExcel()
}
var register$1 = function (editor) {
editor.ui.registry.addButton('excelimport', {
text: '',
tooltip: 'Excel文書インポート',
onAction: function () {
selectLocalImages(editor)
}
});
editor.ui.registry.addMenuItem('excelimport', {
text: '',
tooltip: 'Excel文書インポート',
onAction: function () {
selectLocalImages(editor)
}
});
};
var Buttons = { register: register$1 };
function Plugin () {
global.add('excelimport', function (editor) {
Buttons.register(editor);
});
}
Plugin();
}());
// Word画像化ボタン追加
(function () {
'use strict';
var global = tinymce.util.Tools.resolve('tinymce.PluginManager');
function selectLocalImages(editor) {
WordPaster.getInstance().SetEditor(editor);
WordPaster.getInstance().importWordToImg()
}
var register$1 = function (editor) {
editor.ui.registry.addButton('importwordtoimg', {
text: '',
tooltip: 'Wordを画像に変換',
onAction: function () {
selectLocalImages(editor)
}
});
editor.ui.registry.addMenuItem('importwordtoimg', {
text: '',
tooltip: 'Wordを画像に変換',
onAction: function () {
selectLocalImages(editor)
}
});
};
var Buttons = { register: register$1 };
function Plugin () {
global.add('importwordtoimg', function (editor) {
Buttons.register(editor);
});
}
Plugin();
}());
// ネットワーク画像アップロードボタン追加
(function () {
'use strict';
var global = tinymce.util.Tools.resolve('tinymce.PluginManager');
function selectLocalImages(editor) {
WordPaster.getInstance().SetEditor(editor);
WordPaster.getInstance().UploadNetImg()
}
var register$1 = function (editor) {
editor.ui.registry.addButton('netpaster', {
text: '',
tooltip: 'ネットワーク画像一括アップロード',
onAction: function () {
selectLocalImages(editor)
}
});
editor.ui.registry.addMenuItem('netpaster', {
text: '',
tooltip: 'ネットワーク画像一括アップロード',
onAction: function () {
selectLocalImages(editor)
}
});
};
var Buttons = { register: register$1 };
function Plugin () {
global.add('netpaster', function (editor) {
Buttons.register(editor);
});
}
Plugin();
}());
// PDFインポートボタン追加
(function () {
'use strict';
var global = tinymce.util.Tools.resolve('tinymce.PluginManager');
function selectLocalImages(editor) {
WordPaster.getInstance().SetEditor(editor);
WordPaster.getInstance().ImportPDF()
}
var register$1 = function (editor) {
editor.ui.registry.addButton('pdfimport', {
text: '',
tooltip: 'PDF文書インポート',
onAction: function () {
selectLocalImages(editor)
}
});
editor.ui.registry.addMenuItem('pdfimport', {
text: '',
tooltip: 'PDF文書インポート',
onAction: function () {
selectLocalImages(editor)
}
});
};
var Buttons = { register: register$1 };
function Plugin () {
global.add('pdfimport', function (editor) {
Buttons.register(editor);
});
}
Plugin();
}());
// PPTインポートボタン追加
(function () {
'use strict';
var global = tinymce.util.Tools.resolve('tinymce.PluginManager');
function selectLocalImages(editor) {
WordPaster.getInstance().SetEditor(editor);
WordPaster.getInstance().importPPT()
}
var register$1 = function (editor) {
editor.ui.registry.addButton('pptimport', {
text: '',
tooltip: 'PowerPoint文書インポート',
onAction: function () {
selectLocalImages(editor)
}
});
editor.ui.registry.addMenuItem('pptimport', {
text: '',
tooltip: 'PowerPoint文書インポート',
onAction: function () {
selectLocalImages(editor)
}
});
};
var Buttons = { register: register$1 };
function Plugin () {
global.add('pptimport', function (editor) {
Buttons.register(editor);
});
}
Plugin();
}());
// Wordインポートボタン追加
(function () {
'use strict';
var global = tinymce.util.Tools.resolve('tinymce.PluginManager');
function selectLocalImages(editor) {
WordPaster.getInstance().SetEditor(editor).importWord()
}
var register$1 = function (editor) {
editor.ui.registry.addButton('wordimport', {
text: '',
tooltip: 'Word文書インポート',
onAction: function () {
selectLocalImages(editor)
}
});
editor.ui.registry.addMenuItem('wordimport', {
text: '',
tooltip: 'Word文書インポート',
onAction: function () {
selectLocalImages(editor)
}
});
};
var Buttons = { register: register$1 };
function Plugin () {
global.add('wordimport', function (editor) {
Buttons.register(editor);
});
}
Plugin();
}());
// Word貼り付けボタン追加
(function () {
'use strict';
var global = tinymce.util.Tools.resolve('tinymce.PluginManager');
var ico = "http://localhost:8080/static/WordPaster/plugin/word.png"
function selectLocalImages(editor) {
WordPaster.getInstance().SetEditor(editor).PasteManual()
}
var register$1 = function (editor) {
editor.ui.registry.addButton('wordpaster', {
text: '',
tooltip: 'Word一括貼り付け',
onAction: function () {
selectLocalImages(editor)
}
});
editor.ui.registry.addMenuItem('wordpaster', {
text: '',
tooltip: 'Word一括貼り付け',
onAction: function () {
selectLocalImages(editor)
}
});
};
var Buttons = { register: register$1 };
function Plugin () {
global.add('wordpaster', function (editor) {
Buttons.register(editor);
});
}
Plugin();
}());
オンラインコード:
プラグイン追加
// プラグイン
plugins: {
type: [String, Array],
// default: 'advlist anchor autolink autosave code codesample colorpicker colorpicker contextmenu directionality emoticons fullscreen hr image imagetools importcss insertdatetime link lists media nonbreaking noneditable pagebreak paste preview print save searchreplace spellchecker tabfocus table template textcolor textpattern visualblocks visualchars'
default: 'autoresize code autolink autosave image imagetools paste preview table powertables'
},
オンラインコード確認
コンポーネント初期化
// 初期化
WordPaster.getInstance({
// アップロードAPI:http://www.ncmem.com/doc/view.aspx?id=d88b60a2b0204af1ba62fa66288203ed
PostUrl: 'http://localhost:8891/upload.aspx',
// 画像URLドメイン追加:http://www.ncmem.com/doc/view.aspx?id=704cd302ebd346b486adf39cf4553936
ImageUrl: 'http://localhost:8891{url}',
// ファイルフィールド名設定:http://www.ncmem.com/doc/view.aspx?id=c3ad06c2ae31454cb418ceb2b8da7c45
FileFieldName: 'file',
// 画像URL抽出:http://www.ncmem.com/doc/view.aspx?id=07e3f323d22d4571ad213441ab8530d1
ImageMatch: ''
})
ページ内コンポーネント導入
機能デモ
----
### エディタ
エディタに機能ボタン追加
### Word文書インポート、doc,docx対応
### Excel文書インポート、xls,xlsx対応
### Word貼り付け
Word内容をワンクリック貼り付け、画像自動アップロード、文字スタイル維持。
### Word画像化
Wordファイルをワンクリックインポートし、画像に変換してサーバーへアップロード。
### PDFインポート
PDFファイルをワンクリックインポートし、画像に変換してサーバーへアップロード。
### PPTインポート
PPTファイルをワンクリックインポートし、画像に変換してサーバーへアップロード。
### ネットワーク画像アップロード
ネットワーク画像をワンクリックで自動アップロード。
サンプルダウンロード
----
サンプル完全版ダウンロード