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:
1 parent
7a8279f8e7
commit
fac4a714e0
45 files changed
+28495
-56
No files matched your search
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* 付费配置 → 请求体。
|
||||
*
|
||||
* 受保护附件只提交 asset_key,不提交任何文件路径或公开 URL;服务端会再次
|
||||
* 校验附件归属并重新清洗。
|
||||
*
|
||||
* @package Jio
|
||||
*/
|
||||
|
||||
type PayState = {
|
||||
enabled: boolean;
|
||||
modo: 'cash' | 'points';
|
||||
price: string;
|
||||
points: number;
|
||||
vip1Price: string;
|
||||
vip2Price: string;
|
||||
payLimit: number;
|
||||
downloads: Array<Record<string, any>>;
|
||||
};
|
||||
|
||||
export function normalizePayForRequest(pay: PayState) {
|
||||
const downloads = (pay.downloads || [])
|
||||
.map((row) => {
|
||||
const assetKey = typeof row.assetKey === 'string' ? row.assetKey : '';
|
||||
if (assetKey) {
|
||||
return {
|
||||
type: 'protected',
|
||||
asset_key: assetKey,
|
||||
label: typeof row.label === 'string' ? row.label : '',
|
||||
};
|
||||
}
|
||||
const url = typeof row.url === 'string' ? row.url.trim() : '';
|
||||
if (!url) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
type: 'direct',
|
||||
url,
|
||||
label: typeof row.label === 'string' ? row.label : '',
|
||||
extract_code: typeof row.extractCode === 'string' ? row.extractCode : '',
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
return {
|
||||
enabled: !!pay.enabled,
|
||||
fields: {
|
||||
pay_type: pay.enabled ? (downloads.length > 0 ? 'download' : 'reading') : 'no',
|
||||
pay_modo: pay.modo === 'points' ? 'points' : '0',
|
||||
price: pay.modo === 'points' ? '0.00' : String(pay.price || '0.00'),
|
||||
points_price: pay.modo === 'points' ? Math.max(0, Number(pay.points) || 0) : 0,
|
||||
vip1_price: pay.modo === 'points' ? '0.00' : String(pay.vip1Price || '0.00'),
|
||||
vip2_price: pay.modo === 'points' ? '0.00' : String(pay.vip2Price || '0.00'),
|
||||
pay_limit: Number(pay.payLimit) || 0,
|
||||
downloads,
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user