进行了一些小修复

This commit is contained in:
2026-08-26 16:32:18 +08:00
parent 992af2119d
commit 2cb78edabb
26 changed files with 2091 additions and 2383 deletions
+78 -37
View File
@@ -63,34 +63,22 @@ class DomainModel {
/**
* @return array<string, mixed>
*/
public static function default_post_meta(): array {
return [
'cover_images' => '',
'custom_excerpt' => '',
'header_image_mode' => '',
'show_reading_progress' => '',
'enable_lightbox' => '',
'show_related' => '',
'related_count' => 0,
'enable_like' => '',
'enable_favorite' => '',
'enable_share' => '',
'enable_print' => '',
'enable_pdf' => '',
'copyright_text' => '',
'is_featured' => false,
// §3.8 文章扩展字段
'thumbnail_url' => '',
'featured_slide' => '',
'featured_video' => '',
// Episode list: [ ['url' => string, 'title' => string], ... ]
'featured_video_episode' => [],
'subtitle' => '',
'views' => 0,
'likes' => 0,
'maxheight' => 0,
];
}
public static function default_post_meta(): array {
return [
'thumbnail_url' => '',
'cover_image' => '',
'featured_slide' => '',
'featured_video' => '',
'featured_video_title' => '',
'featured_video_episode' => [],
'subtitle' => '',
'views' => 0,
'like' => 0,
'show_layout' => 'false',
'no_article-navs' => false,
'article_maxheight_xz' => false,
];
}
/**
* @return array<string, mixed>
@@ -112,23 +100,37 @@ class DomainModel {
return [
'pay_type' => self::PAY_TYPE_NO,
'pay_modo' => '0', // '0' cash | 'points' credits (legacy)
'order_expired_s' => false,
'order_expired_time' => 0,
'order_expired_unit' => 'day',
'price' => '0.00',
'points_price' => 0,
'original_price' => '0.00',
'promotion_tag' => '',
'vip1_price' => '0.00',
'vip2_price' => '0.00',
'vip1_points' => 0,
'vip2_points' => 0,
'pay_limit' => 0, // 0 all | 1 VIP1+ | 2 VIP2 only (legacy)
'pay_count' => 0,
'version' => '',
'size' => '',
'license' => '',
'compat' => '',
'demo_url' => '',
'video_url' => '',
'video_pic' => '',
'video_title' => '',
'video_episode' => [],
'video_scale_height' => 0,
'downloads' => [],
'attributes' => [],
'gallery' => [],
'gallery_show' => 1,
'pay_title' => '',
'pay_doc' => '',
'pay_details' => '',
'pay_extra_hide' => '',
'require_login' => false,
'require_captcha' => false,
// §3.8 电商增强字段
@@ -150,15 +152,14 @@ class DomainModel {
/**
* @return array<string, string>
*/
public static function pay_type_labels(): array {
return [
self::PAY_TYPE_NO => '不付费',
self::PAY_TYPE_READING => '付费阅读',
self::PAY_TYPE_DOWNLOAD => '付费下载',
self::PAY_TYPE_GALLERY => '付费图库',
self::PAY_TYPE_VIDEO => '付费视频',
self::PAY_TYPE_VIP => 'VIP 专属',
];
public static function pay_type_labels(): array {
return [
self::PAY_TYPE_NO => '关闭',
self::PAY_TYPE_READING => '付费阅读',
self::PAY_TYPE_DOWNLOAD => '付费下载',
self::PAY_TYPE_GALLERY => '付费图片',
self::PAY_TYPE_VIDEO => '付费视频',
];
}
public static function normalize_pay_type(string $pay_type): string {
@@ -315,24 +316,39 @@ class DomainModel {
$meta['pay_type'] = self::normalize_pay_type((string) ($raw['pay_type'] ?? $meta['pay_type']));
$pay_modo = sanitize_key((string) ($raw['pay_modo'] ?? '0'));
$meta['pay_modo'] = $pay_modo === 'points' ? 'points' : '0';
$meta['order_expired_s'] = !empty($raw['order_expired_s']);
$meta['order_expired_time'] = max(0, (int) ($raw['order_expired_time'] ?? 0));
$order_expired_unit = sanitize_key((string) ($raw['order_expired_unit'] ?? 'day'));
$meta['order_expired_unit'] = in_array($order_expired_unit, ['day', 'hour', 'minute'], true) ? $order_expired_unit : 'day';
$meta['price'] = self::sanitize_balance_decimal($raw['price'] ?? '0');
$meta['points_price'] = self::sanitize_points($raw['points_price'] ?? 0);
$meta['original_price'] = self::sanitize_balance_decimal($raw['original_price'] ?? '0');
$meta['promotion_tag'] = wp_kses_post((string) ($raw['promotion_tag'] ?? ''));
$meta['vip1_price'] = self::sanitize_balance_decimal($raw['vip1_price'] ?? '0');
$meta['vip2_price'] = self::sanitize_balance_decimal($raw['vip2_price'] ?? '0');
$meta['vip1_points'] = self::sanitize_points($raw['vip1_points'] ?? 0);
$meta['vip2_points'] = self::sanitize_points($raw['vip2_points'] ?? 0);
$pay_limit = (int) ($raw['pay_limit'] ?? 0);
$meta['pay_limit'] = in_array($pay_limit, [0, 1, 2], true) ? $pay_limit : 0;
$meta['pay_count'] = (int) ($raw['pay_count'] ?? 0);
$meta['version'] = sanitize_text_field((string) ($raw['version'] ?? ''));
$meta['size'] = sanitize_text_field((string) ($raw['size'] ?? ''));
$meta['license'] = sanitize_text_field((string) ($raw['license'] ?? ''));
$meta['compat'] = sanitize_text_field((string) ($raw['compat'] ?? ''));
$meta['demo_url'] = esc_url_raw((string) ($raw['demo_url'] ?? ''));
$meta['video_url'] = esc_url_raw((string) ($raw['video_url'] ?? ''));
$meta['video_pic'] = esc_url_raw((string) ($raw['video_pic'] ?? ''));
$meta['video_title'] = sanitize_text_field((string) ($raw['video_title'] ?? ''));
$meta['video_episode'] = self::sanitize_video_episodes($raw['video_episode'] ?? []);
$meta['video_scale_height'] = max(0, min(200, (int) ($raw['video_scale_height'] ?? 0)));
$meta['downloads'] = self::sanitize_downloads($raw['downloads'] ?? []);
$meta['attributes'] = self::sanitize_attributes($raw['attributes'] ?? []);
$meta['gallery'] = self::sanitize_gallery($raw['gallery'] ?? []);
$meta['gallery_show'] = max(0, (int) ($raw['gallery_show'] ?? 1));
$meta['pay_title'] = sanitize_text_field((string) ($raw['pay_title'] ?? ''));
$meta['pay_doc'] = wp_kses_post((string) ($raw['pay_doc'] ?? ''));
$meta['pay_details'] = wp_kses_post((string) ($raw['pay_details'] ?? ''));
$meta['pay_extra_hide'] = wp_kses_post((string) ($raw['pay_extra_hide'] ?? ''));
$meta['require_login'] = !empty($raw['require_login']);
$meta['require_captcha'] = !empty($raw['require_captcha']);
@@ -353,6 +369,31 @@ class DomainModel {
return $meta;
}
/**
* @param mixed $value Raw video episode rows.
* @return array<int, array{title:string,url:string}>
*/
private static function sanitize_video_episodes($value): array {
if (!is_array($value)) {
return [];
}
$episodes = [];
foreach ($value as $row) {
if (!is_array($row)) {
continue;
}
$url = esc_url_raw((string) ($row['url'] ?? ''));
if ($url === '') {
continue;
}
$episodes[] = [
'title' => sanitize_text_field((string) ($row['title'] ?? '')),
'url' => $url,
];
}
return array_slice($episodes, 0, 50);
}
private static function sanitize_balance_decimal($value): string {
if (!is_string($value)) {
return '0.00';
+157 -156
View File
@@ -12,172 +12,173 @@ defined('ABSPATH') || exit;
*/
trait FrontSubmitFeatured {
/**
* @param array<string, mixed> $data
*/
private function save_featured_meta(int $post_id, array $data, int $user_id): void {
if (empty($data['featured_data']) && empty($data['featured_type'])) {
return;
}
/**
* @param array<string, mixed> $data
*/
private function save_featured_meta(int $post_id, array $data, int $user_id): void {
if (empty($data['featured_data'])) {
return;
}
$featured = $data['featured_data'] ?? null;
if (is_string($featured)) {
$decoded = json_decode(wp_unslash($featured), true);
$featured = is_array($decoded) ? $decoded : null;
}
if (!is_array($featured)) {
// Flat payload: featured_type + fields.
$featured = [
'type' => sanitize_key((string) ($data['featured_type'] ?? '')),
'data' => $data['featured_payload'] ?? $data,
];
}
$featured = $data['featured_data'] ?? null;
if (is_string($featured)) {
$decoded = json_decode(wp_unslash($featured), true);
$featured = is_array($decoded) ? $decoded : null;
}
if (!is_array($featured)) {
return;
}
$type = sanitize_key((string) ($featured['type'] ?? ''));
$payload = is_array($featured['data'] ?? null) ? $featured['data'] : $featured;
$type = sanitize_key((string) ($featured['type'] ?? ''));
$payload = is_array($featured['data'] ?? null) ? $featured['data'] : $featured;
$post_meta = get_post_meta($post_id, DomainModel::POST_META_KEY, true);
if (!is_array($post_meta)) {
$post_meta = DomainModel::default_post_meta();
}
$post_meta = get_post_meta($post_id, DomainModel::POST_META_KEY, true);
if (!is_array($post_meta)) {
$post_meta = DomainModel::default_post_meta();
}
$post_meta = array_intersect_key($post_meta, DomainModel::default_post_meta());
switch ($type) {
case 'video':
if (!$this->user_can_cap('new_post_video_cover', $user_id) && !$this->user_can_cap('new_post_image_cover', $user_id)) {
return;
}
$video = esc_url_raw((string) ($payload['url'] ?? $payload['video'] ?? ''));
$pic = esc_url_raw((string) ($payload['pic'] ?? $payload['poster'] ?? $payload['cover'] ?? ''));
if ($pic !== '' && !$this->user_can_cap('upload_submit_image', $user_id)) {
$pic = '';
}
$post_meta['featured_video'] = $video;
$post_meta['featured_slide'] = '';
if ($pic !== '' && $this->can_use_front_submit_image_url($pic, $user_id)) {
$post_meta['thumbnail_url'] = $pic;
$post_meta['cover_images'] = $pic;
$this->maybe_set_thumbnail_url($post_id, $pic);
}
break;
switch ($type) {
case 'video':
if (!$this->user_can_cap('new_post_video_cover', $user_id) && !$this->user_can_cap('new_post_image_cover', $user_id)) {
return;
}
$video = esc_url_raw((string) ($payload['url'] ?? ''));
$pic = esc_url_raw((string) ($payload['pic'] ?? ''));
if ($pic !== '' && !$this->user_can_cap('upload_submit_image', $user_id)) {
$pic = '';
}
$post_meta['featured_video'] = $video;
$post_meta['featured_slide'] = '';
if ($pic !== '' && $this->can_use_front_submit_image_url($pic, $user_id)) {
$post_meta['thumbnail_url'] = $pic;
$post_meta['cover_image'] = $pic;
$this->maybe_set_thumbnail_url($post_id, $pic);
}
break;
case 'slide':
if (!$this->user_can_cap('new_post_slide_cover', $user_id) && !$this->user_can_cap('new_post_image_cover', $user_id)) {
return;
}
$ids = $payload['ids'] ?? $payload['slides'] ?? $payload['images'] ?? [];
$urls = [];
if (is_string($ids)) {
$ids = array_filter(array_map('trim', explode(',', $ids)));
}
if (is_array($ids)) {
foreach ($ids as $item) {
if (is_array($item)) {
$u = esc_url_raw((string) ($item['url'] ?? ''));
$attachment_id = absint($item['id'] ?? 0);
if ($attachment_id > 0 && $this->can_use_front_submit_image($attachment_id, $user_id)) {
$src = wp_get_attachment_image_url($attachment_id, 'full');
if ($src) {
$urls[] = $src;
}
} elseif ($u !== '' && $this->user_can_cap('upload_submit_image', $user_id)) {
$urls[] = $u;
}
} elseif (is_numeric($item)) {
$attachment_id = (int) $item;
$src = $this->can_use_front_submit_image($attachment_id, $user_id)
? wp_get_attachment_image_url($attachment_id, 'full')
: false;
if ($src) {
$urls[] = $src;
}
} else {
$u = esc_url_raw((string) $item);
if ($u !== '' && $this->user_can_cap('upload_submit_image', $user_id)) {
$urls[] = $u;
}
}
}
}
$post_meta['featured_slide'] = implode(',', $urls);
$post_meta['featured_video'] = '';
if (!empty($urls[0]) && $this->can_use_front_submit_image_url((string) $urls[0], $user_id)) {
$post_meta['thumbnail_url'] = $urls[0];
$post_meta['cover_images'] = $urls[0];
$this->maybe_set_thumbnail_url($post_id, $urls[0]);
}
break;
case 'slide':
if (!$this->user_can_cap('new_post_slide_cover', $user_id) && !$this->user_can_cap('new_post_image_cover', $user_id)) {
return;
}
$ids = array_is_list($payload) ? $payload : [];
$attachment_ids = [];
$urls = [];
if (is_string($ids)) {
$ids = array_filter(array_map('trim', explode(',', $ids)));
}
if (is_array($ids)) {
foreach ($ids as $item) {
if (is_array($item)) {
$u = esc_url_raw((string) ($item['url'] ?? ''));
$attachment_id = absint($item['id'] ?? 0);
if ($attachment_id > 0 && $this->can_use_front_submit_image($attachment_id, $user_id)) {
$src = wp_get_attachment_image_url($attachment_id, 'full');
if ($src) {
$attachment_ids[] = $attachment_id;
$urls[] = $src;
}
} elseif ($u !== '' && $this->user_can_cap('upload_submit_image', $user_id)) {
$urls[] = $u;
}
} elseif (is_numeric($item)) {
$attachment_id = (int) $item;
$src = $this->can_use_front_submit_image($attachment_id, $user_id)
? wp_get_attachment_image_url($attachment_id, 'full')
: false;
if ($src) {
$attachment_ids[] = $attachment_id;
$urls[] = $src;
}
} else {
$u = esc_url_raw((string) $item);
if ($u !== '' && $this->user_can_cap('upload_submit_image', $user_id)) {
$urls[] = $u;
}
}
}
}
$post_meta['featured_slide'] = implode(',', array_values(array_unique($attachment_ids)));
$post_meta['featured_video'] = '';
if (!empty($urls[0]) && $this->can_use_front_submit_image_url((string) $urls[0], $user_id)) {
$post_meta['thumbnail_url'] = $urls[0];
$post_meta['cover_image'] = $urls[0];
$this->maybe_set_thumbnail_url($post_id, $urls[0]);
}
break;
case 'image':
if (!$this->user_can_cap('new_post_image_cover', $user_id)) {
return;
}
$url = esc_url_raw((string) ($payload['url'] ?? $payload['image'] ?? ''));
$id = absint($payload['id'] ?? 0);
if ($id > 0 && !$this->can_use_front_submit_image($id, $user_id)) {
return;
}
if ($url === '' && $id > 0) {
$src = wp_get_attachment_image_url($id, 'full');
if ($src) {
$url = $src;
}
}
$post_meta['featured_video'] = '';
$post_meta['featured_slide'] = '';
if ($url !== '' && $this->can_use_front_submit_image_url($url, $user_id)) {
$post_meta['thumbnail_url'] = $url;
$post_meta['cover_images'] = $url;
if ($id > 0) {
set_post_thumbnail($post_id, $id);
} elseif ($this->user_can_cap('upload_submit_image', $user_id)) {
$this->maybe_set_thumbnail_url($post_id, $url);
}
}
break;
case 'image':
if (!$this->user_can_cap('new_post_image_cover', $user_id)) {
return;
}
$url = esc_url_raw((string) ($payload['url'] ?? ''));
$id = absint($payload['id'] ?? 0);
if ($id > 0 && !$this->can_use_front_submit_image($id, $user_id)) {
return;
}
if ($url === '' && $id > 0) {
$src = wp_get_attachment_image_url($id, 'full');
if ($src) {
$url = $src;
}
}
$post_meta['featured_video'] = '';
$post_meta['featured_slide'] = '';
if ($url !== '' && $this->can_use_front_submit_image_url($url, $user_id)) {
$post_meta['thumbnail_url'] = $url;
$post_meta['cover_image'] = $url;
if ($id > 0) {
set_post_thumbnail($post_id, $id);
} elseif ($this->user_can_cap('upload_submit_image', $user_id)) {
$this->maybe_set_thumbnail_url($post_id, $url);
}
}
break;
case 'close':
case 'none':
case 'clear':
$post_meta['featured_video'] = '';
$post_meta['featured_slide'] = '';
$post_meta['thumbnail_url'] = '';
$post_meta['cover_images'] = '';
delete_post_thumbnail($post_id);
break;
case 'close':
case 'none':
case 'clear':
$post_meta['featured_video'] = '';
$post_meta['featured_slide'] = '';
$post_meta['thumbnail_url'] = '';
$post_meta['cover_image'] = '';
delete_post_thumbnail($post_id);
break;
default:
return;
}
default:
return;
}
update_post_meta($post_id, DomainModel::POST_META_KEY, $post_meta);
}
unset($post_meta['views'], $post_meta['like']);
update_post_meta($post_id, DomainModel::POST_META_KEY, $post_meta);
}
private function maybe_set_thumbnail_url(int $post_id, string $url): void {
if ($url === '') {
return;
}
// Prefer attachment if URL is local media.
$attach_id = attachment_url_to_postid($url);
if ($attach_id > 0) {
set_post_thumbnail($post_id, $attach_id);
return;
}
$this->set_thumbnail_from_url($post_id, $url);
}
private function maybe_set_thumbnail_url(int $post_id, string $url): void {
if ($url === '') {
return;
}
// Prefer attachment if URL is local media.
$attach_id = attachment_url_to_postid($url);
if ($attach_id > 0) {
set_post_thumbnail($post_id, $attach_id);
return;
}
$this->set_thumbnail_from_url($post_id, $url);
}
private function can_use_front_submit_image(int $attachment_id, int $user_id): bool {
if ($attachment_id <= 0 || !$this->user_can_cap('upload_submit_image', $user_id)) {
return false;
}
$attachment = get_post($attachment_id);
return $attachment instanceof \WP_Post
&& $attachment->post_type === 'attachment'
&& (int) $attachment->post_author === $user_id
&& wp_attachment_is_image($attachment_id);
}
private function can_use_front_submit_image(int $attachment_id, int $user_id): bool {
if ($attachment_id <= 0 || !$this->user_can_cap('upload_submit_image', $user_id)) {
return false;
}
$attachment = get_post($attachment_id);
return $attachment instanceof \WP_Post
&& $attachment->post_type === 'attachment'
&& (int) $attachment->post_author === $user_id
&& wp_attachment_is_image($attachment_id);
}
private function can_use_front_submit_image_url(string $url, int $user_id): bool {
$attachment_id = attachment_url_to_postid($url);
return $attachment_id > 0 && $this->can_use_front_submit_image($attachment_id, $user_id);
}
private function can_use_front_submit_image_url(string $url, int $user_id): bool {
$attachment_id = attachment_url_to_postid($url);
return $attachment_id > 0 && $this->can_use_front_submit_image($attachment_id, $user_id);
}
}
+15 -28
View File
@@ -23,13 +23,13 @@ trait FrontSubmitPay {
}
// Only touch pay meta when the form sent pay fields.
$has_pay_payload = isset($data['aksrt_pay']) || isset($data['pay_enabled']) || isset($data['aksrtpay_s']) || isset($data['pay_type']);
$has_pay_payload = isset($data['aksrt_pay']) || isset($data['pay_enabled']);
if (!$has_pay_payload) {
return ['success' => true];
}
$raw = is_array($data['aksrt_pay'] ?? null) ? $data['aksrt_pay'] : [];
$enabled = !empty($data['pay_enabled']) || !empty($data['aksrtpay_s']);
$enabled = !empty($data['pay_enabled']);
$existing = function_exists('aksrt_get_pay_meta') ? aksrt_get_pay_meta($post_id) : DomainModel::default_pay_meta();
$meta = is_array($existing) ? $existing : DomainModel::default_pay_meta();
@@ -40,22 +40,15 @@ trait FrontSubmitPay {
return ['success' => true];
}
$pay_modo = sanitize_key((string) ($raw['pay_modo'] ?? $data['pay_modo'] ?? '0'));
$pay_modo = sanitize_key((string) ($raw['pay_modo'] ?? '0'));
$pay_modo = $pay_modo === 'points' ? 'points' : '0';
$pay_type = sanitize_key((string) ($raw['pay_type'] ?? $data['pay_type'] ?? DomainModel::PAY_TYPE_READING));
if ($pay_type === '1' || $pay_type === 'reading' || $pay_type === 'payshow') {
$pay_type = DomainModel::normalize_pay_type((string) ($raw['pay_type'] ?? DomainModel::PAY_TYPE_READING));
if ($pay_type === DomainModel::PAY_TYPE_NO) {
$pay_type = DomainModel::PAY_TYPE_READING;
} elseif ($pay_type === '2' || $pay_type === 'download') {
$pay_type = DomainModel::PAY_TYPE_DOWNLOAD;
} else {
$pay_type = DomainModel::normalize_pay_type($pay_type);
if ($pay_type === DomainModel::PAY_TYPE_NO) {
$pay_type = DomainModel::PAY_TYPE_READING;
}
}
$downloads = $this->normalize_front_downloads($raw['downloads'] ?? $data['pay_download'] ?? [], $user_id);
$downloads = $this->normalize_front_downloads($raw['downloads'] ?? [], $user_id);
if (!empty($downloads) && $this->user_can_cap('new_post_pay_download', $user_id)) {
$pay_type = DomainModel::PAY_TYPE_DOWNLOAD;
$meta['downloads'] = $downloads;
@@ -93,10 +86,10 @@ trait FrontSubmitPay {
if ($vip_manual) {
$vip1 = $vip1_enabled
? $this->parse_front_points($raw['vip1_points'] ?? $raw['vip_1_points'] ?? null)
? $this->parse_front_points($raw['vip1_points'] ?? null)
: 0;
$vip2 = $vip2_enabled
? $this->parse_front_points($raw['vip2_points'] ?? $raw['vip_2_points'] ?? null)
? $this->parse_front_points($raw['vip2_points'] ?? null)
: 0;
if (($vip1_enabled && $vip1 === null) || ($vip2_enabled && $vip2 === null)) {
return ['success' => false, 'message' => __('请设置正确的会员积分价格', 'aksrt')];
@@ -121,7 +114,7 @@ trait FrontSubmitPay {
$meta['vip1_price'] = '0.00';
$meta['vip2_price'] = '0.00';
} else {
$price_minor = $this->parse_front_cash_minor($raw['price'] ?? $raw['pay_price'] ?? $data['pay_price'] ?? null);
$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;
if ($price_minor === null) {
@@ -139,10 +132,10 @@ trait FrontSubmitPay {
if ($vip_manual) {
$vip1_minor = $vip1_enabled
? $this->parse_front_cash_minor($raw['vip1_price'] ?? $raw['vip_1_price'] ?? null)
? $this->parse_front_cash_minor($raw['vip1_price'] ?? null)
: 0;
$vip2_minor = $vip2_enabled
? $this->parse_front_cash_minor($raw['vip2_price'] ?? $raw['vip_2_price'] ?? null)
? $this->parse_front_cash_minor($raw['vip2_price'] ?? null)
: 0;
if (($vip1_enabled && $vip1_minor === null) || ($vip2_enabled && $vip2_minor === null)) {
return ['success' => false, 'message' => __('请设置正确的会员价格', 'aksrt')];
@@ -168,13 +161,8 @@ trait FrontSubmitPay {
$meta['vip2_points'] = 0;
}
$pay_limit = (int) ($raw['pay_limit'] ?? $data['pay_limit'] ?? 0);
$pay_limit = (int) ($raw['pay_limit'] ?? 0);
$meta['pay_limit'] = in_array($pay_limit, [0, 1, 2], true) ? $pay_limit : 0;
$meta['require_login'] = !empty($raw['require_login']) || !empty($data['require_login']);
$meta['coupon_s'] = !empty($raw['coupon_s']) || !empty($data['coupon_s']);
if (isset($raw['coupon_desc']) || isset($data['coupon_desc'])) {
$meta['coupon_desc'] = sanitize_textarea_field((string) ($raw['coupon_desc'] ?? $data['coupon_desc'] ?? ''));
}
$this->write_pay_meta($post_id, $meta);
return ['success' => true];
@@ -227,8 +215,7 @@ trait FrontSubmitPay {
if (!is_array($row)) {
continue;
}
// Accept legacy-ish keys + AKSRT keys.
$url = (string) ($row['url'] ?? $row['link'] ?? '');
$url = (string) ($row['url'] ?? '');
$url = esc_url_raw(trim($url));
if ($url === '') {
continue;
@@ -238,10 +225,10 @@ trait FrontSubmitPay {
continue;
}
$out[] = [
'label' => sanitize_text_field((string) ($row['label'] ?? $row['name'] ?? '')),
'label' => sanitize_text_field((string) ($row['label'] ?? '')),
'url' => $url,
'type' => $attachment_id > 0 ? 'direct' : 'external',
'extract_code' => sanitize_text_field((string) ($row['extract_code'] ?? $row['copy_val'] ?? $row['more'] ?? '')),
'extract_code' => sanitize_text_field((string) ($row['extract_code'] ?? '')),
];
}
return DomainModel::sanitize_downloads($out);
-10
View File
@@ -181,16 +181,6 @@ trait FrontSubmitSave {
$this->save_featured_meta((int) $in_id, $data, $user_id);
// Legacy thumbnail URL fallback.
$thumbnail_url = esc_url_raw((string) ($data['thumbnail'] ?? ''));
if ($thumbnail_url !== '' && empty($data['featured_data']) && $this->user_can_cap('upload_submit_image', $user_id)) {
$attachment_id = attachment_url_to_postid($thumbnail_url);
$attachment = $attachment_id > 0 ? get_post($attachment_id) : null;
if ($attachment instanceof \WP_Post && (int) $attachment->post_author === $user_id && wp_attachment_is_image($attachment_id)) {
set_post_thumbnail((int) $in_id, $attachment_id);
}
}
$post = get_post((int) $in_id);
if ($post instanceof \WP_Post) {
do_action($is_edit ? 'aksrt_new_edit_posts' : 'aksrt_new_add_posts', $post);
+304 -788
View File
File diff suppressed because it is too large Load Diff
+231 -518
View File
@@ -1,5 +1,7 @@
<?php
/**
* Article paid-content fields shared by the block and classic editors.
*
* @package AKSRT
*/
@@ -9,550 +11,261 @@ defined('ABSPATH') || exit;
class ResourceMetaBox {
private static $instance = null;
private static $instance = null;
private function __construct() {}
private function __construct() {}
public static function get_instance(): self {
if (null === self::$instance) self::$instance = new self();
return self::$instance;
}
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
public function init(): void {
add_action('add_meta_boxes', [$this, 'register_meta_box']);
add_action('save_post_post', [$this, 'save'], 10, 2);
}
public function init(): void {
add_action('add_meta_boxes', [$this, 'register_meta_box']);
add_action('save_post_post', [$this, 'save'], 10, 2);
add_action('admin_enqueue_scripts', [$this, 'enqueue_assets']);
}
public function register_meta_box(): void {
add_meta_box(
'aksrt_pay_meta',
__('付费内容设置', 'aksrt'),
[$this, 'render'],
'post',
'normal',
'high'
);
}
public function register_meta_box(): void {
add_meta_box('aksrt_pay_meta', __('付费功能', 'aksrt'), [$this, 'render'], 'post', 'normal', 'high');
}
public function render(\WP_Post $post): void {
wp_nonce_field('aksrt_pay_meta_save', 'aksrt_pay_meta_nonce');
public function enqueue_assets(string $hook): void {
if (!in_array($hook, ['post.php', 'post-new.php'], true)) {
return;
}
$screen = function_exists('get_current_screen') ? get_current_screen() : null;
if (!$screen || $screen->post_type !== 'post') {
return;
}
$meta = function_exists('aksrt_get_pay_meta')
? aksrt_get_pay_meta($post)
: DomainModel::default_pay_meta();
wp_enqueue_media();
$css = AKSRT_DIR . '/assets/css/admin/pay-meta-box.css';
$js = AKSRT_DIR . '/assets/js/admin-pay-meta-box.js';
if (is_file($css)) {
wp_enqueue_style('aksrt-pay-meta-box', AKSRT_URI . '/assets/css/admin/pay-meta-box.css', [], (string) filemtime($css));
}
if (is_file($js)) {
wp_enqueue_script('aksrt-pay-meta-box', AKSRT_URI . '/assets/js/admin-pay-meta-box.js', [], (string) filemtime($js), true);
}
}
$pay_types = aksrt_get_pay_types();
$is_paid = ($meta['pay_type'] ?? 'no') !== 'no';
public function render(\WP_Post $post): void {
wp_nonce_field('aksrt_pay_meta_save', 'aksrt_pay_meta_nonce');
$meta = function_exists('aksrt_get_pay_meta') ? aksrt_get_pay_meta($post) : DomainModel::default_pay_meta();
$gallery_ids = [];
foreach ((array) ($meta['gallery'] ?? []) as $url) {
$id = attachment_url_to_postid((string) $url);
if ($id > 0) {
$gallery_ids[] = $id;
}
}
?>
<div class="aksrt-pay-meta" data-aksrt-pay-meta>
<?php $this->radio('pay_type', __('付费模式', 'aksrt'), (string) ($meta['pay_type'] ?? 'no'), aksrt_get_pay_types()); ?>
<div data-pay-enabled>
<?php
$this->radio('pay_limit', __('购买权限', 'aksrt'), (string) ($meta['pay_limit'] ?? 0), [
'0' => __('所有人可购买', 'aksrt'),
'1' => __('VIP1 及以上会员可购买', 'aksrt'),
'2' => __('仅 VIP2 可购买', 'aksrt'),
], __('设置会员专享资源时,请同时配置对应的会员价格。', 'aksrt'));
$this->radio('pay_modo', __('支付类型', 'aksrt'), (string) ($meta['pay_modo'] ?? '0'), [
'0' => __('普通商品(金钱购买)', 'aksrt'),
'points' => __('积分商品(积分购买,依赖于用户积分功能)', 'aksrt'),
]);
?>
$downloads_json = !empty($meta['downloads'])
? wp_json_encode($meta['downloads'], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)
: '';
<div class="aksrt-pay-meta__field">
<div class="aksrt-pay-meta__title"><?php esc_html_e('订单时效', 'aksrt'); ?></div>
<div class="aksrt-pay-meta__control">
<label><input type="checkbox" name="aksrt_pay[order_expired_s]" value="1" <?php checked(!empty($meta['order_expired_s'])); ?> data-order-expired> <?php esc_html_e('启用订单有效期', 'aksrt'); ?></label>
<p class="description"><?php esc_html_e('购买后仅在有效期内可查看内容,超时后需再次购买。', 'aksrt'); ?></p>
</div>
</div>
<div class="aksrt-pay-meta__field compact" data-order-expired-fields>
<div class="aksrt-pay-meta__title"><?php esc_html_e('有效时间', 'aksrt'); ?></div>
<div class="aksrt-pay-meta__control aksrt-pay-meta__inline">
<input type="number" min="0" step="1" name="aksrt_pay[order_expired_time]" value="<?php echo esc_attr((string) ($meta['order_expired_time'] ?? 0)); ?>">
<select name="aksrt_pay[order_expired_unit]">
<option value="day" <?php selected((string) ($meta['order_expired_unit'] ?? 'day'), 'day'); ?>><?php esc_html_e('天', 'aksrt'); ?></option>
<option value="hour" <?php selected((string) ($meta['order_expired_unit'] ?? 'day'), 'hour'); ?>><?php esc_html_e('小时', 'aksrt'); ?></option>
<option value="minute" <?php selected((string) ($meta['order_expired_unit'] ?? 'day'), 'minute'); ?>><?php esc_html_e('分钟', 'aksrt'); ?></option>
</select>
</div>
</div>
$attributes_json = !empty($meta['attributes'])
? wp_json_encode($meta['attributes'], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)
: '';
<div data-pay-modo="points">
<?php $this->number('points_price', __('积分售价', 'aksrt'), (string) ($meta['points_price'] ?? 0), '1', __('积分', 'aksrt')); ?>
<?php $this->number('vip1_points', __('VIP1 积分售价', 'aksrt'), (string) ($meta['vip1_points'] ?? 0), '1', __('积分', 'aksrt'), __('填 0 则为 VIP1 免费', 'aksrt')); ?>
<?php $this->number('vip2_points', __('VIP2 积分售价', 'aksrt'), (string) ($meta['vip2_points'] ?? 0), '1', __('积分', 'aksrt'), __('会员价格不能高于售价,填 0 则为 VIP2 免费。', 'aksrt')); ?>
</div>
<div data-pay-modo="0">
<?php $this->number('price', __('执行价', 'aksrt'), (string) ($meta['price'] ?? '0.00'), '0.01', '', __('填 0 则为免费', 'aksrt')); ?>
<?php $this->number('original_price', __('原价', 'aksrt'), (string) ($meta['original_price'] ?? '0.00'), '0.01', '', __('显示在执行价格前面,并划掉。', 'aksrt')); ?>
<?php $this->textarea('promotion_tag', __('促销标签', 'aksrt'), (string) ($meta['promotion_tag'] ?? ''), 1); ?>
<?php $this->number('vip1_price', __('VIP1 价格', 'aksrt'), (string) ($meta['vip1_price'] ?? '0.00'), '0.01', '', __('填 0 则为 VIP1 免费', 'aksrt')); ?>
<?php $this->number('vip2_price', __('VIP2 价格', 'aksrt'), (string) ($meta['vip2_price'] ?? '0.00'), '0.01', '', __('会员价格不能高于执行价,填 0 则为 VIP2 免费。', 'aksrt')); ?>
</div>
$gallery_json = !empty($meta['gallery'])
? wp_json_encode($meta['gallery'], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)
: '';
?>
<style>
.aksrt-pmb__row { display: grid; grid-template-columns: 140px 1fr; gap: 12px 16px; align-items: start; margin-bottom: 12px; }
.aksrt-pmb__row label { font-weight: 600; color: #1f2937; padding-top: 6px; font-size: 13px; }
.aksrt-pmb__row input[type="text"], .aksrt-pmb__row input[type="number"], .aksrt-pmb__row input[type="url"], .aksrt-pmb__row textarea, .aksrt-pmb__row select { width: 100%; max-width: 560px; }
.aksrt-pmb__row textarea { min-height: 120px; font-family: Consolas, "Liberation Mono", monospace; font-size: 12px; }
.aksrt-pmb__hint { color: #6b7280; font-size: 12px; margin-top: 4px; line-height: 1.5; }
.aksrt-pmb__section { margin-top: 16px; padding-top: 16px; border-top: 1px solid #e5e7eb; }
.aksrt-pmb__section-title { font-size: 14px; font-weight: 700; color: #111827; margin-bottom: 12px; }
.aksrt-pmb__toggle { display: flex; align-items: center; gap: 8px; padding: 12px; background: #f3f4f6; border-radius: 6px; margin-bottom: 16px; }
.aksrt-pmb__toggle label { font-weight: 600; }
.aksrt-pmb__vip-row { display: flex; gap: 12px; flex-wrap: wrap; }
.aksrt-pmb__vip-row > div { flex: 1; min-width: 120px; }
.aksrt-pmb__vip-row label { font-weight: 400; font-size: 12px; color: #6b7280; }
.aksrt-pmb__tools { margin-top: 6px; display: flex; gap: 6px; flex-wrap: wrap; }
.aksrt-pmb__tools button { font-size: 12px; }
.aksrt-pmb__error { color: #dc2626; font-size: 12px; display: none; margin-top: 4px; }
.aksrt-pmb__fields[data-disabled="1"] { opacity: 0.45; pointer-events: none; }
</style>
<?php $this->number('pay_count', __('销量浮动', 'aksrt'), (string) ($meta['pay_count'] ?? 0), '1', '', __('为真实销量增加或减少的数量。', 'aksrt'), false); ?>
<div class="aksrt-pmb__toggle">
<label for="aksrt_pay_type"><?php esc_html_e('付费类型', 'aksrt'); ?></label>
<select id="aksrt_pay_type" name="aksrt_pay_type" style="min-width:160px;">
<?php foreach ($pay_types as $key => $label) : ?>
<option value="<?php echo esc_attr($key); ?>" <?php selected($meta['pay_type'], $key); ?>>
<?php echo esc_html($label); ?>
</option>
<?php endforeach; ?>
</select>
<span class="aksrt-pmb__hint">
<?php esc_html_e('选择「不付费」则为普通文章;选择其他类型将启用对应的付费功能。', 'aksrt'); ?>
</span>
</div>
<div data-pay-type="gallery">
<div class="aksrt-pay-meta__field">
<div class="aksrt-pay-meta__title"><?php esc_html_e('付费图片', 'aksrt'); ?></div>
<div class="aksrt-pay-meta__control">
<input type="hidden" name="aksrt_pay[gallery_ids]" value="<?php echo esc_attr(implode(',', $gallery_ids)); ?>" data-gallery-input>
<div class="aksrt-pay-meta__gallery" data-gallery-preview></div>
<div class="aksrt-pay-meta__buttons"><button type="button" class="button" data-gallery-select><?php esc_html_e('新增图片', 'aksrt'); ?></button><button type="button" class="button-link-delete" data-gallery-clear><?php esc_html_e('清空图片', 'aksrt'); ?></button></div>
</div>
</div>
<?php $this->number('gallery_show', __('免费查看', 'aksrt'), (string) ($meta['gallery_show'] ?? 1), '1', __('张', 'aksrt'), __('不能大于付费图片数量,否则无效。', 'aksrt')); ?>
</div>
<p class="aksrt-pmb__hint" id="aksrt-pmb-reading-hint" hidden>
<?php esc_html_e('付费阅读价格只作用于正文中的“付费可见”片段;未插入片段时,正文保持公开。', 'aksrt'); ?>
</p>
<div data-pay-type="video">
<?php $this->upload('video_url', __('视频资源', 'aksrt'), (string) ($meta['video_url'] ?? ''), 'video', __('输入视频地址或选择、上传本地视频。', 'aksrt')); ?>
<?php $this->upload('video_pic', __('视频封面(可选)', 'aksrt'), (string) ($meta['video_pic'] ?? ''), 'image'); ?>
<?php $this->text('video_title', __('本集标题', 'aksrt'), (string) ($meta['video_title'] ?? ''), __('如需添加剧集则需填写此处', 'aksrt'), __('第1集', 'aksrt')); ?>
<?php $this->group('video_episode', __('更多剧集', 'aksrt'), (array) ($meta['video_episode'] ?? []), 'video_row', __('添加剧集', 'aksrt'), __('为付费视频添加更多剧集。', 'aksrt')); ?>
<?php $this->number('video_scale_height', __('固定长宽比例', 'aksrt'), (string) ($meta['video_scale_height'] ?? 0), '5', '%', __('为 0 则不固定长宽比例。', 'aksrt')); ?>
</div>
<div class="aksrt-pmb__fields" id="aksrt-pmb-fields" data-disabled="<?php echo $is_paid ? '0' : '1'; ?>">
<div data-pay-type="download">
<?php $this->group('downloads', __('资源下载', 'aksrt'), (array) ($meta['downloads'] ?? []), 'download_row', __('添加资源', 'aksrt')); ?>
<?php $this->group('attributes', __('资源属性', 'aksrt'), (array) ($meta['attributes'] ?? []), 'attribute_row', __('添加属性', 'aksrt')); ?>
<?php $this->text('demo_url', __('演示地址', 'aksrt'), (string) ($meta['demo_url'] ?? ''), '', 'https://', 'url'); ?>
</div>
<!-- 基础价格 -->
<div class="aksrt-pmb__section">
<div class="aksrt-pmb__section-title"><?php esc_html_e('价格设置', 'aksrt'); ?></div>
<?php $this->text('pay_title', __('商品标题', 'aksrt'), (string) ($meta['pay_title'] ?? ''), __('可选,如需单独显示付费内容标题请填写。', 'aksrt')); ?>
<?php $this->textarea('pay_doc', __('商品简介', 'aksrt'), (string) ($meta['pay_doc'] ?? ''), 1, __('可选,如需单独显示付费内容介绍请填写。', 'aksrt')); ?>
<?php $this->textarea('pay_details', __('更多详情', 'aksrt'), (string) ($meta['pay_details'] ?? ''), 3, __('显示在付费内容卡片下方,支持安全 HTML。', 'aksrt')); ?>
<?php $this->textarea('pay_extra_hide', __('额外隐藏内容', 'aksrt'), (string) ($meta['pay_extra_hide'] ?? ''), 3, __('付费后显示的额外隐藏内容,支持安全 HTML。', 'aksrt')); ?>
<div class="aksrt-pmb__row">
<label for="aksrt_pay_modo"><?php esc_html_e('支付方式', 'aksrt'); ?></label>
<div>
<select id="aksrt_pay_modo" name="aksrt_pay_modo" style="min-width:160px;">
<option value="0" <?php selected((string) ($meta['pay_modo'] ?? '0'), '0'); ?>><?php esc_html_e('现金', 'aksrt'); ?></option>
<option value="points" <?php selected((string) ($meta['pay_modo'] ?? '0'), 'points'); ?>><?php esc_html_e('积分', 'aksrt'); ?></option>
</select>
<p class="aksrt-pmb__hint"><?php esc_html_e('对齐 legacy pay_modo:现金价或积分价结算。', 'aksrt'); ?></p>
</div>
</div>
<div class="aksrt-pay-meta__notice"><strong><?php esc_html_e('付费阅读', 'aksrt'); ?></strong> <?php esc_html_e('需配合古腾堡“隐藏内容块”或经典编辑器短代码使用:', 'aksrt'); ?> <code>[hidecontent type="payshow"]</code> <?php esc_html_e('隐藏内容', 'aksrt'); ?> <code>[/hidecontent]</code></div>
</div>
</div>
<?php
}
<div class="aksrt-pmb__row">
<label for="aksrt_pay_price"><?php esc_html_e('现金价格', 'aksrt'); ?></label>
<div>
<input type="number" id="aksrt_pay_price" name="aksrt_pay_price" value="<?php echo esc_attr((string) $meta['price']); ?>" min="0" step="0.01">
<p class="aksrt-pmb__hint"><?php esc_html_e('支付方式为「现金」时使用。0 = 免费。', 'aksrt'); ?></p>
</div>
</div>
/** @param array<string, string> $options */
private function radio(string $id, string $title, string $value, array $options, string $description = ''): void {
?>
<fieldset class="aksrt-pay-meta__field"><legend class="aksrt-pay-meta__title"><?php echo esc_html($title); ?></legend><div class="aksrt-pay-meta__control aksrt-pay-meta__radios">
<?php foreach ($options as $option => $label) : ?><label><input type="radio" name="aksrt_pay[<?php echo esc_attr($id); ?>]" value="<?php echo esc_attr((string) $option); ?>" <?php checked($value, (string) $option); ?>> <?php echo esc_html($label); ?></label><?php endforeach; ?>
<?php if ($description !== '') : ?><p class="description"><?php echo esc_html($description); ?></p><?php endif; ?>
</div></fieldset>
<?php
}
<div class="aksrt-pmb__row">
<label for="aksrt_pay_points_price"><?php esc_html_e('积分价格', 'aksrt'); ?></label>
<div>
<input type="number" id="aksrt_pay_points_price" name="aksrt_pay_points_price" value="<?php echo esc_attr((string) ($meta['points_price'] ?? 0)); ?>" min="0" step="1">
<p class="aksrt-pmb__hint"><?php esc_html_e('支付方式为「积分」时使用。0 = 免费。', 'aksrt'); ?></p>
</div>
</div>
private function number(string $id, string $title, string $value, string $step, string $unit = '', string $description = '', bool $non_negative = true): void {
?>
<div class="aksrt-pay-meta__field"><div class="aksrt-pay-meta__title"><label for="aksrt-pay-<?php echo esc_attr($id); ?>"><?php echo esc_html($title); ?></label></div><div class="aksrt-pay-meta__control"><div class="aksrt-pay-meta__number"><input id="aksrt-pay-<?php echo esc_attr($id); ?>" type="number" <?php echo $non_negative ? 'min="0"' : ''; ?> step="<?php echo esc_attr($step); ?>" name="aksrt_pay[<?php echo esc_attr($id); ?>]" value="<?php echo esc_attr($value); ?>"><?php if ($unit !== '') : ?><span><?php echo esc_html($unit); ?></span><?php endif; ?></div><?php if ($description !== '') : ?><p class="description"><?php echo esc_html($description); ?></p><?php endif; ?></div></div>
<?php
}
<div class="aksrt-pmb__row">
<label for="aksrt_pay_original_price"><?php esc_html_e('原价(划线价)', 'aksrt'); ?></label>
<div>
<input type="number" id="aksrt_pay_original_price" name="aksrt_pay_original_price" value="<?php echo esc_attr((string) ($meta['original_price'] ?? '')); ?>" min="0" step="0.01" placeholder="<?php esc_attr_e('可选,显示划线效果', 'aksrt'); ?>">
</div>
</div>
private function text(string $id, string $title, string $value, string $description = '', string $placeholder = '', string $type = 'text'): void {
?>
<div class="aksrt-pay-meta__field"><div class="aksrt-pay-meta__title"><label for="aksrt-pay-<?php echo esc_attr($id); ?>"><?php echo esc_html($title); ?></label></div><div class="aksrt-pay-meta__control"><input id="aksrt-pay-<?php echo esc_attr($id); ?>" type="<?php echo esc_attr($type); ?>" name="aksrt_pay[<?php echo esc_attr($id); ?>]" value="<?php echo esc_attr($value); ?>" placeholder="<?php echo esc_attr($placeholder); ?>"><?php if ($description !== '') : ?><p class="description"><?php echo esc_html($description); ?></p><?php endif; ?></div></div>
<?php
}
<div class="aksrt-pmb__row">
<label for="aksrt_pay_limit"><?php esc_html_e('购买限制', 'aksrt'); ?></label>
<div>
<select id="aksrt_pay_limit" name="aksrt_pay_limit" style="min-width:200px;">
<option value="0" <?php selected((int) ($meta['pay_limit'] ?? 0), 0); ?>><?php esc_html_e('所有用户可购买', 'aksrt'); ?></option>
<option value="1" <?php selected((int) ($meta['pay_limit'] ?? 0), 1); ?>><?php esc_html_e('仅 VIP1 及以上', 'aksrt'); ?></option>
<option value="2" <?php selected((int) ($meta['pay_limit'] ?? 0), 2); ?>><?php esc_html_e('仅 VIP2', 'aksrt'); ?></option>
</select>
<p class="aksrt-pmb__hint"><?php esc_html_e('对齐 legacy pay_limit:限制购买人群(会员专属资源)。', 'aksrt'); ?></p>
</div>
</div>
private function textarea(string $id, string $title, string $value, int $rows, string $description = ''): void {
?>
<div class="aksrt-pay-meta__field"><div class="aksrt-pay-meta__title"><label for="aksrt-pay-<?php echo esc_attr($id); ?>"><?php echo esc_html($title); ?></label></div><div class="aksrt-pay-meta__control"><textarea id="aksrt-pay-<?php echo esc_attr($id); ?>" rows="<?php echo esc_attr((string) $rows); ?>" name="aksrt_pay[<?php echo esc_attr($id); ?>]"><?php echo esc_textarea($value); ?></textarea><?php if ($description !== '') : ?><p class="description"><?php echo esc_html($description); ?></p><?php endif; ?></div></div>
<?php
}
<div class="aksrt-pmb__row">
<label><?php esc_html_e('VIP 专属价', 'aksrt'); ?></label>
<div>
<div class="aksrt-pmb__vip-row">
<div>
<label for="aksrt_pay_vip1_price"><?php esc_html_e('VIP1 现金价', 'aksrt'); ?></label>
<input type="number" id="aksrt_pay_vip1_price" name="aksrt_pay_vip1_price" value="<?php echo esc_attr((string) ($meta['vip1_price'] ?? '')); ?>" min="0" step="0.01" placeholder="0">
</div>
<div>
<label for="aksrt_pay_vip2_price"><?php esc_html_e('VIP2 现金价', 'aksrt'); ?></label>
<input type="number" id="aksrt_pay_vip2_price" name="aksrt_pay_vip2_price" value="<?php echo esc_attr((string) ($meta['vip2_price'] ?? '')); ?>" min="0" step="0.01" placeholder="0">
</div>
<div>
<label for="aksrt_pay_vip1_points"><?php esc_html_e('VIP1 积分价', 'aksrt'); ?></label>
<input type="number" id="aksrt_pay_vip1_points" name="aksrt_pay_vip1_points" value="<?php echo esc_attr((string) ($meta['vip1_points'] ?? '')); ?>" min="0" step="1" placeholder="0">
</div>
<div>
<label for="aksrt_pay_vip2_points"><?php esc_html_e('VIP2 积分价', 'aksrt'); ?></label>
<input type="number" id="aksrt_pay_vip2_points" name="aksrt_pay_vip2_points" value="<?php echo esc_attr((string) ($meta['vip2_points'] ?? '')); ?>" min="0" step="1" placeholder="0">
</div>
</div>
<p class="aksrt-pmb__hint"><?php esc_html_e('VIP 用户专属价。留空或 0 表示不启用该档位优惠。', 'aksrt'); ?></p>
</div>
</div>
</div>
private function upload(string $id, string $title, string $value, string $library, string $description = ''): void {
?>
<div class="aksrt-pay-meta__field"><div class="aksrt-pay-meta__title"><label for="aksrt-pay-<?php echo esc_attr($id); ?>"><?php echo esc_html($title); ?></label></div><div class="aksrt-pay-meta__control"><div class="aksrt-pay-meta__upload"><input id="aksrt-pay-<?php echo esc_attr($id); ?>" type="url" name="aksrt_pay[<?php echo esc_attr($id); ?>]" value="<?php echo esc_attr($value); ?>" data-media-input><button type="button" class="button" data-media-select data-library="<?php echo esc_attr($library); ?>"><?php esc_html_e('选择', 'aksrt'); ?></button><button type="button" class="button-link-delete" data-media-clear><?php esc_html_e('移除', 'aksrt'); ?></button></div><?php if ($description !== '') : ?><p class="description"><?php echo esc_html($description); ?></p><?php endif; ?></div></div>
<?php
}
<!-- 资源信息(download/gallery/video 类型显示) -->
<div class="aksrt-pmb__section" id="aksrt-pmb-resource-fields">
<div class="aksrt-pmb__section-title"><?php esc_html_e('资源信息', 'aksrt'); ?></div>
/** @param array<int, array<string, mixed>> $rows */
private function group(string $id, string $title, array $rows, string $renderer, string $button, string $description = ''): void {
?>
<div class="aksrt-pay-meta__field"><div class="aksrt-pay-meta__title"><?php echo esc_html($title); ?></div><div class="aksrt-pay-meta__control"><?php if ($description !== '') : ?><p class="description"><?php echo esc_html($description); ?></p><?php endif; ?><div data-group-list="<?php echo esc_attr($id); ?>"><?php foreach ($rows as $index => $row) { $this->{$renderer}((int) $index, (array) $row); } ?></div><button type="button" class="button" data-group-add="<?php echo esc_attr($id); ?>"><?php echo esc_html($button); ?></button><template data-group-template="<?php echo esc_attr($id); ?>"><?php $this->{$renderer}('__INDEX__', []); ?></template></div></div>
<?php
}
<div class="aksrt-pmb__row">
<label for="aksrt_pay_version"><?php esc_html_e('版本', 'aksrt'); ?></label>
<input type="text" id="aksrt_pay_version" name="aksrt_pay_version" value="<?php echo esc_attr((string) $meta['version']); ?>" placeholder="v1.0.0">
</div>
/** @param int|string $index */
private function video_row($index, array $row): void {
?>
<div class="aksrt-pay-meta__group" data-group-row><input type="text" name="aksrt_pay[video_episode][<?php echo esc_attr((string) $index); ?>][title]" value="<?php echo esc_attr((string) ($row['title'] ?? '')); ?>" placeholder="<?php esc_attr_e('剧集标题', 'aksrt'); ?>"><div class="aksrt-pay-meta__upload"><input type="url" name="aksrt_pay[video_episode][<?php echo esc_attr((string) $index); ?>][url]" value="<?php echo esc_attr((string) ($row['url'] ?? '')); ?>" placeholder="<?php esc_attr_e('选择视频或填写视频地址', 'aksrt'); ?>" data-media-input><button type="button" class="button" data-media-select data-library="video"><?php esc_html_e('选择', 'aksrt'); ?></button></div><button type="button" class="button-link-delete" data-group-remove><?php esc_html_e('移除', 'aksrt'); ?></button></div>
<?php
}
<div class="aksrt-pmb__row">
<label for="aksrt_pay_size"><?php esc_html_e('文件大小', 'aksrt'); ?></label>
<input type="text" id="aksrt_pay_size" name="aksrt_pay_size" value="<?php echo esc_attr((string) $meta['size']); ?>" placeholder="210 MB">
</div>
/** @param int|string $index */
private function download_row($index, array $row): void {
?>
<div class="aksrt-pay-meta__group" data-group-row><div class="aksrt-pay-meta__upload"><input type="url" name="aksrt_pay[downloads][<?php echo esc_attr((string) $index); ?>][url]" value="<?php echo esc_attr((string) ($row['url'] ?? '')); ?>" placeholder="<?php esc_attr_e('上传文件或输入下载地址', 'aksrt'); ?>" data-media-input><button type="button" class="button" data-media-select data-library=""><?php esc_html_e('选择', 'aksrt'); ?></button></div><input type="text" name="aksrt_pay[downloads][<?php echo esc_attr((string) $index); ?>][label]" value="<?php echo esc_attr((string) ($row['label'] ?? '')); ?>" placeholder="<?php esc_attr_e('按钮文案', 'aksrt'); ?>"><textarea rows="1" name="aksrt_pay[downloads][<?php echo esc_attr((string) $index); ?>][extract_code]" placeholder="<?php esc_attr_e('资源备注、提取码或解压密码', 'aksrt'); ?>"><?php echo esc_textarea((string) ($row['extract_code'] ?? '')); ?></textarea><button type="button" class="button-link-delete" data-group-remove><?php esc_html_e('移除', 'aksrt'); ?></button></div>
<?php
}
<div class="aksrt-pmb__row">
<label for="aksrt_pay_compat"><?php esc_html_e('兼容性', 'aksrt'); ?></label>
<input type="text" id="aksrt_pay_compat" name="aksrt_pay_compat" value="<?php echo esc_attr((string) $meta['compat']); ?>" placeholder="WordPress 6.4+">
</div>
/** @param int|string $index */
private function attribute_row($index, array $row): void {
?>
<div class="aksrt-pay-meta__group aksrt-pay-meta__group--inline" data-group-row><input type="text" name="aksrt_pay[attributes][<?php echo esc_attr((string) $index); ?>][key]" value="<?php echo esc_attr((string) ($row['key'] ?? '')); ?>" placeholder="<?php esc_attr_e('属性名称', 'aksrt'); ?>"><input type="text" name="aksrt_pay[attributes][<?php echo esc_attr((string) $index); ?>][value]" value="<?php echo esc_attr((string) ($row['value'] ?? '')); ?>" placeholder="<?php esc_attr_e('属性内容', 'aksrt'); ?>"><button type="button" class="button-link-delete" data-group-remove><?php esc_html_e('移除', 'aksrt'); ?></button></div>
<?php
}
<div class="aksrt-pmb__row">
<label for="aksrt_pay_license"><?php esc_html_e('授权', 'aksrt'); ?></label>
<input type="text" id="aksrt_pay_license" name="aksrt_pay_license" value="<?php echo esc_attr((string) $meta['license']); ?>" placeholder="MIT / GPL / 自用">
</div>
public function save(int $post_id, \WP_Post $post): void {
if ($post->post_type !== 'post' || (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) || wp_is_post_revision($post_id) || !current_user_can('edit_post', $post_id)) {
return;
}
if (empty($_POST['aksrt_pay_meta_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash((string) $_POST['aksrt_pay_meta_nonce'])), 'aksrt_pay_meta_save')) {
return;
}
<div class="aksrt-pmb__row">
<label for="aksrt_pay_demo_url"><?php esc_html_e('演示地址', 'aksrt'); ?></label>
<input type="url" id="aksrt_pay_demo_url" name="aksrt_pay_demo_url" value="<?php echo esc_attr((string) $meta['demo_url']); ?>" placeholder="https://">
</div>
</div>
$raw = isset($_POST['aksrt_pay']) && is_array($_POST['aksrt_pay']) ? wp_unslash($_POST['aksrt_pay']) : [];
$gallery = [];
$ids = array_values(array_filter(array_map('absint', preg_split('/[\s,]+/', (string) ($raw['gallery_ids'] ?? '')) ?: [])));
foreach (array_slice($ids, 0, 100) as $attachment_id) {
$url = wp_get_attachment_image_url($attachment_id, 'full');
if ($url) {
$gallery[] = $url;
}
}
<!-- 自定义属性 -->
<div class="aksrt-pmb__section">
<div class="aksrt-pmb__section-title"><?php esc_html_e('自定义属性', 'aksrt'); ?></div>
<div class="aksrt-pmb__row">
<label for="aksrt_pay_attributes"><?php esc_html_e('属性列表', 'aksrt'); ?></label>
<div>
<textarea id="aksrt_pay_attributes" name="aksrt_pay_attributes" placeholder='[
{"key": "格式", "value": "PSD"},
{"key": "分辨率", "value": "4K"}
]'><?php echo esc_textarea($attributes_json); ?></textarea>
<p class="aksrt-pmb__hint"><?php esc_html_e('JSON 数组。每项:key(属性名)/ value(属性值)。', 'aksrt'); ?></p>
</div>
</div>
</div>
$meta = DomainModel::sanitize_pay_meta([
'pay_type' => $raw['pay_type'] ?? 'no',
'pay_limit' => $raw['pay_limit'] ?? 0,
'pay_modo' => $raw['pay_modo'] ?? '0',
'order_expired_s' => !empty($raw['order_expired_s']),
'order_expired_time' => $raw['order_expired_time'] ?? 0,
'order_expired_unit' => $raw['order_expired_unit'] ?? 'day',
'price' => (string) ($raw['price'] ?? '0'),
'points_price' => $raw['points_price'] ?? 0,
'original_price' => (string) ($raw['original_price'] ?? '0'),
'promotion_tag' => (string) ($raw['promotion_tag'] ?? ''),
'vip1_price' => (string) ($raw['vip1_price'] ?? '0'),
'vip2_price' => (string) ($raw['vip2_price'] ?? '0'),
'vip1_points' => $raw['vip1_points'] ?? 0,
'vip2_points' => $raw['vip2_points'] ?? 0,
'pay_count' => $raw['pay_count'] ?? 0,
'gallery' => $gallery,
'gallery_show' => $raw['gallery_show'] ?? 1,
'video_url' => (string) ($raw['video_url'] ?? ''),
'video_pic' => (string) ($raw['video_pic'] ?? ''),
'video_title' => (string) ($raw['video_title'] ?? ''),
'video_episode' => $raw['video_episode'] ?? [],
'video_scale_height' => $raw['video_scale_height'] ?? 0,
'downloads' => $raw['downloads'] ?? [],
'attributes' => $raw['attributes'] ?? [],
'demo_url' => (string) ($raw['demo_url'] ?? ''),
'pay_title' => (string) ($raw['pay_title'] ?? ''),
'pay_doc' => (string) ($raw['pay_doc'] ?? ''),
'pay_details' => (string) ($raw['pay_details'] ?? ''),
'pay_extra_hide' => (string) ($raw['pay_extra_hide'] ?? ''),
]);
<!-- 下载源(download 类型显示) -->
<div class="aksrt-pmb__section" id="aksrt-pmb-download-fields">
<div class="aksrt-pmb__section-title"><?php esc_html_e('下载源', 'aksrt'); ?></div>
<div class="aksrt-pmb__row">
<label for="aksrt_pay_downloads"><?php esc_html_e('下载链接', 'aksrt'); ?></label>
<div>
<textarea id="aksrt_pay_downloads" name="aksrt_pay_downloads" placeholder='[
{
"label": "百度网盘",
"url": "https://pan.baidu.com/s/xxx",
"type": "net_disk",
"extract_code": "abcd"
}
]'><?php echo esc_textarea($downloads_json); ?></textarea>
<div class="aksrt-pmb__error" id="aksrt-pmb-dl-err"></div>
<div class="aksrt-pmb__tools">
<button type="button" class="button button-small" id="aksrt-pmb-add-dl"><?php esc_html_e('追加示例条目', 'aksrt'); ?></button>
<button type="button" class="button button-small" id="aksrt-pmb-fmt-dl"><?php esc_html_e('格式化 JSON', 'aksrt'); ?></button>
</div>
<p class="aksrt-pmb__hint"><?php esc_html_e('JSON 数组。每项:label / url / type(direct | net_disk | external)/ extract_code。', 'aksrt'); ?></p>
</div>
</div>
</div>
<!-- 图库 URL(gallery 类型显示) -->
<div class="aksrt-pmb__section" id="aksrt-pmb-gallery-fields" style="display:none;">
<div class="aksrt-pmb__section-title"><?php esc_html_e('付费图库', 'aksrt'); ?></div>
<div class="aksrt-pmb__row">
<label for="aksrt_pay_gallery"><?php esc_html_e('图片 URL 列表', 'aksrt'); ?></label>
<div>
<textarea id="aksrt_pay_gallery" name="aksrt_pay_gallery" placeholder='[
"https://example.com/img1.jpg",
"https://example.com/img2.jpg"
]'><?php echo esc_textarea($gallery_json); ?></textarea>
<p class="aksrt-pmb__hint"><?php esc_html_e('JSON 数组。每项为图片 URL。付费前图片将被模糊遮罩。', 'aksrt'); ?></p>
</div>
</div>
</div>
<!-- 视频 URL(video 类型显示) -->
<div class="aksrt-pmb__section" id="aksrt-pmb-video-fields" style="display:none;">
<div class="aksrt-pmb__section-title"><?php esc_html_e('付费视频', 'aksrt'); ?></div>
<div class="aksrt-pmb__row">
<label for="aksrt_pay_video_url"><?php esc_html_e('视频 URL', 'aksrt'); ?></label>
<div>
<input type="url" id="aksrt_pay_video_url" name="aksrt_pay_video_url" value="<?php echo esc_attr((string) ($meta['video_url'] ?? '')); ?>" placeholder="https://example.com/video.mp4">
<p class="aksrt-pmb__hint"><?php esc_html_e('支持 MP4 直链或视频平台嵌入链接。', 'aksrt'); ?></p>
</div>
</div>
</div>
<!-- 选项覆盖 -->
<div class="aksrt-pmb__section">
<div class="aksrt-pmb__section-title"><?php esc_html_e('选项覆盖', 'aksrt'); ?></div>
<div class="aksrt-pmb__row">
<label><?php esc_html_e('单条覆盖', 'aksrt'); ?></label>
<div>
<div style="display:flex; align-items:center; gap:6px; margin-bottom:6px;">
<input type="checkbox" id="aksrt_pay_require_login" name="aksrt_pay_require_login" value="1" <?php checked($meta['require_login']); ?>>
<label for="aksrt_pay_require_login" style="font-weight:400;padding:0;"><?php esc_html_e('强制登录后才可访问', 'aksrt'); ?></label>
</div>
<div style="display:flex; align-items:center; gap:6px;">
<input type="checkbox" id="aksrt_pay_require_captcha" name="aksrt_pay_require_captcha" value="1" <?php checked($meta['require_captcha']); ?>>
<label for="aksrt_pay_require_captcha" style="font-weight:400;padding:0;"><?php esc_html_e('强制需要人机验证', 'aksrt'); ?></label>
</div>
<p class="aksrt-pmb__hint"><?php esc_html_e('全局开关在后台统一控制,这里仅作为单条强制覆盖。', 'aksrt'); ?></p>
</div>
</div>
</div>
<!-- §3.8 电商增强 -->
<div class="aksrt-pmb__section">
<div class="aksrt-pmb__section-title"><?php esc_html_e('电商增强', 'aksrt'); ?></div>
<div class="aksrt-pmb__row">
<label for="aksrt_pay_virtual_sales"><?php esc_html_e('虚拟销量', 'aksrt'); ?></label>
<div>
<input type="number" id="aksrt_pay_virtual_sales" name="aksrt_pay_virtual_sales" value="<?php echo esc_attr((string) ($meta['virtual_sales'] ?? 0)); ?>" min="0" step="1">
<p class="aksrt-pmb__hint"><?php esc_html_e('在前端展示的销量基础上叠加此数值,用于展示效果。', 'aksrt'); ?></p>
</div>
</div>
<div class="aksrt-pmb__row">
<label for="aksrt_pay_limit_over_price"><?php esc_html_e('超额下载补价', 'aksrt'); ?></label>
<div>
<input type="number" id="aksrt_pay_limit_over_price" name="aksrt_pay_limit_over_price" value="<?php echo esc_attr((string) ($meta['download_limit_over_price'] ?? 0)); ?>" min="0" step="0.01">
<p class="aksrt-pmb__hint"><?php esc_html_e('免费日限额用尽后允许购买的补价(现金/积分随支付方式)。0 = 不允许超额购买。对齐 legacy download_limit_over_price。', 'aksrt'); ?></p>
</div>
</div>
<div class="aksrt-pmb__row">
<label><?php esc_html_e('优惠码', 'aksrt'); ?></label>
<div>
<div style="display:flex; align-items:center; gap:6px; margin-bottom:8px;">
<input type="checkbox" id="aksrt_pay_coupon_s" name="aksrt_pay_coupon_s" value="1" <?php checked(!empty($meta['coupon_s'])); ?>>
<label for="aksrt_pay_coupon_s" style="font-weight:400;padding:0;"><?php esc_html_e('允许使用优惠码(全站开关关闭时仍可对本篇启用)', 'aksrt'); ?></label>
</div>
<textarea id="aksrt_pay_coupon_desc" name="aksrt_pay_coupon_desc" style="min-height:60px;" placeholder="<?php esc_attr_e('本篇优惠码说明,可留空', 'aksrt'); ?>"><?php echo esc_textarea((string) ($meta['coupon_desc'] ?? '')); ?></textarea>
<p class="aksrt-pmb__hint"><?php esc_html_e('对齐 legacy coupon_s / coupon_desc。积分支付不使用优惠码。', 'aksrt'); ?></p>
</div>
</div>
<div class="aksrt-pmb__row">
<label for="aksrt_pay_rebate_discount"><?php esc_html_e('推广优惠金额', 'aksrt'); ?></label>
<div>
<input type="number" id="aksrt_pay_rebate_discount" name="aksrt_pay_rebate_discount" value="<?php echo esc_attr((string) ($meta['pay_rebate_discount'] ?? 0)); ?>" min="0" step="0.01">
<p class="aksrt-pmb__hint"><?php esc_html_e('有有效推荐人且开启推广返佣时,现金购买可先减此金额(买家侧推广折扣,对齐 legacy pay_rebate_discount)。0 = 不启用。', 'aksrt'); ?></p>
</div>
</div>
<div class="aksrt-pmb__row">
<label for="aksrt_pay_stock"><?php esc_html_e('库存', 'aksrt'); ?></label>
<div>
<input type="number" id="aksrt_pay_stock" name="aksrt_pay_stock" value="<?php echo esc_attr((string) ($meta['stock'] ?? -1)); ?>" min="-1" step="1">
<p class="aksrt-pmb__hint"><?php esc_html_e('-1 表示无限库存;>0 表示限量,售完即止。', 'aksrt'); ?></p>
</div>
</div>
<div class="aksrt-pmb__row">
<label for="aksrt_pay_purchase_limit"><?php esc_html_e('限购数量', 'aksrt'); ?></label>
<div>
<input type="number" id="aksrt_pay_purchase_limit" name="aksrt_pay_purchase_limit" value="<?php echo esc_attr((string) ($meta['purchase_limit'] ?? 0)); ?>" min="0" step="1">
<p class="aksrt-pmb__hint"><?php esc_html_e('每用户最多可购买次数,0 表示不限购。', 'aksrt'); ?></p>
</div>
</div>
<div class="aksrt-pmb__row">
<label for="aksrt_pay_card_keys"><?php esc_html_e('自动发货卡密', 'aksrt'); ?></label>
<div>
<textarea id="aksrt_pay_card_keys" name="aksrt_pay_card_keys" placeholder="<?php esc_attr_e('每行一个卡密/激活码,购买后自动发放一个', 'aksrt'); ?>"><?php echo esc_textarea(implode("\n", $meta['card_keys'] ?? [])); ?></textarea>
<p class="aksrt-pmb__hint"><?php esc_html_e('每行一个卡密,购买后自动消耗一个并发给用户。留空则不自动发货。', 'aksrt'); ?></p>
</div>
</div>
<div class="aksrt-pmb__row">
<label for="aksrt_pay_free_preview"><?php esc_html_e('免费试读段落', 'aksrt'); ?></label>
<div>
<textarea id="aksrt_pay_free_preview" name="aksrt_pay_free_preview" style="min-height:80px;" placeholder="<?php esc_attr_e('付费内容的免费预览段落(支持 HTML)', 'aksrt'); ?>"><?php echo esc_textarea($meta['free_preview'] ?? ''); ?></textarea>
<p class="aksrt-pmb__hint"><?php esc_html_e('未付费用户可见的预览内容,支持基础 HTML。留空则不显示预览。', 'aksrt'); ?></p>
</div>
</div>
<div class="aksrt-pmb__row">
<label><?php esc_html_e('销量显示', 'aksrt'); ?></label>
<div style="display:flex; align-items:center; gap:6px;">
<input type="checkbox" id="aksrt_pay_show_sales" name="aksrt_pay_show_sales" value="1" <?php checked($meta['show_sales'] ?? true); ?>>
<label for="aksrt_pay_show_sales" style="font-weight:400;padding:0;"><?php esc_html_e('在前端显示销量', 'aksrt'); ?></label>
</div>
</div>
</div>
</div>
<script>
(function () {
var typeSelect = document.getElementById('aksrt_pay_type');
var fields = document.getElementById('aksrt-pmb-fields');
var dlFields = document.getElementById('aksrt-pmb-download-fields');
var galleryFields = document.getElementById('aksrt-pmb-gallery-fields');
var videoFields = document.getElementById('aksrt-pmb-video-fields');
var resourceFields = document.getElementById('aksrt-pmb-resource-fields');
function updateVisibility() {
var t = typeSelect.value;
var isPaid = t !== 'no';
fields.setAttribute('data-disabled', isPaid ? '0' : '1');
var readingHint = document.getElementById('aksrt-pmb-reading-hint');
if (readingHint) readingHint.hidden = t !== 'reading';
// 根据付费类型显示/隐藏相关字段
if (dlFields) dlFields.style.display = (t === 'download') ? '' : 'none';
if (galleryFields) galleryFields.style.display = (t === 'gallery') ? '' : 'none';
if (videoFields) videoFields.style.display = (t === 'video') ? '' : 'none';
if (resourceFields) resourceFields.style.display = (t === 'download' || t === 'gallery' || t === 'video') ? '' : 'none';
}
if (typeSelect) {
typeSelect.addEventListener('change', updateVisibility);
updateVisibility();
}
// 下载源 JSON 工具
var dlTa = document.getElementById('aksrt_pay_downloads');
var dlErr = document.getElementById('aksrt-pmb-dl-err');
var addDlBtn = document.getElementById('aksrt-pmb-add-dl');
var fmtDlBtn = document.getElementById('aksrt-pmb-fmt-dl');
if (addDlBtn && dlTa) {
addDlBtn.addEventListener('click', function () {
var sample = {label: '百度网盘', url: 'https://pan.baidu.com/s/xxx', type: 'net_disk', extract_code: ''};
try {
var arr = dlTa.value.trim() ? JSON.parse(dlTa.value) : [];
arr.push(sample);
dlTa.value = JSON.stringify(arr, null, 2);
if (dlErr) dlErr.style.display = 'none';
} catch (e) {
if (dlErr) { dlErr.textContent = 'JSON 解析错误: ' + e.message; dlErr.style.display = 'block'; }
}
});
}
if (fmtDlBtn && dlTa) {
fmtDlBtn.addEventListener('click', function () {
try {
var arr = JSON.parse(dlTa.value);
dlTa.value = JSON.stringify(arr, null, 2);
if (dlErr) dlErr.style.display = 'none';
} catch (e) {
if (dlErr) { dlErr.textContent = 'JSON 解析错误: ' + e.message; dlErr.style.display = 'block'; }
}
});
}
})();
</script>
<?php
}
public function save(int $post_id, \WP_Post $post): void {
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return;
if (wp_is_post_revision($post_id)) return;
if ($post->post_type !== 'post') return;
if (empty($_POST['aksrt_pay_meta_nonce'])
|| !wp_verify_nonce((string) $_POST['aksrt_pay_meta_nonce'], 'aksrt_pay_meta_save')) {
return;
}
if (!current_user_can('edit_post', $post_id)) return;
// 付费类型
$allowed_types = array_keys(aksrt_get_pay_types());
$pay_type = sanitize_key((string) ($_POST['aksrt_pay_type'] ?? 'no'));
if (!in_array($pay_type, $allowed_types, true)) {
$pay_type = 'no';
}
// 价格字段
$pay_modo = sanitize_key((string) ($_POST['aksrt_pay_modo'] ?? '0'));
if ($pay_modo !== 'points') {
$pay_modo = '0';
}
$price = trim((string) wp_unslash($_POST['aksrt_pay_price'] ?? '0'));
$points_price = max(0, (int) ($_POST['aksrt_pay_points_price'] ?? 0));
$original_price = trim((string) wp_unslash($_POST['aksrt_pay_original_price'] ?? '0'));
$vip1_price = trim((string) wp_unslash($_POST['aksrt_pay_vip1_price'] ?? '0'));
$vip2_price = trim((string) wp_unslash($_POST['aksrt_pay_vip2_price'] ?? '0'));
$vip1_points = max(0, (int) ($_POST['aksrt_pay_vip1_points'] ?? 0));
$vip2_points = max(0, (int) ($_POST['aksrt_pay_vip2_points'] ?? 0));
$pay_limit = (int) ($_POST['aksrt_pay_limit'] ?? 0);
if (!in_array($pay_limit, [0, 1, 2], true)) {
$pay_limit = 0;
}
// 资源信息
$version = sanitize_text_field((string) ($_POST['aksrt_pay_version'] ?? ''));
$size = sanitize_text_field((string) ($_POST['aksrt_pay_size'] ?? ''));
$compat = sanitize_text_field((string) ($_POST['aksrt_pay_compat'] ?? ''));
$license = sanitize_text_field((string) ($_POST['aksrt_pay_license'] ?? ''));
$demo_url = esc_url_raw((string) ($_POST['aksrt_pay_demo_url'] ?? ''));
$video_url = esc_url_raw((string) ($_POST['aksrt_pay_video_url'] ?? ''));
// 布尔字段
$require_login = !empty($_POST['aksrt_pay_require_login']);
$require_captcha = !empty($_POST['aksrt_pay_require_captcha']);
// JSON 字段
$downloads = $this->parse_json_array($_POST['aksrt_pay_downloads'] ?? '');
$attributes = $this->parse_json_array($_POST['aksrt_pay_attributes'] ?? '');
$gallery = $this->parse_json_array($_POST['aksrt_pay_gallery'] ?? '');
// 清洗下载源
if ($downloads !== null && class_exists('AKSRT\\Core\\Setup') && method_exists('AKSRT\\Core\\Setup', 'sanitize_downloads_meta')) {
$downloads = Setup::sanitize_downloads_meta($downloads);
}
// 保存统一元数据
$pay_meta = DomainModel::sanitize_pay_meta([
'pay_type' => $pay_type,
'pay_modo' => $pay_modo,
'price' => $price,
'points_price' => $points_price,
'original_price' => $original_price,
'vip1_price' => $vip1_price,
'vip2_price' => $vip2_price,
'vip1_points' => $vip1_points,
'vip2_points' => $vip2_points,
'pay_limit' => $pay_limit,
'version' => $version,
'size' => $size,
'compat' => $compat,
'license' => $license,
'demo_url' => $demo_url,
'video_url' => $video_url,
'downloads' => $downloads ?? [],
'attributes' => $attributes ?? [],
'gallery' => $gallery ?? [],
'require_login' => $require_login,
'require_captcha' => $require_captcha,
// §3.8 电商增强字段
'virtual_sales' => $_POST['aksrt_pay_virtual_sales'] ?? 0,
'stock' => $_POST['aksrt_pay_stock'] ?? -1,
'purchase_limit' => $_POST['aksrt_pay_purchase_limit'] ?? 0,
'card_keys' => isset($_POST['aksrt_pay_card_keys']) ? array_filter(array_map('trim', explode("\n", (string) $_POST['aksrt_pay_card_keys']))) : [],
'free_preview' => $_POST['aksrt_pay_free_preview'] ?? '',
'show_sales' => !empty($_POST['aksrt_pay_show_sales']),
'download_limit_over_price' => trim((string) wp_unslash($_POST['aksrt_pay_limit_over_price'] ?? '0')),
'coupon_s' => !empty($_POST['aksrt_pay_coupon_s']),
'coupon_desc' => (string) ($_POST['aksrt_pay_coupon_desc'] ?? ''),
'pay_rebate_discount' => trim((string) wp_unslash($_POST['aksrt_pay_rebate_discount'] ?? '0')),
]);
update_post_meta($post_id, DomainModel::PAY_META_KEY, $pay_meta);
// 同步标量字段用于查询(模仿 legacy 的 aksrtpay_type 同步)
update_post_meta($post_id, DomainModel::PAY_TYPE_KEY, (string) $pay_meta['pay_type']);
$query_price = ($pay_meta['pay_modo'] === 'points')
? (int) $pay_meta['points_price']
: (string) $pay_meta['price'];
update_post_meta($post_id, DomainModel::PAY_PRICE_KEY, $query_price);
update_post_meta($post_id, '_aksrt_pay_modo', (string) $pay_meta['pay_modo']);
update_post_meta($post_id, '_aksrt_pay_limit', (int) $pay_meta['pay_limit']);
// 清理旧的 _aksrt_is_resource 标记
update_post_meta(
$post_id,
DomainModel::RESOURCE_MARKER_KEY,
DomainModel::is_resource_pay_type((string) $pay_meta['pay_type']) ? 1 : 0
);
}
/**
* 安全解析 JSON 数组。
*/
private function parse_json_array(string $raw): ?array {
$raw = trim($raw);
if ($raw === '') return [];
$decoded = json_decode($raw, true);
return is_array($decoded) ? $decoded : null;
}
update_post_meta($post_id, DomainModel::PAY_META_KEY, $meta);
update_post_meta($post_id, DomainModel::PAY_TYPE_KEY, (string) $meta['pay_type']);
update_post_meta($post_id, DomainModel::PAY_PRICE_KEY, $meta['pay_modo'] === 'points' ? (int) $meta['points_price'] : (string) $meta['price']);
update_post_meta($post_id, '_aksrt_pay_modo', (string) $meta['pay_modo']);
update_post_meta($post_id, '_aksrt_pay_limit', (int) $meta['pay_limit']);
update_post_meta($post_id, DomainModel::RESOURCE_MARKER_KEY, DomainModel::is_resource_pay_type((string) $meta['pay_type']) ? 1 : 0);
}
}
+40 -18
View File
@@ -38,7 +38,7 @@ class SEOMetaBox {
foreach (['post', 'page'] as $screen) {
add_meta_box(
'aksrt_seo_meta',
__('独立 SEO', 'aksrt'),
__('独立SEO', 'aksrt'),
[$this, 'render_meta_box'],
$screen,
'normal',
@@ -57,20 +57,35 @@ class SEOMetaBox {
$ai_btn = $this->render_ai_button('post', (int) $post->ID);
$summary_btn = $this->render_summary_button((int) $post->ID);
?>
<div class="aksrt-seo-meta-box">
<p>
<label for="aksrt_seo_title"><strong><?php esc_html_e('SEO 标题', 'aksrt'); ?></strong></label>
<input type="text" class="widefat" id="aksrt_seo_title" name="aksrt_seo_title" value="<?php echo esc_attr($title); ?>" />
</p>
<p>
<label for="aksrt_seo_keywords"><strong><?php esc_html_e('SEO 关键词', 'aksrt'); ?></strong></label>
<input type="text" class="widefat" id="aksrt_seo_keywords" name="aksrt_seo_keywords" value="<?php echo esc_attr($keywords); ?>" placeholder="<?php esc_attr_e('多个关键词用英文逗号分隔', 'aksrt'); ?>" />
</p>
<p>
<label for="aksrt_seo_description"><strong><?php esc_html_e('SEO 描述', 'aksrt'); ?></strong></label>
<textarea class="widefat" rows="3" id="aksrt_seo_description" name="aksrt_seo_description"><?php echo esc_textarea($description); ?></textarea>
<span class="description"><?php esc_html_e('建议 50–150 字。', 'aksrt'); ?><?php echo $ai_btn; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span>
</p>
<div class="aksrt-seo-meta-box">
<div class="notice notice-warning inline"><p><?php esc_html_e('系统默认调用标题、分类标签、描述等自动生成SEO内容,一般无需单独设置SEO内容;如需覆盖,请在下方自定义。', 'aksrt'); ?></p></div>
<div class="aksrt-seo-preview">
<strong><?php esc_html_e('SEO预览', 'aksrt'); ?></strong>
<div class="aksrt-seo-preview__title"><?php echo esc_html($title !== '' ? $title : get_the_title($post)); ?></div>
<div class="aksrt-seo-preview__url"><?php echo esc_html(get_permalink($post)); ?></div>
<div class="aksrt-seo-preview__description"><?php echo esc_html($description !== '' ? $description : wp_trim_words(wp_strip_all_tags((string) $post->post_content), 38, '…')); ?></div>
</div>
<p>
<label for="aksrt_seo_title"><strong><?php esc_html_e('自定义SEO标题', 'aksrt'); ?></strong></label>
<input type="text" class="widefat" id="aksrt_seo_title" name="aksrt_seo_title" value="<?php echo esc_attr($title); ?>" />
<span class="description"><?php esc_html_e('Title 一般建议15到30个字符', 'aksrt'); ?></span>
</p>
<p>
<label for="aksrt_seo_keywords"><strong><?php esc_html_e('自定义SEO关键词', 'aksrt'); ?></strong></label>
<input type="text" class="widefat" id="aksrt_seo_keywords" name="aksrt_seo_keywords" value="<?php echo esc_attr($keywords); ?>" placeholder="<?php esc_attr_e('多个关键词用英文逗号分隔', 'aksrt'); ?>" />
<span class="description"><?php esc_html_e('Keywords 每个关键词用逗号隔开', 'aksrt'); ?></span>
</p>
<p>
<label for="aksrt_seo_description"><strong><?php esc_html_e('自定义SEO描述', 'aksrt'); ?></strong></label>
<textarea class="widefat" rows="3" id="aksrt_seo_description" name="aksrt_seo_description"><?php echo esc_textarea($description); ?></textarea>
<span class="description"><?php esc_html_e('Description 一般建议50到150个字符', 'aksrt'); ?><?php echo $ai_btn; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span>
</p>
<details class="aksrt-seo-advice"><summary><span class="dashicons dashicons-star-filled"></span> <?php esc_html_e('SEO优化建议', 'aksrt'); ?></summary><div>
<p><strong><?php esc_html_e('SEO标题:', 'aksrt'); ?></strong><?php esc_html_e('标题应紧扣主要内容,重要关键词靠前,避免关键词堆砌。', 'aksrt'); ?></p>
<p><strong><?php esc_html_e('SEO关键词:', 'aksrt'); ?></strong><?php esc_html_e('一般建议4到8个,并与网站定位及正文保持一致。', 'aksrt'); ?></p>
<p><strong><?php esc_html_e('SEO描述:', 'aksrt'); ?></strong><?php esc_html_e('使用一段通顺、有吸引力的文字精炼概括页面内容。', 'aksrt'); ?></p>
<p class="description"><?php esc_html_e('优化建议来自互联网,仅供参考。', 'aksrt'); ?></p>
</div></details>
<?php if ($summary_btn !== '') : ?>
<p class="aksrt-ai-summary-row">
<span class="description"><?php esc_html_e('写作辅助:根据正文生成摘要并填入「摘要」字段(不自动保存)。', 'aksrt'); ?></span>
@@ -220,15 +235,22 @@ class SEOMetaBox {
],
]);
wp_add_inline_style('wp-admin', '
wp_add_inline_style('wp-admin', '
.aksrt-ai-seo-button{display:inline-block;margin-left:8px;vertical-align:middle}
.aksrt-ai-seo-generate,.aksrt-ai-summary-generate{cursor:pointer}
.aksrt-ai-seo-generate.is-loading,.aksrt-ai-summary-generate.is-loading{opacity:.6;pointer-events:none}
.aksrt-ai-seo-msg{display:block;margin-top:6px;font-size:12px}
.aksrt-ai-seo-msg.is-error{color:#b32d2e}
.aksrt-ai-seo-msg.is-ok{color:#007017}
.aksrt-ai-summary-row{margin-top:8px;padding-top:8px;border-top:1px solid #eee}
');
.aksrt-ai-summary-row{margin-top:8px;padding-top:8px;border-top:1px solid #eee}
.aksrt-seo-preview{margin:12px 0;padding:14px;border:1px solid #e5e5e5;border-radius:4px;background:#fff}
.aksrt-seo-preview__title{margin-top:8px;color:#1a0dab;font-size:18px;line-height:1.3}
.aksrt-seo-preview__url{margin:4px 0;color:#188038;font-size:13px;word-break:break-all}
.aksrt-seo-preview__description{color:#4d5156;line-height:1.5}
.aksrt-seo-advice{margin-top:12px;border:1px solid #e5e5e5;border-radius:4px}
.aksrt-seo-advice summary{padding:10px 12px;cursor:pointer;font-weight:600}
.aksrt-seo-advice>div{padding:0 12px 10px}
');
}
private function render_ai_button(string $type, int $id): string {