// 功能:从本人业务事实生成消息中心列表,并持久化已读回执;版本:1.0.0。 package user import ( "fmt" "sort" "strings" "time" "git.apinb.com/bsm-sdk/core/errcode" "git.apinb.com/bsm-sdk/core/infra" "git.apinb.com/heqiapp/platforms/backend/api/internal/impl" "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common" "git.apinb.com/heqiapp/platforms/backend/api/internal/models" "github.com/gin-gonic/gin" "gorm.io/gorm/clause" ) type userMessage struct { Key, Category, Title, Summary, StatusText, Target, TargetIdentity string OccurredAt time.Time Read bool } // ListMessages 返回真实订单、工单和公告形成的消息,不生成不存在的安全告警。 func ListMessages(ctx *gin.Context) { account, ok := common.UserAccount(ctx) if !ok { return } messages, err := loadUserMessages(account.ID) if err != nil { infra.Response.Error(ctx, err) return } var receipts []models.UserMessageRead if err = impl.DBService.Where("user_account_id = ?", account.ID).Find(&receipts).Error; err != nil { infra.Response.Error(ctx, err) return } read := make(map[string]bool, len(receipts)) for _, receipt := range receipts { read[receipt.MessageKey] = true } counts := map[string]int{"all": len(messages), "unread": 0, "safety": 0, "order": 0, "service": 0, "notice": 0} rows := make([]gin.H, 0, len(messages)) for _, item := range messages { item.Read = read[item.Key] counts[item.Category]++ if !item.Read { counts["unread"]++ } rows = append(rows, gin.H{"key": item.Key, "category": item.Category, "title": item.Title, "summary": item.Summary, "status_text": item.StatusText, "target": item.Target, "target_identity": item.TargetIdentity, "occurred_at": item.OccurredAt, "read": item.Read}) } infra.Response.Success(ctx, gin.H{"items": rows, "counts": counts}) } // MarkMessagesRead 仅接受当前列表中存在的消息键,防止写入任意或他人对象标识。 func MarkMessagesRead(ctx *gin.Context) { account, ok := common.UserAccount(ctx) if !ok { return } var request struct { Keys []string `json:"keys" binding:"required,min=1,max=200,dive,required,max=96"` } if err := ctx.ShouldBindJSON(&request); err != nil { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } messages, err := loadUserMessages(account.ID) if err != nil { infra.Response.Error(ctx, err) return } allowed := make(map[string]bool, len(messages)) for _, item := range messages { allowed[item.Key] = true } rows := make([]models.UserMessageRead, 0, len(request.Keys)) seen := map[string]bool{} for _, key := range request.Keys { key = strings.TrimSpace(key) if !allowed[key] || seen[key] { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } seen[key] = true rows = append(rows, models.UserMessageRead{Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusEnable}, UserAccountID: account.ID, MessageKey: key, ReadAt: time.Now()}) } if err = impl.DBService.Clauses(clause.OnConflict{DoNothing: true}).Create(&rows).Error; err != nil { infra.Response.Error(ctx, err) return } infra.Response.Success(ctx, gin.H{"read_count": len(rows)}) } func loadUserMessages(userID uint64) ([]userMessage, error) { messages := make([]userMessage, 0) var gasOrders []models.GasorderBasic if err := impl.DBService.Where("user_account_id = ? AND status <> ?", userID, common.StatusArchived).Order("updated_at desc").Limit(50).Find(&gasOrders).Error; err != nil { return nil, err } for _, row := range gasOrders { messages = append(messages, orderMessage("gas", row.Identity, row.OrderNo, row.OrderStatus, row.UpdatedAt)) } var shopOrders []models.EcOrder if err := impl.DBService.Where("user_account_id = ? AND status <> ?", userID, common.StatusArchived).Order("updated_at desc").Limit(50).Find(&shopOrders).Error; err != nil { return nil, err } for _, row := range shopOrders { messages = append(messages, orderMessage("shop", row.Identity, row.OrderNo, row.OrderStatus, row.UpdatedAt)) } var tickets []models.CsTicket if err := impl.DBService.Where("user_account_id = ? AND status <> ? AND category <> ?", userID, common.StatusArchived, "contract_change").Order("updated_at desc").Limit(50).Find(&tickets).Error; err != nil { return nil, err } for _, row := range tickets { messages = append(messages, userMessage{Key: "ticket:" + row.Identity, Category: "service", Title: ticketTitle(row.TicketStatus), Summary: "报修工单 " + row.TicketNo, StatusText: statusText(row.TicketStatus), Target: "ticket", TargetIdentity: row.Identity, OccurredAt: row.UpdatedAt}) } var notices []models.CmsContent if err := impl.DBService.Where("content_type = ? AND publish_status = ? AND status = ?", "notice", "published", common.StatusEnable).Order("updated_at desc").Limit(50).Find(¬ices).Error; err != nil { return nil, err } for _, row := range notices { messages = append(messages, userMessage{Key: "notice:" + row.Identity, Category: "notice", Title: row.Title, Summary: compactMessage(row.Body), StatusText: "平台公告", Target: "content", TargetIdentity: row.Identity, OccurredAt: row.UpdatedAt}) } sort.SliceStable(messages, func(i, j int) bool { return messages[i].OccurredAt.After(messages[j].OccurredAt) }) return messages, nil } func orderMessage(kind, identity, number string, status int, at time.Time) userMessage { name, target := "燃气订单", "gas_order" if kind == "shop" { name, target = "商城订单", "shop_order" } return userMessage{Key: kind + ":" + identity, Category: "order", Title: orderTitle(name, status), Summary: fmt.Sprintf("您的%s(订单号:%s)状态已更新", name, number), StatusText: statusText(status), Target: target, TargetIdentity: identity, OccurredAt: at} } func statusText(status int) string { return map[int]string{16: "待支付", 18: "待配送", 21: "异常", 22: "已取消", 23: "已完成", 29: "运输中", 32: "待受理", 31: "处理中", 33: "配送中", 34: "待确认", 35: "已支付"}[status] } func orderTitle(name string, status int) string { if value := statusText(status); value != "" { return name + value } return name + "状态更新" } func ticketTitle(status int) string { if status == common.StatusCompleted { return "报修工单已完成" } if status == common.StatusRepairing { return "报修工单处理中" } return "报修工单状态更新" } func compactMessage(value string) string { value = strings.Join(strings.Fields(value), " ") if len([]rune(value)) > 70 { return string([]rune(value)[:70]) + "…" } return value }