feat(creator-studio): 前台 Gutenberg 创作工作台与受保护付费附件

前台投稿页改为沉浸式创作工作台:Gutenberg 负责区块解析与序列化,
React + TypeScript 负责工作台外壳与发布抽屉,Jio 服务端继续作为权限、
审核与付费的唯一权威。

- 新增 assets/creator-studio 独立构建(wp-scripts,锁定 WP 6.4 时代包),
  仅在投稿模板加载,构建缺失或游客访问时回退原有 TinyMCE 表单
- 自动保存(4s 防抖 + rev 冲突 409)、本地快照恢复、发布配置抽屉、
  移动端全屏单列与安全区适配
- 图片插入走现有 front-submit 上传管线,保留能力校验与待认领生命周期
- 已发布文章的改稿写入私有版本,审核通过前线上内容/链接/付费条款不变;
  原生后台「改稿审核」一次性应用或退回,并校验源文章并发指纹
- 私有附件新表 jio_private_asset + jio_dl_token.asset_id,schema 升到 3.4.0;
  文件存放 ABSPATH 之外,未配置私有存储时 fail closed
- 下载令牌绑定文章/附件/用户/IP/UA/次数,每次交付复核 Visibility 的
  订单、VIP 与配额判定;中断传输归还次数
- 付费字段改为写入前校验,非法价格不再留下半保存文章
- 新增 creator-studio 契约测试;更新受影响的 schema/下载/投稿契约

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kate522andClaude Opus 5 committed 2026-11-06 14:53:23 +08:00
1 parent 7a8279f8e7
commit fac4a714e0
45 files changed
+28495 -56

No files matched your search

@@ -376,15 +376,19 @@ namespace Jio\Core {
return $cap !== 'new_post_audit_no';
}
private function save_pay_meta(int $post_id, array $data, int $user_id, array $settings): array {
if (isset($data['fake_saved_pay_meta']) && is_array($data['fake_saved_pay_meta'])) {
$GLOBALS['front_pending_state']['pay_meta'][$post_id] = $data['fake_saved_pay_meta'];
\update_post_meta($post_id, DomainModel::PAY_META_KEY, $data['fake_saved_pay_meta']);
}
private function prepare_pay_meta(int $post_id, array $data, int $user_id, array $settings): array {
if (!empty($data['fake_pay_failure'])) {
return ['success' => false, 'message' => 'pay validation failed'];
}
return ['success' => true];
if (isset($data['fake_saved_pay_meta']) && is_array($data['fake_saved_pay_meta'])) {
return ['success' => true, 'write' => true, 'meta' => $data['fake_saved_pay_meta']];
}
return ['success' => true, 'write' => false];
}
private function write_pay_meta(int $post_id, array $meta): void {
$GLOBALS['front_pending_state']['pay_meta'][$post_id] = $meta;
\update_post_meta($post_id, DomainModel::PAY_META_KEY, $meta);
}
private function save_featured_meta(int $post_id, array $data, int $user_id): void {
@@ -537,21 +541,23 @@ namespace {
fp_assert(get_post_meta(441, '_jio_front_submit_media', true) === '', 'removed media should lose only its submit relation marker');
fp_assert(!fp_pending(441) && $GLOBALS['front_pending_state']['deleted'] === [], 'removed media must not be deleted or marked pending again');
// A payment validation error happens after the body write. The saved body is
// protected, but the request's featured values were never persisted and must
// therefore not be considered references.
// A payment validation error is resolved before anything is written, so the
// request leaves no partially saved post and no claimed reference. Featured
// values are never considered persisted either.
fp_reset();
fp_attachment(450, 7, 'image/jpeg');
fp_attachment(451, 7, 'image/jpeg');
$pay_error = $submit->persist(fp_article_data('<p>Body remains persisted after pay error.</p><img data-edit-file-id="450" src="/saved.jpg">', [
$pay_error = $submit->persist(fp_article_data('<p>Body must not persist after pay error.</p><img data-edit-file-id="450" src="/saved.jpg">', [
'fake_pay_failure' => true,
'fake_featured_meta' => ['featured_video_attachment_id' => 0, 'featured_slide' => '451'],
'fake_thumbnail_id' => 451,
]));
fp_assert(empty($pay_error['success']) && (int) ($pay_error['post_id'] ?? 0) > 0, 'pay validation should return its error after the post was written');
fp_assert(!fp_pending(450), 'persisted body reference should be claimed even when payment validation fails');
fp_assert(empty($pay_error['success']), 'pay validation should fail before the post is written');
fp_assert((int) ($pay_error['post_id'] ?? 0) === 0, 'a failed pay validation must not report a saved post');
fp_assert(fp_pending(450), 'body media must stay pending when validation fails before the write');
fp_assert(fp_pending(451), 'unsaved featured media from failed request must not be claimed');
fp_assert($GLOBALS['front_pending_state']['featured_save_calls'] === 0, 'featured metadata must not be saved after payment failure');
fp_assert(($GLOBALS['front_pending_state']['posts'][450]->post_parent ?? 0) === 0, 'validation failure must not bind media to a post');
// Failed insert/update writes cannot claim anything because no saved post is
// synchronized. Test both the create and edit branches.