From 6d61027318d46e9e56d71c0e3b66651d5feac24e Mon Sep 17 00:00:00 2001 From: yml2213 Date: Thu, 16 Jul 2026 18:08:18 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=91=B8=E5=A4=A7=E7=BA=A2?= =?UTF-8?q?=E4=B8=9A=E5=8A=A1=E5=B9=B6=E4=BF=AE=E5=A4=8D=E5=90=8E=E5=8F=B0?= =?UTF-8?q?=E9=93=BE=E6=8E=A5=E6=8C=89=E9=92=AE=E7=99=BD=E5=AD=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 支持分类筛选、搜索、下单支付建群与固定二维码;合并迁移种子数据;后台表格编辑/详情链接恢复主题色可见。 --- backend/internal/model/chat.go | 1 + backend/internal/model/mohong.go | 98 + backend/internal/modules/chat/export.go | 36 + backend/internal/modules/file/handler.go | 7 +- backend/internal/modules/file/service.go | 4 +- .../internal/modules/mohong/category_repo.go | 204 ++ backend/internal/modules/mohong/config.go | 117 + backend/internal/modules/mohong/dto.go | 167 ++ backend/internal/modules/mohong/errors.go | 16 + backend/internal/modules/mohong/handler.go | 415 +++ backend/internal/modules/mohong/order_repo.go | 470 ++++ backend/internal/modules/mohong/presenter.go | 132 + .../internal/modules/mohong/product_repo.go | 303 +++ backend/internal/modules/mohong/repository.go | 19 + backend/internal/modules/mohong/service.go | 173 ++ .../modules/payment/channel_status.go | 33 +- backend/internal/modules/payment/handler.go | 38 + .../modules/payment/mohong_payment.go | 245 ++ .../internal/modules/payment/repository.go | 16 +- backend/internal/modules/payment/service.go | 20 + backend/internal/router/router.go | 56 +- backend/migrations/000028_mohong.sql | 125 + .../000029_mohong_categories_and_seed.sql | 2410 +++++++++++++++++ backend/scripts/mohong_seed_from_har.json | 44 + .../admin/views/AdminMohongCategoriesView.vue | 184 ++ .../admin/views/AdminMohongConfigView.vue | 149 + .../admin/views/AdminMohongOrdersView.vue | 244 ++ .../admin/views/AdminMohongProductsView.vue | 418 +++ .../src/features/listings/views/HomeView.vue | 58 +- .../listings/views/MobileHomeView.css | 69 + .../listings/views/MobileHomeView.vue | 14 + frontend/src/features/mohong/api/mohong.ts | 290 ++ .../mohong/components/MohongProductCard.vue | 177 ++ .../mohong/views/MobileMohongDetailView.vue | 318 +++ .../mohong/views/MobileMohongListView.vue | 386 +++ .../views/MobileMohongOrderDetailView.vue | 339 +++ .../mohong/views/MobileMohongOrdersView.vue | 154 ++ .../mohong/views/MohongDetailView.vue | 531 ++++ .../features/mohong/views/MohongListView.vue | 406 +++ .../mohong/views/MohongOrderDetailView.vue | 437 +++ .../mohong/views/MohongOrdersView.vue | 94 + .../composables/useMobilePaymentCashier.ts | 10 +- .../composables/useOrderPaymentCashier.ts | 8 +- frontend/src/layouts/AdminLayout.vue | 25 + frontend/src/router/adminRoutes.ts | 24 + frontend/src/router/mobileHost.ts | 6 + frontend/src/router/mobileRoutes.ts | 24 + frontend/src/router/publicRoutes.ts | 23 + frontend/src/styles/admin.css | 16 +- 49 files changed, 9533 insertions(+), 20 deletions(-) create mode 100644 backend/internal/model/mohong.go create mode 100644 backend/internal/modules/chat/export.go create mode 100644 backend/internal/modules/mohong/category_repo.go create mode 100644 backend/internal/modules/mohong/config.go create mode 100644 backend/internal/modules/mohong/dto.go create mode 100644 backend/internal/modules/mohong/errors.go create mode 100644 backend/internal/modules/mohong/handler.go create mode 100644 backend/internal/modules/mohong/order_repo.go create mode 100644 backend/internal/modules/mohong/presenter.go create mode 100644 backend/internal/modules/mohong/product_repo.go create mode 100644 backend/internal/modules/mohong/repository.go create mode 100644 backend/internal/modules/mohong/service.go create mode 100644 backend/internal/modules/payment/mohong_payment.go create mode 100644 backend/migrations/000028_mohong.sql create mode 100644 backend/migrations/000029_mohong_categories_and_seed.sql create mode 100644 backend/scripts/mohong_seed_from_har.json create mode 100644 frontend/src/features/admin/views/AdminMohongCategoriesView.vue create mode 100644 frontend/src/features/admin/views/AdminMohongConfigView.vue create mode 100644 frontend/src/features/admin/views/AdminMohongOrdersView.vue create mode 100644 frontend/src/features/admin/views/AdminMohongProductsView.vue create mode 100644 frontend/src/features/mohong/api/mohong.ts create mode 100644 frontend/src/features/mohong/components/MohongProductCard.vue create mode 100644 frontend/src/features/mohong/views/MobileMohongDetailView.vue create mode 100644 frontend/src/features/mohong/views/MobileMohongListView.vue create mode 100644 frontend/src/features/mohong/views/MobileMohongOrderDetailView.vue create mode 100644 frontend/src/features/mohong/views/MobileMohongOrdersView.vue create mode 100644 frontend/src/features/mohong/views/MohongDetailView.vue create mode 100644 frontend/src/features/mohong/views/MohongListView.vue create mode 100644 frontend/src/features/mohong/views/MohongOrderDetailView.vue create mode 100644 frontend/src/features/mohong/views/MohongOrdersView.vue diff --git a/backend/internal/model/chat.go b/backend/internal/model/chat.go index d4ca2f1..d4b5a71 100644 --- a/backend/internal/model/chat.go +++ b/backend/internal/model/chat.go @@ -10,6 +10,7 @@ type ChatConversation struct { ID uint64 `gorm:"primaryKey" json:"id"` OrderID *uint64 `gorm:"uniqueIndex" json:"order_id"` ListingID *uint64 `gorm:"index" json:"listing_id"` + MohongOrderID *uint64 `gorm:"column:mohong_order_id;uniqueIndex" json:"mohong_order_id"` Type string `gorm:"size:32;not null;default:'order_group'" json:"type"` Title string `gorm:"size:128;not null" json:"title"` Status string `gorm:"size:32;not null;default:'active'" json:"status"` diff --git a/backend/internal/model/mohong.go b/backend/internal/model/mohong.go new file mode 100644 index 0000000..f498d00 --- /dev/null +++ b/backend/internal/model/mohong.go @@ -0,0 +1,98 @@ +package model + +import ( + "time" + + "gorm.io/datatypes" +) + +// MohongCategory 摸大红商品分类。 +type MohongCategory struct { + ID uint64 `gorm:"primaryKey" json:"id"` + Name string `gorm:"size:64;not null;uniqueIndex" json:"name"` + Code string `gorm:"size:64;not null;default:''" json:"code"` + SortOrder int `gorm:"column:sort_order;not null;default:0" json:"sort_order"` + Status string `gorm:"size:16;not null;default:'enabled'" json:"status"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +func (MohongCategory) TableName() string { + return "mohong_categories" +} + +// MohongProductCategory 商品与分类多对多(同一商品可出现在大红/九格等)。 +type MohongProductCategory struct { + ProductID uint64 `gorm:"column:product_id;primaryKey" json:"product_id"` + CategoryID uint64 `gorm:"column:category_id;primaryKey;index" json:"category_id"` +} + +func (MohongProductCategory) TableName() string { + return "mohong_product_categories" +} + +// MohongProduct 摸大红商品。 +type MohongProduct struct { + ID uint64 `gorm:"primaryKey" json:"id"` + CategoryID *uint64 `gorm:"column:category_id;index" json:"category_id"` // 主分类(展示用) + Title string `gorm:"size:128;not null" json:"title"` + CoverURL string `gorm:"column:cover_url;size:512;not null;default:''" json:"cover_url"` + ImageURLs datatypes.JSON `gorm:"column:image_urls" json:"image_urls"` + Description string `gorm:"type:text;not null" json:"description"` + PriceCent int64 `gorm:"column:price_cent;not null;default:0" json:"-"` + OriginalPriceCent int64 `gorm:"column:original_price_cent;not null;default:0" json:"-"` + Unit string `gorm:"size:32;not null;default:'份'" json:"unit"` + Stock int `gorm:"not null;default:-1" json:"stock"` + SortOrder int `gorm:"column:sort_order;not null;default:0" json:"sort_order"` + Status string `gorm:"size:16;not null;default:'draft'" json:"status"` + QrcodeImageURL string `gorm:"column:qrcode_image_url;size:512;not null;default:''" json:"qrcode_image_url"` + CreatedBy *uint64 `gorm:"column:created_by" json:"created_by"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +func (MohongProduct) TableName() string { + return "mohong_products" +} + +// MohongOrder 摸大红订单。 +type MohongOrder struct { + ID uint64 `gorm:"primaryKey" json:"id"` + OrderNo string `gorm:"size:64;not null;uniqueIndex" json:"order_no"` + UserID uint64 `gorm:"not null;index" json:"user_id"` + ProductID uint64 `gorm:"not null;index" json:"product_id"` + Quantity int `gorm:"not null;default:1" json:"quantity"` + UnitPriceCent int64 `gorm:"column:unit_price_cent;not null;default:0" json:"-"` + AmountCent int64 `gorm:"column:amount_cent;not null;default:0" json:"-"` + Status string `gorm:"size:32;not null;default:'pending_payment';index" json:"status"` + ProductSnapshot datatypes.JSON `gorm:"column:product_snapshot" json:"product_snapshot"` + QrcodeURLSnapshot string `gorm:"column:qrcode_url_snapshot;size:512;not null;default:''" json:"qrcode_url_snapshot"` + CopyText string `gorm:"type:text;not null" json:"copy_text"` + ConversationID *uint64 `gorm:"column:conversation_id" json:"conversation_id"` + PaidAt *time.Time `json:"paid_at"` + CompletedAt *time.Time `json:"completed_at"` + CancelledAt *time.Time `json:"cancelled_at"` + CancelReason string `gorm:"size:255;not null;default:''" json:"cancel_reason"` + AdminRemark string `gorm:"size:255;not null;default:''" json:"admin_remark"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +func (MohongOrder) TableName() string { + return "mohong_orders" +} + +const ( + MohongCategoryStatusEnabled = "enabled" + MohongCategoryStatusDisabled = "disabled" + + MohongProductStatusDraft = "draft" + MohongProductStatusOnSale = "on_sale" + MohongProductStatusOffSale = "off_sale" + + MohongOrderStatusPendingPayment = "pending_payment" + MohongOrderStatusPaid = "paid" + MohongOrderStatusCompleted = "completed" + MohongOrderStatusCancelled = "cancelled" + MohongOrderStatusRefunded = "refunded" +) diff --git a/backend/internal/modules/chat/export.go b/backend/internal/modules/chat/export.go new file mode 100644 index 0000000..a2b1927 --- /dev/null +++ b/backend/internal/modules/chat/export.go @@ -0,0 +1,36 @@ +package chat + +import ( + "hfb_sys/backend/internal/model" + + "gorm.io/gorm" +) + +// DefaultSupportAdminID 导出默认客服选择逻辑,供其他业务模块建群时复用。 +func DefaultSupportAdminID(tx *gorm.DB) uint64 { + return defaultSupportAdminID(tx) +} + +// SendSystemMessageForTx 在事务内发送系统文本消息。 +func SendSystemMessageForTx(tx *gorm.DB, conversationID uint64, content string) error { + return sendSystemMessage(tx, conversationID, content) +} + +// SendImageMessageForTx 在事务内发送系统图片消息。 +func SendImageMessageForTx(tx *gorm.DB, conversationID uint64, content, imageURL string) error { + if content == "" { + content = "👇 请扫码联系客服" + } + message := model.ChatMessage{ + ConversationID: conversationID, + SenderType: "system", + SenderRole: "system", + ContentType: "image", + Content: content, + AttachmentURLS: encodeStringList([]string{imageURL}), + } + if err := tx.Create(&message).Error; err != nil { + return err + } + return updateConversationLastMessage(tx, conversationID, &message) +} diff --git a/backend/internal/modules/file/handler.go b/backend/internal/modules/file/handler.go index aaa9263..831b27c 100644 --- a/backend/internal/modules/file/handler.go +++ b/backend/internal/modules/file/handler.go @@ -65,7 +65,12 @@ func (h *Handler) writeObject(c *gin.Context, publicOnly bool) { response.BadRequest(c, "文件 key 不正确") return } - if publicOnly && !strings.HasPrefix(key, "home-banner/") && !strings.HasPrefix(key, "avatar/") && !strings.HasPrefix(key, "payment-cert/") && !strings.HasPrefix(key, "announcement/") { + if publicOnly && + !strings.HasPrefix(key, "home-banner/") && + !strings.HasPrefix(key, "avatar/") && + !strings.HasPrefix(key, "payment-cert/") && + !strings.HasPrefix(key, "announcement/") && + !strings.HasPrefix(key, "mohong/") { response.Error(c, http.StatusNotFound, "not_found", "文件不存在或暂不可访问") return } diff --git a/backend/internal/modules/file/service.go b/backend/internal/modules/file/service.go index 2b52b6b..1a35798 100644 --- a/backend/internal/modules/file/service.go +++ b/backend/internal/modules/file/service.go @@ -106,7 +106,7 @@ func normalizeContentType(contentType string, data []byte) string { func fileURLForScene(scene string, key string) string { fileURL := "/api/files/object?key=" + url.QueryEscape(key) - if scene == "home-banner" || scene == "avatar" || scene == "payment-cert" || scene == "announcement" { + if scene == "home-banner" || scene == "avatar" || scene == "payment-cert" || scene == "announcement" || scene == "mohong" { fileURL = "/api/public/files/object?key=" + url.QueryEscape(key) } return fileURL @@ -115,7 +115,7 @@ func fileURLForScene(scene string, key string) string { func normalizeScene(scene string) string { scene = strings.TrimSpace(strings.ToLower(scene)) switch scene { - case "listing", "handoff", "dispute", "realname", "avatar", "home-banner", "chat", "payment-cert", "announcement", "qrcode": + case "listing", "handoff", "dispute", "realname", "avatar", "home-banner", "chat", "payment-cert", "announcement", "qrcode", "mohong": return scene default: return "misc" diff --git a/backend/internal/modules/mohong/category_repo.go b/backend/internal/modules/mohong/category_repo.go new file mode 100644 index 0000000..7ce5786 --- /dev/null +++ b/backend/internal/modules/mohong/category_repo.go @@ -0,0 +1,204 @@ +package mohong + +import ( + "context" + "strings" + + "hfb_sys/backend/internal/model" + + "gorm.io/gorm" +) + +func (r *Repository) ListPublicCategories(ctx context.Context) ([]CategoryDTO, error) { + var rows []model.MohongCategory + if err := r.db.WithContext(ctx). + Where("status = ?", model.MohongCategoryStatusEnabled). + Order("sort_order ASC, id ASC"). + Find(&rows).Error; err != nil { + return nil, err + } + return r.attachCategoryCounts(ctx, rows, true) +} + +func (r *Repository) AdminListCategories(ctx context.Context) ([]CategoryDTO, error) { + var rows []model.MohongCategory + if err := r.db.WithContext(ctx). + Order("sort_order ASC, id ASC"). + Find(&rows).Error; err != nil { + return nil, err + } + return r.attachCategoryCounts(ctx, rows, false) +} + +func (r *Repository) attachCategoryCounts(ctx context.Context, rows []model.MohongCategory, onlyOnSale bool) ([]CategoryDTO, error) { + type countRow struct { + CategoryID uint64 + Cnt int64 + } + var counts []countRow + // 优先按多对多关联统计;兼容仅写了主分类 category_id 的旧数据 + q := r.db.WithContext(ctx).Table("mohong_product_categories AS pc"). + Select("pc.category_id AS category_id, COUNT(DISTINCT pc.product_id) AS cnt"). + Joins("JOIN mohong_products p ON p.id = pc.product_id") + if onlyOnSale { + q = q.Where("p.status = ?", model.MohongProductStatusOnSale) + } + if err := q.Group("pc.category_id").Scan(&counts).Error; err != nil { + return nil, err + } + // 补充仅有主分类、未写入关联表的商品 + var fallback []countRow + fq := r.db.WithContext(ctx).Model(&model.MohongProduct{}). + Select("category_id AS category_id, COUNT(*) AS cnt"). + Where("category_id IS NOT NULL"). + Where("id NOT IN (SELECT product_id FROM mohong_product_categories)") + if onlyOnSale { + fq = fq.Where("status = ?", model.MohongProductStatusOnSale) + } + _ = fq.Group("category_id").Scan(&fallback).Error + for _, c := range fallback { + counts = append(counts, c) + } + countMap := map[uint64]int64{} + for _, c := range counts { + countMap[c.CategoryID] = c.Cnt + } + items := make([]CategoryDTO, 0, len(rows)) + for _, row := range rows { + items = append(items, CategoryDTO{ + ID: row.ID, + Name: row.Name, + Code: row.Code, + SortOrder: row.SortOrder, + Status: row.Status, + ProductCount: countMap[row.ID], + CreatedAt: row.CreatedAt, + UpdatedAt: row.UpdatedAt, + }) + } + return items, nil +} + +func (r *Repository) CreateCategory(ctx context.Context, req CreateCategoryRequest) (*CategoryDTO, error) { + name := strings.TrimSpace(req.Name) + if name == "" { + return nil, ErrInvalidRequest + } + status := strings.TrimSpace(req.Status) + if status == "" { + status = model.MohongCategoryStatusEnabled + } + if status != model.MohongCategoryStatusEnabled && status != model.MohongCategoryStatusDisabled { + return nil, ErrInvalidRequest + } + row := model.MohongCategory{ + Name: name, + Code: strings.TrimSpace(req.Code), + SortOrder: req.SortOrder, + Status: status, + } + if err := r.db.WithContext(ctx).Create(&row).Error; err != nil { + return nil, err + } + return &CategoryDTO{ + ID: row.ID, + Name: row.Name, + Code: row.Code, + SortOrder: row.SortOrder, + Status: row.Status, + CreatedAt: row.CreatedAt, + UpdatedAt: row.UpdatedAt, + }, nil +} + +func (r *Repository) UpdateCategory(ctx context.Context, id uint64, req UpdateCategoryRequest) (*CategoryDTO, error) { + var row model.MohongCategory + if err := r.db.WithContext(ctx).First(&row, id).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return nil, ErrInvalidRequest + } + return nil, err + } + updates := map[string]any{} + if req.Name != nil { + name := strings.TrimSpace(*req.Name) + if name == "" { + return nil, ErrInvalidRequest + } + updates["name"] = name + } + if req.Code != nil { + updates["code"] = strings.TrimSpace(*req.Code) + } + if req.SortOrder != nil { + updates["sort_order"] = *req.SortOrder + } + if req.Status != nil { + status := strings.TrimSpace(*req.Status) + if status != model.MohongCategoryStatusEnabled && status != model.MohongCategoryStatusDisabled { + return nil, ErrInvalidRequest + } + updates["status"] = status + } + if len(updates) > 0 { + if err := r.db.WithContext(ctx).Model(&row).Updates(updates).Error; err != nil { + return nil, err + } + } + if err := r.db.WithContext(ctx).First(&row, id).Error; err != nil { + return nil, err + } + items, err := r.attachCategoryCounts(ctx, []model.MohongCategory{row}, false) + if err != nil { + return nil, err + } + return &items[0], nil +} + +func (r *Repository) DeleteCategory(ctx context.Context, id uint64) error { + return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + if err := tx.Model(&model.MohongProduct{}). + Where("category_id = ?", id). + Update("category_id", nil).Error; err != nil { + return err + } + res := tx.Delete(&model.MohongCategory{}, id) + if res.Error != nil { + return res.Error + } + if res.RowsAffected == 0 { + return ErrInvalidRequest + } + return nil + }) +} + +func (r *Repository) loadCategoryNameMap(ctx context.Context, ids []uint64) map[uint64]string { + out := map[uint64]string{} + if len(ids) == 0 { + return out + } + uniq := make([]uint64, 0, len(ids)) + seen := map[uint64]struct{}{} + for _, id := range ids { + if id == 0 { + continue + } + if _, ok := seen[id]; ok { + continue + } + seen[id] = struct{}{} + uniq = append(uniq, id) + } + if len(uniq) == 0 { + return out + } + var rows []model.MohongCategory + if err := r.db.WithContext(ctx).Where("id IN ?", uniq).Find(&rows).Error; err != nil { + return out + } + for _, row := range rows { + out[row.ID] = row.Name + } + return out +} diff --git a/backend/internal/modules/mohong/config.go b/backend/internal/modules/mohong/config.go new file mode 100644 index 0000000..84d89c7 --- /dev/null +++ b/backend/internal/modules/mohong/config.go @@ -0,0 +1,117 @@ +package mohong + +import ( + "context" + "strings" + + "hfb_sys/backend/internal/model" + + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +const ( + configKeyDefaultQrcodeURL = "mohong.default_qrcode_url" + configKeyGroupWelcomeText = "mohong.group_welcome_text" + configKeyOrderCopyTemplate = "mohong.order_copy_template" + + defaultGroupWelcomeText = "订单已支付,请扫码联系客服,并将下方订单信息复制发送给客服。" + defaultOrderCopyTemplate = "【摸大红订单】\n订单号:{{order_no}}\n下单时间:{{created_at}}\n商品:{{product_title}}\n数量:{{quantity}}\n金额:{{amount}}\n下单人:{{buyer_name}}({{buyer_phone}})" +) + +func (r *Repository) GetConfig(ctx context.Context) (*ConfigDTO, error) { + if r.db == nil { + return nil, ErrDependencyUnavailable + } + keys := []string{configKeyDefaultQrcodeURL, configKeyGroupWelcomeText, configKeyOrderCopyTemplate} + var rows []model.SystemConfig + if err := r.db.WithContext(ctx).Where("`key` IN ?", keys).Find(&rows).Error; err != nil { + return nil, err + } + values := map[string]string{} + for _, row := range rows { + values[row.Key] = row.Value + } + return &ConfigDTO{ + DefaultQrcodeURL: strings.TrimSpace(values[configKeyDefaultQrcodeURL]), + GroupWelcomeText: firstNonEmpty(values[configKeyGroupWelcomeText], defaultGroupWelcomeText), + OrderCopyTemplate: firstNonEmpty(values[configKeyOrderCopyTemplate], defaultOrderCopyTemplate), + }, nil +} + +func (r *Repository) UpdateConfig(ctx context.Context, req UpdateConfigRequest) (*ConfigDTO, error) { + if r.db == nil { + return nil, ErrDependencyUnavailable + } + err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + if req.DefaultQrcodeURL != nil { + if err := upsertConfig(tx, configKeyDefaultQrcodeURL, strings.TrimSpace(*req.DefaultQrcodeURL), "摸大红全局默认固定二维码图片URL"); err != nil { + return err + } + } + if req.GroupWelcomeText != nil { + if err := upsertConfig(tx, configKeyGroupWelcomeText, strings.TrimSpace(*req.GroupWelcomeText), "摸大红订单群欢迎语"); err != nil { + return err + } + } + if req.OrderCopyTemplate != nil { + if err := upsertConfig(tx, configKeyOrderCopyTemplate, strings.TrimSpace(*req.OrderCopyTemplate), "摸大红订单一键复制文案模板"); err != nil { + return err + } + } + return nil + }) + if err != nil { + return nil, err + } + return r.GetConfig(ctx) +} + +func upsertConfig(tx *gorm.DB, key, value, description string) error { + row := model.SystemConfig{ + Key: key, + Value: value, + Description: description, + } + return tx.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "key"}}, + DoUpdates: clause.AssignmentColumns([]string{"value", "description"}), + }).Create(&row).Error +} + +func firstNonEmpty(values ...string) string { + for _, v := range values { + if strings.TrimSpace(v) != "" { + return strings.TrimSpace(v) + } + } + return "" +} + +func (r *Repository) resolveQrcodeURL(tx *gorm.DB, productQrcode string) string { + productQrcode = strings.TrimSpace(productQrcode) + if productQrcode != "" { + return productQrcode + } + var cfg model.SystemConfig + if err := tx.Where("`key` = ?", configKeyDefaultQrcodeURL).First(&cfg).Error; err == nil { + return strings.TrimSpace(cfg.Value) + } + return "" +} + +func (r *Repository) loadWelcomeText(tx *gorm.DB) string { + var cfg model.SystemConfig + if err := tx.Where("`key` = ?", configKeyGroupWelcomeText).First(&cfg).Error; err == nil && strings.TrimSpace(cfg.Value) != "" { + return strings.TrimSpace(cfg.Value) + } + return defaultGroupWelcomeText +} + +func (r *Repository) loadCopyTemplate(tx *gorm.DB) string { + var cfg model.SystemConfig + if err := tx.Where("`key` = ?", configKeyOrderCopyTemplate).First(&cfg).Error; err == nil && strings.TrimSpace(cfg.Value) != "" { + return strings.TrimSpace(cfg.Value) + } + return defaultOrderCopyTemplate +} diff --git a/backend/internal/modules/mohong/dto.go b/backend/internal/modules/mohong/dto.go new file mode 100644 index 0000000..d108763 --- /dev/null +++ b/backend/internal/modules/mohong/dto.go @@ -0,0 +1,167 @@ +package mohong + +import "time" + +type CategoryDTO struct { + ID uint64 `json:"id"` + Name string `json:"name"` + Code string `json:"code"` + SortOrder int `json:"sort_order"` + Status string `json:"status"` + ProductCount int64 `json:"product_count"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type CreateCategoryRequest struct { + Name string `json:"name" binding:"required,max=64"` + Code string `json:"code"` + SortOrder int `json:"sort_order"` + Status string `json:"status"` +} + +type UpdateCategoryRequest struct { + Name *string `json:"name"` + Code *string `json:"code"` + SortOrder *int `json:"sort_order"` + Status *string `json:"status"` +} + +type ProductDTO struct { + ID uint64 `json:"id"` + CategoryID *uint64 `json:"category_id"` + CategoryName string `json:"category_name,omitempty"` + Title string `json:"title"` + CoverURL string `json:"cover_url"` + ImageURLs []string `json:"image_urls"` + Description string `json:"description"` + PriceCent int64 `json:"price_cent"` + Price string `json:"price"` + OriginalPriceCent int64 `json:"original_price_cent"` + OriginalPrice string `json:"original_price,omitempty"` + Unit string `json:"unit"` + Stock int `json:"stock"` + SortOrder int `json:"sort_order"` + Status string `json:"status"` + QrcodeImageURL string `json:"qrcode_image_url,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type ProductListQuery struct { + Keyword string + Status string + CategoryID uint64 + Page int + PageSize int +} + +type CreateProductRequest struct { + CategoryID *uint64 `json:"category_id"` + Title string `json:"title" binding:"required,max=128"` + CoverURL string `json:"cover_url"` + ImageURLs []string `json:"image_urls"` + Description string `json:"description"` + PriceCent int64 `json:"price_cent" binding:"required,min=1"` + OriginalPriceCent int64 `json:"original_price_cent"` + Unit string `json:"unit"` + Stock *int `json:"stock"` + SortOrder int `json:"sort_order"` + Status string `json:"status"` + QrcodeImageURL string `json:"qrcode_image_url"` +} + +type UpdateProductRequest struct { + CategoryID *uint64 `json:"category_id"` + ClearCategory bool `json:"clear_category"` + Title *string `json:"title"` + CoverURL *string `json:"cover_url"` + ImageURLs []string `json:"image_urls"` + Description *string `json:"description"` + PriceCent *int64 `json:"price_cent"` + OriginalPriceCent *int64 `json:"original_price_cent"` + Unit *string `json:"unit"` + Stock *int `json:"stock"` + SortOrder *int `json:"sort_order"` + Status *string `json:"status"` + QrcodeImageURL *string `json:"qrcode_image_url"` +} + +type CreateOrderRequest struct { + ProductID uint64 `json:"product_id" binding:"required"` + Quantity int `json:"quantity" binding:"required,min=1,max=99"` +} + +type OrderDTO struct { + ID uint64 `json:"id"` + OrderNo string `json:"order_no"` + UserID uint64 `json:"user_id"` + ProductID uint64 `json:"product_id"` + Quantity int `json:"quantity"` + UnitPriceCent int64 `json:"unit_price_cent"` + UnitPrice string `json:"unit_price"` + AmountCent int64 `json:"amount_cent"` + Amount string `json:"amount"` + Status string `json:"status"` + ProductTitle string `json:"product_title"` + ProductCoverURL string `json:"product_cover_url"` + ProductUnit string `json:"product_unit"` + QrcodeURLSnapshot string `json:"qrcode_url_snapshot"` + CopyText string `json:"copy_text"` + ConversationID *uint64 `json:"conversation_id"` + BuyerNickname string `json:"buyer_nickname,omitempty"` + BuyerPhone string `json:"buyer_phone,omitempty"` + AdminRemark string `json:"admin_remark,omitempty"` + CancelReason string `json:"cancel_reason,omitempty"` + PaidAt *time.Time `json:"paid_at"` + CompletedAt *time.Time `json:"completed_at"` + CancelledAt *time.Time `json:"cancelled_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type OrderListQuery struct { + UserID uint64 + Status string + Keyword string + Page int + PageSize int +} + +type AdminCompleteOrderRequest struct { + Remark string `json:"remark"` +} + +type AdminCancelOrderRequest struct { + Reason string `json:"reason"` +} + +type ConfigDTO struct { + DefaultQrcodeURL string `json:"default_qrcode_url"` + GroupWelcomeText string `json:"group_welcome_text"` + OrderCopyTemplate string `json:"order_copy_template"` +} + +type UpdateConfigRequest struct { + DefaultQrcodeURL *string `json:"default_qrcode_url"` + GroupWelcomeText *string `json:"group_welcome_text"` + OrderCopyTemplate *string `json:"order_copy_template"` +} + +type PaginatedResult struct { + Items any `json:"items"` + Total int64 `json:"total"` + Page int `json:"page"` + PageSize int `json:"page_size"` +} + +type productSnapshot struct { + ID uint64 `json:"id"` + Title string `json:"title"` + CoverURL string `json:"cover_url"` + ImageURLs []string `json:"image_urls"` + Description string `json:"description"` + PriceCent int64 `json:"price_cent"` + Unit string `json:"unit"` + QrcodeImageURL string `json:"qrcode_image_url"` +} diff --git a/backend/internal/modules/mohong/errors.go b/backend/internal/modules/mohong/errors.go new file mode 100644 index 0000000..1da9bf5 --- /dev/null +++ b/backend/internal/modules/mohong/errors.go @@ -0,0 +1,16 @@ +package mohong + +import "errors" + +var ( + ErrDependencyUnavailable = errors.New("dependency unavailable") + ErrProductNotFound = errors.New("product not found") + ErrProductUnavailable = errors.New("product unavailable") + ErrStockInsufficient = errors.New("stock insufficient") + ErrInvalidRequest = errors.New("invalid request") + ErrOrderNotFound = errors.New("order not found") + ErrOrderCannotPay = errors.New("order cannot pay") + ErrOrderCannotCancel = errors.New("order cannot cancel") + ErrOrderCannotComplete = errors.New("order cannot complete") + ErrUnauthorized = errors.New("unauthorized") +) diff --git a/backend/internal/modules/mohong/handler.go b/backend/internal/modules/mohong/handler.go new file mode 100644 index 0000000..f38b0bd --- /dev/null +++ b/backend/internal/modules/mohong/handler.go @@ -0,0 +1,415 @@ +package mohong + +import ( + "errors" + "strconv" + "strings" + + "hfb_sys/backend/internal/middleware" + "hfb_sys/backend/pkg/response" + + "github.com/gin-gonic/gin" +) + +type Handler struct { + service *Service +} + +func NewHandler(service *Service) *Handler { + return &Handler{service: service} +} + +func (h *Handler) ListCategories(c *gin.Context) { + items, err := h.service.ListPublicCategories(c.Request.Context()) + if err != nil { + response.InternalServerError(c, "获取分类失败") + return + } + response.OK(c, items) +} + +func (h *Handler) ListProducts(c *gin.Context) { + query := ProductListQuery{Keyword: c.Query("keyword")} + if catID, err := strconv.ParseUint(c.Query("category_id"), 10, 64); err == nil { + query.CategoryID = catID + } + query.Page, query.PageSize = parsePagination(c) + result, err := h.service.ListPublicProducts(c.Request.Context(), query) + if err != nil { + response.InternalServerError(c, "获取商品列表失败") + return + } + response.OK(c, result) +} + +func (h *Handler) GetProduct(c *gin.Context) { + id, ok := parseID(c) + if !ok { + return + } + item, err := h.service.FindPublicProduct(c.Request.Context(), id) + if err != nil { + writeError(c, err) + return + } + response.OK(c, item) +} + +func (h *Handler) CreateOrder(c *gin.Context) { + userID, ok := currentUserID(c) + if !ok { + response.Unauthorized(c, "缺少用户上下文") + return + } + var req CreateOrderRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "请求参数不正确") + return + } + item, err := h.service.CreateOrder(c.Request.Context(), userID, req) + if err != nil { + writeError(c, err) + return + } + response.OK(c, item) +} + +func (h *Handler) ListMyOrders(c *gin.Context) { + userID, ok := currentUserID(c) + if !ok { + response.Unauthorized(c, "缺少用户上下文") + return + } + query := OrderListQuery{Status: c.Query("status"), Keyword: c.Query("keyword")} + query.Page, query.PageSize = parsePagination(c) + result, err := h.service.ListOrdersForUser(c.Request.Context(), userID, query) + if err != nil { + response.InternalServerError(c, "获取订单列表失败") + return + } + response.OK(c, result) +} + +func (h *Handler) GetMyOrder(c *gin.Context) { + userID, ok := currentUserID(c) + if !ok { + response.Unauthorized(c, "缺少用户上下文") + return + } + id, ok := parseID(c) + if !ok { + return + } + item, err := h.service.FindOrderForUser(c.Request.Context(), userID, id) + if err != nil { + writeError(c, err) + return + } + response.OK(c, item) +} + +func (h *Handler) CancelMyOrder(c *gin.Context) { + userID, ok := currentUserID(c) + if !ok { + response.Unauthorized(c, "缺少用户上下文") + return + } + id, ok := parseID(c) + if !ok { + return + } + var body struct { + Reason string `json:"reason"` + } + _ = c.ShouldBindJSON(&body) + item, err := h.service.CancelOrder(c.Request.Context(), userID, id, body.Reason) + if err != nil { + writeError(c, err) + return + } + response.OK(c, item) +} + +func (h *Handler) AdminListCategories(c *gin.Context) { + items, err := h.service.AdminListCategories(c.Request.Context()) + if err != nil { + response.InternalServerError(c, "获取分类失败") + return + } + response.OK(c, items) +} + +func (h *Handler) AdminCreateCategory(c *gin.Context) { + var req CreateCategoryRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "请求参数不正确") + return + } + item, err := h.service.CreateCategory(c.Request.Context(), req) + if err != nil { + writeError(c, err) + return + } + response.OK(c, item) +} + +func (h *Handler) AdminUpdateCategory(c *gin.Context) { + id, ok := parseID(c) + if !ok { + return + } + var req UpdateCategoryRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "请求参数不正确") + return + } + item, err := h.service.UpdateCategory(c.Request.Context(), id, req) + if err != nil { + writeError(c, err) + return + } + response.OK(c, item) +} + +func (h *Handler) AdminDeleteCategory(c *gin.Context) { + id, ok := parseID(c) + if !ok { + return + } + if err := h.service.DeleteCategory(c.Request.Context(), id); err != nil { + writeError(c, err) + return + } + response.OK(c, gin.H{"message": "删除成功"}) +} + +func (h *Handler) AdminListProducts(c *gin.Context) { + query := ProductListQuery{ + Keyword: c.Query("keyword"), + Status: c.Query("status"), + } + if catID, err := strconv.ParseUint(c.Query("category_id"), 10, 64); err == nil { + query.CategoryID = catID + } + query.Page, query.PageSize = parsePagination(c) + result, err := h.service.AdminListProducts(c.Request.Context(), query) + if err != nil { + response.InternalServerError(c, "获取商品列表失败") + return + } + response.OK(c, result) +} + +func (h *Handler) AdminGetProduct(c *gin.Context) { + id, ok := parseID(c) + if !ok { + return + } + item, err := h.service.AdminFindProduct(c.Request.Context(), id) + if err != nil { + writeError(c, err) + return + } + response.OK(c, item) +} + +func (h *Handler) AdminCreateProduct(c *gin.Context) { + adminID, ok := currentAdminID(c) + if !ok { + response.Unauthorized(c, "未授权") + return + } + var req CreateProductRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "请求参数不正确") + return + } + item, err := h.service.CreateProduct(c.Request.Context(), adminID, req) + if err != nil { + writeError(c, err) + return + } + response.OK(c, item) +} + +func (h *Handler) AdminUpdateProduct(c *gin.Context) { + id, ok := parseID(c) + if !ok { + return + } + var req UpdateProductRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "请求参数不正确") + return + } + item, err := h.service.UpdateProduct(c.Request.Context(), id, req) + if err != nil { + writeError(c, err) + return + } + response.OK(c, item) +} + +func (h *Handler) AdminDeleteProduct(c *gin.Context) { + id, ok := parseID(c) + if !ok { + return + } + if err := h.service.DeleteProduct(c.Request.Context(), id); err != nil { + writeError(c, err) + return + } + response.OK(c, gin.H{"message": "删除成功"}) +} + +func (h *Handler) AdminListOrders(c *gin.Context) { + query := OrderListQuery{ + Status: c.Query("status"), + Keyword: c.Query("keyword"), + } + if userID, err := strconv.ParseUint(c.Query("user_id"), 10, 64); err == nil { + query.UserID = userID + } + query.Page, query.PageSize = parsePagination(c) + result, err := h.service.AdminListOrders(c.Request.Context(), query) + if err != nil { + response.InternalServerError(c, "获取订单列表失败") + return + } + response.OK(c, result) +} + +func (h *Handler) AdminGetOrder(c *gin.Context) { + id, ok := parseID(c) + if !ok { + return + } + item, err := h.service.AdminFindOrder(c.Request.Context(), id) + if err != nil { + writeError(c, err) + return + } + response.OK(c, item) +} + +func (h *Handler) AdminCompleteOrder(c *gin.Context) { + id, ok := parseID(c) + if !ok { + return + } + var req AdminCompleteOrderRequest + _ = c.ShouldBindJSON(&req) + item, err := h.service.AdminCompleteOrder(c.Request.Context(), id, req.Remark) + if err != nil { + writeError(c, err) + return + } + response.OK(c, item) +} + +func (h *Handler) AdminCancelOrder(c *gin.Context) { + id, ok := parseID(c) + if !ok { + return + } + var req AdminCancelOrderRequest + _ = c.ShouldBindJSON(&req) + item, err := h.service.AdminCancelOrder(c.Request.Context(), id, req.Reason) + if err != nil { + writeError(c, err) + return + } + response.OK(c, item) +} + +func (h *Handler) AdminGetConfig(c *gin.Context) { + item, err := h.service.GetConfig(c.Request.Context()) + if err != nil { + response.InternalServerError(c, "获取配置失败") + return + } + response.OK(c, item) +} + +func (h *Handler) AdminUpdateConfig(c *gin.Context) { + var req UpdateConfigRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "请求参数不正确") + return + } + item, err := h.service.UpdateConfig(c.Request.Context(), req) + if err != nil { + response.InternalServerError(c, "更新配置失败") + return + } + response.OK(c, item) +} + +func writeError(c *gin.Context, err error) { + switch { + case errors.Is(err, ErrProductNotFound), errors.Is(err, ErrOrderNotFound): + response.NotFound(c, err.Error()) + case errors.Is(err, ErrProductUnavailable): + response.Error(c, 409, "product_unavailable", "商品不可购买") + case errors.Is(err, ErrStockInsufficient): + response.Error(c, 409, "stock_insufficient", "库存不足") + case errors.Is(err, ErrOrderCannotPay): + response.Error(c, 409, "order_cannot_pay", "订单无法支付") + case errors.Is(err, ErrOrderCannotCancel): + response.Error(c, 409, "order_cannot_cancel", "订单无法取消") + case errors.Is(err, ErrOrderCannotComplete): + response.Error(c, 409, "order_cannot_complete", "订单无法完成") + case errors.Is(err, ErrInvalidRequest): + response.BadRequest(c, "请求参数不正确") + case errors.Is(err, ErrUnauthorized): + response.Unauthorized(c, "未授权") + default: + msg := strings.TrimSpace(err.Error()) + if msg == "" { + msg = "操作失败" + } + response.InternalServerError(c, msg) + } +} + +func parseID(c *gin.Context) (uint64, bool) { + id, err := strconv.ParseUint(c.Param("id"), 10, 64) + if err != nil || id == 0 { + response.BadRequest(c, "ID不正确") + return 0, false + } + return id, true +} + +func parsePagination(c *gin.Context) (int, int) { + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) + if page < 1 { + page = 1 + } + if pageSize < 1 { + pageSize = 20 + } + if pageSize > 100 { + pageSize = 100 + } + return page, pageSize +} + +func currentUserID(c *gin.Context) (uint64, bool) { + value, ok := c.Get(middleware.ContextUserID) + if !ok { + return 0, false + } + id, ok := value.(uint64) + return id, ok +} + +func currentAdminID(c *gin.Context) (uint64, bool) { + value, ok := c.Get(middleware.ContextAdminID) + if !ok { + return 0, false + } + id, ok := value.(uint64) + return id, ok +} diff --git a/backend/internal/modules/mohong/order_repo.go b/backend/internal/modules/mohong/order_repo.go new file mode 100644 index 0000000..afd54b7 --- /dev/null +++ b/backend/internal/modules/mohong/order_repo.go @@ -0,0 +1,470 @@ +package mohong + +import ( + "context" + "crypto/rand" + "encoding/hex" + "encoding/json" + "fmt" + "strings" + "time" + + "hfb_sys/backend/internal/model" + "hfb_sys/backend/internal/modules/chat" + "hfb_sys/backend/internal/modules/notification" + "hfb_sys/backend/internal/modules/supportgroup" + "hfb_sys/backend/internal/timeutil" + "hfb_sys/backend/pkg/money" + + "gorm.io/datatypes" + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +const ConversationTypeMohongOrder = "mohong_order" + +func (r *Repository) CreateOrder(ctx context.Context, userID uint64, req CreateOrderRequest) (*OrderDTO, error) { + if userID == 0 || req.ProductID == 0 || req.Quantity < 1 || req.Quantity > 99 { + return nil, ErrInvalidRequest + } + var createdID uint64 + err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + var product model.MohongProduct + if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&product, req.ProductID).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return ErrProductNotFound + } + return err + } + if product.Status != model.MohongProductStatusOnSale { + return ErrProductUnavailable + } + if product.PriceCent <= 0 { + return ErrProductUnavailable + } + if product.Stock >= 0 && product.Stock < req.Quantity { + return ErrStockInsufficient + } + // 下单时预扣库存,取消未支付订单时归还。 + if product.Stock >= 0 { + product.Stock -= req.Quantity + if err := tx.Save(&product).Error; err != nil { + return err + } + } + + orderNo, err := newMohongOrderNo() + if err != nil { + return err + } + images := decodeStringList(product.ImageURLs) + snap := productSnapshot{ + ID: product.ID, + Title: product.Title, + CoverURL: product.CoverURL, + ImageURLs: images, + Description: product.Description, + PriceCent: product.PriceCent, + Unit: product.Unit, + QrcodeImageURL: product.QrcodeImageURL, + } + snapRaw, err := json.Marshal(snap) + if err != nil { + return err + } + amount := product.PriceCent * int64(req.Quantity) + order := model.MohongOrder{ + OrderNo: orderNo, + UserID: userID, + ProductID: product.ID, + Quantity: req.Quantity, + UnitPriceCent: product.PriceCent, + AmountCent: amount, + Status: model.MohongOrderStatusPendingPayment, + ProductSnapshot: datatypes.JSON(snapRaw), + CopyText: "", + } + if err := tx.Create(&order).Error; err != nil { + return err + } + orderID := order.ID + if err := notification.Append(tx, notification.Entry{ + UserID: userID, + Type: "order", + Title: "摸大红订单已创建", + Content: "订单已创建,请在有效时间内完成支付。", + BizType: "mohong_order", + BizID: &orderID, + }); err != nil { + return err + } + createdID = order.ID + return nil + }) + if err != nil { + return nil, err + } + return r.FindOrderForUser(ctx, userID, createdID) +} + +func (r *Repository) FindOrderForUser(ctx context.Context, userID, orderID uint64) (*OrderDTO, error) { + var row model.MohongOrder + if err := r.db.WithContext(ctx).Where("id = ? AND user_id = ?", orderID, userID).First(&row).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return nil, ErrOrderNotFound + } + return nil, err + } + return r.orderDTO(ctx, row, false) +} + +func (r *Repository) AdminFindOrder(ctx context.Context, orderID uint64) (*OrderDTO, error) { + var row model.MohongOrder + if err := r.db.WithContext(ctx).First(&row, orderID).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return nil, ErrOrderNotFound + } + return nil, err + } + return r.orderDTO(ctx, row, true) +} + +func (r *Repository) ListOrdersForUser(ctx context.Context, userID uint64, query OrderListQuery) (*PaginatedResult, error) { + query.UserID = userID + return r.listOrders(ctx, query, false) +} + +func (r *Repository) AdminListOrders(ctx context.Context, query OrderListQuery) (*PaginatedResult, error) { + return r.listOrders(ctx, query, true) +} + +func (r *Repository) listOrders(ctx context.Context, query OrderListQuery, includeAdmin bool) (*PaginatedResult, error) { + if query.Page < 1 { + query.Page = 1 + } + if query.PageSize < 1 { + query.PageSize = 20 + } + if query.PageSize > 100 { + query.PageSize = 100 + } + db := r.db.WithContext(ctx).Model(&model.MohongOrder{}) + if query.UserID > 0 { + db = db.Where("user_id = ?", query.UserID) + } + if status := strings.TrimSpace(query.Status); status != "" { + db = db.Where("status = ?", status) + } + if kw := strings.TrimSpace(query.Keyword); kw != "" { + like := "%" + kw + "%" + db = db.Where("order_no LIKE ? OR CAST(id AS CHAR) = ?", like, kw) + } + var total int64 + if err := db.Count(&total).Error; err != nil { + return nil, err + } + var rows []model.MohongOrder + if err := db.Order("id DESC"). + Offset((query.Page - 1) * query.PageSize). + Limit(query.PageSize). + Find(&rows).Error; err != nil { + return nil, err + } + items := make([]OrderDTO, 0, len(rows)) + for _, row := range rows { + dto, err := r.orderDTO(ctx, row, includeAdmin) + if err != nil { + return nil, err + } + items = append(items, *dto) + } + return &PaginatedResult{Items: items, Total: total, Page: query.Page, PageSize: query.PageSize}, nil +} + +func (r *Repository) CancelOrder(ctx context.Context, userID, orderID uint64, reason string) (*OrderDTO, error) { + err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + var order model.MohongOrder + if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). + Where("id = ? AND user_id = ?", orderID, userID). + First(&order).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return ErrOrderNotFound + } + return err + } + if order.Status != model.MohongOrderStatusPendingPayment { + return ErrOrderCannotCancel + } + return r.cancelPendingOrderTx(tx, &order, firstNonEmpty(reason, "用户取消")) + }) + if err != nil { + return nil, err + } + return r.FindOrderForUser(ctx, userID, orderID) +} + +func (r *Repository) AdminCancelOrder(ctx context.Context, orderID uint64, reason string) (*OrderDTO, error) { + err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + var order model.MohongOrder + if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&order, orderID).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return ErrOrderNotFound + } + return err + } + if order.Status != model.MohongOrderStatusPendingPayment && order.Status != model.MohongOrderStatusPaid { + return ErrOrderCannotCancel + } + if order.Status == model.MohongOrderStatusPendingPayment { + return r.cancelPendingOrderTx(tx, &order, firstNonEmpty(reason, "后台取消")) + } + // 已支付:仅标记取消(退款后续可接支付退款)。 + now := time.Now() + order.Status = model.MohongOrderStatusCancelled + order.CancelledAt = &now + order.CancelReason = firstNonEmpty(reason, "后台取消") + return tx.Save(&order).Error + }) + if err != nil { + return nil, err + } + return r.AdminFindOrder(ctx, orderID) +} + +func (r *Repository) AdminCompleteOrder(ctx context.Context, orderID uint64, remark string) (*OrderDTO, error) { + err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + var order model.MohongOrder + if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&order, orderID).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return ErrOrderNotFound + } + return err + } + if order.Status != model.MohongOrderStatusPaid { + return ErrOrderCannotComplete + } + now := time.Now() + order.Status = model.MohongOrderStatusCompleted + order.CompletedAt = &now + if strings.TrimSpace(remark) != "" { + order.AdminRemark = strings.TrimSpace(remark) + } + return tx.Save(&order).Error + }) + if err != nil { + return nil, err + } + return r.AdminFindOrder(ctx, orderID) +} + +// ConfirmPaidFromChannelTx 支付成功后推进摸大红订单:建群、发固定码、写复制文案。 +func (r *Repository) ConfirmPaidFromChannelTx(tx *gorm.DB, orderID uint64) (uint64, error) { + var order model.MohongOrder + if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&order, orderID).Error; err != nil { + return 0, err + } + if order.Status == model.MohongOrderStatusPaid || + order.Status == model.MohongOrderStatusCompleted { + if order.ConversationID != nil { + return *order.ConversationID, nil + } + return 0, nil + } + if order.Status != model.MohongOrderStatusPendingPayment { + return 0, ErrOrderCannotPay + } + + var user model.User + if err := tx.First(&user, order.UserID).Error; err != nil { + return 0, err + } + snap := decodeProductSnapshot(order.ProductSnapshot) + qrcodeURL := r.resolveQrcodeURL(tx, snap.QrcodeImageURL) + copyText := buildCopyText(r.loadCopyTemplate(tx), order, snap, user) + now := time.Now() + + conv, err := ensureMohongOrderConversation(tx, order, user) + if err != nil { + return 0, err + } + welcome := r.loadWelcomeText(tx) + if err := chat.SendSystemMessageForTx(tx, conv.ID, welcome); err != nil { + return 0, err + } + if qrcodeURL != "" { + if err := chat.SendImageMessageForTx(tx, conv.ID, "👇 请扫码联系客服", qrcodeURL); err != nil { + return 0, err + } + } else { + if err := chat.SendSystemMessageForTx(tx, conv.ID, "固定二维码尚未配置,请将下方订单信息复制后联系客服。"); err != nil { + return 0, err + } + } + if copyText != "" { + if err := chat.SendSystemMessageForTx(tx, conv.ID, copyText+"\n\n(长按/复制后发送给客服)"); err != nil { + return 0, err + } + } + + order.Status = model.MohongOrderStatusPaid + order.PaidAt = &now + order.QrcodeURLSnapshot = qrcodeURL + order.CopyText = copyText + order.ConversationID = &conv.ID + if err := tx.Save(&order).Error; err != nil { + return 0, err + } + + orderIDCopy := order.ID + if err := notification.Append(tx, notification.Entry{ + UserID: order.UserID, + Type: "order", + Title: "摸大红订单支付成功", + Content: "支付已完成,请在群内查看二维码与订单信息。", + BizType: "mohong_order", + BizID: &orderIDCopy, + }); err != nil { + return 0, err + } + return conv.ID, nil +} + +func (r *Repository) NotifyNewConversation(conversationID uint64) { + if conversationID > 0 && r.chatNotifier != nil { + r.chatNotifier.NotifyNewConversation(conversationID) + } +} + +func (r *Repository) cancelPendingOrderTx(tx *gorm.DB, order *model.MohongOrder, reason string) error { + // 归还预扣库存 + if order.Quantity > 0 { + var product model.MohongProduct + if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&product, order.ProductID).Error; err == nil { + if product.Stock >= 0 { + product.Stock += order.Quantity + if err := tx.Save(&product).Error; err != nil { + return err + } + } + } + } + now := time.Now() + order.Status = model.MohongOrderStatusCancelled + order.CancelledAt = &now + order.CancelReason = reason + return tx.Save(order).Error +} + +func (r *Repository) orderDTO(ctx context.Context, row model.MohongOrder, includeAdmin bool) (*OrderDTO, error) { + snap := decodeProductSnapshot(row.ProductSnapshot) + var nickname, phone string + var user model.User + if err := r.db.WithContext(ctx).Select("id", "nickname", "phone").First(&user, row.UserID).Error; err == nil { + nickname = user.Nickname + phone = user.Phone + } + dto := toOrderDTO(row, snap, nickname, phone, includeAdmin) + return &dto, nil +} + +func ensureMohongOrderConversation(tx *gorm.DB, order model.MohongOrder, user model.User) (*model.ChatConversation, error) { + var existing model.ChatConversation + err := tx.Where("mohong_order_id = ?", order.ID).First(&existing).Error + if err == nil { + return &existing, nil + } + if err != gorm.ErrRecordNotFound { + return nil, err + } + + now := time.Now() + title := "摸大红 " + order.OrderNo + if name := strings.TrimSpace(user.Nickname); name != "" { + title = "摸大红-" + name + } + conversation := model.ChatConversation{ + MohongOrderID: &order.ID, + Type: ConversationTypeMohongOrder, + Title: title, + Status: "active", + } + if err := tx.Create(&conversation).Error; err != nil { + return nil, err + } + + participants := []model.ChatParticipant{ + { + ConversationID: conversation.ID, + ParticipantType: "user", + ParticipantID: order.UserID, + Role: "buyer", + JoinedAt: now, + LastReadAt: &now, + }, + } + supportID, err := supportgroup.PickSupportAdmin(tx, supportgroup.GroupCodeRenterHandoff) + if err != nil { + return nil, err + } + if supportID <= 0 { + supportID = chat.DefaultSupportAdminID(tx) + } + if supportID > 0 { + participants = append(participants, model.ChatParticipant{ + ConversationID: conversation.ID, + ParticipantType: "admin", + ParticipantID: supportID, + Role: "support", + JoinedAt: now, + }) + } + for _, p := range participants { + if err := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&p).Error; err != nil { + return nil, err + } + } + return &conversation, nil +} + +func buildCopyText(template string, order model.MohongOrder, snap productSnapshot, user model.User) string { + if strings.TrimSpace(template) == "" { + template = defaultOrderCopyTemplate + } + buyerName := strings.TrimSpace(user.Nickname) + if buyerName == "" { + buyerName = "用户" + } + replacer := strings.NewReplacer( + "{{order_no}}", order.OrderNo, + "{{created_at}}", timeutil.ShanghaiNow().Format("2006-01-02 15:04:05"), + "{{product_title}}", snap.Title, + "{{quantity}}", fmt.Sprintf("%d", order.Quantity), + "{{amount}}", money.FormatWithSymbol(order.AmountCent), + "{{buyer_name}}", buyerName, + "{{buyer_phone}}", maskPhone(user.Phone), + "{{unit}}", snap.Unit, + ) + // 下单时间用订单创建时间更准确 + created := order.CreatedAt + if !created.IsZero() { + replacer = strings.NewReplacer( + "{{order_no}}", order.OrderNo, + "{{created_at}}", created.In(timeutil.ShanghaiLocation()).Format("2006-01-02 15:04:05"), + "{{product_title}}", snap.Title, + "{{quantity}}", fmt.Sprintf("%d", order.Quantity), + "{{amount}}", money.FormatWithSymbol(order.AmountCent), + "{{buyer_name}}", buyerName, + "{{buyer_phone}}", maskPhone(user.Phone), + "{{unit}}", snap.Unit, + ) + } + return replacer.Replace(template) +} + +func newMohongOrderNo() (string, error) { + var b [4]byte + if _, err := rand.Read(b[:]); err != nil { + return "", err + } + return fmt.Sprintf("MH%s%s", time.Now().Format("20060102150405"), strings.ToUpper(hex.EncodeToString(b[:]))), nil +} diff --git a/backend/internal/modules/mohong/presenter.go b/backend/internal/modules/mohong/presenter.go new file mode 100644 index 0000000..85c40d4 --- /dev/null +++ b/backend/internal/modules/mohong/presenter.go @@ -0,0 +1,132 @@ +package mohong + +import ( + "encoding/json" + "strings" + + "hfb_sys/backend/internal/model" + "hfb_sys/backend/pkg/money" +) + +func toProductDTO(row model.MohongProduct, includeAdminFields bool) ProductDTO { + images := decodeStringList(row.ImageURLs) + dto := ProductDTO{ + ID: row.ID, + CategoryID: row.CategoryID, + Title: row.Title, + CoverURL: row.CoverURL, + ImageURLs: images, + Description: row.Description, + PriceCent: row.PriceCent, + Price: money.Format(row.PriceCent), + OriginalPriceCent: row.OriginalPriceCent, + Unit: row.Unit, + Stock: row.Stock, + SortOrder: row.SortOrder, + Status: row.Status, + CreatedAt: row.CreatedAt, + UpdatedAt: row.UpdatedAt, + } + if row.OriginalPriceCent > 0 { + dto.OriginalPrice = money.Format(row.OriginalPriceCent) + } + if includeAdminFields { + dto.QrcodeImageURL = row.QrcodeImageURL + } + return dto +} + +func toProductDTOWithCategory(row model.MohongProduct, categoryName string, includeAdminFields bool) ProductDTO { + dto := toProductDTO(row, includeAdminFields) + dto.CategoryName = categoryName + return dto +} + +func toOrderDTO(row model.MohongOrder, snap productSnapshot, buyerNickname, buyerPhone string, includeAdmin bool) OrderDTO { + dto := OrderDTO{ + ID: row.ID, + OrderNo: row.OrderNo, + UserID: row.UserID, + ProductID: row.ProductID, + Quantity: row.Quantity, + UnitPriceCent: row.UnitPriceCent, + UnitPrice: money.Format(row.UnitPriceCent), + AmountCent: row.AmountCent, + Amount: money.Format(row.AmountCent), + Status: row.Status, + ProductTitle: snap.Title, + ProductCoverURL: snap.CoverURL, + ProductUnit: snap.Unit, + QrcodeURLSnapshot: row.QrcodeURLSnapshot, + CopyText: row.CopyText, + ConversationID: row.ConversationID, + CancelReason: row.CancelReason, + PaidAt: row.PaidAt, + CompletedAt: row.CompletedAt, + CancelledAt: row.CancelledAt, + CreatedAt: row.CreatedAt, + UpdatedAt: row.UpdatedAt, + } + if includeAdmin { + dto.BuyerNickname = buyerNickname + dto.BuyerPhone = maskPhone(buyerPhone) + dto.AdminRemark = row.AdminRemark + } else { + dto.BuyerNickname = buyerNickname + dto.BuyerPhone = maskPhone(buyerPhone) + } + return dto +} + +func decodeStringList(raw []byte) []string { + if len(raw) == 0 { + return []string{} + } + var list []string + if err := json.Unmarshal(raw, &list); err != nil { + return []string{} + } + out := make([]string, 0, len(list)) + for _, item := range list { + item = strings.TrimSpace(item) + if item != "" { + out = append(out, item) + } + } + return out +} + +func encodeStringList(list []string) []byte { + clean := make([]string, 0, len(list)) + for _, item := range list { + item = strings.TrimSpace(item) + if item != "" { + clean = append(clean, item) + } + } + if len(clean) == 0 { + return []byte("[]") + } + raw, err := json.Marshal(clean) + if err != nil { + return []byte("[]") + } + return raw +} + +func decodeProductSnapshot(raw []byte) productSnapshot { + var snap productSnapshot + if len(raw) == 0 { + return snap + } + _ = json.Unmarshal(raw, &snap) + return snap +} + +func maskPhone(phone string) string { + phone = strings.TrimSpace(phone) + if len(phone) < 7 { + return phone + } + return phone[:3] + "****" + phone[len(phone)-4:] +} diff --git a/backend/internal/modules/mohong/product_repo.go b/backend/internal/modules/mohong/product_repo.go new file mode 100644 index 0000000..ae1efc3 --- /dev/null +++ b/backend/internal/modules/mohong/product_repo.go @@ -0,0 +1,303 @@ +package mohong + +import ( + "context" + "strings" + + "hfb_sys/backend/internal/model" + + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +func (r *Repository) ListPublicProducts(ctx context.Context, query ProductListQuery) (*PaginatedResult, error) { + query = normalizePage(query) + db := r.db.WithContext(ctx).Model(&model.MohongProduct{}).Where("status = ?", model.MohongProductStatusOnSale) + if query.CategoryID > 0 { + // 多对多分类 + 主分类兜底 + db = db.Where( + `(id IN (SELECT product_id FROM mohong_product_categories WHERE category_id = ?) OR category_id = ?)`, + query.CategoryID, query.CategoryID, + ) + } + if kw := strings.TrimSpace(query.Keyword); kw != "" { + like := "%" + kw + "%" + db = db.Where("(title LIKE ? OR description LIKE ?)", like, like) + } + var total int64 + if err := db.Count(&total).Error; err != nil { + return nil, err + } + var rows []model.MohongProduct + if err := db.Order("sort_order DESC, id DESC"). + Offset((query.Page - 1) * query.PageSize). + Limit(query.PageSize). + Find(&rows).Error; err != nil { + return nil, err + } + return r.productPageDTO(ctx, rows, total, query, false) +} + +func (r *Repository) FindPublicProduct(ctx context.Context, id uint64) (*ProductDTO, error) { + var row model.MohongProduct + if err := r.db.WithContext(ctx). + Where("id = ? AND status = ?", id, model.MohongProductStatusOnSale). + First(&row).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return nil, ErrProductNotFound + } + return nil, err + } + name := "" + if row.CategoryID != nil { + name = r.loadCategoryNameMap(ctx, []uint64{*row.CategoryID})[*row.CategoryID] + } + dto := toProductDTOWithCategory(row, name, false) + return &dto, nil +} + +func (r *Repository) AdminListProducts(ctx context.Context, query ProductListQuery) (*PaginatedResult, error) { + query = normalizePage(query) + db := r.db.WithContext(ctx).Model(&model.MohongProduct{}) + if status := strings.TrimSpace(query.Status); status != "" { + db = db.Where("status = ?", status) + } + if query.CategoryID > 0 { + db = db.Where( + `(id IN (SELECT product_id FROM mohong_product_categories WHERE category_id = ?) OR category_id = ?)`, + query.CategoryID, query.CategoryID, + ) + } + if kw := strings.TrimSpace(query.Keyword); kw != "" { + like := "%" + kw + "%" + db = db.Where("(title LIKE ? OR description LIKE ?)", like, like) + } + var total int64 + if err := db.Count(&total).Error; err != nil { + return nil, err + } + var rows []model.MohongProduct + if err := db.Order("sort_order DESC, id DESC"). + Offset((query.Page - 1) * query.PageSize). + Limit(query.PageSize). + Find(&rows).Error; err != nil { + return nil, err + } + return r.productPageDTO(ctx, rows, total, query, true) +} + +func (r *Repository) productPageDTO(ctx context.Context, rows []model.MohongProduct, total int64, query ProductListQuery, admin bool) (*PaginatedResult, error) { + ids := make([]uint64, 0, len(rows)) + for _, row := range rows { + if row.CategoryID != nil { + ids = append(ids, *row.CategoryID) + } + } + names := r.loadCategoryNameMap(ctx, ids) + items := make([]ProductDTO, 0, len(rows)) + for _, row := range rows { + name := "" + if row.CategoryID != nil { + name = names[*row.CategoryID] + } + items = append(items, toProductDTOWithCategory(row, name, admin)) + } + return &PaginatedResult{Items: items, Total: total, Page: query.Page, PageSize: query.PageSize}, nil +} + +func (r *Repository) AdminFindProduct(ctx context.Context, id uint64) (*ProductDTO, error) { + var row model.MohongProduct + if err := r.db.WithContext(ctx).First(&row, id).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return nil, ErrProductNotFound + } + return nil, err + } + name := "" + if row.CategoryID != nil { + name = r.loadCategoryNameMap(ctx, []uint64{*row.CategoryID})[*row.CategoryID] + } + dto := toProductDTOWithCategory(row, name, true) + return &dto, nil +} + +func (r *Repository) CreateProduct(ctx context.Context, adminID uint64, req CreateProductRequest) (*ProductDTO, error) { + status := strings.TrimSpace(req.Status) + if status == "" { + status = model.MohongProductStatusDraft + } + if !isValidProductStatus(status) { + return nil, ErrInvalidRequest + } + stock := -1 + if req.Stock != nil { + stock = *req.Stock + } + unit := strings.TrimSpace(req.Unit) + if unit == "" { + unit = "份" + } + row := model.MohongProduct{ + CategoryID: normalizeCategoryID(req.CategoryID), + Title: strings.TrimSpace(req.Title), + CoverURL: strings.TrimSpace(req.CoverURL), + ImageURLs: encodeStringList(req.ImageURLs), + Description: strings.TrimSpace(req.Description), + PriceCent: req.PriceCent, + OriginalPriceCent: maxInt64(req.OriginalPriceCent, 0), + Unit: unit, + Stock: stock, + SortOrder: req.SortOrder, + Status: status, + QrcodeImageURL: strings.TrimSpace(req.QrcodeImageURL), + CreatedBy: &adminID, + } + if row.Title == "" || row.PriceCent <= 0 { + return nil, ErrInvalidRequest + } + if err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + if err := tx.Create(&row).Error; err != nil { + return err + } + return syncProductCategories(tx, row.ID, row.CategoryID) + }); err != nil { + return nil, err + } + return r.AdminFindProduct(ctx, row.ID) +} + +func (r *Repository) UpdateProduct(ctx context.Context, id uint64, req UpdateProductRequest) (*ProductDTO, error) { + var row model.MohongProduct + if err := r.db.WithContext(ctx).First(&row, id).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return nil, ErrProductNotFound + } + return nil, err + } + updates := map[string]any{} + if req.ClearCategory { + updates["category_id"] = nil + } else if req.CategoryID != nil { + updates["category_id"] = normalizeCategoryID(req.CategoryID) + } + if req.Title != nil { + title := strings.TrimSpace(*req.Title) + if title == "" { + return nil, ErrInvalidRequest + } + updates["title"] = title + } + if req.CoverURL != nil { + updates["cover_url"] = strings.TrimSpace(*req.CoverURL) + } + if req.ImageURLs != nil { + updates["image_urls"] = encodeStringList(req.ImageURLs) + } + if req.Description != nil { + updates["description"] = strings.TrimSpace(*req.Description) + } + if req.PriceCent != nil { + if *req.PriceCent <= 0 { + return nil, ErrInvalidRequest + } + updates["price_cent"] = *req.PriceCent + } + if req.OriginalPriceCent != nil { + updates["original_price_cent"] = maxInt64(*req.OriginalPriceCent, 0) + } + if req.Unit != nil { + unit := strings.TrimSpace(*req.Unit) + if unit == "" { + unit = "份" + } + updates["unit"] = unit + } + if req.Stock != nil { + updates["stock"] = *req.Stock + } + if req.SortOrder != nil { + updates["sort_order"] = *req.SortOrder + } + if req.Status != nil { + status := strings.TrimSpace(*req.Status) + if !isValidProductStatus(status) { + return nil, ErrInvalidRequest + } + updates["status"] = status + } + if req.QrcodeImageURL != nil { + updates["qrcode_image_url"] = strings.TrimSpace(*req.QrcodeImageURL) + } + if err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + if len(updates) > 0 { + if err := tx.Model(&row).Updates(updates).Error; err != nil { + return err + } + } + // 刷新主分类后同步关联表(至少保证主分类在关联里) + var latest model.MohongProduct + if err := tx.First(&latest, id).Error; err != nil { + return err + } + return syncProductCategories(tx, latest.ID, latest.CategoryID) + }); err != nil { + return nil, err + } + return r.AdminFindProduct(ctx, id) +} + +// syncProductCategories 保证主分类写入多对多表;不删除其它已有关联(种子数据可挂多分类)。 +func syncProductCategories(tx *gorm.DB, productID uint64, primary *uint64) error { + if primary == nil || *primary == 0 { + return nil + } + rel := model.MohongProductCategory{ProductID: productID, CategoryID: *primary} + return tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&rel).Error +} + +func (r *Repository) DeleteProduct(ctx context.Context, id uint64) error { + res := r.db.WithContext(ctx).Delete(&model.MohongProduct{}, id) + if res.Error != nil { + return res.Error + } + if res.RowsAffected == 0 { + return ErrProductNotFound + } + return nil +} + +func isValidProductStatus(status string) bool { + switch status { + case model.MohongProductStatusDraft, model.MohongProductStatusOnSale, model.MohongProductStatusOffSale: + return true + default: + return false + } +} + +func normalizePage(query ProductListQuery) ProductListQuery { + if query.Page < 1 { + query.Page = 1 + } + if query.PageSize < 1 { + query.PageSize = 20 + } + if query.PageSize > 100 { + query.PageSize = 100 + } + return query +} + +func maxInt64(a, b int64) int64 { + if a > b { + return a + } + return b +} + +func normalizeCategoryID(id *uint64) *uint64 { + if id == nil || *id == 0 { + return nil + } + return id +} diff --git a/backend/internal/modules/mohong/repository.go b/backend/internal/modules/mohong/repository.go new file mode 100644 index 0000000..18803f9 --- /dev/null +++ b/backend/internal/modules/mohong/repository.go @@ -0,0 +1,19 @@ +package mohong + +import "gorm.io/gorm" + +// ChatNotifier 会话创建后的异步通知接口。 +type ChatNotifier interface { + NotifyNewConversation(conversationID uint64) +} + +// Repository 摸大红数据访问。 +type Repository struct { + db *gorm.DB + chatNotifier ChatNotifier +} + +// NewRepository 创建仓库。 +func NewRepository(db *gorm.DB, chatNotifier ChatNotifier) *Repository { + return &Repository{db: db, chatNotifier: chatNotifier} +} diff --git a/backend/internal/modules/mohong/service.go b/backend/internal/modules/mohong/service.go new file mode 100644 index 0000000..9d33a43 --- /dev/null +++ b/backend/internal/modules/mohong/service.go @@ -0,0 +1,173 @@ +package mohong + +import "context" + +type Service struct { + repo *Repository +} + +func NewService(repo *Repository) *Service { + return &Service{repo: repo} +} + +func (s *Service) ListPublicCategories(ctx context.Context) ([]CategoryDTO, error) { + if s.repo == nil { + return nil, ErrDependencyUnavailable + } + return s.repo.ListPublicCategories(ctx) +} + +func (s *Service) AdminListCategories(ctx context.Context) ([]CategoryDTO, error) { + if s.repo == nil { + return nil, ErrDependencyUnavailable + } + return s.repo.AdminListCategories(ctx) +} + +func (s *Service) CreateCategory(ctx context.Context, req CreateCategoryRequest) (*CategoryDTO, error) { + if s.repo == nil { + return nil, ErrDependencyUnavailable + } + return s.repo.CreateCategory(ctx, req) +} + +func (s *Service) UpdateCategory(ctx context.Context, id uint64, req UpdateCategoryRequest) (*CategoryDTO, error) { + if s.repo == nil { + return nil, ErrDependencyUnavailable + } + return s.repo.UpdateCategory(ctx, id, req) +} + +func (s *Service) DeleteCategory(ctx context.Context, id uint64) error { + if s.repo == nil { + return ErrDependencyUnavailable + } + return s.repo.DeleteCategory(ctx, id) +} + +func (s *Service) ListPublicProducts(ctx context.Context, query ProductListQuery) (*PaginatedResult, error) { + if s.repo == nil { + return nil, ErrDependencyUnavailable + } + return s.repo.ListPublicProducts(ctx, query) +} + +func (s *Service) FindPublicProduct(ctx context.Context, id uint64) (*ProductDTO, error) { + if s.repo == nil { + return nil, ErrDependencyUnavailable + } + return s.repo.FindPublicProduct(ctx, id) +} + +func (s *Service) AdminListProducts(ctx context.Context, query ProductListQuery) (*PaginatedResult, error) { + if s.repo == nil { + return nil, ErrDependencyUnavailable + } + return s.repo.AdminListProducts(ctx, query) +} + +func (s *Service) AdminFindProduct(ctx context.Context, id uint64) (*ProductDTO, error) { + if s.repo == nil { + return nil, ErrDependencyUnavailable + } + return s.repo.AdminFindProduct(ctx, id) +} + +func (s *Service) CreateProduct(ctx context.Context, adminID uint64, req CreateProductRequest) (*ProductDTO, error) { + if s.repo == nil { + return nil, ErrDependencyUnavailable + } + return s.repo.CreateProduct(ctx, adminID, req) +} + +func (s *Service) UpdateProduct(ctx context.Context, id uint64, req UpdateProductRequest) (*ProductDTO, error) { + if s.repo == nil { + return nil, ErrDependencyUnavailable + } + return s.repo.UpdateProduct(ctx, id, req) +} + +func (s *Service) DeleteProduct(ctx context.Context, id uint64) error { + if s.repo == nil { + return ErrDependencyUnavailable + } + return s.repo.DeleteProduct(ctx, id) +} + +func (s *Service) CreateOrder(ctx context.Context, userID uint64, req CreateOrderRequest) (*OrderDTO, error) { + if s.repo == nil { + return nil, ErrDependencyUnavailable + } + return s.repo.CreateOrder(ctx, userID, req) +} + +func (s *Service) FindOrderForUser(ctx context.Context, userID, orderID uint64) (*OrderDTO, error) { + if s.repo == nil { + return nil, ErrDependencyUnavailable + } + return s.repo.FindOrderForUser(ctx, userID, orderID) +} + +func (s *Service) ListOrdersForUser(ctx context.Context, userID uint64, query OrderListQuery) (*PaginatedResult, error) { + if s.repo == nil { + return nil, ErrDependencyUnavailable + } + return s.repo.ListOrdersForUser(ctx, userID, query) +} + +func (s *Service) CancelOrder(ctx context.Context, userID, orderID uint64, reason string) (*OrderDTO, error) { + if s.repo == nil { + return nil, ErrDependencyUnavailable + } + return s.repo.CancelOrder(ctx, userID, orderID, reason) +} + +func (s *Service) AdminListOrders(ctx context.Context, query OrderListQuery) (*PaginatedResult, error) { + if s.repo == nil { + return nil, ErrDependencyUnavailable + } + return s.repo.AdminListOrders(ctx, query) +} + +func (s *Service) AdminFindOrder(ctx context.Context, orderID uint64) (*OrderDTO, error) { + if s.repo == nil { + return nil, ErrDependencyUnavailable + } + return s.repo.AdminFindOrder(ctx, orderID) +} + +func (s *Service) AdminCompleteOrder(ctx context.Context, orderID uint64, remark string) (*OrderDTO, error) { + if s.repo == nil { + return nil, ErrDependencyUnavailable + } + return s.repo.AdminCompleteOrder(ctx, orderID, remark) +} + +func (s *Service) AdminCancelOrder(ctx context.Context, orderID uint64, reason string) (*OrderDTO, error) { + if s.repo == nil { + return nil, ErrDependencyUnavailable + } + return s.repo.AdminCancelOrder(ctx, orderID, reason) +} + +func (s *Service) GetConfig(ctx context.Context) (*ConfigDTO, error) { + if s.repo == nil { + return nil, ErrDependencyUnavailable + } + return s.repo.GetConfig(ctx) +} + +func (s *Service) UpdateConfig(ctx context.Context, req UpdateConfigRequest) (*ConfigDTO, error) { + if s.repo == nil { + return nil, ErrDependencyUnavailable + } + return s.repo.UpdateConfig(ctx, req) +} + +// Repo 暴露给支付模块注入。 +func (s *Service) Repo() *Repository { + if s == nil { + return nil + } + return s.repo +} diff --git a/backend/internal/modules/payment/channel_status.go b/backend/internal/modules/payment/channel_status.go index 1f7f2a8..535ee6d 100644 --- a/backend/internal/modules/payment/channel_status.go +++ b/backend/internal/modules/payment/channel_status.go @@ -45,16 +45,31 @@ func isOrderPaymentTerminalStatus(status string) bool { } func (r *Repository) confirmPaid(ctx context.Context, payment *model.PaymentOrder, status string, paidAt time.Time, raw map[string]string, source string) error { var newConvID uint64 + var notifyFn func(uint64) err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { if payment.OrderID != 0 { - if r.orderRepo == nil { - return ErrDependencyUnavailable + switch payment.BizType { + case "mohong_pay": + if r.mohongRepo == nil { + return ErrDependencyUnavailable + } + convID, err := r.mohongRepo.ConfirmPaidFromChannelTx(tx, payment.OrderID) + if err != nil { + return err + } + newConvID = convID + notifyFn = r.mohongRepo.NotifyNewConversation + default: + if r.orderRepo == nil { + return ErrDependencyUnavailable + } + convID, err := r.orderRepo.ConfirmPaidFromChannelTx(tx, payment.OrderID) + if err != nil { + return err + } + newConvID = convID + notifyFn = r.orderRepo.NotifyNewConversation } - convID, err := r.orderRepo.ConfirmPaidFromChannelTx(tx, payment.OrderID) - if err != nil { - return err - } - newConvID = convID } var latest model.PaymentOrder @@ -75,8 +90,8 @@ func (r *Repository) confirmPaid(ctx context.Context, payment *model.PaymentOrde if err != nil { return err } - if newConvID > 0 && r.orderRepo != nil { - r.orderRepo.NotifyNewConversation(newConvID) + if newConvID > 0 && notifyFn != nil { + notifyFn(newConvID) } if latest, err := r.findPaymentByID(ctx, payment.ID); err == nil { if runtimeConfig, err := r.runtimeConfigForPayment(ctx, latest); err == nil { diff --git a/backend/internal/modules/payment/handler.go b/backend/internal/modules/payment/handler.go index d4bdf32..192beca 100644 --- a/backend/internal/modules/payment/handler.go +++ b/backend/internal/modules/payment/handler.go @@ -91,6 +91,26 @@ func (h *Handler) Start(c *gin.Context) { response.OK(c, item) } +func (h *Handler) StartMohong(c *gin.Context) { + userID, ok := currentUserID(c) + if !ok { + response.Unauthorized(c, "缺少用户上下文") + return + } + orderID, ok := parseID(c) + if !ok { + return + } + var req StartPaymentRequest + _ = c.ShouldBindJSON(&req) + item, err := h.service.StartMohong(c.Request.Context(), userID, orderID, req, c.ClientIP()) + if err != nil { + writePaymentError(c, err) + return + } + response.OK(c, item) +} + func (h *Handler) Query(c *gin.Context) { userID, ok := currentUserID(c) if !ok { @@ -109,6 +129,24 @@ func (h *Handler) Query(c *gin.Context) { response.OK(c, item) } +func (h *Handler) QueryMohong(c *gin.Context) { + userID, ok := currentUserID(c) + if !ok { + response.Unauthorized(c, "缺少用户上下文") + return + } + orderID, ok := parseID(c) + if !ok { + return + } + item, err := h.service.QueryMohong(c.Request.Context(), userID, orderID) + if err != nil { + writePaymentError(c, err) + return + } + response.OK(c, item) +} + func (h *Handler) QueryRefundStatus(c *gin.Context) { orderID, ok := parseID(c) if !ok { diff --git a/backend/internal/modules/payment/mohong_payment.go b/backend/internal/modules/payment/mohong_payment.go new file mode 100644 index 0000000..1d7a666 --- /dev/null +++ b/backend/internal/modules/payment/mohong_payment.go @@ -0,0 +1,245 @@ +package payment + +import ( + "context" + "time" + + "hfb_sys/backend/internal/model" + + "go.uber.org/zap" + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +const bizTypeMohongPay = "mohong_pay" + +// StartMohong 发起摸大红订单支付。 +func (r *Repository) StartMohong(ctx context.Context, userID uint64, orderID uint64, req StartPaymentRequest, clientIP string) (*PaymentDTO, error) { + req.PayWay = normalizePayWay(req.PayWay) + defaultConfig, err := r.defaultRuntimeConfig(ctx, req.PayWay) + if err != nil { + return nil, ErrPaymentUnavailable + } + payment, orderRow, err := r.prepareMohongPayment(ctx, userID, orderID, req, *defaultConfig) + if err != nil { + return nil, err + } + runtimeConfig, err := r.runtimeConfigForPayment(ctx, payment) + if err != nil { + return nil, ErrPaymentUnavailable + } + if payment.Status == "paid" { + r.recordConfigUsage(ctx, runtimeConfig, payment) + dto := toDTO(*payment) + return &dto, nil + } + if runtimeConfig.isMockMode() { + if err := r.confirmPaid(ctx, payment, "2", time.Now(), map[string]string{ + "mock": "true", + "third_order_id": payment.ThirdOrderID, + "leshua_order_id": payment.ProviderOrderID, + "status": "2", + }, channelSourceMock); err != nil { + return nil, err + } + latest, err := r.findPaymentByID(ctx, payment.ID) + if err != nil { + return nil, err + } + r.recordConfigUsage(ctx, runtimeConfig, latest) + dto := toDTO(*latest) + return &dto, nil + } + if payment.Status == "paying" && (payment.TDCode != "" || payment.JSPayURL != "" || payment.JSPayInfo != "") { + r.recordConfigUsage(ctx, runtimeConfig, payment) + dto := toDTO(*payment) + return &dto, nil + } + if runtimeConfig.Channel == nil { + _ = r.markPaymentFailed(ctx, payment.ID, nil, "payment channel unavailable") + return nil, ErrPaymentUnavailable + } + + r.log().Info("mohong payment start", paymentLogFields(ctx, appendFields( + paymentOrderFields(payment), + runtimeConfigFields(runtimeConfig), + )..., + )...) + resp, err := runtimeConfig.Channel.CreatePayment(ctx, channelCreatePaymentRequest{ + ThirdOrderID: payment.ThirdOrderID, + AmountCent: payment.AmountCent, + PayWay: payment.PayWay, + JSPayFlag: payment.JSPayFlag, + NotifyURL: runtimeConfig.NotifyURL, + JumpURL: runtimeConfig.JumpURL, + ClientIP: clientIP, + Body: "摸大红订单 " + orderRow.OrderNo, + Attach: orderRow.OrderNo, + }) + if err != nil { + _ = r.markPaymentFailed(ctx, payment.ID, nil, err.Error()) + r.log().Warn("mohong payment request failed", paymentLogFields(ctx, appendFields( + paymentOrderFields(payment), + runtimeConfigFields(runtimeConfig), + []zap.Field{zap.Error(err)}, + )..., + )...) + return nil, err + } + if !resp.OK { + _ = r.markPaymentFailed(ctx, payment.ID, resp.Raw, resp.ErrorMessage) + return nil, ErrPaymentUnavailable + } + if err := r.db.WithContext(ctx).Model(&model.PaymentOrder{}).Where("id = ?", payment.ID).Updates(map[string]any{ + "status": "paying", + "provider_order_id": resp.ProviderOrderID, + "pay_way": firstNonEmpty(resp.PayWay, payment.PayWay), + "td_code": resp.TDCode, + "jspay_url": resp.JSPayURL, + "jspay_info": resp.JSPayInfo, + "raw_request": jsonMap(resp.RawRequest), + "raw_response": jsonMap(withRawSource(resp.Raw, channelSourceCreate)), + }).Error; err != nil { + return nil, err + } + latest, err := r.findPaymentByID(ctx, payment.ID) + if err != nil { + return nil, err + } + r.recordConfigUsage(ctx, runtimeConfig, latest) + dto := toDTO(*latest) + return &dto, nil +} + +func (r *Repository) prepareMohongPayment(ctx context.Context, userID uint64, orderID uint64, req StartPaymentRequest, runtimeConfig runtimePaymentConfig) (*model.PaymentOrder, *model.MohongOrder, error) { + var paymentID uint64 + var orderRow model.MohongOrder + err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + var row model.MohongOrder + if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). + Where("id = ? AND user_id = ?", orderID, userID). + First(&row).Error; err != nil { + return err + } + if row.Status != model.MohongOrderStatusPendingPayment { + return ErrPaymentCannotStart + } + if row.AmountCent <= 0 { + return ErrPaymentCannotStart + } + var existing model.PaymentOrder + err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). + Where("order_id = ? AND biz_type = ?", row.ID, bizTypeMohongPay). + Order("id DESC"). + First(&existing).Error + if err == nil { + if canReuseOrderPayment(existing, runtimeConfig) { + existing.PayWay = firstNonEmpty(req.PayWay, existing.PayWay, runtimeConfig.PayWay, "ZFBZF") + existing.JSPayFlag = firstNonEmpty(req.JSPayFlag, existing.JSPayFlag, runtimeConfig.JSPayFlag, "2") + existing.AmountCent = row.AmountCent + existing.PaymentConfigID = firstNonZero(existing.PaymentConfigID, runtimeConfig.ID) + existing.Provider = firstNonEmpty(existing.Provider, runtimeConfig.Provider) + existing.MerchantID = firstNonEmpty(existing.MerchantID, runtimeConfig.MerchantID) + if existing.Provider == "mock" && existing.ProviderOrderID == "" { + existing.ProviderOrderID = "MOCK" + existing.ThirdOrderID + } + if err := tx.Save(&existing).Error; err != nil { + return err + } + paymentID = existing.ID + orderRow = row + return nil + } + } else if err != gorm.ErrRecordNotFound { + return err + } + payment, err := newMohongPayment(row, req, runtimeConfig) + if err != nil { + return err + } + if err := tx.Create(&payment).Error; err != nil { + return err + } + paymentID = payment.ID + orderRow = row + return nil + }) + if err != nil { + return nil, nil, err + } + payment, err := r.findPaymentByID(ctx, paymentID) + if err != nil { + return nil, nil, err + } + return payment, &orderRow, nil +} + +func newMohongPayment(row model.MohongOrder, req StartPaymentRequest, runtimeConfig runtimePaymentConfig) (model.PaymentOrder, error) { + paymentNo, err := newPaymentNo() + if err != nil { + return model.PaymentOrder{}, err + } + payment := model.PaymentOrder{ + PaymentNo: paymentNo, + OrderID: row.ID, + OrderNo: row.OrderNo, + UserID: row.UserID, + PaymentConfigID: runtimeConfig.ID, + Provider: runtimeConfig.Provider, + MerchantID: runtimeConfig.MerchantID, + ThirdOrderID: paymentNo, + ProviderOrderID: "", + PayWay: firstNonEmpty(req.PayWay, runtimeConfig.PayWay, "ZFBZF"), + JSPayFlag: firstNonEmpty(req.JSPayFlag, runtimeConfig.JSPayFlag, "2"), + AmountCent: row.AmountCent, + BizType: bizTypeMohongPay, + Status: "created", + } + if runtimeConfig.isMockMode() { + payment.ProviderOrderID = "MOCK" + paymentNo + payment.TDCode = "mock://payment/pay/" + paymentNo + } + return payment, nil +} + +// QueryMohong 查询摸大红订单支付状态。 +func (r *Repository) QueryMohong(ctx context.Context, userID uint64, orderID uint64) (*PaymentDTO, error) { + var payment model.PaymentOrder + if err := r.db.WithContext(ctx). + Where("order_id = ? AND user_id = ? AND biz_type = ?", orderID, userID, bizTypeMohongPay). + Order("id DESC"). + First(&payment).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return nil, ErrPaymentNotFound + } + return nil, err + } + if isOrderPaymentTerminalStatus(payment.Status) { + dto := toDTO(payment) + return &dto, nil + } + runtimeConfig, err := r.runtimeConfigForPayment(ctx, &payment) + if err != nil { + return nil, ErrPaymentUnavailable + } + if runtimeConfig.isMockMode() { + dto := toDTO(payment) + return &dto, nil + } + if runtimeConfig.Channel == nil { + return nil, ErrPaymentUnavailable + } + resp, err := runtimeConfig.Channel.QueryPayment(ctx, payment.ThirdOrderID, payment.ProviderOrderID) + if err != nil { + return nil, err + } + if err := r.applyChannelStatus(ctx, &payment, resp.Status, resp.PayTime, resp.Raw, channelSourceQuery); err != nil { + return nil, err + } + latest, err := r.findPaymentByID(ctx, payment.ID) + if err != nil { + return nil, err + } + dto := toDTO(*latest) + return &dto, nil +} diff --git a/backend/internal/modules/payment/repository.go b/backend/internal/modules/payment/repository.go index 49adfd7..7b21744 100644 --- a/backend/internal/modules/payment/repository.go +++ b/backend/internal/modules/payment/repository.go @@ -13,10 +13,17 @@ import ( "gorm.io/gorm" ) +// MohongOrderConfirmer 摸大红订单支付确认接口,避免 payment 与 mohong 循环依赖。 +type MohongOrderConfirmer interface { + ConfirmPaidFromChannelTx(tx *gorm.DB, orderID uint64) (uint64, error) + NotifyNewConversation(conversationID uint64) +} + type Repository struct { db *gorm.DB configRepo *paymentconfig.Repository orderRepo *order.Repository + mohongRepo MohongOrderConfirmer logger *zap.Logger } @@ -82,6 +89,13 @@ func WithLogger(logger *zap.Logger) RepositoryOption { } } +// WithMohongRepo 注入摸大红订单确认器。 +func WithMohongRepo(repo MohongOrderConfirmer) RepositoryOption { + return func(r *Repository) { + r.mohongRepo = repo + } +} + func (r *Repository) log() *zap.Logger { if r == nil || r.logger == nil { return zap.NewNop() @@ -237,7 +251,7 @@ func (r *Repository) recordConfigUsage(ctx context.Context, runtimeConfig *runti if r.configRepo == nil || runtimeConfig == nil || payment == nil || runtimeConfig.ID == 0 { return } - if payment.BizType != "order_pay" || payment.Status != "paid" { + if (payment.BizType != "order_pay" && payment.BizType != "mohong_pay") || payment.Status != "paid" { return } if err := r.configRepo.RecordUsage(ctx, runtimeConfig.ID, payment.ID, runtimeConfig.Provider, runtimeConfig.MerchantID, payment.AmountCent, payment.BizType); err != nil { diff --git a/backend/internal/modules/payment/service.go b/backend/internal/modules/payment/service.go index a2aba2f..9906a01 100644 --- a/backend/internal/modules/payment/service.go +++ b/backend/internal/modules/payment/service.go @@ -32,6 +32,16 @@ func (s *Service) Start(ctx context.Context, userID uint64, orderID uint64, req return s.repo.Start(ctx, userID, orderID, req, clientIP) } +func (s *Service) StartMohong(ctx context.Context, userID uint64, orderID uint64, req StartPaymentRequest, clientIP string) (*PaymentDTO, error) { + if s.repo == nil { + return nil, ErrDependencyUnavailable + } + if userID == 0 || orderID == 0 { + return nil, ErrPaymentCannotStart + } + return s.repo.StartMohong(ctx, userID, orderID, req, clientIP) +} + func (s *Service) Query(ctx context.Context, userID uint64, orderID uint64) (*PaymentDTO, error) { if s.repo == nil { return nil, ErrDependencyUnavailable @@ -42,6 +52,16 @@ func (s *Service) Query(ctx context.Context, userID uint64, orderID uint64) (*Pa return s.repo.Query(ctx, userID, orderID) } +func (s *Service) QueryMohong(ctx context.Context, userID uint64, orderID uint64) (*PaymentDTO, error) { + if s.repo == nil { + return nil, ErrDependencyUnavailable + } + if userID == 0 || orderID == 0 { + return nil, ErrPaymentNotFound + } + return s.repo.QueryMohong(ctx, userID, orderID) +} + func (s *Service) HandleLeshuaNotify(ctx context.Context, params map[string]string, rawPayload string, contentType string) (*NotifyResult, error) { if s.repo == nil { return nil, ErrDependencyUnavailable diff --git a/backend/internal/router/router.go b/backend/internal/router/router.go index 3c0a85c..f194501 100644 --- a/backend/internal/router/router.go +++ b/backend/internal/router/router.go @@ -24,6 +24,7 @@ import ( "hfb_sys/backend/internal/modules/dispute" filemodule "hfb_sys/backend/internal/modules/file" "hfb_sys/backend/internal/modules/listing" + "hfb_sys/backend/internal/modules/mohong" "hfb_sys/backend/internal/modules/notification" "hfb_sys/backend/internal/modules/order" "hfb_sys/backend/internal/modules/payment" @@ -254,8 +255,21 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine { paymentConfigHandler = paymentconfig.NewHandler(paymentConfigService) } + var mohongRepo *mohong.Repository + var mohongService *mohong.Service + var mohongHandler *mohong.Handler if deps.DB != nil { - paymentRepo = payment.NewRepository(deps.DB, paymentConfigRepo, orderRepo, payment.WithLogger(logger)) + mohongRepo = mohong.NewRepository(deps.DB, chatRepo) + mohongService = mohong.NewService(mohongRepo) + mohongHandler = mohong.NewHandler(mohongService) + } + + if deps.DB != nil { + paymentOpts := []payment.RepositoryOption{payment.WithLogger(logger)} + if mohongRepo != nil { + paymentOpts = append(paymentOpts, payment.WithMohongRepo(mohongRepo)) + } + paymentRepo = payment.NewRepository(deps.DB, paymentConfigRepo, orderRepo, paymentOpts...) } paymentService := payment.NewService(paymentRepo) paymentHandler := payment.NewHandler(paymentService, payment.WithHandlerLogger(logger)) @@ -414,6 +428,25 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine { sellerPickupRoutes.GET("", pickupHandler.ListForSeller) } + // 摸大红:商品公开,下单需登录+实名 + if mohongHandler != nil { + mohongPublic := api.Group("/mohong") + { + mohongPublic.GET("/categories", mohongHandler.ListCategories) + mohongPublic.GET("/products", mohongHandler.ListProducts) + mohongPublic.GET("/products/:id", mohongHandler.GetProduct) + } + mohongAuth := api.Group("/mohong", requireAuth) + { + mohongAuth.POST("/orders", requireRealname, mohongHandler.CreateOrder) + mohongAuth.GET("/orders", mohongHandler.ListMyOrders) + mohongAuth.GET("/orders/:id", mohongHandler.GetMyOrder) + mohongAuth.POST("/orders/:id/cancel", mohongHandler.CancelMyOrder) + mohongAuth.POST("/orders/:id/start-payment", requireRealname, paymentHandler.StartMohong) + mohongAuth.GET("/orders/:id/query-payment", paymentHandler.QueryMohong) + } + } + orderRoutes := api.Group("/orders", requireAuth) { orderRoutes.POST("", requireRealname, orderHandler.Create) @@ -565,6 +598,27 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine { adminRoutes.POST("/listings/:id/reject", requirePerm("listing:reject"), listingHandler.Reject) adminRoutes.POST("/listings/:id/offline", requirePerm("listing:offline"), listingHandler.AdminOffline) adminRoutes.POST("/listings/:id/mark-abnormal", requirePerm("listing:offline"), listingHandler.AdminMarkAbnormal) + + // 摸大红 + if mohongHandler != nil { + // 商品编辑需要拉分类列表,故 GET 用 product_view + adminRoutes.GET("/mohong/categories", requirePerm("mohong:product_view"), mohongHandler.AdminListCategories) + adminRoutes.POST("/mohong/categories", requirePerm("mohong:category"), mohongHandler.AdminCreateCategory) + adminRoutes.PUT("/mohong/categories/:id", requirePerm("mohong:category"), mohongHandler.AdminUpdateCategory) + adminRoutes.DELETE("/mohong/categories/:id", requirePerm("mohong:category"), mohongHandler.AdminDeleteCategory) + adminRoutes.GET("/mohong/products", requirePerm("mohong:product_view"), mohongHandler.AdminListProducts) + adminRoutes.GET("/mohong/products/:id", requirePerm("mohong:product_view"), mohongHandler.AdminGetProduct) + adminRoutes.POST("/mohong/products", requirePerm("mohong:product_manage"), mohongHandler.AdminCreateProduct) + adminRoutes.PUT("/mohong/products/:id", requirePerm("mohong:product_manage"), mohongHandler.AdminUpdateProduct) + adminRoutes.DELETE("/mohong/products/:id", requirePerm("mohong:product_manage"), mohongHandler.AdminDeleteProduct) + adminRoutes.GET("/mohong/orders", requirePerm("mohong:order_view"), mohongHandler.AdminListOrders) + adminRoutes.GET("/mohong/orders/:id", requirePerm("mohong:order_view"), mohongHandler.AdminGetOrder) + adminRoutes.POST("/mohong/orders/:id/complete", requirePerm("mohong:order_manage"), mohongHandler.AdminCompleteOrder) + adminRoutes.POST("/mohong/orders/:id/cancel", requirePerm("mohong:order_manage"), mohongHandler.AdminCancelOrder) + adminRoutes.GET("/mohong/config", requirePerm("mohong:config"), mohongHandler.AdminGetConfig) + adminRoutes.PUT("/mohong/config", requirePerm("mohong:config"), mohongHandler.AdminUpdateConfig) + } + adminRoutes.GET("/disputes", requirePerm("dispute:view"), disputeHandler.AdminList) adminRoutes.POST("/disputes/:id/arbitrate", requirePerm("dispute:arbitrate"), disputeHandler.AdminArbitrate) adminRoutes.GET("/finance/dashboard", requirePerm("wallet:view"), adminFinanceHandler.Dashboard) diff --git a/backend/migrations/000028_mohong.sql b/backend/migrations/000028_mohong.sql new file mode 100644 index 0000000..1a4fc81 --- /dev/null +++ b/backend/migrations/000028_mohong.sql @@ -0,0 +1,125 @@ +-- +goose Up +-- +goose StatementBegin + +-- 摸大红商品表 +CREATE TABLE IF NOT EXISTS mohong_products ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + title VARCHAR(128) NOT NULL COMMENT '商品标题', + cover_url VARCHAR(512) NOT NULL DEFAULT '' COMMENT '封面图', + image_urls JSON NULL COMMENT '详情图列表', + description TEXT NOT NULL COMMENT '商品描述', + price_cent BIGINT NOT NULL DEFAULT 0 COMMENT '售价(分)', + original_price_cent BIGINT NOT NULL DEFAULT 0 COMMENT '原价(分,0表示不展示)', + unit VARCHAR(32) NOT NULL DEFAULT '份' COMMENT '单位', + stock INT NOT NULL DEFAULT -1 COMMENT '库存,-1表示不限', + sort_order INT NOT NULL DEFAULT 0 COMMENT '排序,越大越靠前', + status VARCHAR(16) NOT NULL DEFAULT 'draft' COMMENT 'draft草稿 on_sale上架 off_sale下架', + qrcode_image_url VARCHAR(512) NOT NULL DEFAULT '' COMMENT '专属固定二维码,空则用全局默认', + created_by BIGINT UNSIGNED NULL COMMENT '创建管理员', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + KEY idx_mohong_products_status_sort (status, sort_order, id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='摸大红商品'; + +-- 摸大红订单表 +CREATE TABLE IF NOT EXISTS mohong_orders ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + order_no VARCHAR(64) NOT NULL COMMENT '订单号', + user_id BIGINT UNSIGNED NOT NULL COMMENT '下单用户', + product_id BIGINT UNSIGNED NOT NULL COMMENT '商品ID', + quantity INT NOT NULL DEFAULT 1 COMMENT '数量', + unit_price_cent BIGINT NOT NULL DEFAULT 0 COMMENT '下单单价快照(分)', + amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '应付总额(分)', + status VARCHAR(32) NOT NULL DEFAULT 'pending_payment' COMMENT 'pending_payment/paid/completed/cancelled/refunded', + product_snapshot JSON NULL COMMENT '商品快照', + qrcode_url_snapshot VARCHAR(512) NOT NULL DEFAULT '' COMMENT '支付时解析的二维码快照', + copy_text TEXT NOT NULL COMMENT '可复制订单文案', + conversation_id BIGINT UNSIGNED NULL COMMENT '订单群会话ID', + paid_at DATETIME NULL COMMENT '支付时间', + completed_at DATETIME NULL COMMENT '完成时间', + cancelled_at DATETIME NULL COMMENT '取消时间', + cancel_reason VARCHAR(255) NOT NULL DEFAULT '' COMMENT '取消原因', + admin_remark VARCHAR(255) NOT NULL DEFAULT '' COMMENT '后台备注', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY uk_mohong_orders_order_no (order_no), + KEY idx_mohong_orders_user_status (user_id, status, id), + KEY idx_mohong_orders_product (product_id), + KEY idx_mohong_orders_status_created (status, created_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='摸大红订单'; + +-- 会话表增加摸大红订单关联 +ALTER TABLE chat_conversations + ADD COLUMN mohong_order_id BIGINT UNSIGNED NULL COMMENT '摸大红订单ID' AFTER listing_id, + ADD UNIQUE KEY uk_chat_conversations_mohong_order_id (mohong_order_id); + +-- 业务配置 +INSERT INTO system_configs (`key`, `value`, description) VALUES +('mohong.default_qrcode_url', '', '摸大红全局默认固定二维码图片URL'), +('mohong.group_welcome_text', '订单已支付,请扫码联系客服,并将下方订单信息复制发送给客服。', '摸大红订单群欢迎语'), +('mohong.order_copy_template', '【摸大红订单】\n订单号:{{order_no}}\n下单时间:{{created_at}}\n商品:{{product_title}}\n数量:{{quantity}}\n金额:{{amount}}\n下单人:{{buyer_name}}({{buyer_phone}})', '摸大红订单一键复制文案模板') +ON DUPLICATE KEY UPDATE description = VALUES(description); + +-- 权限 +INSERT INTO permissions (code, name, resource, action) VALUES +('mohong:product_view', '查看摸大红商品', 'mohong', 'product_view'), +('mohong:product_manage', '管理摸大红商品', 'mohong', 'product_manage'), +('mohong:order_view', '查看摸大红订单', 'mohong', 'order_view'), +('mohong:order_manage', '管理摸大红订单', 'mohong', 'order_manage'), +('mohong:config', '摸大红业务配置', 'mohong', 'config') +ON DUPLICATE KEY UPDATE + name = VALUES(name), + resource = VALUES(resource), + action = VALUES(action); + +INSERT IGNORE INTO role_permissions (role_id, permission_id) +SELECT r.id, p.id +FROM roles r +JOIN permissions p ON p.code IN ( + 'mohong:product_view', 'mohong:product_manage', + 'mohong:order_view', 'mohong:order_manage', + 'mohong:config' +) +WHERE r.code = 'super_admin'; + +INSERT IGNORE INTO role_permissions (role_id, permission_id) +SELECT r.id, p.id +FROM roles r +JOIN permissions p ON p.code IN ( + 'mohong:product_view', 'mohong:order_view', 'mohong:order_manage' +) +WHERE r.code = 'cs'; + +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin + +DELETE rp FROM role_permissions rp +JOIN permissions p ON p.id = rp.permission_id +WHERE p.code IN ( + 'mohong:product_view', 'mohong:product_manage', + 'mohong:order_view', 'mohong:order_manage', + 'mohong:config' +); + +DELETE FROM permissions WHERE code IN ( + 'mohong:product_view', 'mohong:product_manage', + 'mohong:order_view', 'mohong:order_manage', + 'mohong:config' +); + +DELETE FROM system_configs WHERE `key` IN ( + 'mohong.default_qrcode_url', + 'mohong.group_welcome_text', + 'mohong.order_copy_template' +); + +ALTER TABLE chat_conversations + DROP INDEX uk_chat_conversations_mohong_order_id, + DROP COLUMN mohong_order_id; + +DROP TABLE IF EXISTS mohong_orders; +DROP TABLE IF EXISTS mohong_products; + +-- +goose StatementEnd diff --git a/backend/migrations/000029_mohong_categories_and_seed.sql b/backend/migrations/000029_mohong_categories_and_seed.sql new file mode 100644 index 0000000..9e88e67 --- /dev/null +++ b/backend/migrations/000029_mohong_categories_and_seed.sql @@ -0,0 +1,2410 @@ +-- +goose Up +-- +goose StatementBegin +-- 摸大红:分类 + 多对多关联 + 抓包商品(大红抓包 2.har) +SET NAMES utf8mb4; + +CREATE TABLE IF NOT EXISTS mohong_categories ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + name VARCHAR(64) NOT NULL COMMENT '分类名称', + code VARCHAR(64) NOT NULL DEFAULT '' COMMENT '可选编码', + sort_order INT NOT NULL DEFAULT 0 COMMENT '排序,越小越靠前', + status VARCHAR(16) NOT NULL DEFAULT 'enabled' COMMENT 'enabled/disabled', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY uk_mohong_categories_name (name), + KEY idx_mohong_categories_status_sort (status, sort_order, id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='摸大红分类'; + +CREATE TABLE IF NOT EXISTS mohong_product_categories ( + product_id BIGINT UNSIGNED NOT NULL, + category_id BIGINT UNSIGNED NOT NULL, + PRIMARY KEY (product_id, category_id), + KEY idx_mohong_pc_category (category_id, product_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='摸大红商品分类关联'; + +-- 主分类字段(可能已存在) +SET @col_exists := ( + SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mohong_products' AND COLUMN_NAME = 'category_id' +); +SET @sql := IF(@col_exists = 0, + 'ALTER TABLE mohong_products ADD COLUMN category_id BIGINT UNSIGNED NULL COMMENT ''分类ID(主)'' AFTER id, ADD KEY idx_mohong_products_category (category_id, status, sort_order)', + 'SELECT 1'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +INSERT INTO permissions (code, name, resource, action) VALUES +('mohong:category', '管理摸大红分类', 'mohong', 'category') +ON DUPLICATE KEY UPDATE name = VALUES(name), resource = VALUES(resource), action = VALUES(action); + +INSERT IGNORE INTO role_permissions (role_id, permission_id) +SELECT r.id, p.id FROM roles r JOIN permissions p ON p.code = 'mohong:category' WHERE r.code = 'super_admin'; + +INSERT INTO mohong_categories (name, code, sort_order, status, created_at, updated_at) +SELECT '炫彩', 'ext_116106', 0, 'enabled', NOW(), NOW() +WHERE NOT EXISTS (SELECT 1 FROM mohong_categories WHERE name = '炫彩'); + +INSERT INTO mohong_categories (name, code, sort_order, status, created_at, updated_at) +SELECT '任务', 'ext_116668', 1, 'enabled', NOW(), NOW() +WHERE NOT EXISTS (SELECT 1 FROM mohong_categories WHERE name = '任务'); + +INSERT INTO mohong_categories (name, code, sort_order, status, created_at, updated_at) +SELECT 'AW', 'ext_116175', 2, 'enabled', NOW(), NOW() +WHERE NOT EXISTS (SELECT 1 FROM mohong_categories WHERE name = 'AW'); + +INSERT INTO mohong_categories (name, code, sort_order, status, created_at, updated_at) +SELECT '大红', 'ext_116160', 3, 'enabled', NOW(), NOW() +WHERE NOT EXISTS (SELECT 1 FROM mohong_categories WHERE name = '大红'); + +INSERT INTO mohong_categories (name, code, sort_order, status, created_at, updated_at) +SELECT '四格大红', 'ext_116161', 4, 'enabled', NOW(), NOW() +WHERE NOT EXISTS (SELECT 1 FROM mohong_categories WHERE name = '四格大红'); + +INSERT INTO mohong_categories (name, code, sort_order, status, created_at, updated_at) +SELECT '六格大红', 'ext_116162', 5, 'enabled', NOW(), NOW() +WHERE NOT EXISTS (SELECT 1 FROM mohong_categories WHERE name = '六格大红'); + +INSERT INTO mohong_categories (name, code, sort_order, status, created_at, updated_at) +SELECT '九格大红', 'ext_116163', 6, 'enabled', NOW(), NOW() +WHERE NOT EXISTS (SELECT 1 FROM mohong_categories WHERE name = '九格大红'); + +INSERT INTO mohong_categories (name, code, sort_order, status, created_at, updated_at) +SELECT '十二格大红', 'ext_116164', 7, 'enabled', NOW(), NOW() +WHERE NOT EXISTS (SELECT 1 FROM mohong_categories WHERE name = '十二格大红'); + +INSERT INTO mohong_categories (name, code, sort_order, status, created_at, updated_at) +SELECT '哈弗币', 'ext_117413', 100, 'enabled', NOW(), NOW() +WHERE NOT EXISTS (SELECT 1 FROM mohong_categories WHERE name = '哈弗币'); + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '跟车', 'https://xcx-1429798857.cos.ap-guangzhou.myqcloud.com/uploads/mall6361/20260710/5314818/d0b13402eb01531671951f4d53f42785.jpg', CAST('[]' AS JSON), '跟车', 6800, 0, '件', -1, 20000, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '炫彩' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '跟车') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +SET p.category_id = c.id, + p.cover_url = 'https://xcx-1429798857.cos.ap-guangzhou.myqcloud.com/uploads/mall6361/20260710/5314818/d0b13402eb01531671951f4d53f42785.jpg', + p.price_cent = 6800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 20000, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '跟车' ELSE p.description END +WHERE p.title = '跟车'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +WHERE p.title = '跟车'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '整套扑克牌', 'https://xcx-1429798857.cos.ap-guangzhou.myqcloud.com/uploads/mall6361/20260703/5314818/cb8713f99cb033d073eaa231008db374.png', CAST('[]' AS JSON), '整套扑克牌', 28800, 0, '件', -1, 19999, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '炫彩' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '整套扑克牌') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +SET p.category_id = c.id, + p.cover_url = 'https://xcx-1429798857.cos.ap-guangzhou.myqcloud.com/uploads/mall6361/20260703/5314818/cb8713f99cb033d073eaa231008db374.png', + p.price_cent = 28800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19999, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '整套扑克牌' ELSE p.description END +WHERE p.title = '整套扑克牌'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +WHERE p.title = '整套扑克牌'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '炫彩露小娜', 'https://xcx-1429798857.cos.ap-guangzhou.myqcloud.com/uploads/mall6361/20260703/5314818/158226ce839d9f19f92905df064dcbdb.png', CAST('[]' AS JSON), '炫彩露小娜', 16800, 0, '件', -1, 19998, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '炫彩' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '炫彩露小娜') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +SET p.category_id = c.id, + p.cover_url = 'https://xcx-1429798857.cos.ap-guangzhou.myqcloud.com/uploads/mall6361/20260703/5314818/158226ce839d9f19f92905df064dcbdb.png', + p.price_cent = 16800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19998, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '炫彩露小娜' ELSE p.description END +WHERE p.title = '炫彩露小娜'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +WHERE p.title = '炫彩露小娜'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '炫彩鸟蛋', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260401/3586411/bcf4dc13c01cd77919551d7a8c5493fe.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '炫彩鸟蛋', 18800, 25800, '件', -1, 19997, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '炫彩' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '炫彩鸟蛋') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260401/3586411/bcf4dc13c01cd77919551d7a8c5493fe.png?x-oss-process=style/zhutu', + p.price_cent = 18800, + p.original_price_cent = 25800, + p.unit = '件', + p.sort_order = 19997, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '炫彩鸟蛋' ELSE p.description END +WHERE p.title = '炫彩鸟蛋'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +WHERE p.title = '炫彩鸟蛋'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '炫彩乌小蛋', 'https://xcx-1429798857.cos.ap-guangzhou.myqcloud.com/uploads/mall6361/20260510/5314818/c6c52c6a352f0422ac5ada30423692a1.png', CAST('[]' AS JSON), '炫彩乌小蛋', 9800, 23800, '件', -1, 19996, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '炫彩' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '炫彩乌小蛋') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +SET p.category_id = c.id, + p.cover_url = 'https://xcx-1429798857.cos.ap-guangzhou.myqcloud.com/uploads/mall6361/20260510/5314818/c6c52c6a352f0422ac5ada30423692a1.png', + p.price_cent = 9800, + p.original_price_cent = 23800, + p.unit = '件', + p.sort_order = 19996, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '炫彩乌小蛋' ELSE p.description END +WHERE p.title = '炫彩乌小蛋'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +WHERE p.title = '炫彩乌小蛋'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '炫彩蓝小登', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260401/3586411/af46637d3f6a8d27b8a5c9eb6b31632f.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '炫彩蓝小登', 5800, 15800, '件', -1, 19995, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '炫彩' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '炫彩蓝小登') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260401/3586411/af46637d3f6a8d27b8a5c9eb6b31632f.png?x-oss-process=style/zhutu', + p.price_cent = 5800, + p.original_price_cent = 15800, + p.unit = '件', + p.sort_order = 19995, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '炫彩蓝小登' ELSE p.description END +WHERE p.title = '炫彩蓝小登'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +WHERE p.title = '炫彩蓝小登'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '吴彦祖之境', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260401/3586411/a80fbd6dab236af5203515df08133ac0.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '吴彦祖之境', 10800, 15800, '件', -1, 19994, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '炫彩' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '吴彦祖之境') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260401/3586411/a80fbd6dab236af5203515df08133ac0.png?x-oss-process=style/zhutu', + p.price_cent = 10800, + p.original_price_cent = 15800, + p.unit = '件', + p.sort_order = 19994, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '吴彦祖之境' ELSE p.description END +WHERE p.title = '吴彦祖之境'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +WHERE p.title = '吴彦祖之境'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '好运爆红烟花', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/abcd0c7de74d8b53e9ca3b4d8817b1a4.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '好运爆红烟花', 1000, 0, '件', -1, 19993, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '炫彩' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '好运爆红烟花') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/abcd0c7de74d8b53e9ca3b4d8817b1a4.png?x-oss-process=style/zhutu', + p.price_cent = 1000, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19993, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '好运爆红烟花' ELSE p.description END +WHERE p.title = '好运爆红烟花'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +WHERE p.title = '好运爆红烟花'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '炫彩麦小蛋', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260401/3586411/7c6c7355701589e3be4ec1aef04166e5.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '炫彩麦小蛋', 15800, 28800, '件', -1, 19992, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '炫彩' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '炫彩麦小蛋') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260401/3586411/7c6c7355701589e3be4ec1aef04166e5.png?x-oss-process=style/zhutu', + p.price_cent = 15800, + p.original_price_cent = 28800, + p.unit = '件', + p.sort_order = 19992, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '炫彩麦小蛋' ELSE p.description END +WHERE p.title = '炫彩麦小蛋'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +WHERE p.title = '炫彩麦小蛋'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '炫彩克小圈', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260401/3586411/166bef4f0513c8bba80a242f788c9880.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '炫彩克小圈', 5800, 15800, '件', -1, 19991, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '炫彩' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '炫彩克小圈') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260401/3586411/166bef4f0513c8bba80a242f788c9880.png?x-oss-process=style/zhutu', + p.price_cent = 5800, + p.original_price_cent = 15800, + p.unit = '件', + p.sort_order = 19991, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '炫彩克小圈' ELSE p.description END +WHERE p.title = '炫彩克小圈'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +WHERE p.title = '炫彩克小圈'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '勇者之证', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260401/3586411/1e51afedd35ec8451dfdf14bdeaa5723.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '勇者之证', 11800, 21800, '件', -1, 19990, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '炫彩' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '勇者之证') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260401/3586411/1e51afedd35ec8451dfdf14bdeaa5723.png?x-oss-process=style/zhutu', + p.price_cent = 11800, + p.original_price_cent = 21800, + p.unit = '件', + p.sort_order = 19990, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '勇者之证' ELSE p.description END +WHERE p.title = '勇者之证'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +WHERE p.title = '勇者之证'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '渡鸦的录音数据-1', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260401/3586411/01cc42d1d85d4517329f8a3d54060014.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '渡鸦的录音数据-1', 5800, 8800, '件', -1, 19989, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '炫彩' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '渡鸦的录音数据-1') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260401/3586411/01cc42d1d85d4517329f8a3d54060014.png?x-oss-process=style/zhutu', + p.price_cent = 5800, + p.original_price_cent = 8800, + p.unit = '件', + p.sort_order = 19989, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '渡鸦的录音数据-1' ELSE p.description END +WHERE p.title = '渡鸦的录音数据-1'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +WHERE p.title = '渡鸦的录音数据-1'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '炫彩阿米娅', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260401/3586411/32e50b212afcfe18b2f4d148db2da9f9.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '炫彩阿米娅', 5800, 15800, '件', -1, 19988, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '炫彩' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '炫彩阿米娅') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260401/3586411/32e50b212afcfe18b2f4d148db2da9f9.png?x-oss-process=style/zhutu', + p.price_cent = 5800, + p.original_price_cent = 15800, + p.unit = '件', + p.sort_order = 19988, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '炫彩阿米娅' ELSE p.description END +WHERE p.title = '炫彩阿米娅'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +WHERE p.title = '炫彩阿米娅'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '特级八宝粥', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260406/3586411/cb8e609d9755be37b6f270a87a539cb1.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '特级八宝粥', 18800, 0, '件', -1, 19987, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '炫彩' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '特级八宝粥') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260406/3586411/cb8e609d9755be37b6f270a87a539cb1.png?x-oss-process=style/zhutu', + p.price_cent = 18800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19987, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '特级八宝粥' ELSE p.description END +WHERE p.title = '特级八宝粥'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +WHERE p.title = '特级八宝粥'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '乙已玄武', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260401/3586411/1749b2fd338b292e6f13884916687f88.jpg?x-oss-process=style/zhutu', CAST('[]' AS JSON), '乙已玄武', 10800, 18800, '件', -1, 19986, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '炫彩' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '乙已玄武') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260401/3586411/1749b2fd338b292e6f13884916687f88.jpg?x-oss-process=style/zhutu', + p.price_cent = 10800, + p.original_price_cent = 18800, + p.unit = '件', + p.sort_order = 19986, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '乙已玄武' ELSE p.description END +WHERE p.title = '乙已玄武'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +WHERE p.title = '乙已玄武'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '炫彩拉小宅', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260401/3586411/ecba2387fc2292425737f3102118a43c.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '炫彩拉小宅', 5800, 15800, '件', -1, 19985, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '炫彩' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '炫彩拉小宅') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260401/3586411/ecba2387fc2292425737f3102118a43c.png?x-oss-process=style/zhutu', + p.price_cent = 5800, + p.original_price_cent = 15800, + p.unit = '件', + p.sort_order = 19985, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '炫彩拉小宅' ELSE p.description END +WHERE p.title = '炫彩拉小宅'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +WHERE p.title = '炫彩拉小宅'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '典狱长的证章', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260406/3586411/8ae8d769d9a1976e578f5920f4daac94.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '典狱长的证章', 6800, 0, '件', -1, 19984, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '炫彩' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '典狱长的证章') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260406/3586411/8ae8d769d9a1976e578f5920f4daac94.png?x-oss-process=style/zhutu', + p.price_cent = 6800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19984, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '典狱长的证章' ELSE p.description END +WHERE p.title = '典狱长的证章'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +WHERE p.title = '典狱长的证章'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '契约之钥', 'https://xcx-1429798857.cos.ap-guangzhou.myqcloud.com/uploads/mall6361/20260514/5314818/0a38a0556b4a0ef63782f79367848729.png', CAST('[]' AS JSON), '契约之钥', 9800, 0, '件', -1, 19983, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '炫彩' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '契约之钥') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +SET p.category_id = c.id, + p.cover_url = 'https://xcx-1429798857.cos.ap-guangzhou.myqcloud.com/uploads/mall6361/20260514/5314818/0a38a0556b4a0ef63782f79367848729.png', + p.price_cent = 9800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19983, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '契约之钥' ELSE p.description END +WHERE p.title = '契约之钥'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +WHERE p.title = '契约之钥'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '劳拉', 'https://xcx-1429798857.cos.ap-guangzhou.myqcloud.com/uploads/mall6361/20260514/5314818/63d27b7eaf0db7cc94cf939fcc2b286e.png', CAST('[]' AS JSON), '劳拉', 5800, 0, '件', -1, 19982, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '炫彩' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '劳拉') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +SET p.category_id = c.id, + p.cover_url = 'https://xcx-1429798857.cos.ap-guangzhou.myqcloud.com/uploads/mall6361/20260514/5314818/63d27b7eaf0db7cc94cf939fcc2b286e.png', + p.price_cent = 5800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19982, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '劳拉' ELSE p.description END +WHERE p.title = '劳拉'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +WHERE p.title = '劳拉'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '“六”级弹', 'https://xcx-1429798857.cos.ap-guangzhou.myqcloud.com/uploads/mall6361/20260510/5314818/00df4a4757e8a03f08bc77f5e56c5763.png', CAST('[]' AS JSON), '“六”级弹', 5800, 0, '件', -1, 19981, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '炫彩' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '“六”级弹') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +SET p.category_id = c.id, + p.cover_url = 'https://xcx-1429798857.cos.ap-guangzhou.myqcloud.com/uploads/mall6361/20260510/5314818/00df4a4757e8a03f08bc77f5e56c5763.png', + p.price_cent = 5800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19981, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '“六”级弹' ELSE p.description END +WHERE p.title = '“六”级弹'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +WHERE p.title = '“六”级弹'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '炫彩足球', 'https://xcx-1429798857.cos.ap-guangzhou.myqcloud.com/uploads/mall6361/20260510/5314818/7ec64e0d59e06700b6c29176533a974d.png', CAST('[]' AS JSON), '炫彩足球', 9800, 19800, '件', -1, 19980, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '炫彩' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '炫彩足球') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +SET p.category_id = c.id, + p.cover_url = 'https://xcx-1429798857.cos.ap-guangzhou.myqcloud.com/uploads/mall6361/20260510/5314818/7ec64e0d59e06700b6c29176533a974d.png', + p.price_cent = 9800, + p.original_price_cent = 19800, + p.unit = '件', + p.sort_order = 19980, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '炫彩足球' ELSE p.description END +WHERE p.title = '炫彩足球'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +WHERE p.title = '炫彩足球'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '炫彩比利郎', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260401/3586411/ecae1317321df66b140115083476a987.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '炫彩比利郎', 4800, 8800, '件', -1, 19979, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '炫彩' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '炫彩比利郎') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260401/3586411/ecae1317321df66b140115083476a987.png?x-oss-process=style/zhutu', + p.price_cent = 4800, + p.original_price_cent = 8800, + p.unit = '件', + p.sort_order = 19979, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '炫彩比利郎' ELSE p.description END +WHERE p.title = '炫彩比利郎'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +WHERE p.title = '炫彩比利郎'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '至纯源石', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260401/3586411/37dd770f2e440489b146c49981807bf0.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '至纯源石', 3800, 7800, '件', -1, 19978, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '炫彩' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '至纯源石') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260401/3586411/37dd770f2e440489b146c49981807bf0.png?x-oss-process=style/zhutu', + p.price_cent = 3800, + p.original_price_cent = 7800, + p.unit = '件', + p.sort_order = 19978, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '至纯源石' ELSE p.description END +WHERE p.title = '至纯源石'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +WHERE p.title = '至纯源石'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '外星人电脑', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260401/3586411/2743812c2b333478200f6755b0604874.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '外星人电脑', 18800, 25800, '件', -1, 19977, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '炫彩' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '外星人电脑') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260401/3586411/2743812c2b333478200f6755b0604874.png?x-oss-process=style/zhutu', + p.price_cent = 18800, + p.original_price_cent = 25800, + p.unit = '件', + p.sort_order = 19977, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '外星人电脑' ELSE p.description END +WHERE p.title = '外星人电脑'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +WHERE p.title = '外星人电脑'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '马上转运', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260401/3586411/47472c273db051f2925d7f6128913387.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '马上转运', 4800, 13800, '件', -1, 19976, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '炫彩' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '马上转运') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260401/3586411/47472c273db051f2925d7f6128913387.png?x-oss-process=style/zhutu', + p.price_cent = 4800, + p.original_price_cent = 13800, + p.unit = '件', + p.sort_order = 19976, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '马上转运' ELSE p.description END +WHERE p.title = '马上转运'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +WHERE p.title = '马上转运'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '马上起飞', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260401/3586411/377a6a5b65cef229586ea0a2d279a5a7.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '马上起飞', 4800, 0, '件', -1, 19975, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '炫彩' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '马上起飞') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260401/3586411/377a6a5b65cef229586ea0a2d279a5a7.png?x-oss-process=style/zhutu', + p.price_cent = 4800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19975, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '马上起飞' ELSE p.description END +WHERE p.title = '马上起飞'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +WHERE p.title = '马上起飞'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '巨兽机甲模型', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260401/3586411/2bae2355874694cf172d341c5fbfdbf0.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '巨兽机甲模型', 6800, 10800, '件', -1, 19974, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '炫彩' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '巨兽机甲模型') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260401/3586411/2bae2355874694cf172d341c5fbfdbf0.png?x-oss-process=style/zhutu', + p.price_cent = 6800, + p.original_price_cent = 10800, + p.unit = '件', + p.sort_order = 19974, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '巨兽机甲模型' ELSE p.description END +WHERE p.title = '巨兽机甲模型'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +WHERE p.title = '巨兽机甲模型'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '红鲤王', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260401/3586411/db9dc4a8ee3f14f5c0c9c59f2e207afd.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '红鲤王', 4800, 10800, '件', -1, 19973, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '炫彩' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '红鲤王') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260401/3586411/db9dc4a8ee3f14f5c0c9c59f2e207afd.png?x-oss-process=style/zhutu', + p.price_cent = 4800, + p.original_price_cent = 10800, + p.unit = '件', + p.sort_order = 19973, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '红鲤王' ELSE p.description END +WHERE p.title = '红鲤王'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +WHERE p.title = '红鲤王'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '阿萨拉极品平安果', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260401/3586411/d0fe10d3593968cde0a6359873573243.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '阿萨拉极品平安果', 2800, 5800, '件', -1, 19972, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '炫彩' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '阿萨拉极品平安果') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260401/3586411/d0fe10d3593968cde0a6359873573243.png?x-oss-process=style/zhutu', + p.price_cent = 2800, + p.original_price_cent = 5800, + p.unit = '件', + p.sort_order = 19972, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '阿萨拉极品平安果' ELSE p.description END +WHERE p.title = '阿萨拉极品平安果'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +WHERE p.title = '阿萨拉极品平安果'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '炫彩赤小霄', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260401/3586411/3b4960fa2f2db810cd83f6fdec14e60c.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '炫彩赤小霄', 5800, 21800, '件', -1, 19971, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '炫彩' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '炫彩赤小霄') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260401/3586411/3b4960fa2f2db810cd83f6fdec14e60c.png?x-oss-process=style/zhutu', + p.price_cent = 5800, + p.original_price_cent = 21800, + p.unit = '件', + p.sort_order = 19971, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '炫彩赤小霄' ELSE p.description END +WHERE p.title = '炫彩赤小霄'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +WHERE p.title = '炫彩赤小霄'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '炫彩魏小龙', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260401/3586411/7e9ebf92468acda9f5dcc1f4159a0f3d.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '炫彩魏小龙', 6800, 19800, '件', -1, 19970, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '炫彩' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '炫彩魏小龙') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260401/3586411/7e9ebf92468acda9f5dcc1f4159a0f3d.png?x-oss-process=style/zhutu', + p.price_cent = 6800, + p.original_price_cent = 19800, + p.unit = '件', + p.sort_order = 19970, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '炫彩魏小龙' ELSE p.description END +WHERE p.title = '炫彩魏小龙'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +WHERE p.title = '炫彩魏小龙'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '木雕', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260403/3586411/6c6e56eb6b7a381a6952d34960844baf.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '木雕', 3800, 0, '件', -1, 19969, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '炫彩' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '木雕') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260403/3586411/6c6e56eb6b7a381a6952d34960844baf.png?x-oss-process=style/zhutu', + p.price_cent = 3800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19969, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '木雕' ELSE p.description END +WHERE p.title = '木雕'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +WHERE p.title = '木雕'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '黄金鸟蛋', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260401/3586411/d144a495adb520877a5d87f5c839103a.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '黄金鸟蛋', 1800, 5800, '件', -1, 19968, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '炫彩' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '黄金鸟蛋') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260401/3586411/d144a495adb520877a5d87f5c839103a.png?x-oss-process=style/zhutu', + p.price_cent = 1800, + p.original_price_cent = 5800, + p.unit = '件', + p.sort_order = 19968, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '黄金鸟蛋' ELSE p.description END +WHERE p.title = '黄金鸟蛋'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '炫彩' +WHERE p.title = '黄金鸟蛋'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '3X3安全箱', 'https://xcx-1429798857.cos.ap-guangzhou.myqcloud.com/uploads/mall6361/20260626/5314818/160048f6dbdde3bc6d15769aef212967.jpg', CAST('[]' AS JSON), '3X3安全箱', 58800, 0, '件', -1, 19967, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '任务' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '3X3安全箱') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '任务' +SET p.category_id = c.id, + p.cover_url = 'https://xcx-1429798857.cos.ap-guangzhou.myqcloud.com/uploads/mall6361/20260626/5314818/160048f6dbdde3bc6d15769aef212967.jpg', + p.price_cent = 58800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19967, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '3X3安全箱' ELSE p.description END +WHERE p.title = '3X3安全箱'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '任务' +WHERE p.title = '3X3安全箱'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '收集者', 'https://xcx-1429798857.cos.ap-guangzhou.myqcloud.com/uploads/mall6361/20260626/5314818/5c53d5007b6a133e6f031470478e4a45.png', CAST('[]' AS JSON), '收集者', 600, 0, '件', -1, 19966, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '任务' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '收集者') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '任务' +SET p.category_id = c.id, + p.cover_url = 'https://xcx-1429798857.cos.ap-guangzhou.myqcloud.com/uploads/mall6361/20260626/5314818/5c53d5007b6a133e6f031470478e4a45.png', + p.price_cent = 600, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19966, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '收集者' ELSE p.description END +WHERE p.title = '收集者'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '任务' +WHERE p.title = '收集者'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '阶段性任务', 'https://xcx-1429798857.cos.ap-guangzhou.myqcloud.com/uploads/mall6361/20260626/5314818/574c59b4b9b415e4498db4b7753feaa3.png', CAST('[]' AS JSON), '阶段性任务', 12000, 0, '件', -1, 19965, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '任务' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '阶段性任务') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '任务' +SET p.category_id = c.id, + p.cover_url = 'https://xcx-1429798857.cos.ap-guangzhou.myqcloud.com/uploads/mall6361/20260626/5314818/574c59b4b9b415e4498db4b7753feaa3.png', + p.price_cent = 12000, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19965, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '阶段性任务' ELSE p.description END +WHERE p.title = '阶段性任务'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '任务' +WHERE p.title = '阶段性任务'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '天才少年(自备24AW)', 'https://xcx-1429798857.cos.ap-guangzhou.myqcloud.com/uploads/mall6361/20260527/5314818/e09fbeb934bde9966732980f5b56839e.png', CAST('[]' AS JSON), '天才少年(自备24AW)', 6800, 0, '件', -1, 19964, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '任务' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '天才少年(自备24AW)') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '任务' +SET p.category_id = c.id, + p.cover_url = 'https://xcx-1429798857.cos.ap-guangzhou.myqcloud.com/uploads/mall6361/20260527/5314818/e09fbeb934bde9966732980f5b56839e.png', + p.price_cent = 6800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19964, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '天才少年(自备24AW)' ELSE p.description END +WHERE p.title = '天才少年(自备24AW)'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '任务' +WHERE p.title = '天才少年(自备24AW)'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '200AW', 'https://xcx-1429798857.cos.ap-guangzhou.myqcloud.com/uploads/mall6361/20260510/5314818/82133425c6d46b36cb0c0956cea725ee.png', CAST('[]' AS JSON), '200AW', 31000, 0, '件', -1, 19963, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = 'AW' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '200AW') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = 'AW' +SET p.category_id = c.id, + p.cover_url = 'https://xcx-1429798857.cos.ap-guangzhou.myqcloud.com/uploads/mall6361/20260510/5314818/82133425c6d46b36cb0c0956cea725ee.png', + p.price_cent = 31000, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19963, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '200AW' ELSE p.description END +WHERE p.title = '200AW'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = 'AW' +WHERE p.title = '200AW'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '100AW', 'https://xcx-1429798857.cos.ap-guangzhou.myqcloud.com/uploads/mall6361/20260510/5314818/82133425c6d46b36cb0c0956cea725ee.png', CAST('[]' AS JSON), '100AW', 15500, 0, '件', -1, 19962, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = 'AW' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '100AW') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = 'AW' +SET p.category_id = c.id, + p.cover_url = 'https://xcx-1429798857.cos.ap-guangzhou.myqcloud.com/uploads/mall6361/20260510/5314818/82133425c6d46b36cb0c0956cea725ee.png', + p.price_cent = 15500, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19962, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '100AW' ELSE p.description END +WHERE p.title = '100AW'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = 'AW' +WHERE p.title = '100AW'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '复苏呼吸机', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/6ba5bbc67f735ed8d504e08003a8c70f.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '复苏呼吸机', 24800, 32800, '件', -1, 19961, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '大红' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '复苏呼吸机') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '大红' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/6ba5bbc67f735ed8d504e08003a8c70f.png?x-oss-process=style/zhutu', + p.price_cent = 24800, + p.original_price_cent = 32800, + p.unit = '件', + p.sort_order = 19961, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '复苏呼吸机' ELSE p.description END +WHERE p.title = '复苏呼吸机'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '大红' +WHERE p.title = '复苏呼吸机'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '九格大红' +WHERE p.title = '复苏呼吸机'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '打字机', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260403/3586411/54b668d24fb8185af37af5e2d72ba4c6.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '打字机', 24800, 0, '件', -1, 19960, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '大红' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '打字机') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '大红' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260403/3586411/54b668d24fb8185af37af5e2d72ba4c6.png?x-oss-process=style/zhutu', + p.price_cent = 24800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19960, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '打字机' ELSE p.description END +WHERE p.title = '打字机'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '大红' +WHERE p.title = '打字机'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '十二格大红' +WHERE p.title = '打字机'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '微型反应炉', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260403/3586411/15dc3e1591707923b59f3ee97660867d.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '微型反应炉', 24800, 0, '件', -1, 19959, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '大红' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '微型反应炉') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '大红' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260403/3586411/15dc3e1591707923b59f3ee97660867d.png?x-oss-process=style/zhutu', + p.price_cent = 24800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19959, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '微型反应炉' ELSE p.description END +WHERE p.title = '微型反应炉'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '大红' +WHERE p.title = '微型反应炉'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '九格大红' +WHERE p.title = '微型反应炉'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '电子脚铐', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/d92fbf972f4e22c5d65efcd94f7285ac.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '电子脚铐', 5800, 0, '件', -1, 19958, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '大红' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '电子脚铐') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '大红' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/d92fbf972f4e22c5d65efcd94f7285ac.png?x-oss-process=style/zhutu', + p.price_cent = 5800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19958, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '电子脚铐' ELSE p.description END +WHERE p.title = '电子脚铐'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '大红' +WHERE p.title = '电子脚铐'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '万金泪冠', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260403/3586411/3cf46e352fb870cee016d30e545b4304.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '万金泪冠', 8800, 0, '件', -1, 19957, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '大红' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '万金泪冠') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '大红' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260403/3586411/3cf46e352fb870cee016d30e545b4304.png?x-oss-process=style/zhutu', + p.price_cent = 8800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19957, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '万金泪冠' ELSE p.description END +WHERE p.title = '万金泪冠'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '大红' +WHERE p.title = '万金泪冠'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '九格大红' +WHERE p.title = '万金泪冠'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '绝密服务器', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260403/3586411/face265395570c89b6a95b6867fa2052.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '绝密服务器', 8800, 0, '件', -1, 19956, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '大红' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '绝密服务器') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '大红' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260403/3586411/face265395570c89b6a95b6867fa2052.png?x-oss-process=style/zhutu', + p.price_cent = 8800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19956, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '绝密服务器' ELSE p.description END +WHERE p.title = '绝密服务器'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '大红' +WHERE p.title = '绝密服务器'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '九格大红' +WHERE p.title = '绝密服务器'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '“天圆地方”', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260403/3586411/3cf9601c6dbab3ffcd10bf44123ba3ff.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '“天圆地方”', 5800, 0, '件', -1, 19955, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '大红' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '“天圆地方”') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '大红' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260403/3586411/3cf9601c6dbab3ffcd10bf44123ba3ff.png?x-oss-process=style/zhutu', + p.price_cent = 5800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19955, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '“天圆地方”' ELSE p.description END +WHERE p.title = '“天圆地方”'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '大红' +WHERE p.title = '“天圆地方”'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '六格大红' +WHERE p.title = '“天圆地方”'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '“纵横”', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/2ca75865ededa0847dd332646e2a7524.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '“纵横”', 8800, 0, '件', -1, 19954, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '大红' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '“纵横”') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '大红' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/2ca75865ededa0847dd332646e2a7524.png?x-oss-process=style/zhutu', + p.price_cent = 8800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19954, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '“纵横”' ELSE p.description END +WHERE p.title = '“纵横”'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '大红' +WHERE p.title = '“纵横”'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '九格大红' +WHERE p.title = '“纵横”'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '装甲车电池', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260403/3586411/e66551d1a4245d4480fe450f7cef1ed0.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '装甲车电池', 24800, 0, '件', -1, 19953, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '大红' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '装甲车电池') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '大红' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260403/3586411/e66551d1a4245d4480fe450f7cef1ed0.png?x-oss-process=style/zhutu', + p.price_cent = 24800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19953, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '装甲车电池' ELSE p.description END +WHERE p.title = '装甲车电池'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '大红' +WHERE p.title = '装甲车电池'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '九格大红' +WHERE p.title = '装甲车电池'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, 'ECMO', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/8bb76c76afd6f5f3b05f5673b78481d0.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), 'ECMO', 18800, 26800, '件', -1, 19952, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '大红' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = 'ECMO') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '大红' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/8bb76c76afd6f5f3b05f5673b78481d0.png?x-oss-process=style/zhutu', + p.price_cent = 18800, + p.original_price_cent = 26800, + p.unit = '件', + p.sort_order = 19952, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN 'ECMO' ELSE p.description END +WHERE p.title = 'ECMO'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '大红' +WHERE p.title = 'ECMO'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '九格大红' +WHERE p.title = 'ECMO'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '火箭燃料', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260406/3586411/9db49a797de667a0897d5f31f3e6fa9d.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '火箭燃料', 16800, 0, '件', -1, 19951, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '大红' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '火箭燃料') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '大红' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260406/3586411/9db49a797de667a0897d5f31f3e6fa9d.png?x-oss-process=style/zhutu', + p.price_cent = 16800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19951, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '火箭燃料' ELSE p.description END +WHERE p.title = '火箭燃料'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '大红' +WHERE p.title = '火箭燃料'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '十二格大红' +WHERE p.title = '火箭燃料'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '暗星燃料单元', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260406/3586411/43f5639eb5bd54f0b0e315a04746e805.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '暗星燃料单元', 16800, 0, '件', -1, 19950, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '大红' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '暗星燃料单元') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '大红' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260406/3586411/43f5639eb5bd54f0b0e315a04746e805.png?x-oss-process=style/zhutu', + p.price_cent = 16800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19950, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '暗星燃料单元' ELSE p.description END +WHERE p.title = '暗星燃料单元'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '大红' +WHERE p.title = '暗星燃料单元'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '十二格大红' +WHERE p.title = '暗星燃料单元'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '福利补偿设备', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260406/3586411/232651755bff4b65aa890a63d6fae4c4.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '福利补偿设备', 14800, 0, '件', -1, 19949, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '大红' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '福利补偿设备') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '大红' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260406/3586411/232651755bff4b65aa890a63d6fae4c4.png?x-oss-process=style/zhutu', + p.price_cent = 14800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19949, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '福利补偿设备' ELSE p.description END +WHERE p.title = '福利补偿设备'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '大红' +WHERE p.title = '福利补偿设备'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '十二格大红' +WHERE p.title = '福利补偿设备'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '飞行记录仪', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/c5d506dcf6e04805228a428412b7fdf4.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '飞行记录仪', 6800, 0, '件', -1, 19948, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '大红' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '飞行记录仪') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '大红' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/c5d506dcf6e04805228a428412b7fdf4.png?x-oss-process=style/zhutu', + p.price_cent = 6800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19948, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '飞行记录仪' ELSE p.description END +WHERE p.title = '飞行记录仪'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '大红' +WHERE p.title = '飞行记录仪'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '六格大红' +WHERE p.title = '飞行记录仪'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '刀片服务器', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/2bad3711de577860d20baa392e68e382.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '刀片服务器', 16800, 0, '件', -1, 19947, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '大红' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '刀片服务器') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '大红' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/2bad3711de577860d20baa392e68e382.png?x-oss-process=style/zhutu', + p.price_cent = 16800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19947, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '刀片服务器' ELSE p.description END +WHERE p.title = '刀片服务器'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '大红' +WHERE p.title = '刀片服务器'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '十二格大红' +WHERE p.title = '刀片服务器'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '高速磁盘阵列', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/b01e546e2636e0d356dedad5f4b0ee2b.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '高速磁盘阵列', 16800, 0, '件', -1, 19946, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '大红' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '高速磁盘阵列') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '大红' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/b01e546e2636e0d356dedad5f4b0ee2b.png?x-oss-process=style/zhutu', + p.price_cent = 16800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19946, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '高速磁盘阵列' ELSE p.description END +WHERE p.title = '高速磁盘阵列'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '大红' +WHERE p.title = '高速磁盘阵列'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '十二格大红' +WHERE p.title = '高速磁盘阵列'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '曼德尔超算单元', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/bd47d419092b7849a56dbf10d887d3f5.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '曼德尔超算单元', 8800, 15800, '件', -1, 19945, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '大红' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '曼德尔超算单元') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '大红' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/bd47d419092b7849a56dbf10d887d3f5.png?x-oss-process=style/zhutu', + p.price_cent = 8800, + p.original_price_cent = 15800, + p.unit = '件', + p.sort_order = 19945, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '曼德尔超算单元' ELSE p.description END +WHERE p.title = '曼德尔超算单元'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '大红' +WHERE p.title = '曼德尔超算单元'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '九格大红' +WHERE p.title = '曼德尔超算单元'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '便携军用雷达', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/f6d17a962dc2b9099fc9896fe5c3de9e.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '便携军用雷达', 8800, 15800, '件', -1, 19944, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '大红' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '便携军用雷达') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '大红' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/f6d17a962dc2b9099fc9896fe5c3de9e.png?x-oss-process=style/zhutu', + p.price_cent = 8800, + p.original_price_cent = 15800, + p.unit = '件', + p.sort_order = 19944, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '便携军用雷达' ELSE p.description END +WHERE p.title = '便携军用雷达'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '大红' +WHERE p.title = '便携军用雷达'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '九格大红' +WHERE p.title = '便携军用雷达'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '高级咖啡豆', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/9bf756058eb5b593d590f9090d18c3ac.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '高级咖啡豆', 1000, 0, '件', -1, 19943, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '大红' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '高级咖啡豆') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '大红' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/9bf756058eb5b593d590f9090d18c3ac.png?x-oss-process=style/zhutu', + p.price_cent = 1000, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19943, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '高级咖啡豆' ELSE p.description END +WHERE p.title = '高级咖啡豆'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '大红' +WHERE p.title = '高级咖啡豆'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '奥莉薇娅香槟', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/1b0144c534389220a2ba7843f2a6d604.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '奥莉薇娅香槟', 2800, 0, '件', -1, 19942, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '大红' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '奥莉薇娅香槟') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '大红' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/1b0144c534389220a2ba7843f2a6d604.png?x-oss-process=style/zhutu', + p.price_cent = 2800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19942, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '奥莉薇娅香槟' ELSE p.description END +WHERE p.title = '奥莉薇娅香槟'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '大红' +WHERE p.title = '奥莉薇娅香槟'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '强力吸尘器', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/6286cff5d370437dd60347500f369096.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '强力吸尘器', 6800, 0, '件', -1, 19941, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '大红' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '强力吸尘器') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '大红' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/6286cff5d370437dd60347500f369096.png?x-oss-process=style/zhutu', + p.price_cent = 6800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19941, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '强力吸尘器' ELSE p.description END +WHERE p.title = '强力吸尘器'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '大红' +WHERE p.title = '强力吸尘器'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '六格大红' +WHERE p.title = '强力吸尘器'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '扫拖一体机器人', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/9a763b5c461540ba2d766de08215f54c.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '扫拖一体机器人', 8800, 0, '件', -1, 19940, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '大红' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '扫拖一体机器人') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '大红' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/9a763b5c461540ba2d766de08215f54c.png?x-oss-process=style/zhutu', + p.price_cent = 8800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19940, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '扫拖一体机器人' ELSE p.description END +WHERE p.title = '扫拖一体机器人'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '大红' +WHERE p.title = '扫拖一体机器人'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '九格大红' +WHERE p.title = '扫拖一体机器人'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '超声波切割刀', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/228f51fe3825837065d33200e8f545a5.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '超声波切割刀', 2800, 0, '件', -1, 19939, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '大红' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '超声波切割刀') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '大红' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/228f51fe3825837065d33200e8f545a5.png?x-oss-process=style/zhutu', + p.price_cent = 2800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19939, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '超声波切割刀' ELSE p.description END +WHERE p.title = '超声波切割刀'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '大红' +WHERE p.title = '超声波切割刀'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '军用炮弹', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/288e43feacfe03133b8cbfe0c8d73aac.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '军用炮弹', 6800, 0, '件', -1, 19938, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '大红' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '军用炮弹') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '大红' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/288e43feacfe03133b8cbfe0c8d73aac.png?x-oss-process=style/zhutu', + p.price_cent = 6800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19938, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '军用炮弹' ELSE p.description END +WHERE p.title = '军用炮弹'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '大红' +WHERE p.title = '军用炮弹'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '六格大红' +WHERE p.title = '军用炮弹'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '强化碳纤维板', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/fa9c77020fb22b6330ba354156d569d2.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '强化碳纤维板', 8800, 0, '件', -1, 19937, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '大红' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '强化碳纤维板') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '大红' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/fa9c77020fb22b6330ba354156d569d2.png?x-oss-process=style/zhutu', + p.price_cent = 8800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19937, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '强化碳纤维板' ELSE p.description END +WHERE p.title = '强化碳纤维板'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '大红' +WHERE p.title = '强化碳纤维板'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '九格大红' +WHERE p.title = '强化碳纤维板'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '呼吸机', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/18e5a352dad2578fcbf116f046e8ad06.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '呼吸机', 4800, 0, '件', -1, 19936, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '大红' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '呼吸机') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '大红' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/18e5a352dad2578fcbf116f046e8ad06.png?x-oss-process=style/zhutu', + p.price_cent = 4800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19936, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '呼吸机' ELSE p.description END +WHERE p.title = '呼吸机'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '大红' +WHERE p.title = '呼吸机'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '四格大红' +WHERE p.title = '呼吸机'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '医疗机器人', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/4cfb6b44fa399e833166f4bdce9a7973.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '医疗机器人', 6800, 0, '件', -1, 19935, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '大红' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '医疗机器人') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '大红' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/4cfb6b44fa399e833166f4bdce9a7973.png?x-oss-process=style/zhutu', + p.price_cent = 6800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19935, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '医疗机器人' ELSE p.description END +WHERE p.title = '医疗机器人'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '大红' +WHERE p.title = '医疗机器人'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '六格大红' +WHERE p.title = '医疗机器人'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '自动体外除颤器', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/fce0e168ae41cb00a4b00f9e43b6f9c4.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '自动体外除颤器', 5800, 0, '件', -1, 19934, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '大红' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '自动体外除颤器') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '大红' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/fce0e168ae41cb00a4b00f9e43b6f9c4.png?x-oss-process=style/zhutu', + p.price_cent = 5800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19934, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '自动体外除颤器' ELSE p.description END +WHERE p.title = '自动体外除颤器'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '大红' +WHERE p.title = '自动体外除颤器'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '六格大红' +WHERE p.title = '自动体外除颤器'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '恒星敏感器', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/bd19cc778769ecb17606ef5cc45f56dd.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '恒星敏感器', 2800, 0, '件', -1, 19933, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '大红' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '恒星敏感器') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '大红' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/bd19cc778769ecb17606ef5cc45f56dd.png?x-oss-process=style/zhutu', + p.price_cent = 2800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19933, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '恒星敏感器' ELSE p.description END +WHERE p.title = '恒星敏感器'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '大红' +WHERE p.title = '恒星敏感器'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '定位接收器', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/8bfa9f20369b8eb91bed60780f6caa05.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '定位接收器', 1000, 0, '件', -1, 19932, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '大红' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '定位接收器') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '大红' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/8bfa9f20369b8eb91bed60780f6caa05.png?x-oss-process=style/zhutu', + p.price_cent = 1000, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19932, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '定位接收器' ELSE p.description END +WHERE p.title = '定位接收器'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '大红' +WHERE p.title = '定位接收器'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '军用无人机', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/e1ab2cd3542645314843b5e56bcade7f.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '军用无人机', 4800, 0, '件', -1, 19931, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '大红' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '军用无人机') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '大红' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/e1ab2cd3542645314843b5e56bcade7f.png?x-oss-process=style/zhutu', + p.price_cent = 4800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19931, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '军用无人机' ELSE p.description END +WHERE p.title = '军用无人机'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '大红' +WHERE p.title = '军用无人机'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '四格大红' +WHERE p.title = '军用无人机'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '摄影机', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/d18d60edd31ca94722b307e3372b39e2.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '摄影机', 4800, 0, '件', -1, 19930, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '大红' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '摄影机') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '大红' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/d18d60edd31ca94722b307e3372b39e2.png?x-oss-process=style/zhutu', + p.price_cent = 4800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19930, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '摄影机' ELSE p.description END +WHERE p.title = '摄影机'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '大红' +WHERE p.title = '摄影机'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '四格大红' +WHERE p.title = '摄影机'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '军用电台', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/bd524ec217e28eabe87dd0fe1733e98f.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '军用电台', 5800, 0, '件', -1, 19929, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '大红' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '军用电台') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '大红' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/bd524ec217e28eabe87dd0fe1733e98f.png?x-oss-process=style/zhutu', + p.price_cent = 5800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19929, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '军用电台' ELSE p.description END +WHERE p.title = '军用电台'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '大红' +WHERE p.title = '军用电台'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '四格大红' +WHERE p.title = '军用电台'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '军用信息终端', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/c1493db2367d7d5746bc9799dc2386e4.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '军用信息终端', 4800, 0, '件', -1, 19928, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '大红' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '军用信息终端') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '大红' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/c1493db2367d7d5746bc9799dc2386e4.png?x-oss-process=style/zhutu', + p.price_cent = 4800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19928, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '军用信息终端' ELSE p.description END +WHERE p.title = '军用信息终端'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '大红' +WHERE p.title = '军用信息终端'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '四格大红' +WHERE p.title = '军用信息终端'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '笔记本电脑', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/b54644364cde9e122da27708097e353e.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '笔记本电脑', 8800, 0, '件', -1, 19927, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '大红' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '笔记本电脑') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '大红' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/b54644364cde9e122da27708097e353e.png?x-oss-process=style/zhutu', + p.price_cent = 8800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19927, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '笔记本电脑' ELSE p.description END +WHERE p.title = '笔记本电脑'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '大红' +WHERE p.title = '笔记本电脑'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '六格大红' +WHERE p.title = '笔记本电脑'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '黄金鳄鱼头雕像', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260403/3586411/5d53470ad5a76cdfeb2ab00601d58a6b.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '黄金鳄鱼头雕像', 4800, 0, '件', -1, 19926, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '大红' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '黄金鳄鱼头雕像') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '大红' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260403/3586411/5d53470ad5a76cdfeb2ab00601d58a6b.png?x-oss-process=style/zhutu', + p.price_cent = 4800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19926, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '黄金鳄鱼头雕像' ELSE p.description END +WHERE p.title = '黄金鳄鱼头雕像'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '大红' +WHERE p.title = '黄金鳄鱼头雕像'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '四格大红' +WHERE p.title = '黄金鳄鱼头雕像'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '滑膛枪展品', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260403/3586411/d43f59eb39bd9a9fb4a03108e6e32e38.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '滑膛枪展品', 6800, 0, '件', -1, 19925, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '大红' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '滑膛枪展品') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '大红' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260403/3586411/d43f59eb39bd9a9fb4a03108e6e32e38.png?x-oss-process=style/zhutu', + p.price_cent = 6800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19925, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '滑膛枪展品' ELSE p.description END +WHERE p.title = '滑膛枪展品'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '大红' +WHERE p.title = '滑膛枪展品'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '便携式生命支持系统', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260403/3586411/f269aa59aacb4d629ed4ef06885810c6.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '便携式生命支持系统', 2800, 0, '件', -1, 19924, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '大红' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '便携式生命支持系统') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '大红' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260403/3586411/f269aa59aacb4d629ed4ef06885810c6.png?x-oss-process=style/zhutu', + p.price_cent = 2800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19924, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '便携式生命支持系统' ELSE p.description END +WHERE p.title = '便携式生命支持系统'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '大红' +WHERE p.title = '便携式生命支持系统'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '克劳迪乌斯半身像', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260403/3586411/03263778c7f48953b256a413d5e16e27.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '克劳迪乌斯半身像', 6800, 0, '件', -1, 19923, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '大红' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '克劳迪乌斯半身像') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '大红' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260403/3586411/03263778c7f48953b256a413d5e16e27.png?x-oss-process=style/zhutu', + p.price_cent = 6800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19923, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '克劳迪乌斯半身像' ELSE p.description END +WHERE p.title = '克劳迪乌斯半身像'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '大红' +WHERE p.title = '克劳迪乌斯半身像'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '六格大红' +WHERE p.title = '克劳迪乌斯半身像'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '雷斯的留声机', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260403/3586411/9883434f60f986111bb1a68e02d96801.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '雷斯的留声机', 6800, 0, '件', -1, 19922, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '大红' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '雷斯的留声机') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '大红' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260403/3586411/9883434f60f986111bb1a68e02d96801.png?x-oss-process=style/zhutu', + p.price_cent = 6800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19922, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '雷斯的留声机' ELSE p.description END +WHERE p.title = '雷斯的留声机'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '大红' +WHERE p.title = '雷斯的留声机'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '六格大红' +WHERE p.title = '雷斯的留声机'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '步战车模型', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260403/3586411/93a13b90d62c8103d2d6d5f1bd90e384.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '步战车模型', 6800, 0, '件', -1, 19921, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '大红' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '步战车模型') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '大红' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260403/3586411/93a13b90d62c8103d2d6d5f1bd90e384.png?x-oss-process=style/zhutu', + p.price_cent = 6800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19921, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '步战车模型' ELSE p.description END +WHERE p.title = '步战车模型'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '大红' +WHERE p.title = '步战车模型'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '六格大红' +WHERE p.title = '步战车模型'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '名窑瓷器', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260403/3586411/a5b6ced31dad717b209dff53101db760.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '名窑瓷器', 12800, 0, '件', -1, 19920, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '大红' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '名窑瓷器') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '大红' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260403/3586411/a5b6ced31dad717b209dff53101db760.png?x-oss-process=style/zhutu', + p.price_cent = 12800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19920, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '名窑瓷器' ELSE p.description END +WHERE p.title = '名窑瓷器'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '大红' +WHERE p.title = '名窑瓷器'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '十二格大红' +WHERE p.title = '名窑瓷器'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '印象派名画', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260403/3586411/13061e69d45ded42de64bdbc98c90338.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '印象派名画', 8800, 0, '件', -1, 19919, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '大红' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '印象派名画') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '大红' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260403/3586411/13061e69d45ded42de64bdbc98c90338.png?x-oss-process=style/zhutu', + p.price_cent = 8800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19919, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '印象派名画' ELSE p.description END +WHERE p.title = '印象派名画'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '大红' +WHERE p.title = '印象派名画'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '九格大红' +WHERE p.title = '印象派名画'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '主战坦克模型', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/6cb718f74e8024501bcbd542f4cc898b.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '主战坦克模型', 8800, 0, '件', -1, 19918, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '大红' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '主战坦克模型') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '大红' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260402/3586411/6cb718f74e8024501bcbd542f4cc898b.png?x-oss-process=style/zhutu', + p.price_cent = 8800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19918, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '主战坦克模型' ELSE p.description END +WHERE p.title = '主战坦克模型'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '大红' +WHERE p.title = '主战坦克模型'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '九格大红' +WHERE p.title = '主战坦克模型'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '瓦斯罐', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260403/3586411/4c31f13bb61bf12dd3a3009d897d6a86.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '瓦斯罐', 6800, 0, '件', -1, 19917, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '大红' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '瓦斯罐') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '大红' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260403/3586411/4c31f13bb61bf12dd3a3009d897d6a86.png?x-oss-process=style/zhutu', + p.price_cent = 6800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19917, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '瓦斯罐' ELSE p.description END +WHERE p.title = '瓦斯罐'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '大红' +WHERE p.title = '瓦斯罐'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '四格大红' +WHERE p.title = '瓦斯罐'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '动力电池组', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260403/3586411/ad637ac6b2cae3f814a2e7aa6d03ee45.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '动力电池组', 20800, 0, '件', -1, 19916, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '大红' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '动力电池组') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '大红' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260403/3586411/ad637ac6b2cae3f814a2e7aa6d03ee45.png?x-oss-process=style/zhutu', + p.price_cent = 20800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19916, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '动力电池组' ELSE p.description END +WHERE p.title = '动力电池组'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '大红' +WHERE p.title = '动力电池组'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '十二格大红' +WHERE p.title = '动力电池组'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '试制聚变供能单元', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260403/3586411/ef4d31b029fe46358e3b63d05d94c965.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '试制聚变供能单元', 8800, 0, '件', -1, 19915, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '大红' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '试制聚变供能单元') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '大红' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260403/3586411/ef4d31b029fe46358e3b63d05d94c965.png?x-oss-process=style/zhutu', + p.price_cent = 8800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19915, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '试制聚变供能单元' ELSE p.description END +WHERE p.title = '试制聚变供能单元'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '大红' +WHERE p.title = '试制聚变供能单元'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '九格大红' +WHERE p.title = '试制聚变供能单元'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '量子存储', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260403/3586411/eccbbccced7b0c5248caf4d4eb67df89.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '量子存储', 400, 0, '件', -1, 19914, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '大红' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '量子存储') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '大红' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260403/3586411/eccbbccced7b0c5248caf4d4eb67df89.png?x-oss-process=style/zhutu', + p.price_cent = 400, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19914, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '量子存储' ELSE p.description END +WHERE p.title = '量子存储'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '大红' +WHERE p.title = '量子存储'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '潮汐监狱地图-4', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260403/3586411/4f33f1239229e9b518d9f7c0bad374bd.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '潮汐监狱地图-4', 2800, 0, '件', -1, 19913, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '大红' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '潮汐监狱地图-4') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '大红' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260403/3586411/4f33f1239229e9b518d9f7c0bad374bd.png?x-oss-process=style/zhutu', + p.price_cent = 2800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19913, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '潮汐监狱地图-4' ELSE p.description END +WHERE p.title = '潮汐监狱地图-4'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '大红' +WHERE p.title = '潮汐监狱地图-4'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '云存储阵列', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260403/3586411/bab342c829353d81b74f5dd6dd551e49.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '云存储阵列', 6800, 0, '件', -1, 19912, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '大红' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '云存储阵列') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '大红' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260403/3586411/bab342c829353d81b74f5dd6dd551e49.png?x-oss-process=style/zhutu', + p.price_cent = 6800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19912, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '云存储阵列' ELSE p.description END +WHERE p.title = '云存储阵列'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '大红' +WHERE p.title = '云存储阵列'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '六格大红' +WHERE p.title = '云存储阵列'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '黄金瞪羚', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260403/3586411/3181ee2a198aeb587996f8e4b9dc9037.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '黄金瞪羚', 3800, 0, '件', -1, 19911, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '大红' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '黄金瞪羚') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '大红' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260403/3586411/3181ee2a198aeb587996f8e4b9dc9037.png?x-oss-process=style/zhutu', + p.price_cent = 3800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19911, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '黄金瞪羚' ELSE p.description END +WHERE p.title = '黄金瞪羚'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '大红' +WHERE p.title = '黄金瞪羚'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '四格大红' +WHERE p.title = '黄金瞪羚'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '卫星锅', 'https://oss-sc.juhuodong.com/uploads/mall5875/20260406/3586411/41d96de0ef9487b78724f5493b467504.png?x-oss-process=style/zhutu', CAST('[]' AS JSON), '卫星锅', 3800, 0, '件', -1, 19910, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '大红' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '卫星锅') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '大红' +SET p.category_id = c.id, + p.cover_url = 'https://oss-sc.juhuodong.com/uploads/mall5875/20260406/3586411/41d96de0ef9487b78724f5493b467504.png?x-oss-process=style/zhutu', + p.price_cent = 3800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19910, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '卫星锅' ELSE p.description END +WHERE p.title = '卫星锅'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '大红' +WHERE p.title = '卫星锅'; + +INSERT INTO mohong_products (category_id, title, cover_url, image_urls, description, price_cent, original_price_cent, unit, stock, sort_order, status, qrcode_image_url, created_at, updated_at) +SELECT c.id, '1亿哈弗币', 'https://xcx-1429798857.cos.ap-guangzhou.myqcloud.com/uploads/mall6361/20260527/5314818/5dc2faa533f3b3a424034a9ba44c97e0.png', CAST('[]' AS JSON), '1亿哈弗币', 78800, 0, '件', -1, 19909, 'on_sale', '', NOW(), NOW() +FROM mohong_categories c WHERE c.name = '哈弗币' +AND NOT EXISTS (SELECT 1 FROM mohong_products p WHERE p.title = '1亿哈弗币') +LIMIT 1; + +UPDATE mohong_products p +JOIN mohong_categories c ON c.name = '哈弗币' +SET p.category_id = c.id, + p.cover_url = 'https://xcx-1429798857.cos.ap-guangzhou.myqcloud.com/uploads/mall6361/20260527/5314818/5dc2faa533f3b3a424034a9ba44c97e0.png', + p.price_cent = 78800, + p.original_price_cent = 0, + p.unit = '件', + p.sort_order = 19909, + p.status = 'on_sale', + p.description = CASE WHEN p.description = '' THEN '1亿哈弗币' ELSE p.description END +WHERE p.title = '1亿哈弗币'; + +INSERT IGNORE INTO mohong_product_categories (product_id, category_id) +SELECT p.id, c.id FROM mohong_products p +JOIN mohong_categories c ON c.name = '哈弗币' +WHERE p.title = '1亿哈弗币'; + +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +-- 种子数据不回滚;仅移除关联表与权限(category_id 列保留) +DROP TABLE IF EXISTS mohong_product_categories; +DELETE rp FROM role_permissions rp +JOIN permissions p ON p.id = rp.permission_id +WHERE p.code = 'mohong:category'; +DELETE FROM permissions WHERE code = 'mohong:category'; +-- +goose StatementEnd + diff --git a/backend/scripts/mohong_seed_from_har.json b/backend/scripts/mohong_seed_from_har.json new file mode 100644 index 0000000..1d9c7de --- /dev/null +++ b/backend/scripts/mohong_seed_from_har.json @@ -0,0 +1,44 @@ +{ + "source": "/Users/yml/Desktop/抓包/大红抓包 2.har", + "note": "seed embedded in migrations/000029_mohong_categories_and_seed.sql", + "unique_products": 92, + "relations": 133, + "categories": [ + { + "name": "炫彩", + "count": 33 + }, + { + "name": "任务", + "count": 4 + }, + { + "name": "AW", + "count": 2 + }, + { + "name": "大红", + "count": 52 + }, + { + "name": "四格大红", + "count": 8 + }, + { + "name": "六格大红", + "count": 11 + }, + { + "name": "九格大红", + "count": 14 + }, + { + "name": "十二格大红", + "count": 8 + }, + { + "name": "哈弗币", + "count": 1 + } + ] +} \ No newline at end of file diff --git a/frontend/src/features/admin/views/AdminMohongCategoriesView.vue b/frontend/src/features/admin/views/AdminMohongCategoriesView.vue new file mode 100644 index 0000000..486800f --- /dev/null +++ b/frontend/src/features/admin/views/AdminMohongCategoriesView.vue @@ -0,0 +1,184 @@ + + + + + diff --git a/frontend/src/features/admin/views/AdminMohongConfigView.vue b/frontend/src/features/admin/views/AdminMohongConfigView.vue new file mode 100644 index 0000000..8bda22a --- /dev/null +++ b/frontend/src/features/admin/views/AdminMohongConfigView.vue @@ -0,0 +1,149 @@ + + + + + diff --git a/frontend/src/features/admin/views/AdminMohongOrdersView.vue b/frontend/src/features/admin/views/AdminMohongOrdersView.vue new file mode 100644 index 0000000..afd8cf5 --- /dev/null +++ b/frontend/src/features/admin/views/AdminMohongOrdersView.vue @@ -0,0 +1,244 @@ + + + + + diff --git a/frontend/src/features/admin/views/AdminMohongProductsView.vue b/frontend/src/features/admin/views/AdminMohongProductsView.vue new file mode 100644 index 0000000..3abe240 --- /dev/null +++ b/frontend/src/features/admin/views/AdminMohongProductsView.vue @@ -0,0 +1,418 @@ + + + + + diff --git a/frontend/src/features/listings/views/HomeView.vue b/frontend/src/features/listings/views/HomeView.vue index 5df492e..abfb41b 100644 --- a/frontend/src/features/listings/views/HomeView.vue +++ b/frontend/src/features/listings/views/HomeView.vue @@ -1,6 +1,6 @@ + + + + diff --git a/frontend/src/features/mohong/views/MobileMohongDetailView.vue b/frontend/src/features/mohong/views/MobileMohongDetailView.vue new file mode 100644 index 0000000..ecea8c1 --- /dev/null +++ b/frontend/src/features/mohong/views/MobileMohongDetailView.vue @@ -0,0 +1,318 @@ + + + + + diff --git a/frontend/src/features/mohong/views/MobileMohongListView.vue b/frontend/src/features/mohong/views/MobileMohongListView.vue new file mode 100644 index 0000000..d22017d --- /dev/null +++ b/frontend/src/features/mohong/views/MobileMohongListView.vue @@ -0,0 +1,386 @@ + + + + + diff --git a/frontend/src/features/mohong/views/MobileMohongOrderDetailView.vue b/frontend/src/features/mohong/views/MobileMohongOrderDetailView.vue new file mode 100644 index 0000000..3abae55 --- /dev/null +++ b/frontend/src/features/mohong/views/MobileMohongOrderDetailView.vue @@ -0,0 +1,339 @@ + + + + + diff --git a/frontend/src/features/mohong/views/MobileMohongOrdersView.vue b/frontend/src/features/mohong/views/MobileMohongOrdersView.vue new file mode 100644 index 0000000..9014994 --- /dev/null +++ b/frontend/src/features/mohong/views/MobileMohongOrdersView.vue @@ -0,0 +1,154 @@ + + + + + diff --git a/frontend/src/features/mohong/views/MohongDetailView.vue b/frontend/src/features/mohong/views/MohongDetailView.vue new file mode 100644 index 0000000..1397a80 --- /dev/null +++ b/frontend/src/features/mohong/views/MohongDetailView.vue @@ -0,0 +1,531 @@ + + + + + diff --git a/frontend/src/features/mohong/views/MohongListView.vue b/frontend/src/features/mohong/views/MohongListView.vue new file mode 100644 index 0000000..a6d10ba --- /dev/null +++ b/frontend/src/features/mohong/views/MohongListView.vue @@ -0,0 +1,406 @@ + + + + + diff --git a/frontend/src/features/mohong/views/MohongOrderDetailView.vue b/frontend/src/features/mohong/views/MohongOrderDetailView.vue new file mode 100644 index 0000000..4e75d56 --- /dev/null +++ b/frontend/src/features/mohong/views/MohongOrderDetailView.vue @@ -0,0 +1,437 @@ + + + + + diff --git a/frontend/src/features/mohong/views/MohongOrdersView.vue b/frontend/src/features/mohong/views/MohongOrdersView.vue new file mode 100644 index 0000000..405d9fa --- /dev/null +++ b/frontend/src/features/mohong/views/MohongOrdersView.vue @@ -0,0 +1,94 @@ + + + + + diff --git a/frontend/src/features/orders/composables/useMobilePaymentCashier.ts b/frontend/src/features/orders/composables/useMobilePaymentCashier.ts index 2ce20ca..456d99e 100644 --- a/frontend/src/features/orders/composables/useMobilePaymentCashier.ts +++ b/frontend/src/features/orders/composables/useMobilePaymentCashier.ts @@ -1,5 +1,6 @@ import { showDialog, showToast } from 'vant' +import type { PaymentOrder } from '@/features/orders/api/orders' import { paymentPayURL, useOrderPaymentCashier, @@ -14,7 +15,11 @@ export { paymentPayURL as mobilePaymentPayURL } * 并处理微信 / 支付宝内嵌浏览器的直接跳转拉起支付。对外 API 名保持不变, * Mobile 视图无需改动。 */ -export function useMobilePaymentCashier() { + +export function useMobilePaymentCashier(options?: { + queryPayment?: (orderId: number) => Promise + paidMessage?: string +}) { function isInAppPaymentBrowser(): boolean { if (typeof navigator === 'undefined') return false const ua = navigator.userAgent || '' @@ -32,9 +37,10 @@ export function useMobilePaymentCashier() { confirmButtonText: '知道了', }) }, - paidMessage: '支付成功,正在进入群消息', + paidMessage: options?.paidMessage || '支付成功,正在进入群消息', pollIntervalMs: 2500, qrWidth: 220, + queryPayment: options?.queryPayment, openExternalURL: url => { if (isHTTPURL(url) && isInAppPaymentBrowser()) { window.location.href = url diff --git a/frontend/src/features/orders/composables/useOrderPaymentCashier.ts b/frontend/src/features/orders/composables/useOrderPaymentCashier.ts index 9ee9376..cf5d4c0 100644 --- a/frontend/src/features/orders/composables/useOrderPaymentCashier.ts +++ b/frontend/src/features/orders/composables/useOrderPaymentCashier.ts @@ -41,6 +41,11 @@ export interface UseOrderPaymentCashierOptions { */ openExternalURL?: (url: string) => boolean /** 弹窗显隐控制方式。PC 用 el-dialog(v-model 一个 ref),Mobile 用 van-popup。返回 true 表示用 ref,false 表示 composable 内部 ref。 */ + /** + * 自定义支付状态查询。默认走租号订单 `/orders/:id/query-payment`。 + * 摸大红等独立业务可注入自己的查询函数。 + */ + queryPayment?: (orderId: number) => Promise } /** @@ -131,7 +136,8 @@ export function useOrderPaymentCashier(options: UseOrderPaymentCashierOptions) { checkingPayment.value = true const previousPayURL = payURL.value try { - const payment = await queryOrderPayment(activePayment.value.order_id) + const query = options.queryPayment || queryOrderPayment + const payment = await query(activePayment.value.order_id) activePayment.value = payment if (payment.paid) { await handlePaid() diff --git a/frontend/src/layouts/AdminLayout.vue b/frontend/src/layouts/AdminLayout.vue index 00d7de9..7ac0d1b 100644 --- a/frontend/src/layouts/AdminLayout.vue +++ b/frontend/src/layouts/AdminLayout.vue @@ -15,6 +15,7 @@ import { Money, Operation, Picture, + Present, ScaleToOriginal, Service, Shop, @@ -103,6 +104,30 @@ const allNavGroups: NavGroup[] = [ icon: DocumentChecked, permission: 'listing:approve', }, + { + label: '摸大红分类', + to: adminPath('mohong/categories'), + icon: Present, + permission: 'mohong:category', + }, + { + label: '摸大红商品', + to: adminPath('mohong/products'), + icon: Present, + permission: 'mohong:product_view', + }, + { + label: '摸大红订单', + to: adminPath('mohong/orders'), + icon: Tickets, + permission: 'mohong:order_view', + }, + { + label: '摸大红配置', + to: adminPath('mohong/config'), + icon: Setting, + permission: 'mohong:config', + }, ], }, { diff --git a/frontend/src/router/adminRoutes.ts b/frontend/src/router/adminRoutes.ts index 6e29753..321588d 100644 --- a/frontend/src/router/adminRoutes.ts +++ b/frontend/src/router/adminRoutes.ts @@ -72,6 +72,30 @@ export const adminRoutes: RouteRecordRaw[] = [ component: () => import('@/features/admin/views/AdminListingReviewView.vue'), meta: adminMeta, }, + { + path: adminPath('mohong/categories'), + name: 'admin-mohong-categories', + component: () => import('@/features/admin/views/AdminMohongCategoriesView.vue'), + meta: adminMeta, + }, + { + path: adminPath('mohong/products'), + name: 'admin-mohong-products', + component: () => import('@/features/admin/views/AdminMohongProductsView.vue'), + meta: adminMeta, + }, + { + path: adminPath('mohong/orders'), + name: 'admin-mohong-orders', + component: () => import('@/features/admin/views/AdminMohongOrdersView.vue'), + meta: adminMeta, + }, + { + path: adminPath('mohong/config'), + name: 'admin-mohong-config', + component: () => import('@/features/admin/views/AdminMohongConfigView.vue'), + meta: adminMeta, + }, { path: adminPath('disputes'), name: 'admin-disputes', diff --git a/frontend/src/router/mobileHost.ts b/frontend/src/router/mobileHost.ts index c682a9a..4dd4522 100644 --- a/frontend/src/router/mobileHost.ts +++ b/frontend/src/router/mobileHost.ts @@ -23,6 +23,9 @@ export function getMobilePath(path: string): string { if (path === '/listings' || path.startsWith('/listings/')) { return `/m${path}` } + if (path === '/mohong' || path.startsWith('/mohong/')) { + return `/m${path}` + } if (path === '/orders' || path.startsWith('/orders/')) { return `/m${path}` } @@ -65,6 +68,9 @@ export function getPcPath(path: string): string { if (subPath === '/listings' || subPath.startsWith('/listings/')) { return subPath } + if (subPath === '/mohong' || subPath.startsWith('/mohong/')) { + return subPath + } if (subPath === '/orders' || subPath.startsWith('/orders/')) { return subPath } diff --git a/frontend/src/router/mobileRoutes.ts b/frontend/src/router/mobileRoutes.ts index cab301d..fd78f84 100644 --- a/frontend/src/router/mobileRoutes.ts +++ b/frontend/src/router/mobileRoutes.ts @@ -91,6 +91,30 @@ export const mobileRoutes: RouteRecordRaw[] = [ component: () => import('@/features/wallet/views/MobileWithdrawalView.vue'), meta: { layout: 'blank', requiresAuth: true }, }, + { + path: '/m/mohong', + name: 'mobile-mohong-list', + component: () => import('@/features/mohong/views/MobileMohongListView.vue'), + meta: { layout: 'blank' }, + }, + { + path: '/m/mohong/orders', + name: 'mobile-mohong-orders', + component: () => import('@/features/mohong/views/MobileMohongOrdersView.vue'), + meta: { layout: 'blank', requiresAuth: true }, + }, + { + path: '/m/mohong/orders/:id', + name: 'mobile-mohong-order-detail', + component: () => import('@/features/mohong/views/MobileMohongOrderDetailView.vue'), + meta: { layout: 'blank', requiresAuth: true }, + }, + { + path: '/m/mohong/:id', + name: 'mobile-mohong-detail', + component: () => import('@/features/mohong/views/MobileMohongDetailView.vue'), + meta: { layout: 'blank' }, + }, { path: '/m/orders', name: 'mobile-orders', diff --git a/frontend/src/router/publicRoutes.ts b/frontend/src/router/publicRoutes.ts index 75230c6..8fbaf04 100644 --- a/frontend/src/router/publicRoutes.ts +++ b/frontend/src/router/publicRoutes.ts @@ -16,4 +16,27 @@ export const publicRoutes: RouteRecordRaw[] = [ name: 'listing-detail', component: () => import('@/features/listings/views/ListingDetailView.vue'), }, + // 摸大红 PC 端;移动端访问会被守卫重写到 /m/mohong* + { + path: '/mohong', + name: 'mohong-list', + component: () => import('@/features/mohong/views/MohongListView.vue'), + }, + { + path: '/mohong/orders', + name: 'mohong-orders', + component: () => import('@/features/mohong/views/MohongOrdersView.vue'), + meta: { requiresAuth: true }, + }, + { + path: '/mohong/orders/:id', + name: 'mohong-order-detail', + component: () => import('@/features/mohong/views/MohongOrderDetailView.vue'), + meta: { requiresAuth: true }, + }, + { + path: '/mohong/:id', + name: 'mohong-detail', + component: () => import('@/features/mohong/views/MohongDetailView.vue'), + }, ] diff --git a/frontend/src/styles/admin.css b/frontend/src/styles/admin.css index 69e8903..5abd4d5 100644 --- a/frontend/src/styles/admin.css +++ b/frontend/src/styles/admin.css @@ -348,19 +348,31 @@ } /* ========== Button Enhancements ========== */ -.el-button--primary:not(.is-text) { +/* 仅实体主按钮强制白字;link/text 需保留主题色,否则表格「编辑」等会白字看不见 */ +.el-button--primary:not(.is-text):not(.is-link) { font-weight: 700 !important; --el-button-text-color: #ffffff; --el-button-hover-text-color: #ffffff; --el-button-active-text-color: #ffffff; } -.el-button--primary:not(.is-text) span { +.el-button--primary:not(.is-text):not(.is-link) span { color: #ffffff !important; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.15); font-weight: 700; } +.el-button.is-link.el-button--primary { + --el-button-text-color: var(--el-color-primary); + --el-button-hover-text-color: var(--el-color-primary-light-3); + --el-button-active-text-color: var(--el-color-primary-dark-2); +} + +.el-button.is-link.el-button--primary span { + color: inherit !important; + text-shadow: none; +} + .eyebrow { margin: 0 0 8px; color: #4f7cff;