feat(tenant/kol): let KOLs self-subscribe to their own published packages

Add /kol/manage/packages/:id/self-subscription POST/DELETE on the tenant
side so a KOL can grant themselves access to their own published package
without going through the marketplace approval flow. Reject self-subscribe
through the marketplace with a clear error, expose owned_by_current_tenant
on package responses, and surface a self_subscription block on KolPackage
Response. The admin-web KOL workspace gets 订阅自己 / 取消订阅 entries with
matching messaging in the marketplace detail view.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-29 22:06:57 +08:00
parent 088dbb0ec7
commit 4dd8a1bb0e
9 changed files with 472 additions and 77 deletions
+11
View File
@@ -449,6 +449,17 @@ export const kolManageApi = {
{},
);
},
selfSubscribePackage(id: number) {
return apiClient.post<KolPackageSummary, Record<string, never>>(
`/api/tenant/kol/manage/packages/${id}/self-subscription`,
{},
);
},
cancelSelfSubscription(id: number) {
return apiClient.remove<KolPackageSummary>(
`/api/tenant/kol/manage/packages/${id}/self-subscription`,
);
},
deletePackage(id: number) {
return apiClient.remove<null>(`/api/tenant/kol/manage/packages/${id}`);
},
@@ -11,6 +11,8 @@ import {
CloudUploadOutlined,
InboxOutlined,
DeleteOutlined,
CheckCircleOutlined,
StopOutlined,
} from "@ant-design/icons-vue";
import KolPackageFormModal from "@/components/kol/KolPackageFormModal.vue";
@@ -79,6 +81,24 @@ const archivePackageMutation = useMutation({
onError: (error) => message.error(formatError(error)),
});
const selfSubscribePackageMutation = useMutation({
mutationFn: (id: number) => kolManageApi.selfSubscribePackage(id),
onSuccess: () => {
message.success("已订阅自己的订阅包");
invalidateKolPackageAccess();
},
onError: (error) => message.error(formatError(error)),
});
const cancelSelfSubscriptionMutation = useMutation({
mutationFn: (id: number) => kolManageApi.cancelSelfSubscription(id),
onSuccess: () => {
message.success("已取消订阅自己的订阅包");
invalidateKolPackageAccess();
},
onError: (error) => message.error(formatError(error)),
});
const deletePackageMutation = useMutation({
mutationFn: (id: number) => kolManageApi.deletePackage(id),
onSuccess: (_, id) => {
@@ -150,6 +170,12 @@ const deletePromptMutation = useMutation({
},
});
function invalidateKolPackageAccess() {
void queryClient.invalidateQueries({ queryKey: ["kol", "packages"] });
void queryClient.invalidateQueries({ queryKey: ["kol", "my-subscription-prompts"] });
void queryClient.invalidateQueries({ queryKey: ["workspace"] });
}
const packageInitialValue = computed<Partial<CreateKolPackageRequest> | undefined>(() => {
if (!editingPackage.value) return undefined;
return {
@@ -220,6 +246,18 @@ function packageStatusLabel(status: string) {
if (status === "archived") return t("kol.manage.packageStatus.archived");
return t("kol.manage.packageStatus.draft");
}
function isSelfSubscribed(pkg: KolPackageSummary) {
return pkg.self_subscription?.status === "active";
}
function canSelfSubscribe(pkg: KolPackageSummary) {
return pkg.status === "published" && !isSelfSubscribed(pkg);
}
function canCancelSelfSubscription(pkg: KolPackageSummary) {
return pkg.self_subscription?.status === "active";
}
</script>
<template>
@@ -267,6 +305,7 @@ function packageStatusLabel(status: string) {
>
{{ packageStatusLabel(item.status) }}
</a-tag>
<a-tag v-if="isSelfSubscribed(item)" color="blue" size="small">自己已订阅</a-tag>
<span>{{ t("kol.marketplace.prompts", { count: item.prompt_count }) }}</span>
</div>
</div>
@@ -290,6 +329,19 @@ function packageStatusLabel(status: string) {
>
<InboxOutlined /> {{ t('kol.manage.archivePackage') }}
</a-menu-item>
<a-menu-item
v-if="canSelfSubscribe(item)"
@click="selfSubscribePackageMutation.mutate(item.id)"
>
<CheckCircleOutlined /> 订阅自己
</a-menu-item>
<a-menu-item
v-if="canCancelSelfSubscription(item)"
danger
@click="cancelSelfSubscriptionMutation.mutate(item.id)"
>
<StopOutlined /> 取消订阅
</a-menu-item>
<a-menu-divider />
<a-menu-item danger @click="handleDeletePackage(item.id)">
<DeleteOutlined /> {{ t('common.delete') }}
@@ -49,6 +49,7 @@ const subscribeMutation = useMutation({
const pkg = computed(() => packageQuery.data.value);
const resolvedCoverUrl = computed(() => resolveApiURL(pkg.value?.cover_url));
const subscription = computed(() => pkg.value?.subscription);
const isOwnPackage = computed(() => Boolean(pkg.value?.owned_by_current_tenant));
const subscriptionStatusMeta = computed(() => {
if (!subscription.value) return null;
@@ -67,7 +68,7 @@ const subscriptionStatusMeta = computed(() => {
});
function handleSubscribe() {
if (pkg.value) {
if (pkg.value && !isOwnPackage.value) {
subscribeMutation.mutate(pkg.value.id);
}
}
@@ -182,7 +183,11 @@ function goBack() {
<a-col :xs="24" :lg="8">
<a-card class="subscription-card">
<h3 class="card-title">订阅状态</h3>
<div v-if="!subscription" class="not-subscribed">
<div v-if="!subscription && isOwnPackage" class="not-subscribed">
<p class="status-text">这是你自己发布的订阅包不能在模版市场申请订阅</p>
<p class="owner-hint">如需自己使用请回到 KOL 工作台在订阅包菜单中点击订阅自己</p>
</div>
<div v-else-if="!subscription" class="not-subscribed">
<p class="status-text">您尚未订阅该 KOL 提示词包</p>
<a-button
type="primary"
@@ -208,7 +213,7 @@ function goBack() {
</div>
</div>
<a-button
v-if="subscription.status === 'expired' || subscription.status === 'revoked'"
v-if="!isOwnPackage && (subscription.status === 'expired' || subscription.status === 'revoked')"
type="primary"
block
size="large"
@@ -471,6 +476,17 @@ function goBack() {
margin-bottom: 24px;
}
.owner-hint {
color: #64748b;
font-size: 13px;
line-height: 1.7;
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 12px;
margin: -8px 0 4px;
}
.status-badge {
margin-bottom: 20px;
}