优化后台高频查询性能

This commit is contained in:
yml2213
2026-08-29 00:16:16 +08:00
parent c87883cc65
commit c156c478d8
17 changed files with 684 additions and 175 deletions
@@ -0,0 +1,43 @@
package chat
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"time"
)
const adminChatCountsCacheTTL = 5 * time.Second
func (r *Repository) adminChatCountsCacheKey(principal Principal, filter, stage, keyword string) string {
sum := sha256.Sum256([]byte(keyword))
return fmt.Sprintf("admin-chat:counts:v2:%s:%d:%s:%s:%s", principal.Type, principal.ID, filter, stage, hex.EncodeToString(sum[:8]))
}
func (r *Repository) loadAdminChatCountsCache(ctx context.Context, principal Principal, filter, stage, keyword string) *AdminConversationCountsDTO {
if r.redis == nil {
return nil
}
raw, err := r.redis.Get(ctx, r.adminChatCountsCacheKey(principal, filter, stage, keyword)).Bytes()
if err != nil {
return nil
}
var value AdminConversationCountsDTO
if err := json.Unmarshal(raw, &value); err != nil {
return nil
}
return &value
}
func (r *Repository) storeAdminChatCountsCache(ctx context.Context, principal Principal, filter, stage, keyword string, value *AdminConversationCountsDTO) {
if r.redis == nil || value == nil {
return
}
raw, err := json.Marshal(value)
if err != nil {
return
}
_ = r.redis.Set(ctx, r.adminChatCountsCacheKey(principal, filter, stage, keyword), raw, adminChatCountsCacheTTL).Err()
}
@@ -139,3 +139,61 @@ func TestListAdminConversationsUsesPersonalStateAndBatchData(t *testing.T) {
t.Fatalf("客服回复后待处理数量 = %d, want 0", result.Total)
}
}
func TestListAdminConversationsUsesListingOrderSnapshot(t *testing.T) {
db := setupAdminListTestDB(t)
repo := NewRepository(db, nil, nil)
admin := model.AdminUser{Username: "cs-snapshot", Nickname: "客服快照", Status: "active"}
owner := model.User{Phone: "13800000011", Nickname: "号主"}
renter := model.User{Phone: "13800000012", Nickname: "租客"}
if err := db.Create(&admin).Error; err != nil {
t.Fatalf("创建客服失败: %v", err)
}
if err := db.Create(&owner).Error; err != nil {
t.Fatalf("创建号主失败: %v", err)
}
if err := db.Create(&renter).Error; err != nil {
t.Fatalf("创建租客失败: %v", err)
}
listing := model.RentalListing{ListingNo: "L202608250002", OwnerID: owner.ID, AccountID: 2, Status: "active"}
if err := db.Create(&listing).Error; err != nil {
t.Fatalf("创建商品失败: %v", err)
}
conversation := model.ChatConversation{ListingID: &listing.ID, Type: ConversationTypeListingGroup, Title: "发布群", Status: "active"}
if err := db.Create(&conversation).Error; err != nil {
t.Fatalf("创建发布群失败: %v", err)
}
if err := db.Create(&model.ChatParticipant{ConversationID: conversation.ID, ParticipantType: "admin", ParticipantID: admin.ID, Role: "support", JoinedAt: time.Now()}).Error; err != nil {
t.Fatalf("创建客服成员失败: %v", err)
}
order := model.RentalOrder{
OrderNo: "RO-SNAPSHOT-1", ListingID: listing.ID, AccountID: listing.AccountID,
OwnerID: owner.ID, RenterID: renter.ID, Status: "pending_handoff", HandoffStatus: "pending_owner", RefundStatus: "none",
}
if err := db.Create(&order).Error; err != nil {
t.Fatalf("创建订单失败: %v", err)
}
result, err := repo.ListConversationsWithFilter(t.Context(), Principal{Type: "admin", ID: admin.ID}, 1, 20, adminChatFilterMine, adminChatStageHandoff, "")
if err != nil {
t.Fatalf("按交接阶段查询发布群失败: %v", err)
}
items := result.Items.([]ConversationDTO)
if result.Total != 1 || len(items) != 1 {
t.Fatalf("发布群数量 = total %d/items %d, want 1/1", result.Total, len(items))
}
if items[0].LatestOrderID == nil || *items[0].LatestOrderID != order.ID || items[0].LatestOrderStatus != "pending_handoff" {
t.Fatalf("最新订单快照未生效: %+v", items[0])
}
order.Status = "renting"
if err := db.Save(&order).Error; err != nil {
t.Fatalf("更新订单状态失败: %v", err)
}
result, err = repo.ListConversationsWithFilter(t.Context(), Principal{Type: "admin", ID: admin.ID}, 1, 20, adminChatFilterMine, adminChatStageRenting, "")
if err != nil || result.Total != 1 {
t.Fatalf("更新后使用中筛选失败: total=%d err=%v", result.Total, err)
}
}
+6
View File
@@ -113,6 +113,12 @@ func (r *Repository) SendMessage(ctx context.Context, principal Principal, conve
conversation.LastMessageID = &message.ID
conversation.LastMessagePreview = messagePreview(message.Content, req.AttachmentURLS)
conversation.LastMessageAt = &message.CreatedAt
if message.AdminAttentionType != "" || message.SenderType == "user" {
conversation.LastAttentionMessageID = message.ID
}
if message.SenderType == "admin" {
conversation.LastAdminMessageID = message.ID
}
if err := tx.Save(&conversation).Error; err != nil {
return err
}
+21 -25
View File
@@ -233,11 +233,11 @@ func (r *Repository) listAdminConversations(ctx context.Context, principal Princ
c.last_message_preview, c.last_message_at, c.created_at, c.updated_at,
COALESCE(cp_me.role, 'admin') AS role,
COALESCE(cas.remark, '') AS admin_remark,
COALESCE(explicit_lo.id, listing_lo.id) AS latest_order_id,
COALESCE(explicit_lo.order_no, listing_lo.order_no) AS latest_order_no,
COALESCE(explicit_lo.status, listing_lo.status) AS latest_order_status,
COALESCE(explicit_lo.handoff_status, listing_lo.handoff_status) AS latest_handoff_status,
COALESCE(explicit_lo.refund_status, listing_lo.refund_status) AS latest_refund_status,
COALESCE(explicit_lo.id, c.latest_order_id) AS latest_order_id,
COALESCE(explicit_lo.order_no, c.latest_order_no) AS latest_order_no,
COALESCE(explicit_lo.status, c.latest_order_status) AS latest_order_status,
COALESCE(explicit_lo.handoff_status, c.latest_order_handoff_status) AS latest_handoff_status,
COALESCE(explicit_lo.refund_status, c.latest_order_refund_status) AS latest_refund_status,
lm.sender_type AS last_sender_type, lm.sender_id AS last_sender_id, lm.sender_role AS last_sender_role,
CASE WHEN c.type = 'general_support' AND (` + adminNeedsReplyExpression() + `) THEN 1 ELSE 0 END AS needs_reply,
(
@@ -273,10 +273,17 @@ func (r *Repository) listAdminConversations(ctx context.Context, principal Princ
}
func (r *Repository) AdminConversationCounts(ctx context.Context, principal Principal, filter string, stage string, keyword string) (*AdminConversationCountsDTO, error) {
result, err := r.adminConversationCounts(ctx, principal, normalizeAdminChatFilter(filter), normalizeAdminChatStage(stage), strings.TrimSpace(keyword))
filter = normalizeAdminChatFilter(filter)
stage = normalizeAdminChatStage(stage)
keyword = strings.TrimSpace(keyword)
if cached := r.loadAdminChatCountsCache(ctx, principal, filter, stage, keyword); cached != nil {
return cached, nil
}
result, err := r.adminConversationCounts(ctx, principal, filter, stage, keyword)
if err != nil {
return nil, err
}
r.storeAdminChatCountsCache(ctx, principal, filter, stage, keyword, result.DTO)
return result.DTO, nil
}
@@ -297,13 +304,12 @@ func (r *Repository) adminConversationBase(ctx context.Context, principal Princi
Joins("LEFT JOIN chat_admin_conversation_states AS cas ON cas.conversation_id = c.id AND cas.admin_user_id = ?", principal.ID).
Joins("LEFT JOIN chat_messages AS lm ON lm.id = c.last_message_id").
Joins("LEFT JOIN rental_orders AS explicit_lo ON explicit_lo.id = c.order_id").
Joins("LEFT JOIN (SELECT ro.* FROM rental_orders AS ro JOIN (SELECT listing_id, MAX(id) AS max_id FROM rental_orders GROUP BY listing_id) AS latest ON latest.max_id = ro.id) AS listing_lo ON listing_lo.listing_id = c.listing_id AND c.order_id IS NULL").
Joins("LEFT JOIN rental_listings AS l ON l.id = COALESCE(explicit_lo.listing_id, listing_lo.listing_id, c.listing_id)")
Joins("LEFT JOIN rental_listings AS l ON l.id = COALESCE(explicit_lo.listing_id, c.listing_id)")
if keyword != "" {
db = db.Joins("LEFT JOIN users AS renter ON renter.id = COALESCE(explicit_lo.renter_id, listing_lo.renter_id)").
Joins("LEFT JOIN users AS owner ON owner.id = COALESCE(explicit_lo.owner_id, listing_lo.owner_id, l.owner_id)")
db = db.Joins("LEFT JOIN users AS renter ON renter.id = explicit_lo.renter_id").
Joins("LEFT JOIN users AS owner ON owner.id = COALESCE(explicit_lo.owner_id, l.owner_id)")
like := "%" + keyword + "%"
db = db.Where(`c.title LIKE ? OR c.last_message_preview LIKE ? OR COALESCE(explicit_lo.order_no, listing_lo.order_no) LIKE ? OR l.listing_no LIKE ?
db = db.Where(`c.title LIKE ? OR c.last_message_preview LIKE ? OR COALESCE(explicit_lo.order_no, c.latest_order_no) LIKE ? OR l.listing_no LIKE ?
OR renter.phone LIKE ? OR owner.phone LIKE ?
OR EXISTS (
SELECT 1 FROM chat_participants AS cp_kw
@@ -410,9 +416,9 @@ func adminChatOwnershipCondition(filter string, principal Principal) (string, []
}
func adminChatStageCondition(stage string, principal Principal) (string, []interface{}) {
latestID := "COALESCE(explicit_lo.id, listing_lo.id)"
latestStatus := "COALESCE(explicit_lo.status, listing_lo.status)"
latestRefundStatus := "COALESCE(explicit_lo.refund_status, listing_lo.refund_status)"
latestID := "COALESCE(explicit_lo.id, c.latest_order_id)"
latestStatus := "COALESCE(explicit_lo.status, c.latest_order_status)"
latestRefundStatus := "COALESCE(explicit_lo.refund_status, c.latest_order_refund_status)"
switch stage {
case adminChatStagePending:
return "c.type = 'general_support' AND (" + adminNeedsReplyExpression() + ")", nil
@@ -434,17 +440,7 @@ func adminChatStageCondition(stage string, principal Principal) (string, []inter
}
func adminNeedsReplyExpression() string {
return `COALESCE((
SELECT MAX(cm_attention.id)
FROM chat_messages AS cm_attention
WHERE cm_attention.conversation_id = c.id
AND (cm_attention.admin_attention_type <> '' OR cm_attention.sender_type = 'user')
), 0) > COALESCE((
SELECT MAX(cm_admin.id)
FROM chat_messages AS cm_admin
WHERE cm_admin.conversation_id = c.id
AND cm_admin.sender_type = 'admin'
), 0)`
return "c.last_attention_message_id > c.last_admin_message_id"
}
// ArchiveListingConversation 将商品关联的发布群标记为已归档(解散),并写入系统提示。