fix(admin): replicate reference front-submit settings page 1:1

- Rewrite post_list/submit schema (PHP + JS) with the reference theme's
  field set, order, defaults and dependencies; topics fields excluded
- Map PostDisplay::get_submit_settings() and the FrontSubmit runtime to
  the new keys without legacy fallback
- Guest submission follows the capability matrix; category required on
  submit; pay limits read from the cash/points fieldset
- Drop dead front_submit_* settings, template branches and JS config
- Add submit settings reference contract test
This commit is contained in:
2026-09-24 12:29:19 +08:00
parent ed71314986
commit 264f2c51b2
14 changed files with 354 additions and 292 deletions
+18 -47
View File
@@ -297,56 +297,27 @@ class SchemaPostList {
'submit' => [
'key' => 'submit',
'title' => '前台投稿',
'version' => '1.0.0',
'version' => '2.0.0',
'option_key' => 'aksrt_settings',
'groups' => [
['key' => 'basic', 'title' => '基础配置', 'fields' => [
['key' => 'front_submit_enabled', 'label' => '开启前台投稿', 'type' => 'switch', 'default' => true],
['key' => 'front_submit_roles', 'label' => '允许投稿的角色', 'type' => 'input', 'default' => 'author,editor,administrator'],
['key' => 'post_article_btn_txte', 'label' => '投稿按钮文案', 'type' => 'input', 'default' => '发布文章'],
['key' => 'post_article_title_strlen_limit_min', 'label' => '标题最少字数', 'type' => 'number', 'default' => 5, 'min' => 1, 'max' => 100],
['key' => 'post_article_title_strlen_limit_max', 'label' => '标题最多字数', 'type' => 'number', 'default' => 30, 'min' => 5, 'max' => 200],
['key' => 'post_t_placeholder', 'label' => '标题占位符', 'type' => 'input', 'default' => '', 'description' => '留空使用默认'],
['key' => 'post_c_placeholder', 'label' => '内容占位符', 'type' => 'textarea', 'default' => '', 'description' => '留空使用默认'],
]],
['key' => 'fields', 'title' => '表单字段', 'fields' => [
['key' => 'front_submit_title', 'label' => '标题', 'type' => 'switch', 'default' => true],
['key' => 'front_submit_content', 'label' => '正文', 'type' => 'switch', 'default' => true],
['key' => 'front_submit_excerpt', 'label' => '摘要', 'type' => 'switch', 'default' => true],
['key' => 'front_submit_cover', 'label' => '封面图', 'type' => 'switch', 'default' => true],
['key' => 'front_submit_category', 'label' => '分类选择', 'type' => 'switch', 'default' => true],
['key' => 'post_article_cat', 'label' => '允许投稿分类', 'type' => 'taxonomy_select', 'taxonomy' => 'category', 'multiple' => true, 'default' => [], 'help' => '为空则允许全部分类'],
['key' => 'front_submit_guest_enabled', 'label' => '允许免登录投稿', 'type' => 'switch', 'default' => false, 'help' => '开启后访客可投稿;文章归属到下方用户'],
['key' => 'post_article_user', 'label' => '免登录归属用户', 'type' => 'user_select', 'default' => 0, 'help' => '免登录投稿时文章归属的用户,搜索选择'],
['key' => 'front_submit_tags', 'label' => '标签输入', 'type' => 'switch', 'default' => true],
['key' => 'post_article_custom_filter_s', 'label' => '高级筛选选择', 'type' => 'switch', 'default' => true, 'help' => '投稿时可选择自定义筛选维度值'],
['key' => 'front_submit_custom_fields', 'label' => '自定义字段', 'type' => 'textarea', 'default' => ''],
]],
['key' => 'review', 'title' => '审核策略', 'fields' => [
['key' => 'front_submit_review_mode', 'label' => '审核模式', 'type' => 'radio', 'default' => 'pending', 'options' => [
['label' => '待审核', 'value' => 'pending'],
['label' => '直接发布', 'value' => 'publish'],
['label' => '自动草稿', 'value' => 'draft'],
['key' => 'submit', 'title' => '前台投稿', 'fields' => [
['key' => 'post_article_s', 'label' => '启用前台投稿', 'type' => 'switcher', 'default' => true, 'description' => '此功能启用后,可以在权限管理中设置用户的发布、审核权限'],
['key' => 'post_article_btn_txte', 'label' => '按钮文字', 'type' => 'input', 'default' => '发布文章', 'dependsOn' => ['key' => 'post_article_s', 'value' => true], 'description' => '投稿按钮的显示的文字,推荐4个字更加协调'],
['key' => 'post_article_cat', 'label' => '投稿允许选择的分类', 'type' => 'taxonomy_select', 'taxonomy' => 'category', 'multiple' => true, 'default' => [], 'placeholder' => '输入关键词以搜索分类', 'dependsOn' => ['key' => 'post_article_s', 'value' => true], 'description' => '投稿允许选择的分类,为空则允许选择全部分类,所选分类下如果有子分类则会自动显示,如果选择了上级分类,则不要再选该分类下的子分类。注意:请确保所选分类包含子分类总数不要超过50个,否则可能会引起页面崩溃'],
['key' => 'post_article_custom_filter_s', 'label' => '允许选择高级筛选', 'type' => 'switcher', 'default' => true, 'dependsOn' => ['key' => 'post_article_s', 'value' => true], 'description' => '此功能启用后,请先在文章列表/高级筛选中添加高级筛选明细'],
['key' => 'post_article_tag', 'label' => '允许插入标签', 'type' => 'switcher', 'default' => true, 'dependsOn' => ['key' => 'post_article_s', 'value' => true], 'description' => '前台投稿时候允许设置文章标签'],
['key' => 'post_article_user', 'label' => '免登陆投稿发布用户', 'type' => 'user_select', 'default' => 1, 'dependsOn' => ['key' => 'post_article_s', 'value' => true], 'description' => '当选择投稿权限为无需登录就能投稿时,投稿文章的用户'],
['key' => 'post_article_title_strlen_limit', 'label' => '标题字数限制', 'type' => 'between_number', 'default' => ['min' => 5, 'max' => 30], 'unit' => '字', 'description' => '限制标题字数可有效的防止灌水等无意义内容(英文字符按0.5个字计算)'],
['key' => 'post_article_pay_free_s', 'label' => '允许设置为免费资源', 'type' => 'switcher', 'default' => true, 'dependsOn' => ['key' => 'post_article_s', 'value' => true], 'description' => '付费内容金额允许设置为 0'],
['key' => 'post_article_pay_price_limit', 'label' => '付费内容金额限制', 'type' => 'fieldset', 'default' => ['price' => ['min' => 0, 'max' => 99999], 'points' => ['min' => 0, 'max' => 0]], 'description' => '限制用户允许设置的金额,防止金额过高不合理(填0为不限制)', 'fields' => [
['key' => 'price', 'label' => '现金金额限制', 'type' => 'between_number', 'default' => ['min' => 0, 'max' => 99999]],
['key' => 'points', 'label' => '积分金额限制', 'type' => 'between_number', 'default' => ['min' => 0, 'max' => 0]],
]],
['key' => 'front_submit_cover_required', 'label' => '封面图必填', 'type' => 'switch', 'default' => false],
['key' => 'front_submit_max_images', 'label' => '正文最大图片数', 'type' => 'number', 'default' => 5, 'min' => 0, 'max' => 50],
]],
['key' => 'editor', 'title' => '编辑器设置', 'fields' => [
['key' => 'front_submit_editor_mode', 'label' => '编辑器模式', 'type' => 'radio', 'default' => 'rich', 'options' => [
['label' => '富文本', 'value' => 'rich'],
['label' => 'Markdown', 'value' => 'markdown'],
['label' => '纯文本', 'value' => 'plain'],
]],
['key' => 'front_submit_draft_save', 'label' => '自动保存草稿', 'type' => 'switch', 'default' => true],
['key' => 'front_submit_leave_warning', 'label' => '离开页面提醒', 'type' => 'switch', 'default' => true],
]],
['key' => 'paid', 'title' => '付费投稿权限', 'fields' => [
['key' => 'post_article_pay_free_s', 'label' => '允许设置免费', 'type' => 'switch', 'default' => true],
['key' => 'post_article_pay_price_min', 'label' => '最低价格', 'type' => 'input', 'sanitize_type' => 'money_decimal', 'default' => '0.00', 'min' => 0, 'max' => 9999, 'help' => '投稿可设置的最低付费金额'],
['key' => 'post_article_pay_price_max', 'label' => '最高价格', 'type' => 'input', 'sanitize_type' => 'money_decimal', 'default' => '100.00', 'min' => 0, 'max' => 9999, 'help' => '投稿可设置的最高付费金额'],
['key' => 'post_article_pay_vip_price_s', 'label' => '允许设置会员价', 'type' => 'switch', 'default' => true],
['key' => 'post_article_pay_vip_1_discount', 'label' => 'VIP1 折扣(%)', 'type' => 'number', 'default' => 80, 'min' => 0, 'max' => 100, 'help' => 'VIP1 会员折扣百分比,80 即 8 折'],
['key' => 'post_article_pay_vip_2_discount', 'label' => 'VIP2 折扣(%)', 'type' => 'number', 'default' => 50, 'min' => 0, 'max' => 100, 'help' => 'VIP2 会员折扣百分比,50 即 5 折'],
['key' => 'post_article_pay_vip_price_s', 'label' => '付费内容允许设置会员价', 'type' => 'switcher', 'default' => true, 'dependsOn' => ['key' => 'post_article_s', 'value' => true], 'description' => '前台投稿时对拥有设置付费内容权限的用户,是否开启设置会员价格。开启此项,会直接在前台显示设置会员价的选项;关闭此项,则用户只能设置普通价格,会员价则按照下方设置的折扣自动计算'],
['key' => 'post_article_pay_vip_1_discount', 'label' => '一级会员折扣', 'type' => 'number', 'default' => 100, 'min' => 0, 'max' => 100, 'unit' => '%', 'dependsOn' => ['all' => [['key' => 'post_article_s', 'value' => true], ['key' => 'post_article_pay_vip_price_s', 'value' => false]]]],
['key' => 'post_article_pay_vip_2_discount', 'label' => '二级会员折扣', 'type' => 'number', 'default' => 100, 'min' => 0, 'max' => 100, 'unit' => '%', 'dependsOn' => ['all' => [['key' => 'post_article_s', 'value' => true], ['key' => 'post_article_pay_vip_price_s', 'value' => false]]], 'description' => '执行价的百分之多少,0为免费,100为没有折扣,不能高于100'],
['key' => 'post_t_placeholder', 'label' => '投稿标题占位符', 'type' => 'input', 'default' => '请输入标题'],
['key' => 'post_c_placeholder', 'label' => '投稿内容占位符', 'type' => 'input', 'default' => '请输入内容'],
]],
],
],
+12 -12
View File
@@ -227,29 +227,33 @@ class FrontSubmit {
}
return [
'enabled' => true,
'btn_text' => '发布文章',
'title_min' => 5,
'title_max' => 30,
'title_placeholder' => '请输入标题',
'content_placeholder' => '请输入内容',
'allowed_cat_ids' => [],
'default_user_id' => 0,
'guest_enabled' => false,
'allow_category' => true,
'default_user_id' => 1,
'allow_tags' => true,
'allow_cover' => true,
'custom_filter_s' => true,
'review_mode' => 'pending',
'pay_free_s' => true,
'pay_price_min' => 0,
'pay_price_max' => 100,
'pay_price_max' => 0,
'pay_points_min' => 0,
'pay_points_max' => 0,
'pay_vip_price_s' => true,
'pay_vip_1_enabled' => false,
'pay_vip_2_enabled' => false,
'pay_vip_1_discount' => 80,
'pay_vip_2_discount' => 50,
'pay_vip_1_discount' => 100,
'pay_vip_2_discount' => 100,
];
}
/**
* Whether the user (or guest) may open / create a submission.
*
* Guest submission is granted through the capability matrix ("all" column
* of new_post_add), mirroring the reference theme.
*/
public function can_submit(?int $user_id = null): bool {
$settings = $this->settings();
@@ -261,10 +265,6 @@ class FrontSubmit {
$user_id = get_current_user_id();
}
if ($user_id <= 0) {
return !empty($settings['guest_enabled']) && (int) ($settings['default_user_id'] ?? 0) > 0;
}
return $this->user_can_cap('new_post_add', $user_id);
}
+6 -11
View File
@@ -73,8 +73,8 @@ trait FrontSubmitPay {
if ($pay_modo === 'points') {
$points = $this->parse_front_points($raw['points_price'] ?? $data['points_price'] ?? null);
$min = $this->parse_front_points_limit($settings['pay_price_min'] ?? '0.00');
$max = $this->parse_front_points_limit($settings['pay_price_max'] ?? '0.00');
$min = max(0, (int) ($settings['pay_points_min'] ?? 0));
$max = max(0, (int) ($settings['pay_points_max'] ?? 0));
if ($points === null) {
return ['success' => false, 'message' => __('请设置正确的积分价格', 'aksrt')];
}
@@ -119,8 +119,10 @@ trait FrontSubmitPay {
$meta['vip2_price'] = '0.00';
} else {
$price_minor = $this->parse_front_cash_minor($raw['price'] ?? null);
$min_minor = $this->parse_front_cash_minor($settings['pay_price_min'] ?? '0') ?? 0;
$max_minor = $this->parse_front_cash_minor($settings['pay_price_max'] ?? '0') ?? 0;
$min_yuan = max(0, (int) ($settings['pay_price_min'] ?? 0));
$max_yuan = max(0, (int) ($settings['pay_price_max'] ?? 0));
$min_minor = $min_yuan > 0 ? \AKSRT\Ledger\AssetAmount::balance((string) $min_yuan)->minor() : 0;
$max_minor = $max_yuan > 0 ? \AKSRT\Ledger\AssetAmount::balance((string) $max_yuan)->minor() : 0;
if ($price_minor === null) {
return ['success' => false, 'message' => __('请设置正确的价格', 'aksrt')];
}
@@ -199,13 +201,6 @@ trait FrontSubmitPay {
}
}
private function parse_front_points_limit($value): int {
if (!is_string($value) || !preg_match('/^(0|[1-9]\d*)(?:\.00)?$/', trim($value), $matches)) {
return 0;
}
return $this->parse_front_points((string) $matches[1]) ?? 0;
}
/**
* @param array<string, mixed> $meta
*/
+10 -18
View File
@@ -32,7 +32,7 @@ trait FrontSubmitSave {
}
if ($is_guest) {
if (empty($settings['guest_enabled']) || (int) $settings['default_user_id'] <= 0) {
if ((int) $settings['default_user_id'] <= 0) {
return ['success' => false, 'message' => __('请先登录', 'aksrt')];
}
if ($is_draft) {
@@ -93,19 +93,16 @@ trait FrontSubmitSave {
if ($this->text_length(wp_strip_all_tags($content)) < 10) {
return ['success' => false, 'message' => __('文章内容过少', 'aksrt')];
}
if (!empty($settings['allow_category']) && empty($categories)) {
if (empty($categories)) {
return ['success' => false, 'message' => __('请选择文章分类', 'aksrt')];
}
if (!empty($settings['cover_required']) && empty($data['featured_data']) && empty($data['thumbnail'])) {
return ['success' => false, 'message' => __('请设置封面图', 'aksrt')];
}
}
// Allowed category whitelist.
$allowed = $settings['allowed_cat_ids'] ?? [];
if (!empty($allowed) && !empty($categories)) {
$categories = array_values(array_intersect($categories, $allowed));
if (!$is_draft && empty($categories) && !empty($settings['allow_category'])) {
if (!$is_draft && empty($categories)) {
return ['success' => false, 'message' => __('所选分类不可用', 'aksrt')];
}
}
@@ -126,7 +123,7 @@ trait FrontSubmitSave {
$status = 'draft';
if (!$is_draft) {
$status = $this->resolve_publish_status($user_id, $settings);
$status = $this->resolve_publish_status($user_id);
}
$can_iframe = $this->user_can_cap('new_post_iframe_video', $user_id);
@@ -173,7 +170,7 @@ trait FrontSubmitSave {
}
// Re-apply categories when empty update should clear (logged-in only).
if (!empty($settings['allow_category']) && isset($data['category'])) {
if (isset($data['category'])) {
wp_set_post_categories((int) $in_id, $categories);
}
if (!empty($settings['allow_tags']) && array_key_exists('tags', $data) && $tags_raw === '') {
@@ -233,17 +230,12 @@ trait FrontSubmitSave {
}
/**
* @param array<string, mixed> $settings
* Publish status for a non-draft submission.
*
* Mirrors the reference theme: pending unless the user holds the
* "publish without review" capability.
*/
private function resolve_publish_status(int $user_id, array $settings): string {
$mode = (string) ($settings['review_mode'] ?? 'pending');
if ($mode === 'draft') {
return 'draft';
}
if ($mode === 'publish') {
return 'publish';
}
// pending mode: skip audit when capability grants it.
private function resolve_publish_status(int $user_id): string {
if ($this->user_can_cap('new_post_audit_no', $user_id)) {
return 'publish';
}
+21 -43
View File
@@ -297,65 +297,43 @@ class PostDisplay {
public static function get_submit_settings(): array {
$settings = Settings::get_all();
$money_setting = static function ($value, string $default): string {
if (!is_string($value)) {
return $default;
}
try {
return \AKSRT\Ledger\AssetAmount::balance($value)->decimal();
} catch (\InvalidArgumentException $e) {
return $default;
}
};
$title_min = max(1, min(100, (int) ($settings['post_article_title_strlen_limit_min'] ?? 5)));
$title_max = max(5, min(200, (int) ($settings['post_article_title_strlen_limit_max'] ?? 30)));
$title_limit = is_array($settings['post_article_title_strlen_limit'] ?? null)
? $settings['post_article_title_strlen_limit']
: [];
$title_min = max(1, min(100, (int) ($title_limit['min'] ?? 5)));
$title_max = max(5, min(200, (int) ($title_limit['max'] ?? 30)));
if ($title_max < $title_min) {
$title_max = $title_min;
}
$review_mode = sanitize_key((string) ($settings['front_submit_review_mode'] ?? 'pending'));
if (!in_array($review_mode, ['pending', 'publish', 'draft'], true)) {
$review_mode = 'pending';
}
$editor_mode = sanitize_key((string) ($settings['front_submit_editor_mode'] ?? 'rich'));
if (!in_array($editor_mode, ['rich', 'markdown', 'plain'], true)) {
$editor_mode = 'rich';
}
// 付费金额限制:现金/积分各自独立,0 表示不限制。
$pay_price_limit = is_array($settings['post_article_pay_price_limit'] ?? null)
? $settings['post_article_pay_price_limit']
: [];
$cash_limit = is_array($pay_price_limit['price'] ?? null) ? $pay_price_limit['price'] : [];
$points_limit = is_array($pay_price_limit['points'] ?? null) ? $pay_price_limit['points'] : [];
return [
'enabled' => !isset($settings['front_submit_enabled']) || !empty($settings['front_submit_enabled']),
'enabled' => !isset($settings['post_article_s']) || !empty($settings['post_article_s']),
'btn_text' => sanitize_text_field((string) ($settings['post_article_btn_txte'] ?? '')) ?: __('发布文章', 'aksrt'),
'title_min' => $title_min,
'title_max' => $title_max,
'title_placeholder' => sanitize_text_field((string) ($settings['post_t_placeholder'] ?? '')) ?: __('请输入标题', 'aksrt'),
'content_placeholder' => sanitize_textarea_field((string) ($settings['post_c_placeholder'] ?? '')) ?: __('请输入内容', 'aksrt'),
'allowed_cat_ids' => is_array($settings['post_article_cat'] ?? null) ? array_values(array_filter(array_map('intval', $settings['post_article_cat']))) : [],
'default_user_id' => max(0, (int) ($settings['post_article_user'] ?? 0)),
'guest_enabled' => !empty($settings['front_submit_guest_enabled']),
'allow_title' => !isset($settings['front_submit_title']) || !empty($settings['front_submit_title']),
'allow_content' => !isset($settings['front_submit_content']) || !empty($settings['front_submit_content']),
'allow_excerpt' => !isset($settings['front_submit_excerpt']) || !empty($settings['front_submit_excerpt']),
'allow_cover' => !isset($settings['front_submit_cover']) || !empty($settings['front_submit_cover']),
'allow_category' => !isset($settings['front_submit_category']) || !empty($settings['front_submit_category']),
'allow_tags' => !isset($settings['front_submit_tags']) || !empty($settings['front_submit_tags']),
'tags_s' => !isset($settings['front_submit_tags']) || !empty($settings['front_submit_tags']),
'cover_required' => !empty($settings['front_submit_cover_required']),
'max_images' => max(0, min(50, (int) ($settings['front_submit_max_images'] ?? 5))),
'review_mode' => $review_mode,
'editor_mode' => $editor_mode,
'draft_save' => !isset($settings['front_submit_draft_save']) || !empty($settings['front_submit_draft_save']),
'leave_warning' => !isset($settings['front_submit_leave_warning']) || !empty($settings['front_submit_leave_warning']),
'default_user_id' => max(0, (int) ($settings['post_article_user'] ?? 1)),
'allow_tags' => !isset($settings['post_article_tag']) || !empty($settings['post_article_tag']),
'custom_filter_s' => !isset($settings['post_article_custom_filter_s']) || !empty($settings['post_article_custom_filter_s']),
'pay_free_s' => !isset($settings['post_article_pay_free_s']) || !empty($settings['post_article_pay_free_s']),
'pay_price_min' => $money_setting($settings['post_article_pay_price_min'] ?? '0.00', '0.00'),
'pay_price_max' => $money_setting($settings['post_article_pay_price_max'] ?? '100.00', '100.00'),
'pay_price_min' => max(0, (int) ($cash_limit['min'] ?? 0)),
'pay_price_max' => max(0, (int) ($cash_limit['max'] ?? 0)),
'pay_points_min' => max(0, (int) ($points_limit['min'] ?? 0)),
'pay_points_max' => max(0, (int) ($points_limit['max'] ?? 0)),
'pay_vip_price_s' => !isset($settings['post_article_pay_vip_price_s']) || !empty($settings['post_article_pay_vip_price_s']),
'pay_vip_1_enabled' => !empty($settings['pay_user_vip_1_s']),
'pay_vip_2_enabled' => !empty($settings['pay_user_vip_2_s']),
'pay_vip_1_discount' => max(0, min(100, (int) ($settings['post_article_pay_vip_1_discount'] ?? 80))),
'pay_vip_2_discount' => max(0, min(100, (int) ($settings['post_article_pay_vip_2_discount'] ?? 50))),
'custom_filter_s' => !isset($settings['post_article_custom_filter_s']) || !empty($settings['post_article_custom_filter_s']),
'pay_vip_1_discount' => max(0, min(100, (int) ($settings['post_article_pay_vip_1_discount'] ?? 100))),
'pay_vip_2_discount' => max(0, min(100, (int) ($settings['post_article_pay_vip_2_discount'] ?? 100))),
];
}