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
@@ -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
}