package service import ( "database/sql" "errors" "fmt" "strings" "time" "cmautobuy/admin/model" "cmautobuy/admin/repository" ) const maxShopNameLength = 500 type ShopListResult struct { Items []model.Shop UnlinkedShopeeCount int } func ListShops(db *sql.DB, actor *model.User) (ShopListResult, error) { if actor == nil || !actor.IsAdmin() { return ShopListResult{}, ErrAdminRequired } items, err := repository.ListShops(db) if err != nil { return ShopListResult{}, err } unlinked, err := repository.CountUnlinkedShopeeShops(db) if err != nil { return ShopListResult{}, err } return ShopListResult{Items: items, UnlinkedShopeeCount: unlinked}, nil } func ShopOptions(db *sql.DB) ([]model.Shop, error) { return repository.ListShopOptions(db) } func CreateShop(db *sql.DB, actor *model.User, displayName, sybAlias, shopeeAlias string, now time.Time) error { if actor == nil || !actor.IsAdmin() { return ErrAdminRequired } displayName, sybAlias, shopeeAlias, err := validateShopInput(displayName, sybAlias, shopeeAlias) if err != nil { return err } shopID, err := randomID("SHOP-", 16) if err != nil { return fmt.Errorf("生成店铺编号失败: %w", err) } at := now.UTC().Format(model.TimeLayout) tx, err := db.Begin() if err != nil { return err } defer tx.Rollback() if err := repository.InsertShop(tx, model.Shop{ShopID: shopID, DisplayName: displayName, NormalizedName: displayName, Enabled: true, CreatedByUserID: actor.UserID, CreatedAt: at, UpdatedAt: at}); err != nil { return shopValidationError(err) } if err := insertShopAlias(tx, shopID, "syb", sybAlias, true, at); err != nil { return shopValidationError(err) } if err := insertShopAlias(tx, shopID, "shopee", shopeeAlias, true, at); err != nil { return shopValidationError(err) } if err := repository.RebuildShopAssociations(tx, shopID); err != nil { return err } return tx.Commit() } func UpdateShop(db *sql.DB, actor *model.User, shopID, displayName, sybAlias, shopeeAlias string, now time.Time) error { if actor == nil || !actor.IsAdmin() { return ErrAdminRequired } shopID = strings.TrimSpace(shopID) if shopID == "" { return &validationError{field: "shop_id", message: "店铺编号不能为空"} } displayName, sybAlias, shopeeAlias, err := validateShopInput(displayName, sybAlias, shopeeAlias) if err != nil { return err } at := now.UTC().Format(model.TimeLayout) tx, err := db.Begin() if err != nil { return err } defer tx.Rollback() if _, found, err := repository.GetShop(tx, shopID); err != nil { return err } else if !found { return &validationError{field: "shop_id", message: "店铺不存在,请刷新页面后重试"} } sybEnabled, hasSybAlias, err := repository.GetShopAliasEnabled(tx, shopID, "syb") if err != nil { return err } if !hasSybAlias { sybEnabled = true } if _, err := repository.UpdateShopName(tx, shopID, displayName, displayName, at); err != nil { return shopValidationError(err) } if err := replaceShopAlias(tx, shopID, "syb", sybAlias, sybEnabled, at); err != nil { return shopValidationError(err) } if err := replaceShopAlias(tx, shopID, "shopee", shopeeAlias, true, at); err != nil { return shopValidationError(err) } if err := repository.RebuildShopAssociations(tx, shopID); err != nil { return err } return tx.Commit() } func SetShopEnabled(db *sql.DB, actor *model.User, shopID string, enabled bool, now time.Time) error { if actor == nil || !actor.IsAdmin() { return ErrAdminRequired } found, err := repository.SetShopEnabled(db, strings.TrimSpace(shopID), enabled, now.UTC().Format(model.TimeLayout)) if err != nil { return err } if !found { return &validationError{field: "shop_id", message: "店铺不存在,请刷新页面后重试"} } return nil } func SetShopSybEnabled(db *sql.DB, actor *model.User, shopID string, enabled bool, now time.Time) error { if actor == nil || !actor.IsAdmin() { return ErrAdminRequired } found, err := repository.SetShopSybEnabled(db, strings.TrimSpace(shopID), enabled, now.UTC().Format(model.TimeLayout)) if err != nil { return err } if !found { return &validationError{field: "shop_id", message: "该店铺没有配置 SYB 店铺名称"} } return nil } func DeleteShop(db *sql.DB, actor *model.User, shopID string) error { if actor == nil || !actor.IsAdmin() { return ErrAdminRequired } deleted, err := repository.DeleteDisabledShop(db, strings.TrimSpace(shopID)) if errors.Is(err, repository.ErrShopHasReferences) { return &validationError{field: "shop_id", message: "店铺仍有关联数据,只能保留为停用状态"} } if err != nil { return err } if !deleted { return &validationError{field: "shop_id", message: "店铺不存在或仍在启用,请先停用"} } return nil } func validateShopInput(displayName, sybAlias, shopeeAlias string) (string, string, string, error) { displayName = strings.TrimSpace(displayName) sybAlias = strings.TrimSpace(sybAlias) shopeeAlias = strings.TrimSpace(shopeeAlias) if displayName == "" { return "", "", "", &validationError{field: "display_name", message: "业务店铺名称不能为空"} } for field, value := range map[string]string{"display_name": displayName, "syb_alias": sybAlias, "shopee_alias": shopeeAlias} { if len([]rune(value)) > maxShopNameLength { return "", "", "", &validationError{field: field, message: "店铺名称最多 500 个字符"} } } if sybAlias == "" && shopeeAlias == "" { return "", "", "", &validationError{field: "syb_alias", message: "SYB 或蝦皮店铺名称至少填写一个"} } return displayName, sybAlias, shopeeAlias, nil } func insertShopAlias(q repository.Execer, shopID, channel, alias string, enabled bool, at string) error { if alias == "" { return nil } aliasID, err := randomID("ALIAS-", 16) if err != nil { return err } return repository.InsertShopAlias(q, model.ShopChannelAlias{AliasID: aliasID, ShopID: shopID, Channel: channel, AliasName: alias, NormalizedAlias: alias, Enabled: enabled, CreatedAt: at, UpdatedAt: at}) } func replaceShopAlias(q repository.Execer, shopID, channel, alias string, enabled bool, at string) error { aliasID, err := randomID("ALIAS-", 16) if err != nil { return err } return repository.ReplaceShopAlias(q, aliasID, shopID, channel, alias, at, enabled) } func shopValidationError(err error) error { switch { case errors.Is(err, repository.ErrShopNameExists): return &validationError{field: "display_name", message: "业务店铺名称已经存在"} case errors.Is(err, repository.ErrShopAliasExists): return &validationError{field: "syb_alias", message: "该渠道店铺名称已关联到其他业务店铺"} default: return err } }