fix: Enhance Kol Variable Rendering and Management

- Updated the regex for placeholder matching to support more flexible key formats.
- Refactored variable storage to allow both ID and key lookups in rendering.
- Improved schema validation to check for duplicate keys and enforce non-empty keys.
- Added new utility functions for variable value lookup and display name generation.
- Enhanced tests to cover new key-based variable scenarios.
- Refactored KolPrompt repository to simplify prompt storage and management, including the removal of obsolete revision handling.
- Introduced new API endpoints for saving, activating, and archiving prompts.
- Added new utility functions for handling Kol placeholders and platform options in the admin web.
- Implemented database migrations to simplify prompt storage structure and ensure data integrity.
This commit is contained in:
2026-04-18 00:47:57 +08:00
parent 614ca4a2ea
commit 3ef0807456
32 changed files with 2414 additions and 3518 deletions
+57 -8
View File
@@ -23,6 +23,8 @@ import (
type seedState struct {
TenantID int64
UserID int64
TestTenantID int64
TestUserID int64
PlanID int64
BrandID int64
KeywordPrimaryID int64
@@ -129,6 +131,8 @@ func main() {
fmt.Println("Seed data inserted successfully.")
fmt.Printf(" Tenant ID: %d\n", state.TenantID)
fmt.Printf(" User ID: %d (admin@geo.local / Admin@123)\n", state.UserID)
fmt.Printf(" Test Tenant ID: %d\n", state.TestTenantID)
fmt.Printf(" Test User ID: %d (test@geo.local / Test@123)\n", state.TestUserID)
fmt.Printf(" Plan ID: %d (free)\n", state.PlanID)
fmt.Printf(" Brand ID: %d (轻氧口腔)\n", state.BrandID)
fmt.Printf(" Plugin Installation ID: %d\n", state.PluginInstallationID)
@@ -176,6 +180,10 @@ func seedBusinessData(ctx context.Context, pool *pgxpool.Pool) (*seedState, erro
if err := ensureTemplates(ctx, tx); err != nil {
return nil, err
}
state.TestTenantID, state.TestUserID, err = ensureSecondaryTenantUser(ctx, tx, state.PlanID)
if err != nil {
return nil, err
}
state.BrandID, err = ensureBrand(ctx, tx, state.TenantID)
if err != nil {
@@ -280,48 +288,89 @@ func seedMonitoringData(ctx context.Context, pool *pgxpool.Pool, state *seedStat
}
func ensureTenant(ctx context.Context, tx pgx.Tx) (int64, error) {
return ensureNamedTenant(ctx, tx, "GEO Demo")
}
func ensureNamedTenant(ctx context.Context, tx pgx.Tx, name string) (int64, error) {
var tenantID int64
err := tx.QueryRow(ctx, `
WITH ensured AS (
INSERT INTO tenants (name, status)
VALUES ('GEO Demo', 'active')
VALUES ($1, 'active')
ON CONFLICT DO NOTHING
RETURNING id
)
SELECT id FROM ensured
UNION ALL
SELECT id FROM tenants WHERE name = 'GEO Demo'
SELECT id FROM tenants WHERE name = $1
LIMIT 1
`).Scan(&tenantID)
`, name).Scan(&tenantID)
return tenantID, wrapSeedErr("insert tenant", err)
}
func ensureUser(ctx context.Context, tx pgx.Tx, passwordHash string) (int64, error) {
return ensureNamedUser(ctx, tx, "admin@geo.local", "Admin", passwordHash)
}
func ensureNamedUser(ctx context.Context, tx pgx.Tx, email, name, passwordHash string) (int64, error) {
var userID int64
err := tx.QueryRow(ctx, `
WITH ensured AS (
INSERT INTO users (email, password_hash, name, status)
VALUES ('admin@geo.local', $1, 'Admin', 'active')
VALUES ($1, $2, $3, 'active')
ON CONFLICT DO NOTHING
RETURNING id
)
SELECT id FROM ensured
UNION ALL
SELECT id FROM users WHERE email = 'admin@geo.local'
SELECT id FROM users WHERE email = $1
LIMIT 1
`, passwordHash).Scan(&userID)
`, email, passwordHash, name).Scan(&userID)
return userID, wrapSeedErr("insert user", err)
}
func ensureMembership(ctx context.Context, tx pgx.Tx, tenantID, userID int64) error {
return ensureMembershipWithRole(ctx, tx, tenantID, userID, "tenant_admin")
}
func ensureMembershipWithRole(ctx context.Context, tx pgx.Tx, tenantID, userID int64, role string) error {
_, err := tx.Exec(ctx, `
INSERT INTO tenant_memberships (tenant_id, user_id, tenant_role)
VALUES ($1, $2, 'tenant_admin')
VALUES ($1, $2, $3)
ON CONFLICT DO NOTHING
`, tenantID, userID)
`, tenantID, userID, role)
return wrapSeedErr("insert membership", err)
}
func ensureSecondaryTenantUser(ctx context.Context, tx pgx.Tx, planID int64) (int64, int64, error) {
hash, err := bcrypt.GenerateFromPassword([]byte("Test@123"), bcrypt.DefaultCost)
if err != nil {
return 0, 0, fmt.Errorf("hash secondary user password: %w", err)
}
tenantID, err := ensureNamedTenant(ctx, tx, "test")
if err != nil {
return 0, 0, err
}
userID, err := ensureNamedUser(ctx, tx, "test@geo.local", "test", string(hash))
if err != nil {
return 0, 0, err
}
if err := ensureMembershipWithRole(ctx, tx, tenantID, userID, "tenant_admin"); err != nil {
return 0, 0, err
}
if err := ensureSubscription(ctx, tx, tenantID, planID); err != nil {
return 0, 0, err
}
if err := ensureQuotaLedger(ctx, tx, tenantID); err != nil {
return 0, 0, err
}
return tenantID, userID, nil
}
func ensurePlan(ctx context.Context, tx pgx.Tx) (int64, error) {
var planID int64
err := tx.QueryRow(ctx, `