feat(ECR-020): QualityFeedback 问答质量反馈并 Closed
新增 ask_quality_feedback、运营/C端评分 API 与 admin-h5 问答反馈区;禁改消息/UGC/真支付。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
// QualityFeedbackRow is AskOperations QualityFeedback.
|
||||
type QualityFeedbackRow struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
ThreadID uuid.UUID `json:"thread_id"`
|
||||
MessageID *uuid.UUID `json:"message_id,omitempty"`
|
||||
Source string `json:"source"`
|
||||
Rating int `json:"rating"`
|
||||
Tag *string `json:"tag,omitempty"`
|
||||
Note *string `json:"note,omitempty"`
|
||||
CreatedByAdmin *uuid.UUID `json:"created_by_admin,omitempty"`
|
||||
CreatedByUser *uuid.UUID `json:"created_by_user,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// ListQualityFeedback returns recent feedback rows.
|
||||
func (r *AdminRepo) ListQualityFeedback(ctx context.Context, limit, offset int) ([]QualityFeedbackRow, error) {
|
||||
if limit <= 0 || limit > 100 {
|
||||
limit = 20
|
||||
}
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
rows, err := r.Pool.Query(ctx, `
|
||||
SELECT id, thread_id, message_id, source, rating, tag, note,
|
||||
created_by_admin, created_by_user, created_at
|
||||
FROM ask_quality_feedback
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $1 OFFSET $2`, limit, offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
return scanQualityFeedback(rows)
|
||||
}
|
||||
|
||||
func scanQualityFeedback(rows pgx.Rows) ([]QualityFeedbackRow, error) {
|
||||
var out []QualityFeedbackRow
|
||||
for rows.Next() {
|
||||
var f QualityFeedbackRow
|
||||
if err := rows.Scan(
|
||||
&f.ID, &f.ThreadID, &f.MessageID, &f.Source, &f.Rating, &f.Tag, &f.Note,
|
||||
&f.CreatedByAdmin, &f.CreatedByUser, &f.CreatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, f)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// CreateAdminQualityFeedback inserts ops feedback + audit.
|
||||
func (r *AdminRepo) CreateAdminQualityFeedback(
|
||||
ctx context.Context, adminID, threadID uuid.UUID, messageID *uuid.UUID, rating int, tag, note *string,
|
||||
) (*QualityFeedbackRow, error) {
|
||||
if err := validateFeedback(rating, tag, note); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tag, note = cleanTag(tag), cleanNote(note)
|
||||
ok, err := r.askThreadExists(ctx, threadID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !ok {
|
||||
return nil, errors.New("ask thread not found")
|
||||
}
|
||||
tx, err := r.Pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
var f QualityFeedbackRow
|
||||
err = tx.QueryRow(ctx, `
|
||||
INSERT INTO ask_quality_feedback(thread_id, message_id, source, rating, tag, note, created_by_admin)
|
||||
VALUES ($1,$2,'admin',$3,$4,$5,$6)
|
||||
RETURNING id, thread_id, message_id, source, rating, tag, note, created_by_admin, created_by_user, created_at`,
|
||||
threadID, messageID, rating, tag, note, adminID,
|
||||
).Scan(&f.ID, &f.ThreadID, &f.MessageID, &f.Source, &f.Rating, &f.Tag, &f.Note,
|
||||
&f.CreatedByAdmin, &f.CreatedByUser, &f.CreatedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
meta, _ := json.Marshal(map[string]any{"rating": rating, "thread_id": threadID.String()})
|
||||
if _, err := tx.Exec(ctx, `
|
||||
INSERT INTO admin_audit_logs(admin_id, action, target_type, target_id, meta)
|
||||
VALUES ($1,'ask.feedback.create','ask_thread',$2,$3)`,
|
||||
adminID, threadID.String(), meta); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &f, nil
|
||||
}
|
||||
|
||||
// CreateUserQualityFeedback inserts C-end feedback for owned thread.
|
||||
func (r *AskRepo) CreateUserQualityFeedback(
|
||||
ctx context.Context, userID, threadID uuid.UUID, messageID *uuid.UUID, rating int, tag, note *string,
|
||||
) (*QualityFeedbackRow, error) {
|
||||
if err := validateFeedback(rating, tag, note); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tag, note = cleanTag(tag), cleanNote(note)
|
||||
_, err := r.GetThreadForUser(ctx, userID, threadID)
|
||||
if err != nil {
|
||||
return nil, errors.New("ask thread not found")
|
||||
}
|
||||
var f QualityFeedbackRow
|
||||
err = r.Pool.QueryRow(ctx, `
|
||||
INSERT INTO ask_quality_feedback(thread_id, message_id, source, rating, tag, note, created_by_user)
|
||||
VALUES ($1,$2,'user',$3,$4,$5,$6)
|
||||
RETURNING id, thread_id, message_id, source, rating, tag, note, created_by_admin, created_by_user, created_at`,
|
||||
threadID, messageID, rating, tag, note, userID,
|
||||
).Scan(&f.ID, &f.ThreadID, &f.MessageID, &f.Source, &f.Rating, &f.Tag, &f.Note,
|
||||
&f.CreatedByAdmin, &f.CreatedByUser, &f.CreatedAt)
|
||||
return &f, err
|
||||
}
|
||||
|
||||
func (r *AdminRepo) askThreadExists(ctx context.Context, threadID uuid.UUID) (bool, error) {
|
||||
var n int
|
||||
err := r.Pool.QueryRow(ctx, `
|
||||
SELECT 1 FROM ask_threads WHERE id=$1 AND deleted_at IS NULL`, threadID).Scan(&n)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return false, nil
|
||||
}
|
||||
return err == nil, err
|
||||
}
|
||||
|
||||
func validateFeedback(rating int, tag, note *string) error {
|
||||
if rating < 1 || rating > 5 {
|
||||
return errors.New("rating must be 1-5")
|
||||
}
|
||||
if tag != nil && *tag != "" {
|
||||
switch *tag {
|
||||
case "helpful", "off_topic", "unsafe", "other":
|
||||
default:
|
||||
return errors.New("invalid tag")
|
||||
}
|
||||
}
|
||||
if note != nil && utf8.RuneCountInString(*note) > 500 {
|
||||
return errors.New("note too long")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func cleanTag(tag *string) *string {
|
||||
if tag == nil || *tag == "" {
|
||||
return nil
|
||||
}
|
||||
return tag
|
||||
}
|
||||
|
||||
func cleanNote(note *string) *string {
|
||||
if note == nil || *note == "" {
|
||||
return nil
|
||||
}
|
||||
return note
|
||||
}
|
||||
Reference in New Issue
Block a user