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

+34 -4
View File
@@ -336,10 +336,22 @@ if ($is_need_purchase && $user_amount > 0) {
<?php foreach ($downloads as $idx => $row) :
$label = isset($row['label']) ? trim((string) $row['label']) : '';
$type = isset($row['type']) ? (string) $row['type'] : 'direct';
$asset_key = class_exists('\Jio\Content\PrivateAssets')
? \Jio\Content\PrivateAssets::sanitize_asset_key((string) ($row['asset_key'] ?? ''))
: '';
$is_protected = $asset_key !== '';
$url = isset($row['url']) ? (string) $row['url'] : '';
if ($url === '') {
// 直连模式没有可用的原始地址,空源不渲染。
continue;
if (!$is_protected) {
if ($url === '') {
// 直连模式没有可用的原始地址,空源不渲染。
continue;
}
} else {
// 受保护附件按设计没有公开 URL,只带 asset_key。
$url = '';
}
if ($is_protected && $label === '') {
$label = trim((string) ($row['file_name'] ?? ''));
}
$extract_code = isset($row['extract_code']) ? trim((string) $row['extract_code']) : '';
$has_extract = $extract_code !== '';
@@ -349,7 +361,9 @@ if ($is_need_purchase && $user_amount > 0) {
$button_class = isset($row['class']) ? trim((string) $row['class']) : '';
$has_copy = $copy_val !== '';
$service = function_exists('jio_detect_cloud_service') ? jio_detect_cloud_service($url) : ['key' => 'other', 'name' => $label, 'color' => '#6b7280'];
$service = $is_protected
? ['key' => 'protected', 'name' => __('受保护下载', 'jio'), 'color' => '#267665']
: (function_exists('jio_detect_cloud_service') ? jio_detect_cloud_service($url) : ['key' => 'other', 'name' => $label, 'color' => '#6b7280']);
if ($label === '') {
$label = strpos($service['name'], '本地') === 0 ? sprintf(__('下载源 %d', 'jio'), $idx + 1) : (string) $service['name'];
}
@@ -376,6 +390,21 @@ if ($is_need_purchase && $user_amount > 0) {
?>
<li class="resource-download-box__item">
<div class="resource-download-box__row">
<?php if ($is_protected && !$is_need_login) : ?>
<button
type="button"
class="resource-download-box__btn btn-primary resource-download-box__btn--service-protected<?php echo esc_attr($btn_modifier); ?><?php echo esc_attr($btn_extra_class); ?>"
data-jio-protected-asset="<?php echo esc_attr($asset_key); ?>"
data-jio-protected-post="<?php echo esc_attr((string) $post_id); ?>"
style="--service-color: <?php echo esc_attr($service['color']); ?>"
>
<?php echo $btn_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
<span><?php echo esc_html($btn_text); ?></span>
<span class="resource-download-box__service-badge" style="background: <?php echo esc_attr($service['color']); ?>">
<?php echo esc_html($service['name']); ?>
</span>
</button>
<?php else : ?>
<a
class="resource-download-box__btn btn-primary resource-download-box__btn--service-<?php echo esc_attr($service['key']); ?><?php echo esc_attr($btn_modifier); ?><?php echo esc_attr($btn_extra_class); ?>"
href="<?php echo esc_url($btn_href); ?>"<?php echo $btn_target; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?><?php echo $btn_rel; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
@@ -389,6 +418,7 @@ if ($is_need_purchase && $user_amount > 0) {
<?php echo esc_html($service['name']); ?>
</span>
</a>
<?php endif; ?>
<?php if ($has_extract || $has_copy) : ?>
<button type="button" class="resource-download-box__btn resource-download-box__btn--extract"
<?php if ($has_copy && $copy_key !== '') : ?>data-toggle="tooltip" title="<?php echo esc_attr(sprintf(__('点击复制%s', 'jio'), $copy_key)); ?>"<?php endif; ?>