feat(auth,prompt-rule): add password change with session revocation and scope prompt rules by brand
Frontend CI / Frontend (push) Successful in 3m8s
Backend CI / Backend (push) Successful in 14m48s

Add self-service password change that re-hashes the credential and bumps a
per-user session version so all existing access/refresh tokens are rejected.
Session version is tracked in Redis with an in-memory fallback and enforced in
middleware and refresh.

Scope prompt rules and rule groups to a brand: new brand_id column (migrated to
a "未归属" fallback brand for existing rows), brand-filtered queries/indexes, and
brand-aware admin-web tabs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-20 18:09:53 +08:00
parent 9f9e4f8e19
commit e045e00fbf
41 changed files with 1135 additions and 167 deletions
@@ -26,6 +26,11 @@ type ClaimedGenerationTask struct {
const generationCleanupTimeout = 30 * time.Second
type promptRuleTaskContext struct {
PromptRuleID int64
BrandID int64
}
func newGenerationCleanupContext() (context.Context, context.CancelFunc) {
return context.WithTimeout(context.Background(), generationCleanupTimeout)
}
@@ -215,13 +220,20 @@ func (s *PromptRuleGenerationService) ProcessQueuedTask(ctx context.Context, tas
}
rule := extractPromptRuleSnapshot(inputParams)
if rule == nil {
promptRuleID, err := s.resolvePromptRuleIDForTask(ctx, task)
if rule == nil || int64(extractInt(inputParams, "prompt_rule_id")) <= 0 || int64(extractInt(inputParams, "brand_id")) <= 0 {
taskContext, err := s.resolvePromptRuleTaskContext(ctx, task)
if err != nil {
s.failGeneration(context.Background(), job, initialTitle, "task_load", err)
return
}
rule, err = s.loadPromptRule(ctx, task.TenantID, promptRuleID)
inputParams["prompt_rule_id"] = taskContext.PromptRuleID
inputParams["brand_id"] = taskContext.BrandID
}
if rule == nil {
promptRuleID := int64(extractInt(inputParams, "prompt_rule_id"))
brandID := int64(extractInt(inputParams, "brand_id"))
var err error
rule, err = s.loadPromptRule(ctx, task.TenantID, brandID, promptRuleID)
if err != nil {
s.failGeneration(context.Background(), job, initialTitle, "task_load", err)
return
@@ -254,6 +266,47 @@ func (s *PromptRuleGenerationService) ProcessQueuedTask(ctx context.Context, tas
s.executeGeneration(ctx, job)
}
func (s *PromptRuleGenerationService) resolvePromptRuleTaskContext(ctx context.Context, task ClaimedGenerationTask) (promptRuleTaskContext, error) {
resolved := promptRuleTaskContext{
PromptRuleID: int64(extractInt(task.InputParams, "prompt_rule_id")),
BrandID: int64(extractInt(task.InputParams, "brand_id")),
}
if resolved.PromptRuleID > 0 && resolved.BrandID > 0 {
return resolved, nil
}
if task.ArticleID <= 0 {
return resolved, fmt.Errorf("article context missing for prompt rule task")
}
var articlePromptRuleID int64
var articleBrandID int64
if err := s.pool.QueryRow(ctx, `
SELECT COALESCE(prompt_rule_id, 0), brand_id
FROM articles
WHERE id = $1
AND tenant_id = $2
AND source_type = 'custom_generation'
AND deleted_at IS NULL
`, task.ArticleID, task.TenantID).Scan(&articlePromptRuleID, &articleBrandID); err != nil {
return resolved, fmt.Errorf("load task prompt rule context: %w", err)
}
if resolved.PromptRuleID <= 0 {
resolved.PromptRuleID = articlePromptRuleID
}
if resolved.BrandID <= 0 {
resolved.BrandID = articleBrandID
}
if resolved.PromptRuleID <= 0 {
return resolved, fmt.Errorf("prompt rule context missing for prompt rule task")
}
if resolved.BrandID <= 0 {
return resolved, fmt.Errorf("brand context missing for prompt rule task")
}
return resolved, nil
}
func (s *PromptRuleGenerationService) resolvePromptRuleIDForTask(ctx context.Context, task ClaimedGenerationTask) (int64, error) {
if promptRuleID := int64(extractInt(task.InputParams, "prompt_rule_id")); promptRuleID > 0 {
return promptRuleID, nil