package service import ( "errors" "net/url" "strings" "time" "unicode/utf8" "github.com/freefire/jiang13-bbs/model" "gorm.io/gorm" ) // 友情链接状态 const ( FriendLinkStatusPending = "pending" FriendLinkStatusApproved = "approved" FriendLinkStatusRejected = "rejected" ) // 字段长度上限 const ( friendLinkMaxName = 64 friendLinkMaxURL = 512 friendLinkMaxDescription = 255 friendLinkMaxLogoURL = 512 friendLinkMaxCategory = 32 friendLinkMaxReciprocal = 512 friendLinkMaxReject = 255 ) var ( // ErrFriendLinkInvalid 参数校验失败 ErrFriendLinkInvalid = errors.New("友情链接参数无效") // ErrFriendLinkNotFound 记录不存在 ErrFriendLinkNotFound = errors.New("友情链接不存在") ) // FriendLinkInput 申请/新建/更新共用入参(指针区分「未传」与「显式清空」由调用方决定) type FriendLinkInput struct { Name string `json:"name"` URL string `json:"url"` Description string `json:"description"` LogoURL string `json:"logo_url"` Category string `json:"category"` ReciprocalURL string `json:"reciprocal_url"` } // FriendLinkService 友情链接申请 + 后台 CRUD/审批 type FriendLinkService struct { db *gorm.DB } func NewFriendLinkService(db *gorm.DB) *FriendLinkService { return &FriendLinkService{db: db} } // validateInput 校验并归一化入参;返回归一化后的值 func validateFriendLinkInput(in FriendLinkInput) (FriendLinkInput, error) { name := strings.TrimSpace(in.Name) if name == "" || utf8.RuneCountInString(name) > friendLinkMaxName { return FriendLinkInput{}, ErrFriendLinkInvalid } rawURL := strings.TrimSpace(in.URL) if rawURL == "" || len(rawURL) > friendLinkMaxURL { return FriendLinkInput{}, ErrFriendLinkInvalid } if u, err := url.Parse(rawURL); err != nil || u.Scheme == "" || u.Host == "" || (u.Scheme != "http" && u.Scheme != "https") { return FriendLinkInput{}, ErrFriendLinkInvalid } desc := strings.TrimSpace(in.Description) if utf8.RuneCountInString(desc) > friendLinkMaxDescription { return FriendLinkInput{}, ErrFriendLinkInvalid } logo := strings.TrimSpace(in.LogoURL) if len(logo) > friendLinkMaxLogoURL { return FriendLinkInput{}, ErrFriendLinkInvalid } if logo != "" { if u, err := url.Parse(logo); err != nil || u.Scheme == "" || u.Host == "" || (u.Scheme != "http" && u.Scheme != "https") { return FriendLinkInput{}, ErrFriendLinkInvalid } } category := strings.TrimSpace(in.Category) if utf8.RuneCountInString(category) > friendLinkMaxCategory { return FriendLinkInput{}, ErrFriendLinkInvalid } reciprocal := strings.TrimSpace(in.ReciprocalURL) if len(reciprocal) > friendLinkMaxReciprocal { return FriendLinkInput{}, ErrFriendLinkInvalid } if reciprocal != "" { if u, err := url.Parse(reciprocal); err != nil || u.Scheme == "" || u.Host == "" || (u.Scheme != "http" && u.Scheme != "https") { return FriendLinkInput{}, ErrFriendLinkInvalid } } return FriendLinkInput{ Name: name, URL: rawURL, Description: desc, LogoURL: logo, Category: category, ReciprocalURL: reciprocal, }, nil } // ListApproved 前台公开列表:仅 approved,按 sort_order ASC, id ASC func (s *FriendLinkService) ListApproved() ([]model.FriendLink, error) { var list []model.FriendLink err := s.db.Where("status = ?", FriendLinkStatusApproved). Order("sort_order ASC, id ASC").Find(&list).Error if list == nil { list = []model.FriendLink{} } return list, err } // ListAll 后台全量列表(含 pending/rejected),按 status, sort_order, id func (s *FriendLinkService) ListAll() ([]model.FriendLink, error) { var list []model.FriendLink err := s.db.Order("status ASC, sort_order ASC, id ASC").Find(&list).Error if list == nil { list = []model.FriendLink{} } return list, err } // ListMine 申请人本人提交的全部申请(含待审/拒绝),按提交时间倒序 func (s *FriendLinkService) ListMine(userID uint) ([]model.FriendLink, error) { var list []model.FriendLink err := s.db.Where("applicant_user_id = ?", userID). Order("created_at DESC, id DESC").Find(&list).Error if list == nil { list = []model.FriendLink{} } return list, err } // OwnerUpdate 申请人修改自己的申请(名称/链接/回链/LOGO);非待审状态的记录修改后回退待审 func (s *FriendLinkService) OwnerUpdate(id, userID uint, in FriendLinkInput) error { norm, err := validateFriendLinkInput(in) if err != nil { return err } return s.db.Transaction(func(tx *gorm.DB) error { var fl model.FriendLink if err := tx.First(&fl, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return ErrFriendLinkNotFound } return err } // 仅申请人本人可改;他人访问统一按不存在处理,不暴露记录存在性 if fl.ApplicantUserID == nil || *fl.ApplicantUserID != userID { return ErrFriendLinkNotFound } updates := map[string]interface{}{ "name": norm.Name, "url": norm.URL, "logo_url": norm.LogoURL, "reciprocal_url": norm.ReciprocalURL, "updated_at": time.Now(), } if fl.Status != FriendLinkStatusPending { updates["status"] = FriendLinkStatusPending updates["reject_reason"] = "" } return tx.Model(&fl).Updates(updates).Error }) } // nextSortOrder 取当前 approved 记录中最大 sort_order + 1(新建/审批通过时使用) func (s *FriendLinkService) nextSortOrder() (int, error) { var maxOrder int err := s.db.Model(&model.FriendLink{}). Where("status = ?", FriendLinkStatusApproved). Select("COALESCE(MAX(sort_order), 0)").Row().Scan(&maxOrder) if err != nil { return 0, err } return maxOrder + 1, nil } // Apply 用户申请:status=pending func (s *FriendLinkService) Apply(in FriendLinkInput, applicantUserID uint) (*model.FriendLink, error) { norm, err := validateFriendLinkInput(in) if err != nil { return nil, err } uid := applicantUserID now := time.Now() fl := &model.FriendLink{ Name: norm.Name, URL: norm.URL, Description: norm.Description, LogoURL: norm.LogoURL, Category: norm.Category, ReciprocalURL: norm.ReciprocalURL, Status: FriendLinkStatusPending, SortOrder: 0, ApplicantUserID: &uid, CreatedAt: now, UpdatedAt: now, } if err := s.db.Create(fl).Error; err != nil { return nil, err } return fl, nil } // AdminCreate 站长直接新建:status=approved,自动分配 sort_order func (s *FriendLinkService) AdminCreate(in FriendLinkInput) (*model.FriendLink, error) { norm, err := validateFriendLinkInput(in) if err != nil { return nil, err } order, err := s.nextSortOrder() if err != nil { return nil, err } now := time.Now() fl := &model.FriendLink{ Name: norm.Name, URL: norm.URL, Description: norm.Description, LogoURL: norm.LogoURL, Category: norm.Category, ReciprocalURL: norm.ReciprocalURL, Status: FriendLinkStatusApproved, SortOrder: order, CreatedAt: now, UpdatedAt: now, } if err := s.db.Create(fl).Error; err != nil { return nil, err } return fl, nil } // AdminUpdate 全量更新内容(不改 status/sort_order/applicant) func (s *FriendLinkService) AdminUpdate(id uint, in FriendLinkInput) error { norm, err := validateFriendLinkInput(in) if err != nil { return err } res := s.db.Model(&model.FriendLink{}).Where("id = ?", id). Updates(map[string]interface{}{ "name": norm.Name, "url": norm.URL, "description": norm.Description, "logo_url": norm.LogoURL, "category": norm.Category, "reciprocal_url": norm.ReciprocalURL, "updated_at": time.Now(), }) if res.Error != nil { return res.Error } if res.RowsAffected == 0 { return ErrFriendLinkNotFound } return nil } // AdminSetStatus 审批:approved 时自动补 sort_order;rejected 时写 reject_reason func (s *FriendLinkService) AdminSetStatus(id uint, status, rejectReason string) error { status = strings.TrimSpace(status) if status != FriendLinkStatusApproved && status != FriendLinkStatusRejected && status != FriendLinkStatusPending { return ErrFriendLinkInvalid } if status == FriendLinkStatusRejected && utf8.RuneCountInString(rejectReason) > friendLinkMaxReject { return ErrFriendLinkInvalid } return s.db.Transaction(func(tx *gorm.DB) error { var fl model.FriendLink if err := tx.First(&fl, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return ErrFriendLinkNotFound } return err } updates := map[string]interface{}{ "status": status, "updated_at": time.Now(), "reject_reason": "", } switch status { case FriendLinkStatusApproved: if fl.SortOrder == 0 { var maxOrder int if err := tx.Model(&model.FriendLink{}). Where("status = ? AND id <> ?", FriendLinkStatusApproved, id). Select("COALESCE(MAX(sort_order), 0)").Row().Scan(&maxOrder); err != nil { return err } updates["sort_order"] = maxOrder + 1 } case FriendLinkStatusRejected: updates["reject_reason"] = rejectReason case FriendLinkStatusPending: // 切回 pending 视为下线,不动 sort_order } return tx.Model(&fl).Updates(updates).Error }) } // AdminDelete 软删除 func (s *FriendLinkService) AdminDelete(id uint) error { res := s.db.Delete(&model.FriendLink{}, id) if res.Error != nil { return res.Error } if res.RowsAffected == 0 { return ErrFriendLinkNotFound } return nil }