优化后台高频查询性能

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
+9 -1
View File
@@ -313,7 +313,15 @@ func applyMigrations(t *testing.T, db *sql.DB) {
t.Fatal("无法定位当前测试文件")
}
migrationDir := filepath.Join(filepath.Dir(currentFile), "..", "..", "migrations")
for _, name := range []string{"000001_init.sql", "000002_dispute_cancel_snapshot.sql"} {
entries, err := os.ReadDir(migrationDir)
if err != nil {
t.Fatalf("读取迁移目录失败: %v", err)
}
for _, entry := range entries {
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".sql") {
continue
}
name := entry.Name()
migrationPath := filepath.Join(migrationDir, name)
raw, err := os.ReadFile(migrationPath)
if err != nil {
+14 -14
View File
@@ -21,7 +21,7 @@ func setupRefundRetryTestDB(t *testing.T) *gorm.DB {
if err != nil {
t.Fatalf("创建测试数据库失败: %v", err)
}
if err := db.AutoMigrate(&model.PaymentOrder{}, &model.RentalOrder{}); err != nil {
if err := db.AutoMigrate(&model.PaymentOrder{}, &model.RentalOrder{}, &model.ChatConversation{}); err != nil {
t.Fatalf("数据库迁移失败: %v", err)
}
return db
@@ -165,19 +165,19 @@ func TestResetRetryClearsRetryFields(t *testing.T) {
func createArbitrationOrphanFixture(t *testing.T, db *gorm.DB, suffix string, settlementStatus string) model.RentalOrder {
t.Helper()
order := model.RentalOrder{
OrderNo: "ORD" + suffix,
ListingID: 1,
AccountID: 1,
OwnerID: 1,
RenterID: 2,
RentAmountCent: 1000,
DepositAmountCent: 500,
Status: "closed",
HandoffStatus: "arbitrated",
SettlementStatus: settlementStatus,
RefundStatus: "pending",
RefundAmountCent: 800,
UpdatedAt: time.Now().Add(-20 * time.Minute), // 超过 10min 静默窗口
OrderNo: "ORD" + suffix,
ListingID: 1,
AccountID: 1,
OwnerID: 1,
RenterID: 2,
RentAmountCent: 1000,
DepositAmountCent: 500,
Status: "closed",
HandoffStatus: "arbitrated",
SettlementStatus: settlementStatus,
RefundStatus: "pending",
RefundAmountCent: 800,
UpdatedAt: time.Now().Add(-20 * time.Minute), // 超过 10min 静默窗口
}
if err := db.Create(&order).Error; err != nil {
t.Fatalf("create order failed: %v", err)
+41 -12
View File
@@ -4,21 +4,32 @@ import (
"time"
"gorm.io/datatypes"
"gorm.io/gorm"
)
type ChatConversation struct {
ID uint64 `gorm:"primaryKey" json:"id"`
OrderID *uint64 `gorm:"uniqueIndex" json:"order_id"`
ListingID *uint64 `gorm:"index" json:"listing_id"`
Type string `gorm:"size:32;not null;default:'order_group'" json:"type"`
SupportScene string `gorm:"column:support_scene;size:32;not null;default:''" json:"support_scene"`
Title string `gorm:"size:128;not null" json:"title"`
Status string `gorm:"size:32;not null;default:'active'" json:"status"`
LastMessageID *uint64 `json:"last_message_id"`
LastMessagePreview string `gorm:"size:255;not null;default:''" json:"last_message_preview"`
LastMessageAt *time.Time `json:"last_message_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
ID uint64 `gorm:"primaryKey" json:"id"`
OrderID *uint64 `gorm:"uniqueIndex" json:"order_id"`
ListingID *uint64 `gorm:"index" json:"listing_id"`
// 发布群没有固定订单时,以下字段保存该商品最新订单的轻量快照,供客服列表筛选使用。
// 这样读取会话列表不必每次扫描全部订单再按商品分组。
LatestOrderID *uint64 `gorm:"index" json:"-"`
LatestOrderNo string `gorm:"size:64;not null;default:''" json:"-"`
LatestOrderStatus string `gorm:"size:32;not null;default:'';index" json:"-"`
LatestOrderHandoffStatus string `gorm:"size:32;not null;default:''" json:"-"`
LatestOrderRefundStatus string `gorm:"size:32;not null;default:''" json:"-"`
// 待回复判断只需比较两个游标,避免每个会话再扫描聊天消息表。
LastAttentionMessageID uint64 `gorm:"not null;default:0" json:"-"`
LastAdminMessageID uint64 `gorm:"not null;default:0" json:"-"`
Type string `gorm:"size:32;not null;default:'order_group'" json:"type"`
SupportScene string `gorm:"column:support_scene;size:32;not null;default:''" json:"support_scene"`
Title string `gorm:"size:128;not null" json:"title"`
Status string `gorm:"size:32;not null;default:'active'" json:"status"`
LastMessageID *uint64 `json:"last_message_id"`
LastMessagePreview string `gorm:"size:255;not null;default:''" json:"last_message_preview"`
LastMessageAt *time.Time `json:"last_message_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (ChatConversation) TableName() string {
@@ -76,6 +87,24 @@ func (ChatMessage) TableName() string {
return "chat_messages"
}
// AfterCreate 维护客服列表所需的两个消息游标;列表页只比较游标,不再对每个会话聚合消息表。
func (m *ChatMessage) AfterCreate(tx *gorm.DB) error {
if m.ID == 0 || m.ConversationID == 0 {
return nil
}
updates := map[string]any{}
if m.AdminAttentionType != "" || m.SenderType == "user" {
updates["last_attention_message_id"] = m.ID
}
if m.SenderType == "admin" {
updates["last_admin_message_id"] = m.ID
}
if len(updates) == 0 {
return nil
}
return tx.Model(&ChatConversation{}).Where("id = ?", m.ConversationID).Updates(updates).Error
}
type ChatQrCode struct {
ID uint64 `gorm:"primaryKey" json:"id"`
ImageURL string `gorm:"size:512;not null" json:"image_url"`
+19
View File
@@ -4,6 +4,7 @@ import (
"time"
"gorm.io/datatypes"
"gorm.io/gorm"
)
type RentalOrder struct {
@@ -73,6 +74,24 @@ func (RentalOrder) TableName() string {
return "rental_orders"
}
// AfterSave 将商品关联发布群的最新订单快照一并更新。
// 客服列表会高频按订单状态筛选,直接读这个快照可避免每次聚合全量 rental_orders。
func (o *RentalOrder) AfterSave(tx *gorm.DB) error {
if o.ID == 0 || o.ListingID == 0 {
return nil
}
return tx.Model(&ChatConversation{}).
Where("listing_id = ? AND order_id IS NULL", o.ListingID).
Where("latest_order_id IS NULL OR latest_order_id <= ?", o.ID).
Updates(map[string]any{
"latest_order_id": o.ID,
"latest_order_no": o.OrderNo,
"latest_order_status": o.Status,
"latest_order_handoff_status": o.HandoffStatus,
"latest_order_refund_status": o.RefundStatus,
}).Error
}
// 押金暂扣状态:客服可对进行中订单的押金退款进行暂扣,订单照常结算,
// 但本应原路退给租客的押金部分挂起不退,后续由客服手动归还。
const (
@@ -7,6 +7,9 @@ import (
)
func (r *Repository) Dashboard(ctx context.Context, query DashboardQuery) (*DashboardDTO, error) {
if cached := r.loadDashboardCache(ctx, query); cached != nil {
return cached, nil
}
dailyItems, err := r.dailyItems(ctx, query)
if err != nil {
return nil, err
@@ -31,7 +34,7 @@ func (r *Repository) Dashboard(ctx context.Context, query DashboardQuery) (*Dash
if err != nil {
return nil, err
}
return &DashboardDTO{
result := &DashboardDTO{
Summary: *summary,
DailyItems: dailyItems,
PickupSummary: *pickup,
@@ -39,7 +42,9 @@ func (r *Repository) Dashboard(ctx context.Context, query DashboardQuery) (*Dash
DisbursementSummary: *disbursement,
OperatingExpenseSummary: *operatingExpense,
GeneratedAt: timeutil.ShanghaiNow(),
}, nil
}
r.storeDashboardCache(ctx, query, result)
return result, nil
}
func (r *Repository) operatingExpenseSummary(ctx context.Context, query DashboardQuery) (*OperatingExpenseSummaryDTO, error) {
@@ -0,0 +1,41 @@
package adminfinance
import (
"context"
"encoding/json"
"fmt"
"time"
)
const dashboardCacheTTL = time.Minute
func (r *Repository) dashboardCacheKey(query DashboardQuery) string {
return fmt.Sprintf("admin-finance:dashboard:v1:%d:%d", query.StartDate.Unix(), query.EndDate.Unix())
}
func (r *Repository) loadDashboardCache(ctx context.Context, query DashboardQuery) *DashboardDTO {
if r.redis == nil {
return nil
}
raw, err := r.redis.Get(ctx, r.dashboardCacheKey(query)).Bytes()
if err != nil {
return nil
}
var value DashboardDTO
if err := json.Unmarshal(raw, &value); err != nil {
return nil
}
return &value
}
func (r *Repository) storeDashboardCache(ctx context.Context, query DashboardQuery, value *DashboardDTO) {
if r.redis == nil || value == nil {
return
}
raw, err := json.Marshal(value)
if err != nil {
return
}
// 缓存不可用时静默降级到实时统计,不能影响财务页面可用性。
_ = r.redis.Set(ctx, r.dashboardCacheKey(query), raw, dashboardCacheTTL).Err()
}
@@ -1,13 +1,19 @@
package adminfinance
import (
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
type Repository struct {
db *gorm.DB
db *gorm.DB
redis *redis.Client
}
func NewRepository(db *gorm.DB) *Repository {
return &Repository{db: db}
func NewRepository(db *gorm.DB, redisClient ...*redis.Client) *Repository {
repo := &Repository{db: db}
if len(redisClient) > 0 {
repo.redis = redisClient[0]
}
return repo
}
@@ -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 将商品关联的发布群标记为已归档(解散),并写入系统提示。
+9 -10
View File
@@ -98,9 +98,9 @@ func (r *Repository) applyAdminListFilters(db *gorm.DB, query AdminListQuery) *g
like := "%" + keyword + "%"
if id, err := strconv.ParseUint(keyword, 10, 64); err == nil {
db = db.Where(
`(l.listing_no LIKE ? OR EXISTS (SELECT 1 FROM rental_orders AS o WHERE o.listing_id = l.id AND o.order_no LIKE ?) OR l.id = ? OR l.account_id = ? OR l.owner_id = ?)`,
like,
like,
`(l.listing_no = ? OR EXISTS (SELECT 1 FROM rental_orders AS o WHERE o.listing_id = l.id AND o.order_no = ?) OR l.id = ? OR l.account_id = ? OR l.owner_id = ?)`,
keyword,
keyword,
id,
id,
id,
@@ -204,22 +204,21 @@ func (r *Repository) findDTO(ctx context.Context, where string, args ...any) (*L
}
func (r *Repository) baseQuery(ctx context.Context) *gorm.DB {
uploadSource := r.db.WithContext(ctx).Table("listing_uploads").
Select("listing_id, MAX(id) AS upload_id, MAX(NULLIF(source_channel, '')) AS source_channel").
Where("listing_id IS NOT NULL").
Group("listing_id")
return r.db.WithContext(ctx).Table("rental_listings AS l").
Select(`l.*, a.title, a.description, a.game_name, a.server_region, a.login_platform, a.rank_level,
a.haf_coin_amount, a.asset_summary, a.screenshot_urls, COALESCE(u.phone, '') AS owner_phone, COALESCE(u.nickname, '') AS owner_nickname,
CASE WHEN lu.upload_id IS NULL THEN 0 ELSE 1 END AS is_external_upload,
CASE WHEN lu.id IS NULL THEN 0 ELSE 1 END AS is_external_upload,
CASE
WHEN lu.upload_id IS NULL THEN ?
WHEN lu.id IS NULL THEN ?
WHEN COALESCE(lu.source_channel, '') = '' THEN ?
ELSE lu.source_channel
END AS source_channel`, sourceChannelWebsite, sourceChannelExternalUnknown).
Joins("JOIN game_accounts AS a ON a.id = l.account_id").
Joins("LEFT JOIN users AS u ON u.id = l.owner_id").
Joins("LEFT JOIN (?) AS lu ON lu.listing_id = l.id", uploadSource)
// 逐商品按 listing_id 索引定位最后一次导入,避免每次列表查询都对全量导入记录分组。
Joins(`LEFT JOIN listing_uploads AS lu ON lu.id = (
SELECT MAX(lu_latest.id) FROM listing_uploads AS lu_latest WHERE lu_latest.listing_id = l.id
)`)
}
func (r *Repository) findForReviewUpdate(tx *gorm.DB, listingID uint64) (*model.RentalListing, *model.GameAccount, error) {
+5 -6
View File
@@ -100,12 +100,11 @@ func applyAdminOrderFilters(db *gorm.DB, query AdminOrderQuery) *gorm.DB {
like := "%" + keyword + "%"
if id, err := strconv.ParseUint(keyword, 10, 64); err == nil {
db = db.Where(
`(o.order_no LIKE ? OR l.listing_no LIKE ? OR a.title LIKE ? OR owner.phone LIKE ? OR renter.phone LIKE ? OR o.id = ? OR o.listing_id = ? OR o.owner_id = ? OR o.renter_id = ?)`,
like,
like,
like,
like,
like,
`(o.order_no = ? OR l.listing_no = ? OR owner.phone = ? OR renter.phone = ? OR o.id = ? OR o.listing_id = ? OR o.owner_id = ? OR o.renter_id = ?)`,
keyword,
keyword,
keyword,
keyword,
id,
id,
id,
+1 -1
View File
@@ -134,7 +134,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
adminDashboardHandler := admindashboard.NewHandler(adminDashboardService)
var adminFinanceRepo *adminfinance.Repository
if deps.DB != nil {
adminFinanceRepo = adminfinance.NewRepository(deps.DB)
adminFinanceRepo = adminfinance.NewRepository(deps.DB, deps.Redis)
}
adminFinanceService := adminfinance.NewService(adminFinanceRepo)
adminFinanceHandler := adminfinance.NewHandler(adminFinanceService)