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
@@ -198,3 +198,20 @@ func (q *Queries) GetUserByLoginIdentifier(ctx context.Context, loginIdentifier
)
return i, err
}
const updateUserPassword = `-- name: UpdateUserPassword :exec
UPDATE users
SET password_hash = $1,
updated_at = NOW()
WHERE id = $2 AND deleted_at IS NULL
`
type UpdateUserPasswordParams struct {
PasswordHash string `json:"password_hash"`
ID int64 `json:"id"`
}
func (q *Queries) UpdateUserPassword(ctx context.Context, arg UpdateUserPasswordParams) error {
_, err := q.db.Exec(ctx, updateUserPassword, arg.PasswordHash, arg.ID)
return err
}
@@ -691,6 +691,7 @@ type PromptRule struct {
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
DeletedAt pgtype.Timestamptz `json:"deleted_at"`
BrandID int64 `json:"brand_id"`
}
type PromptRuleGroup struct {
@@ -701,6 +702,7 @@ type PromptRuleGroup struct {
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
DeletedAt pgtype.Timestamptz `json:"deleted_at"`
BrandID int64 `json:"brand_id"`
}
type PromptRuleKnowledgeGroup struct {
@@ -15,14 +15,16 @@ const countPromptRules = `-- name: CountPromptRules :one
SELECT COUNT(*)
FROM prompt_rules
WHERE tenant_id = $1
AND brand_id = $2
AND deleted_at IS NULL
AND ($2::bigint IS NULL OR group_id = $2)
AND ($3::text IS NULL OR status = $3)
AND ($4::text IS NULL OR name ILIKE '%' || $4 || '%')
AND ($3::bigint IS NULL OR group_id = $3)
AND ($4::text IS NULL OR status = $4)
AND ($5::text IS NULL OR name ILIKE '%' || $5 || '%')
`
type CountPromptRulesParams struct {
TenantID int64 `json:"tenant_id"`
BrandID int64 `json:"brand_id"`
GroupID pgtype.Int8 `json:"group_id"`
Status pgtype.Text `json:"status"`
Keyword pgtype.Text `json:"keyword"`
@@ -31,6 +33,7 @@ type CountPromptRulesParams struct {
func (q *Queries) CountPromptRules(ctx context.Context, arg CountPromptRulesParams) (int64, error) {
row := q.db.QueryRow(ctx, countPromptRules,
arg.TenantID,
arg.BrandID,
arg.GroupID,
arg.Status,
arg.Keyword,
@@ -44,26 +47,33 @@ const countPromptRulesUngrouped = `-- name: CountPromptRulesUngrouped :one
SELECT COUNT(*)
FROM prompt_rules
WHERE tenant_id = $1
AND brand_id = $2
AND deleted_at IS NULL
AND group_id IS NULL
`
func (q *Queries) CountPromptRulesUngrouped(ctx context.Context, tenantID int64) (int64, error) {
row := q.db.QueryRow(ctx, countPromptRulesUngrouped, tenantID)
type CountPromptRulesUngroupedParams struct {
TenantID int64 `json:"tenant_id"`
BrandID int64 `json:"brand_id"`
}
func (q *Queries) CountPromptRulesUngrouped(ctx context.Context, arg CountPromptRulesUngroupedParams) (int64, error) {
row := q.db.QueryRow(ctx, countPromptRulesUngrouped, arg.TenantID, arg.BrandID)
var count int64
err := row.Scan(&count)
return count, err
}
const createPromptRule = `-- name: CreatePromptRule :one
INSERT INTO prompt_rules (tenant_id, group_id, name, prompt_content, scene, default_tone, default_word_count)
VALUES ($1, $2, $3, $4,
$5, $6, $7)
INSERT INTO prompt_rules (tenant_id, brand_id, group_id, name, prompt_content, scene, default_tone, default_word_count)
VALUES ($1, $2, $3, $4, $5,
$6, $7, $8)
RETURNING id, created_at
`
type CreatePromptRuleParams struct {
TenantID int64 `json:"tenant_id"`
BrandID int64 `json:"brand_id"`
GroupID pgtype.Int8 `json:"group_id"`
Name string `json:"name"`
PromptContent string `json:"prompt_content"`
@@ -80,6 +90,7 @@ type CreatePromptRuleRow struct {
func (q *Queries) CreatePromptRule(ctx context.Context, arg CreatePromptRuleParams) (CreatePromptRuleRow, error) {
row := q.db.QueryRow(ctx, createPromptRule,
arg.TenantID,
arg.BrandID,
arg.GroupID,
arg.Name,
arg.PromptContent,
@@ -93,13 +104,14 @@ func (q *Queries) CreatePromptRule(ctx context.Context, arg CreatePromptRulePara
}
const createPromptRuleGroup = `-- name: CreatePromptRuleGroup :one
INSERT INTO prompt_rule_groups (tenant_id, name, sort_order)
VALUES ($1, $2, $3)
INSERT INTO prompt_rule_groups (tenant_id, brand_id, name, sort_order)
VALUES ($1, $2, $3, $4)
RETURNING id, created_at
`
type CreatePromptRuleGroupParams struct {
TenantID int64 `json:"tenant_id"`
BrandID int64 `json:"brand_id"`
Name string `json:"name"`
SortOrder int32 `json:"sort_order"`
}
@@ -110,7 +122,12 @@ type CreatePromptRuleGroupRow struct {
}
func (q *Queries) CreatePromptRuleGroup(ctx context.Context, arg CreatePromptRuleGroupParams) (CreatePromptRuleGroupRow, error) {
row := q.db.QueryRow(ctx, createPromptRuleGroup, arg.TenantID, arg.Name, arg.SortOrder)
row := q.db.QueryRow(ctx, createPromptRuleGroup,
arg.TenantID,
arg.BrandID,
arg.Name,
arg.SortOrder,
)
var i CreatePromptRuleGroupRow
err := row.Scan(&i.ID, &i.CreatedAt)
return i, err
@@ -121,12 +138,13 @@ SELECT id, tenant_id, group_id, name, prompt_content,
scene, default_tone, default_word_count, status,
created_at, updated_at
FROM prompt_rules
WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL
WHERE id = $1 AND tenant_id = $2 AND brand_id = $3 AND deleted_at IS NULL
`
type GetPromptRuleByIDParams struct {
ID int64 `json:"id"`
TenantID int64 `json:"tenant_id"`
BrandID int64 `json:"brand_id"`
}
type GetPromptRuleByIDRow struct {
@@ -144,7 +162,7 @@ type GetPromptRuleByIDRow struct {
}
func (q *Queries) GetPromptRuleByID(ctx context.Context, arg GetPromptRuleByIDParams) (GetPromptRuleByIDRow, error) {
row := q.db.QueryRow(ctx, getPromptRuleByID, arg.ID, arg.TenantID)
row := q.db.QueryRow(ctx, getPromptRuleByID, arg.ID, arg.TenantID, arg.BrandID)
var i GetPromptRuleByIDRow
err := row.Scan(
&i.ID,
@@ -165,18 +183,23 @@ func (q *Queries) GetPromptRuleByID(ctx context.Context, arg GetPromptRuleByIDPa
const listAllPromptRulesSimple = `-- name: ListAllPromptRulesSimple :many
SELECT id, name, status
FROM prompt_rules
WHERE tenant_id = $1 AND deleted_at IS NULL AND status = 'enabled'
WHERE tenant_id = $1 AND brand_id = $2 AND deleted_at IS NULL AND status = 'enabled'
ORDER BY name
`
type ListAllPromptRulesSimpleParams struct {
TenantID int64 `json:"tenant_id"`
BrandID int64 `json:"brand_id"`
}
type ListAllPromptRulesSimpleRow struct {
ID int64 `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
}
func (q *Queries) ListAllPromptRulesSimple(ctx context.Context, tenantID int64) ([]ListAllPromptRulesSimpleRow, error) {
rows, err := q.db.Query(ctx, listAllPromptRulesSimple, tenantID)
func (q *Queries) ListAllPromptRulesSimple(ctx context.Context, arg ListAllPromptRulesSimpleParams) ([]ListAllPromptRulesSimpleRow, error) {
rows, err := q.db.Query(ctx, listAllPromptRulesSimple, arg.TenantID, arg.BrandID)
if err != nil {
return nil, err
}
@@ -199,10 +222,15 @@ const listPromptRuleGroups = `-- name: ListPromptRuleGroups :many
SELECT id, tenant_id, name, sort_order, created_at, updated_at
FROM prompt_rule_groups
WHERE tenant_id = $1 AND deleted_at IS NULL
WHERE tenant_id = $1 AND brand_id = $2 AND deleted_at IS NULL
ORDER BY sort_order, created_at
`
type ListPromptRuleGroupsParams struct {
TenantID int64 `json:"tenant_id"`
BrandID int64 `json:"brand_id"`
}
type ListPromptRuleGroupsRow struct {
ID int64 `json:"id"`
TenantID int64 `json:"tenant_id"`
@@ -215,8 +243,8 @@ type ListPromptRuleGroupsRow struct {
// ============================================================
// Prompt Rule Groups
// ============================================================
func (q *Queries) ListPromptRuleGroups(ctx context.Context, tenantID int64) ([]ListPromptRuleGroupsRow, error) {
rows, err := q.db.Query(ctx, listPromptRuleGroups, tenantID)
func (q *Queries) ListPromptRuleGroups(ctx context.Context, arg ListPromptRuleGroupsParams) ([]ListPromptRuleGroupsRow, error) {
rows, err := q.db.Query(ctx, listPromptRuleGroups, arg.TenantID, arg.BrandID)
if err != nil {
return nil, err
}
@@ -249,19 +277,21 @@ SELECT pr.id, pr.tenant_id, pr.group_id, pr.name, pr.prompt_content,
pr.created_at, pr.updated_at,
COUNT(a.id)::INT AS article_count
FROM prompt_rules pr
LEFT JOIN articles a ON a.prompt_rule_id = pr.id AND a.deleted_at IS NULL
LEFT JOIN articles a ON a.prompt_rule_id = pr.id AND a.brand_id = pr.brand_id AND a.deleted_at IS NULL
WHERE pr.tenant_id = $1
AND pr.brand_id = $2
AND pr.deleted_at IS NULL
AND ($2::bigint IS NULL OR pr.group_id = $2)
AND ($3::text IS NULL OR pr.status = $3)
AND ($4::text IS NULL OR pr.name ILIKE '%' || $4 || '%')
AND ($3::bigint IS NULL OR pr.group_id = $3)
AND ($4::text IS NULL OR pr.status = $4)
AND ($5::text IS NULL OR pr.name ILIKE '%' || $5 || '%')
GROUP BY pr.id
ORDER BY pr.created_at DESC
LIMIT $6 OFFSET $5
LIMIT $7 OFFSET $6
`
type ListPromptRulesParams struct {
TenantID int64 `json:"tenant_id"`
BrandID int64 `json:"brand_id"`
GroupID pgtype.Int8 `json:"group_id"`
Status pgtype.Text `json:"status"`
Keyword pgtype.Text `json:"keyword"`
@@ -290,6 +320,7 @@ type ListPromptRulesRow struct {
func (q *Queries) ListPromptRules(ctx context.Context, arg ListPromptRulesParams) ([]ListPromptRulesRow, error) {
rows, err := q.db.Query(ctx, listPromptRules,
arg.TenantID,
arg.BrandID,
arg.GroupID,
arg.Status,
arg.Keyword,
@@ -333,17 +364,19 @@ SELECT pr.id, pr.tenant_id, pr.group_id, pr.name, pr.prompt_content,
pr.created_at, pr.updated_at,
COUNT(a.id)::INT AS article_count
FROM prompt_rules pr
LEFT JOIN articles a ON a.prompt_rule_id = pr.id AND a.deleted_at IS NULL
LEFT JOIN articles a ON a.prompt_rule_id = pr.id AND a.brand_id = pr.brand_id AND a.deleted_at IS NULL
WHERE pr.tenant_id = $1
AND pr.brand_id = $2
AND pr.deleted_at IS NULL
AND pr.group_id IS NULL
GROUP BY pr.id
ORDER BY pr.created_at DESC
LIMIT $3 OFFSET $2
LIMIT $4 OFFSET $3
`
type ListPromptRulesUngroupedParams struct {
TenantID int64 `json:"tenant_id"`
BrandID int64 `json:"brand_id"`
PageOffset int32 `json:"page_offset"`
PageSize int32 `json:"page_size"`
}
@@ -364,7 +397,12 @@ type ListPromptRulesUngroupedRow struct {
}
func (q *Queries) ListPromptRulesUngrouped(ctx context.Context, arg ListPromptRulesUngroupedParams) ([]ListPromptRulesUngroupedRow, error) {
rows, err := q.db.Query(ctx, listPromptRulesUngrouped, arg.TenantID, arg.PageOffset, arg.PageSize)
rows, err := q.db.Query(ctx, listPromptRulesUngrouped,
arg.TenantID,
arg.BrandID,
arg.PageOffset,
arg.PageSize,
)
if err != nil {
return nil, err
}
@@ -398,31 +436,33 @@ func (q *Queries) ListPromptRulesUngrouped(ctx context.Context, arg ListPromptRu
const softDeletePromptRule = `-- name: SoftDeletePromptRule :exec
UPDATE prompt_rules SET deleted_at = NOW(), updated_at = NOW()
WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL
WHERE id = $1 AND tenant_id = $2 AND brand_id = $3 AND deleted_at IS NULL
`
type SoftDeletePromptRuleParams struct {
ID int64 `json:"id"`
TenantID int64 `json:"tenant_id"`
BrandID int64 `json:"brand_id"`
}
func (q *Queries) SoftDeletePromptRule(ctx context.Context, arg SoftDeletePromptRuleParams) error {
_, err := q.db.Exec(ctx, softDeletePromptRule, arg.ID, arg.TenantID)
_, err := q.db.Exec(ctx, softDeletePromptRule, arg.ID, arg.TenantID, arg.BrandID)
return err
}
const softDeletePromptRuleGroup = `-- name: SoftDeletePromptRuleGroup :exec
UPDATE prompt_rule_groups SET deleted_at = NOW(), updated_at = NOW()
WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL
WHERE id = $1 AND tenant_id = $2 AND brand_id = $3 AND deleted_at IS NULL
`
type SoftDeletePromptRuleGroupParams struct {
ID int64 `json:"id"`
TenantID int64 `json:"tenant_id"`
BrandID int64 `json:"brand_id"`
}
func (q *Queries) SoftDeletePromptRuleGroup(ctx context.Context, arg SoftDeletePromptRuleGroupParams) error {
_, err := q.db.Exec(ctx, softDeletePromptRuleGroup, arg.ID, arg.TenantID)
_, err := q.db.Exec(ctx, softDeletePromptRuleGroup, arg.ID, arg.TenantID, arg.BrandID)
return err
}
@@ -432,7 +472,7 @@ SET name = $1, group_id = $2,
prompt_content = $3,
scene = $4, default_tone = $5,
default_word_count = $6, updated_at = NOW()
WHERE id = $7 AND tenant_id = $8 AND deleted_at IS NULL
WHERE id = $7 AND tenant_id = $8 AND brand_id = $9 AND deleted_at IS NULL
`
type UpdatePromptRuleParams struct {
@@ -444,6 +484,7 @@ type UpdatePromptRuleParams struct {
DefaultWordCount pgtype.Int4 `json:"default_word_count"`
ID int64 `json:"id"`
TenantID int64 `json:"tenant_id"`
BrandID int64 `json:"brand_id"`
}
func (q *Queries) UpdatePromptRule(ctx context.Context, arg UpdatePromptRuleParams) error {
@@ -456,13 +497,14 @@ func (q *Queries) UpdatePromptRule(ctx context.Context, arg UpdatePromptRulePara
arg.DefaultWordCount,
arg.ID,
arg.TenantID,
arg.BrandID,
)
return err
}
const updatePromptRuleGroup = `-- name: UpdatePromptRuleGroup :exec
UPDATE prompt_rule_groups SET name = $1, sort_order = $2, updated_at = NOW()
WHERE id = $3 AND tenant_id = $4 AND deleted_at IS NULL
WHERE id = $3 AND tenant_id = $4 AND brand_id = $5 AND deleted_at IS NULL
`
type UpdatePromptRuleGroupParams struct {
@@ -470,6 +512,7 @@ type UpdatePromptRuleGroupParams struct {
SortOrder int32 `json:"sort_order"`
ID int64 `json:"id"`
TenantID int64 `json:"tenant_id"`
BrandID int64 `json:"brand_id"`
}
func (q *Queries) UpdatePromptRuleGroup(ctx context.Context, arg UpdatePromptRuleGroupParams) error {
@@ -478,22 +521,29 @@ func (q *Queries) UpdatePromptRuleGroup(ctx context.Context, arg UpdatePromptRul
arg.SortOrder,
arg.ID,
arg.TenantID,
arg.BrandID,
)
return err
}
const updatePromptRuleStatus = `-- name: UpdatePromptRuleStatus :exec
UPDATE prompt_rules SET status = $1, updated_at = NOW()
WHERE id = $2 AND tenant_id = $3 AND deleted_at IS NULL
WHERE id = $2 AND tenant_id = $3 AND brand_id = $4 AND deleted_at IS NULL
`
type UpdatePromptRuleStatusParams struct {
Status string `json:"status"`
ID int64 `json:"id"`
TenantID int64 `json:"tenant_id"`
BrandID int64 `json:"brand_id"`
}
func (q *Queries) UpdatePromptRuleStatus(ctx context.Context, arg UpdatePromptRuleStatusParams) error {
_, err := q.db.Exec(ctx, updatePromptRuleStatus, arg.Status, arg.ID, arg.TenantID)
_, err := q.db.Exec(ctx, updatePromptRuleStatus,
arg.Status,
arg.ID,
arg.TenantID,
arg.BrandID,
)
return err
}
@@ -24,7 +24,7 @@ type Querier interface {
CountImageAssets(ctx context.Context, arg CountImageAssetsParams) (int32, error)
CountImageReferences(ctx context.Context, arg CountImageReferencesParams) (int32, error)
CountPromptRules(ctx context.Context, arg CountPromptRulesParams) (int64, error)
CountPromptRulesUngrouped(ctx context.Context, tenantID int64) (int64, error)
CountPromptRulesUngrouped(ctx context.Context, arg CountPromptRulesUngroupedParams) (int64, error)
CountPublishedArticlesByTenantAndBrand(ctx context.Context, arg CountPublishedArticlesByTenantAndBrandParams) (int64, error)
CountScheduleTasks(ctx context.Context, arg CountScheduleTasksParams) (int64, error)
CountUncategorizedImages(ctx context.Context, tenantID int64) (int32, error)
@@ -111,7 +111,7 @@ type Querier interface {
// Cross-tenant read: used by platform admin for subscription fan-out.
// Excludes expired subscriptions so new prompts are not granted to them.
ListActiveSubscribersForPackage(ctx context.Context, packageID int64) ([]KolSubscription, error)
ListAllPromptRulesSimple(ctx context.Context, tenantID int64) ([]ListAllPromptRulesSimpleRow, error)
ListAllPromptRulesSimple(ctx context.Context, arg ListAllPromptRulesSimpleParams) ([]ListAllPromptRulesSimpleRow, error)
ListArticles(ctx context.Context, arg ListArticlesParams) ([]ListArticlesRow, error)
ListBrands(ctx context.Context, tenantID int64) ([]ListBrandsRow, error)
ListCompetitors(ctx context.Context, arg ListCompetitorsParams) ([]ListCompetitorsRow, error)
@@ -132,7 +132,7 @@ type Querier interface {
// ============================================================
// Prompt Rule Groups
// ============================================================
ListPromptRuleGroups(ctx context.Context, tenantID int64) ([]ListPromptRuleGroupsRow, error)
ListPromptRuleGroups(ctx context.Context, arg ListPromptRuleGroupsParams) ([]ListPromptRuleGroupsRow, error)
// ============================================================
// Prompt Rules
// ============================================================
@@ -208,6 +208,7 @@ type Querier interface {
UpdateScheduleTask(ctx context.Context, arg UpdateScheduleTaskParams) error
UpdateScheduleTaskStatus(ctx context.Context, arg UpdateScheduleTaskStatusParams) error
UpdateTaskRecordStatus(ctx context.Context, arg UpdateTaskRecordStatusParams) error
UpdateUserPassword(ctx context.Context, arg UpdateUserPasswordParams) error
UpsertDesktopAccount(ctx context.Context, arg UpsertDesktopAccountParams) (UpsertDesktopAccountRow, error)
UpsertImageReference(ctx context.Context, arg UpsertImageReferenceParams) error
}