优化后台高频查询性能
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 将商品关联的发布群标记为已归档(解散),并写入系统提示。
|
||||
|
||||
Reference in New Issue
Block a user