Files
geo/server/internal/tenant/app/kol_subscription_admin_service.go
T
root b2605abd6a feat: Enhance Kol Generation Service with web search and knowledge group support
- Added `EnableWebSearch` and `KnowledgeGroupIDs` fields to `KolGenerationSubmitRequest`.
- Updated `Submit` method in `KolGenerationService` to handle new request fields.
- Integrated web search and knowledge group validation based on card configuration.
- Introduced caching mechanisms in `KolPackageService`, `KolPromptService`, and `KolSubscriptionAdminService` to improve performance.
- Implemented knowledge context resolution in `KolGenerationWorker` to enrich prompts with relevant knowledge snippets.
- Added utility functions for handling card configuration in both backend and frontend.
- Created tests for knowledge extraction and rendering to ensure accuracy and reliability.
2026-04-18 13:47:32 +08:00

265 lines
8.7 KiB
Go

package app
import (
"context"
"encoding/json"
"time"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/geo-platform/tenant-api/internal/shared/auditlog"
sharedcache "github.com/geo-platform/tenant-api/internal/shared/cache"
"github.com/geo-platform/tenant-api/internal/shared/middleware"
"github.com/geo-platform/tenant-api/internal/shared/response"
"github.com/geo-platform/tenant-api/internal/tenant/repository"
)
type ManualBindKolSubscriptionInput struct {
TenantID int64 `json:"tenant_id" binding:"required"`
PackageID int64 `json:"package_id" binding:"required"`
EndAt *time.Time `json:"end_at"`
}
type KolSubscriptionAdminService struct {
pool *pgxpool.Pool
auditLogs *auditlog.AsyncWriter
marketplaceRepo repository.KolMarketplaceRepository
subRepo repository.KolSubscriptionRepository
promptRepo repository.KolPromptRepository
cache sharedcache.Cache
}
func NewKolSubscriptionAdminService(
pool *pgxpool.Pool,
auditLogs *auditlog.AsyncWriter,
marketplaceRepo repository.KolMarketplaceRepository,
subRepo repository.KolSubscriptionRepository,
promptRepo repository.KolPromptRepository,
) *KolSubscriptionAdminService {
return &KolSubscriptionAdminService{
pool: pool,
auditLogs: auditLogs,
marketplaceRepo: marketplaceRepo,
subRepo: subRepo,
promptRepo: promptRepo,
}
}
func (s *KolSubscriptionAdminService) WithCache(c sharedcache.Cache) *KolSubscriptionAdminService {
s.cache = c
return s
}
func (s *KolSubscriptionAdminService) Approve(ctx context.Context, id int64, endAt *time.Time, operatorID int64) (*KolSubscriptionResponse, error) {
tx, err := s.pool.Begin(ctx)
if err != nil {
return nil, response.ErrInternal(50096, "kol_subscription_approve_tx_failed", "failed to begin subscription approval transaction")
}
defer tx.Rollback(ctx)
subTx := repository.NewKolSubscriptionRepository(tx)
marketplaceTx := repository.NewKolMarketplaceRepository(tx)
promptTx := repository.NewKolPromptRepository(tx)
sub, err := subTx.GetByIDAny(ctx, id)
if err != nil {
return nil, response.ErrInternal(50093, "kol_subscription_query_failed", err.Error())
}
if sub == nil {
return nil, errKolSubscriptionNotFound
}
if sub.Status != "pending" {
return nil, errKolSubscriptionNotPending
}
pkg, err := marketplaceTx.GetPublished(ctx, sub.PackageID)
if err != nil {
return nil, response.ErrInternal(50090, "kol_marketplace_query_failed", err.Error())
}
if pkg == nil {
return nil, errKolMarketplacePackageNotFound
}
if err := subTx.Approve(ctx, id, sub.TenantID, operatorID, endAt); err != nil {
return nil, response.ErrInternal(50097, "kol_subscription_approve_failed", err.Error())
}
if err := grantActivePackagePrompts(ctx, subTx, promptTx, sub.TenantID, id, pkg.TenantID, pkg.ID); err != nil {
return nil, err
}
updated, err := subTx.GetByIDAny(ctx, id)
if err != nil {
return nil, response.ErrInternal(50093, "kol_subscription_query_failed", err.Error())
}
if err := tx.Commit(ctx); err != nil {
return nil, response.ErrInternal(50098, "kol_subscription_approve_commit_failed", "failed to commit subscription approval")
}
InvalidateKolPromptCaches(ctx, s.cache)
s.logAdminAction(ctx, "approve", operatorID, updated.TenantID, updated.ID, map[string]interface{}{
"id": updated.ID,
"tenant_id": updated.TenantID,
"package_id": updated.PackageID,
"status": updated.Status,
"end_at": timeStringPtr(endAt),
})
return newKolSubscriptionResponse(updated), nil
}
func (s *KolSubscriptionAdminService) ManualBind(ctx context.Context, input ManualBindKolSubscriptionInput, operatorID int64) (*KolSubscriptionResponse, error) {
if input.TenantID <= 0 {
return nil, response.ErrBadRequest(40071, "kol_subscription_tenant_required", "tenant_id is required")
}
if input.PackageID <= 0 {
return nil, response.ErrBadRequest(40072, "kol_subscription_package_required", "package_id is required")
}
tx, err := s.pool.Begin(ctx)
if err != nil {
return nil, response.ErrInternal(50099, "kol_subscription_bind_tx_failed", "failed to begin manual bind transaction")
}
defer tx.Rollback(ctx)
subTx := repository.NewKolSubscriptionRepository(tx)
marketplaceTx := repository.NewKolMarketplaceRepository(tx)
promptTx := repository.NewKolPromptRepository(tx)
pkg, err := marketplaceTx.GetPublished(ctx, input.PackageID)
if err != nil {
return nil, response.ErrInternal(50090, "kol_marketplace_query_failed", err.Error())
}
if pkg == nil {
return nil, errKolMarketplacePackageNotFound
}
existing, err := subTx.GetActiveForTenant(ctx, input.TenantID, input.PackageID)
if err != nil {
return nil, response.ErrInternal(50093, "kol_subscription_query_failed", err.Error())
}
if existing != nil {
return nil, errKolSubscriptionExists
}
sub, err := subTx.Create(ctx, input.TenantID, input.PackageID)
if err != nil {
if isUniqueConstraintError(err, "uk_kol_subscription_active") {
return nil, errKolSubscriptionExists
}
return nil, response.ErrInternal(50100, "kol_subscription_create_failed", err.Error())
}
if err := subTx.Approve(ctx, sub.ID, input.TenantID, operatorID, input.EndAt); err != nil {
return nil, response.ErrInternal(50097, "kol_subscription_approve_failed", err.Error())
}
if err := grantActivePackagePrompts(ctx, subTx, promptTx, input.TenantID, sub.ID, pkg.TenantID, pkg.ID); err != nil {
return nil, err
}
updated, err := subTx.GetByIDAny(ctx, sub.ID)
if err != nil {
return nil, response.ErrInternal(50093, "kol_subscription_query_failed", err.Error())
}
if err := tx.Commit(ctx); err != nil {
return nil, response.ErrInternal(50101, "kol_subscription_bind_commit_failed", "failed to commit manual bind")
}
InvalidateKolPromptCaches(ctx, s.cache)
s.logAdminAction(ctx, "manual_bind", operatorID, updated.TenantID, updated.ID, map[string]interface{}{
"id": updated.ID,
"tenant_id": updated.TenantID,
"package_id": updated.PackageID,
"status": updated.Status,
"end_at": timeStringPtr(input.EndAt),
})
return newKolSubscriptionResponse(updated), nil
}
func (s *KolSubscriptionAdminService) Revoke(ctx context.Context, id int64, operatorID int64) error {
tx, err := s.pool.Begin(ctx)
if err != nil {
return response.ErrInternal(50102, "kol_subscription_revoke_tx_failed", "failed to begin revoke transaction")
}
defer tx.Rollback(ctx)
subTx := repository.NewKolSubscriptionRepository(tx)
sub, err := subTx.GetByIDAny(ctx, id)
if err != nil {
return response.ErrInternal(50093, "kol_subscription_query_failed", err.Error())
}
if sub == nil {
return errKolSubscriptionNotFound
}
if err := subTx.RevokeByTenant(ctx, id, sub.TenantID); err != nil {
return response.ErrInternal(50103, "kol_subscription_revoke_failed", err.Error())
}
if err := subTx.RevokeAccessRowsBySubscription(ctx, id, sub.TenantID); err != nil {
return response.ErrInternal(50104, "kol_subscription_access_revoke_failed", err.Error())
}
if err := tx.Commit(ctx); err != nil {
return response.ErrInternal(50105, "kol_subscription_revoke_commit_failed", "failed to commit subscription revoke")
}
InvalidateKolPromptCaches(ctx, s.cache)
s.logAdminAction(ctx, "revoke", operatorID, sub.TenantID, sub.ID, map[string]interface{}{
"id": sub.ID,
"tenant_id": sub.TenantID,
"package_id": sub.PackageID,
"status": "revoked",
})
return nil
}
func grantActivePackagePrompts(
ctx context.Context,
subRepo repository.KolSubscriptionRepository,
promptRepo repository.KolPromptRepository,
subscriberTenantID, subscriptionID, creatorTenantID, packageID int64,
) error {
prompts, err := promptRepo.ListActiveByPackage(ctx, creatorTenantID, packageID)
if err != nil {
return response.ErrInternal(50092, "kol_marketplace_prompt_query_failed", err.Error())
}
for _, prompt := range prompts {
if err := subRepo.CreateAccessRow(ctx, repository.CreateAccessRowInput{
TenantID: subscriberTenantID,
SubscriptionID: subscriptionID,
PackageID: packageID,
PromptID: prompt.ID,
}); err != nil {
return response.ErrInternal(50084, "kol_subscription_access_create_failed", err.Error())
}
}
return nil
}
func (s *KolSubscriptionAdminService) logAdminAction(
ctx context.Context,
action string,
operatorID, targetTenantID, subscriptionID int64,
after map[string]interface{},
) {
if s.auditLogs == nil {
return
}
afterJSON, _ := json.Marshal(after)
result := "success"
resourceType := "kol_subscription"
requestID := middleware.RequestIDFromContext(ctx)
s.auditLogs.Log(auditlog.Entry{
OperatorID: operatorID,
TenantID: &targetTenantID,
Module: "kol_admin",
Action: action,
ResourceType: &resourceType,
ResourceID: &subscriptionID,
RequestID: nilIfEmptyString(requestID),
AfterJSON: afterJSON,
Result: &result,
})
}