fix(submit): make the draft chain survive continuous writing, reloads and flaky networks
Autosave only ran after 4s of idle, and every keystroke reset that timer, so a writer who never stopped had zero saves (measured: 15s of typing = 0 requests). Saving also never rebound the URL, so a new draft could not be found again after a reload, the local backup was cleared on every autosave, and a failed save had no retry and stayed failed once the network came back. Client - autosave: keep the idle debounce but add a 30s max-wait window that is not reset by further keystrokes, and retry failures at 5s / 15s / 60s - bind `?edit=<id>` into the URL once the server returns a post id, so reloads, new tabs and history all reopen the same draft - only clear the local backup when the save settled and nothing changed since - key the local backup by draft id instead of the constant `jio-studio-0` - restore snapshots through hydrateBlocks so code blocks come back editable - never disable publish while an autosave is in flight, and mark autosave states separately (自动保存中… / 已自动保存 / 自动保存失败,将重试) - on 409, adopt the server rev and tell the author instead of failing silently - status bar reports the real save state instead of a bogus Markdown hint Server - draft saves no longer rewrite the lifecycle: pending / future / publish / private posts keep their status, so an autosave cannot pull a submitted post out of the review queue - drafts and autosaves stop consuming the publish flood quota, which is meant to cap actual publishing (measured: 4 drafts used to burn 4 slots) - autosave skips wp_save_post_revision for both plain drafts and submission versions; manual saves and submits still create a revision to roll back to - media-only drafts (image / table / code / hr) count as content - the response now carries the resulting status so the client can tell "just submitted" from "still under review" Tests cover the max-wait window, retry schedule, URL binding, backup clearing, status labelling and every server-side draft rule.
This commit is contained in:
1 parent
ec582ac9f9
commit
a5386f72c6
10 files changed
+330
-49
No files matched your search
@@ -235,6 +235,7 @@ final class CreatorRest {
|
||||
'content' => $payload['content'],
|
||||
'base_rev' => absint($request->get_param('rev')),
|
||||
'snapshot' => $this->snapshot_from_payload($payload),
|
||||
'autosave' => $is_auto,
|
||||
], false);
|
||||
if (is_wp_error($result)) {
|
||||
return $result;
|
||||
@@ -252,6 +253,7 @@ final class CreatorRest {
|
||||
'tags' => implode(',', array_map('strval', $payload['tags'])),
|
||||
'featured_data'=> $payload['featured'],
|
||||
'schedule' => $payload['schedule'],
|
||||
'autosave' => $is_auto,
|
||||
];
|
||||
foreach ($payload['custom'] as $key => $value) {
|
||||
$data['cf_' . sanitize_key((string) $key)] = $value;
|
||||
|
||||
@@ -17,7 +17,7 @@ trait FrontSubmitSave {
|
||||
*
|
||||
* @param array<string, mixed> $data Request payload.
|
||||
* @param string $action posts_save|posts_draft
|
||||
* @return array{success:bool,message:string,post_id?:int,url?:string,goto?:string,reload?:bool,time?:string,posts_id?:int}
|
||||
* @return array{success:bool,message:string,post_id?:int,url?:string,goto?:string,reload?:bool,time?:string,posts_id?:int,status?:string}
|
||||
*/
|
||||
public function persist(array $data, string $action = 'posts_save'): array {
|
||||
$user_id = get_current_user_id();
|
||||
@@ -26,6 +26,8 @@ trait FrontSubmitSave {
|
||||
$is_guest = $user_id <= 0;
|
||||
$is_edit = $posts_id > 0;
|
||||
$is_draft = $action === 'posts_draft';
|
||||
// 自动保存是后台动作:不建版本、不扣防刷配额、不改动审核状态。
|
||||
$is_auto = $is_draft && !empty($data['autosave']);
|
||||
|
||||
if (empty($settings['enabled'])) {
|
||||
return ['success' => false, 'message' => __('投稿功能已关闭', 'jio')];
|
||||
@@ -73,7 +75,9 @@ trait FrontSubmitSave {
|
||||
if (trim(wp_strip_all_tags($content)) === '') {
|
||||
return ['success' => false, 'message' => __('还未填写任何内容', 'jio')];
|
||||
}
|
||||
} elseif ($title === '' && trim(wp_strip_all_tags($content)) === '') {
|
||||
} elseif ($title === '' && !$this->draft_has_content($content)) {
|
||||
// 图片 / 表格 / 代码块这类媒体块即使没有文字也算草稿内容,
|
||||
// 只贴了一张图的草稿不能被判成「什么都没写」。
|
||||
return ['success' => false, 'message' => __('请先写点什么再保存', 'jio')];
|
||||
}
|
||||
|
||||
@@ -126,7 +130,9 @@ trait FrontSubmitSave {
|
||||
}
|
||||
|
||||
// [防刷机制]发布限制(后台「扩展&增强 → 网站安全」),管理员不受限。
|
||||
if (!$is_edit && !current_user_can('manage_options') && class_exists('\Jio\Extensions\SecurityHardening')) {
|
||||
// 草稿与自动保存不计数:配额防的是刷发布,把写作中的保存也算进去会让
|
||||
// 正常写一篇长文就把额度用光。只有正式投稿(posts_save)计一次。
|
||||
if (!$is_draft && !$is_edit && !current_user_can('manage_options') && class_exists('\Jio\Extensions\SecurityHardening')) {
|
||||
$security = \Jio\Settings\ExtensionRuntime::get_security_settings();
|
||||
$brush_subject = $is_guest
|
||||
? (string) (function_exists('jio_get_client_ip') ? jio_get_client_ip() : ($_SERVER['REMOTE_ADDR'] ?? ''))
|
||||
@@ -149,6 +155,13 @@ trait FrontSubmitSave {
|
||||
$status = 'future';
|
||||
}
|
||||
}
|
||||
} elseif ($is_edit) {
|
||||
// 草稿动作不能改写已有文章的生命周期:自动保存发生在投稿 / 审核期间时,
|
||||
// 把 pending 或 future 直接写成 draft 会让文章悄悄掉出审核队列与排期。
|
||||
$current_status = (string) get_post_status($posts_id);
|
||||
$status = in_array($current_status, ['pending', 'future', 'publish', 'private'], true)
|
||||
? $current_status
|
||||
: 'draft';
|
||||
}
|
||||
|
||||
// Paid fields are validated before anything is written, so an invalid
|
||||
@@ -193,7 +206,15 @@ trait FrontSubmitSave {
|
||||
|
||||
if ($is_edit) {
|
||||
$postarr['ID'] = $posts_id;
|
||||
if ($is_auto) {
|
||||
// 自动保存每隔几秒就可能落一次,逐次建 revision 会把版本表撑到几百条。
|
||||
// 版本只留给手动「保存草稿」与正式投稿,自动保存静默覆盖当前版本。
|
||||
remove_action('post_updated', 'wp_save_post_revision');
|
||||
}
|
||||
$in_id = wp_update_post($postarr, true);
|
||||
if ($is_auto) {
|
||||
add_action('post_updated', 'wp_save_post_revision');
|
||||
}
|
||||
} else {
|
||||
$postarr['post_author'] = $author_id;
|
||||
$in_id = wp_insert_post($postarr, true);
|
||||
@@ -238,6 +259,7 @@ trait FrontSubmitSave {
|
||||
'post_id' => (int) $in_id,
|
||||
'posts_id' => (int) $in_id,
|
||||
'url' => $url,
|
||||
'status' => $status,
|
||||
];
|
||||
|
||||
switch ($status) {
|
||||
@@ -254,8 +276,12 @@ trait FrontSubmitSave {
|
||||
}
|
||||
break;
|
||||
case 'pending':
|
||||
$send['message'] = __('内容已提交,正在等待审核', 'jio');
|
||||
if ($user_id > 0 && function_exists('jio_get_user_home_url')) {
|
||||
// 自动保存落到待审文章上时不是「刚提交」,文案与跳转都不能再走提交流程。
|
||||
$send['message'] = $is_draft
|
||||
? __('已保存,文章仍在审核队列中', 'jio')
|
||||
: __('内容已提交,正在等待审核', 'jio');
|
||||
$send['time'] = current_time('mysql');
|
||||
if (!$is_draft && $user_id > 0 && function_exists('jio_get_user_home_url')) {
|
||||
$send['reload'] = true;
|
||||
$send['goto'] = add_query_arg(
|
||||
['tab' => 'post', 'post_status' => 'pending'],
|
||||
@@ -263,6 +289,16 @@ trait FrontSubmitSave {
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'publish':
|
||||
case 'private':
|
||||
// 已发布文章的草稿保存不能触发「文章已发布」与整页跳转。
|
||||
$send['message'] = $is_draft ? __('已保存线上文章', 'jio') : __('文章已发布', 'jio');
|
||||
$send['time'] = current_time('mysql');
|
||||
if (!$is_draft) {
|
||||
$send['reload'] = true;
|
||||
$send['goto'] = $url;
|
||||
}
|
||||
break;
|
||||
case 'draft':
|
||||
$send['message'] = __('草稿已保存', 'jio');
|
||||
$send['time'] = current_time('mysql');
|
||||
@@ -344,6 +380,19 @@ trait FrontSubmitSave {
|
||||
return strlen($text);
|
||||
}
|
||||
|
||||
/**
|
||||
* 草稿是否已经有可保存的内容。
|
||||
*
|
||||
* 纯文字的草稿看文字,图片 / 表格 / 代码块 / 分割线这类块即使没有文字也算内容;
|
||||
* 与前端 `plainText` + 块级元素判断保持同一口径,避免两边对「空草稿」结论不同。
|
||||
*/
|
||||
private function draft_has_content(string $content): bool {
|
||||
if (trim(wp_strip_all_tags($content)) !== '') {
|
||||
return true;
|
||||
}
|
||||
return (bool) preg_match('/<(?:img|table|pre|hr|iframe|video|audio|figure)\b/i', $content);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $data
|
||||
* @param array<string,mixed> $settings
|
||||
|
||||
@@ -192,7 +192,7 @@ final class SubmissionVersion {
|
||||
/**
|
||||
* 创建或更新作者的工作版本。
|
||||
*
|
||||
* @param array{title?:string,content?:string,excerpt?:string,snapshot?:array<string,mixed>,base_rev?:int} $data
|
||||
* @param array{title?:string,content?:string,excerpt?:string,snapshot?:array<string,mixed>,base_rev?:int,autosave?:bool} $data
|
||||
* @return array{success:bool,message?:string,version_id?:int,rev?:int,conflict?:bool}|\WP_Error
|
||||
*/
|
||||
public function save_working(int $source_id, int $user_id, array $data, bool $submit = false) {
|
||||
@@ -265,7 +265,16 @@ final class SubmissionVersion {
|
||||
'post_excerpt' => $excerpt,
|
||||
'post_status' => $next_status === self::STATUS_PENDING ? 'pending' : 'draft',
|
||||
];
|
||||
// 自动保存不建 revision:几秒一次地写版本表会把改稿记录撑满,
|
||||
// 需要回滚的节点由手动保存与提交产生。
|
||||
$is_auto = !empty($data['autosave']);
|
||||
if ($is_auto) {
|
||||
remove_action('post_updated', 'wp_save_post_revision');
|
||||
}
|
||||
$updated = wp_update_post($update, true);
|
||||
if ($is_auto) {
|
||||
add_action('post_updated', 'wp_save_post_revision');
|
||||
}
|
||||
if (is_wp_error($updated)) {
|
||||
return new \WP_Error('jio_version_failed', $updated->get_error_message(), ['status' => 500]);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user