diff --git a/plugins/neatstudio.membership/src/ServiceProvider.php b/plugins/neatstudio.membership/src/ServiceProvider.php index c3f75ea..36f409d 100644 --- a/plugins/neatstudio.membership/src/ServiceProvider.php +++ b/plugins/neatstudio.membership/src/ServiceProvider.php @@ -53,15 +53,24 @@ class ServiceProvider extends PluginServiceProvider } }, 10); - // 付费文章:[paid]...[/paid] 对无权限访客隐藏为 teaser;整篇锁定渲染 paywall + // 付费文章:[paid]...[/paid] —— 无权限访客替换为 teaser;有权限替换为付费内容块(避免原始标签裸露在页面) $manager->addFilter('post.rendered', function (string $html, \App\Models\Post $post) { $service = app(Services\MembershipService::class); $user = auth()->user(); - if (preg_match('/\[paid\](.*?)\[\/paid\]/s', $html) && ! $service->canReadPaidContent($user, $post)) { + if (preg_match('/\[paid\](.*?)\[\/paid\]/s', $html)) { + // Markdown 转换会给短代码包上

,先去掉,避免替换后残留未闭合标签 + $html = preg_replace('/

\s*\[(\/?paid)\]\s*<\/p>/i', '[$1]', $html); + $html = preg_replace_callback( '/\[paid\](.*?)\[\/paid\]/s', - fn () => view('membership.paid-teaser', ['post' => $post])->render(), + function (array $m) use ($service, $user, $post) { + if ($service->canReadPaidContent($user, $post)) { + return '

'; + } + + return view('membership.paid-teaser', ['post' => $post])->render(); + }, $html ); } diff --git a/tests/Feature/MembershipFlowTest.php b/tests/Feature/MembershipFlowTest.php index 9652426..50db5b5 100644 --- a/tests/Feature/MembershipFlowTest.php +++ b/tests/Feature/MembershipFlowTest.php @@ -87,21 +87,24 @@ class MembershipFlowTest extends TestCase 'published_at' => now(), ]); - // 游客:付费部分被替换为 teaser + // 游客:付费部分被替换为 teaser,且不出现裸 [paid] 标签 $this->get('/posts/paid-post.shtml') ->assertOk() ->assertDontSee('付费隐藏内容') + ->assertDontSee('[paid]') ->assertSee('本文包含付费内容') ->assertSee('登录后查看'); - // 会员:可见 + // 会员:可见,且 [paid] 标签被替换为付费内容块(不裸露) $this->actingAs($this->user)->post('/membership/'.$this->plan->id.'/subscribe'); $payment = Payment::query()->where('user_id', $this->user->id)->first(); $this->actingAs($this->user)->post('/pay/sandbox/'.$payment->order_no.'/confirm'); $this->actingAs($this->user)->get('/posts/paid-post.shtml') ->assertOk() - ->assertSee('付费隐藏内容'); + ->assertSee('付费隐藏内容') + ->assertSee('paid-content') + ->assertDontSee('[paid]'); } public function test_unlock_post_creates_payment_with_payable_and_grants_access(): void diff --git a/themes/demo/assets/style.css b/themes/demo/assets/style.css index c940d26..256fb7f 100644 --- a/themes/demo/assets/style.css +++ b/themes/demo/assets/style.css @@ -61,3 +61,7 @@ a:hover { color: var(--d-accent-dark); } .main-layout { grid-template-columns: 1fr; } .d-header .d-container { flex-direction: column; align-items: flex-start; } } + +/* 已解锁的付费内容块 */ +.paid-content { border: 1px solid var(--d-border); border-left: 4px solid var(--d-accent); background: #fff; border-radius: 8px; padding: 12px 14px; margin: 12px 0; } +.paid-content-label { font-size: 12px; color: var(--d-accent); font-weight: 700; margin-bottom: 6px; } diff --git a/themes/modern/assets/style.css b/themes/modern/assets/style.css index 8bf454b..a67e7b1 100644 --- a/themes/modern/assets/style.css +++ b/themes/modern/assets/style.css @@ -186,3 +186,7 @@ a:hover { opacity: 0.8; } .paywall-actions .btn { margin: 0 6px; } .paid-teaser { border: 1px dashed var(--border); background: var(--bg-muted, #f6f6f6); color: var(--muted); border-radius: 10px; padding: 14px 16px; margin: 14px 0; font-size: 14px; } .paid-teaser .btn { margin-left: 8px; } + +/* 已解锁的付费内容块 */ +.paid-content { border: 1px solid var(--border); border-left: 4px solid var(--accent); background: var(--bg); border-radius: 10px; padding: 14px 16px; margin: 14px 0; } +.paid-content-label { font-size: 12px; color: var(--accent); font-weight: 600; margin-bottom: 6px; } diff --git a/themes/sablog/assets/style.css b/themes/sablog/assets/style.css index 8f2e390..d48ec13 100644 --- a/themes/sablog/assets/style.css +++ b/themes/sablog/assets/style.css @@ -163,3 +163,7 @@ a:hover { color: #f60; text-decoration: underline; } /* 社交账号:横向胶囊列表 */ .social-links { list-style: none; display: flex; flex-wrap: wrap; gap: 6px; margin: 0; padding: 0; } .social-links li { font-size: 12px; background: #f7f9fb; border: 1px solid #d8dfe6; border-radius: 999px; padding: 3px 10px; } + +/* 已解锁的付费内容块 */ +.paid-content { border: 1px dashed #f5c6cb; border-left: 4px solid #f60; background: #fff8f0; border-radius: 6px; padding: 12px 14px; margin: 12px 0; } +.paid-content-label { font-size: 12px; color: #f60; font-weight: 700; margin-bottom: 6px; }