package models import ( "fmt" "strings" "unicode/utf8" "xorm.io/builder" "code.gitea.io/gitea/modules/timeutil" ) const RECOMMOND_TYPE = 5 const NORMAL_TYPE = 0 const IMAGE_STATUS_COMMIT = 0 const IMAGE_STATUS_SUCCESS = 1 const IMAGE_STATUS_Failed = 2 type Image struct { ID int64 `xorm:"pk autoincr" json:"id"` Type int `xorm:"INDEX NOT NULL" json:"type"` //0 normal 5官方推荐,中间值保留为后续扩展 CloudbrainType int `xorm:"INDEX NOT NULL" json:"cloudbrainType"` //0 云脑一 1云脑二 UID int64 `xorm:"INDEX NOT NULL" json:"uid"` IsPrivate bool `xorm:"INDEX NOT NULL" json:"isPrivate"` Tag string `xorm:"varchar(100) UNIQUE" json:"tag"` Description string `xorm:"varchar(765)" json:"description"` Topics []string `xorm:"TEXT JSON" json:"topics"` Place string `xorm:"varchar(300)" json:"place"` NumStars int `xorm:"NOT NULL DEFAULT 0" json:"numStars"` IsStar bool `xorm:"-" json:"isStar"` UserName string `xorm:"-" json:"userName"` RelAvatarLink string `xorm:"-" json:"relAvatarLink"` Status int `xorm:"INDEX NOT NULL DEFAULT 0" json:"status"` //0代表正在提交,1提交完成,2提交失败 CreatedUnix timeutil.TimeStamp `xorm:"INDEX created" json:"createdUnix"` UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated" json:"updatedUnix"` } type ImageList []*Image type ImageStar struct { ID int64 `xorm:"pk autoincr"` UID int64 `xorm:"UNIQUE(s)"` ImageID int64 `xorm:"UNIQUE(s)"` CreatedUnix timeutil.TimeStamp `xorm:"created"` } type ImageTopic struct { ID int64 `xorm:"pk autoincr"` Name string `xorm:"UNIQUE VARCHAR(105)"` ImageCount int CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` } type ImageTopicRelation struct { ImageID int64 `xorm:"UNIQUE(s)"` TopicID int64 `xorm:"UNIQUE(s)"` } type SearchImageOptions struct { Keyword string UID int64 Status int IncludePublicOnly bool IncludeOfficialOnly bool IncludePrivateOnly bool IncludeStarByMe bool IncludeCustom bool IncludeOwnerOnly bool Topics string CloudbrainType int ListOptions SearchOrderBy } type ErrorImageTagExist struct { Tag string } type ErrorImageCommitting struct { Tag string } type ImagesPageResult struct { Count int64 `json:"count"` Images []*Image `json:"images"` } func (err ErrorImageTagExist) Error() string { return fmt.Sprintf("Image already exists [tag: %s]", err.Tag) } func (err ErrorImageCommitting) Error() string { return fmt.Sprintf("Image already exists [tag: %s]", err.Tag) } type ErrImageNotExist struct { ID int64 Tag string } func (err ErrImageNotExist) Error() string { return fmt.Sprintf("Image does not exist [id: %d] [tag: %s]", err.ID, err.Tag) } func IsErrorImageCommitting(err error) bool { _, ok := err.(ErrorImageCommitting) return ok } func IsErrImageNotExist(err error) bool { _, ok := err.(ErrImageNotExist) return ok } func IsErrImageTagExist(err error) bool { _, ok := err.(ErrorImageTagExist) return ok } func IsImageExist(tag string) (bool, error) { return x.Exist(&Image{ Tag: tag, }) } func IsImageExistByUser(tag string, uid int64) (bool, error) { return x.Exist(&Image{ Tag: tag, UID: uid, Status: IMAGE_STATUS_SUCCESS, }) } type FindImageTopicOptions struct { ListOptions ImageID int64 Keyword string } func (opts *FindImageTopicOptions) toConds() builder.Cond { var cond = builder.NewCond() if opts.ImageID > 0 { cond = cond.And(builder.Eq{"image_topic_relation.image_id": opts.ImageID}) } if opts.Keyword != "" { cond = cond.And(builder.Like{"image_topic.name", strings.ToLower(opts.Keyword)}) } return cond } func GetImageByID(id int64) (*Image, error) { rel := new(Image) has, err := x. ID(id). Get(rel) if err != nil { return nil, err } else if !has { return nil, ErrImageNotExist{ID: id} } return rel, nil } func GetImageByTag(tag string) (*Image, error) { image := &Image{Tag: tag} has, err := x. Get(image) if err != nil { return nil, err } else if !has { return nil, ErrImageNotExist{Tag: tag} } return image, nil } func SanitizeAndValidateImageTopics(topics []string) (validTopics []string, invalidTopics []string) { validTopics = make([]string, 0) mValidTopics := make(map[string]struct{}) invalidTopics = make([]string, 0) for _, topic := range topics { topic = strings.TrimSpace(strings.ToLower(topic)) // ignore empty string if len(topic) == 0 { continue } // ignore same topic twice if _, ok := mValidTopics[topic]; ok { continue } if utf8.RuneCountInString(topic) <= 35 { validTopics = append(validTopics, topic) mValidTopics[topic] = struct{}{} } else { invalidTopics = append(invalidTopics, topic) } } return validTopics, invalidTopics } func FindImageTopics(opts *FindImageTopicOptions) (topics []*ImageTopic, err error) { sess := x.Select("image_topic.*").Where(opts.toConds()) if opts.ImageID > 0 { sess.Join("INNER", "image_topic_relation", "image_topic_relation.topic_id = image_topic.id") } if opts.PageSize != 0 && opts.Page != 0 { sess = opts.setSessionPagination(sess) } return topics, sess.Desc("image_topic.image_count").Find(&topics) } func SaveImageTopics(imageID int64, topicNames ...string) error { topics, err := FindImageTopics(&FindImageTopicOptions{ ImageID: imageID, }) if err != nil { return err } sess := x.NewSession() defer sess.Close() if err := sess.Begin(); err != nil { return err } var addedTopicNames []string for _, topicName := range topicNames { if strings.TrimSpace(topicName) == "" { continue } var found bool for _, t := range topics { if strings.EqualFold(topicName, t.Name) { found = true break } } if !found { addedTopicNames = append(addedTopicNames, topicName) } } var removeTopics []*ImageTopic for _, t := range topics { var found bool for _, topicName := range topicNames { if strings.EqualFold(topicName, t.Name) { found = true break } } if !found { removeTopics = append(removeTopics, t) } } for _, topicName := range addedTopicNames { _, err := addTopicByNameToImage(sess, imageID, topicName) if err != nil { return err } } for _, topic := range removeTopics { err := removeTopicFromImage(sess, imageID, topic) if err != nil { return err } } topicNames = make([]string, 0, 25) if err := sess.Table("image_topic").Cols("name"). Join("INNER", "image_topic_relation", "image_topic_relation.topic_id = image_topic.id"). Where("image_topic_relation.image_id = ?", imageID).Desc("image_topic.image_count").Find(&topicNames); err != nil { return err } if _, err := sess.ID(imageID).Cols("topics").Update(&Image{ Topics: topicNames, }); err != nil { return err } return sess.Commit() } func addTopicByNameToImage(e Engine, imageID int64, topicName string) (*ImageTopic, error) { var topic ImageTopic has, err := e.Where("name = ?", topicName).Get(&topic) if err != nil { return nil, err } if !has { topic.Name = topicName topic.ImageCount = 1 if _, err := e.Insert(&topic); err != nil { return nil, err } } else { topic.ImageCount++ if _, err := e.ID(topic.ID).Cols("image_count").Update(&topic); err != nil { return nil, err } } if _, err := e.Insert(&ImageTopicRelation{ ImageID: imageID, TopicID: topic.ID, }); err != nil { return nil, err } return &topic, nil } func removeTopicFromImage(e Engine, imageId int64, topic *ImageTopic) error { topic.ImageCount-- if _, err := e.ID(topic.ID).Cols("image_count").Update(topic); err != nil { return err } if _, err := e.Delete(&ImageTopicRelation{ ImageID: imageId, TopicID: topic.ID, }); err != nil { return err } return nil } func SearchImage(opts *SearchImageOptions) (ImageList, int64, error) { cond := SearchImageCondition(opts) return SearchImageByCondition(opts, cond) } func SearchImageCondition(opts *SearchImageOptions) builder.Cond { var cond = builder.NewCond() if len(opts.Keyword) > 0 { var subQueryCond = builder.NewCond() for _, v := range strings.Split(opts.Keyword, ",") { subQueryCond = subQueryCond.Or(builder.Like{"LOWER(image_topic.name)", strings.ToLower(v)}) } subQuery := builder.Select("image_topic_relation.image_id").From("image_topic_relation"). Join("INNER", "image_topic", "image_topic.id = image_topic_relation.topic_id"). Where(subQueryCond). GroupBy("image_topic_relation.image_id") var keywordCond = builder.In("id", subQuery) var likes = builder.NewCond() for _, v := range strings.Split(opts.Keyword, ",") { likes = likes.Or(builder.Like{"LOWER(tag)", strings.ToLower(v)}) likes = likes.Or(builder.Like{"LOWER(description)", strings.ToLower(v)}) } keywordCond = keywordCond.Or(likes) cond = cond.And(keywordCond) } if len(opts.Topics) > 0 { //标签精确匹配 var subQueryCond = builder.NewCond() for _, v := range strings.Split(opts.Keyword, ",") { subQueryCond = subQueryCond.Or(builder.Eq{"LOWER(image_topic.name)": strings.ToLower(v)}) subQuery := builder.Select("image_topic_relation.image_id").From("image_topic_relation"). Join("INNER", "image_topic", "image_topic.id = image_topic_relation.topic_id"). Where(subQueryCond). GroupBy("image_topic_relation.image_id") var topicCond = builder.In("id", subQuery) cond = cond.And(topicCond) } } if opts.IncludePublicOnly { cond = cond.And(builder.Eq{"is_private": false}) } if opts.IncludePrivateOnly { cond = cond.And(builder.Eq{"is_private": true}) } if opts.IncludeOwnerOnly { cond = cond.And(builder.Eq{"uid": opts.UID}) } if opts.IncludeOfficialOnly { cond = cond.And(builder.Eq{"type": RECOMMOND_TYPE}) } if opts.Status >= 0 { cond = cond.And(builder.Eq{"status": opts.Status}) } if opts.IncludeStarByMe { subQuery := builder.Select("image_id").From("image_star"). Where(builder.Eq{"uid": opts.UID}) var starCond = builder.In("id", subQuery) cond = cond.And(starCond) } if opts.CloudbrainType > 0 { cond = cond.And(builder.Eq{"cloudbrain_type": opts.CloudbrainType}) } return cond } func SearchImageByCondition(opts *SearchImageOptions, cond builder.Cond) (ImageList, int64, error) { if opts.Page <= 0 { opts.Page = 1 } var err error sess := x.NewSession() defer sess.Close() images := make(ImageList, 0, opts.PageSize) count, err := sess.Where(cond).Count(new(Image)) if err != nil { return nil, 0, fmt.Errorf("Count: %v", err) } sess.Where(cond).OrderBy(opts.SearchOrderBy.String()) if opts.PageSize > 0 { sess.Limit(opts.PageSize, (opts.Page-1)*opts.PageSize) } if err = sess.Find(&images); err != nil { return nil, 0, fmt.Errorf("Images: %v", err) } if err = images.loadAttributes(sess, opts.UID); err != nil { return nil, 0, fmt.Errorf("LoadAttributes: %v", err) } return images, count, nil } func (images ImageList) loadAttributes(e Engine, uid int64) error { if len(images) == 0 { return nil } set := make(map[int64]struct{}) for i := range images { set[images[i].UID] = struct{}{} } // Load creators. users := make(map[int64]*User, len(set)) if err := e.Table("\"user\""). Cols("name", "lower_name", "avatar", "email"). Where("id > 0"). In("id", keysInt64(set)). Find(&users); err != nil { return fmt.Errorf("find users: %v", err) } for i := range images { if users[images[i].UID] != nil { images[i].UserName = users[images[i].UID].Name images[i].RelAvatarLink = users[images[i].UID].RelAvatarLink() } else { images[i].UserName = "" images[i].RelAvatarLink = "" } if uid == -1 { images[i].IsStar = false } else { images[i].IsStar = isImageStaring(e, uid, images[i].ID) } } return nil } func GetCommittingImageCount() int { total, err := x.Where("status =?", 0).Count(new(Image)) if err != nil { return 0 } return int(total) } func CreateLocalImage(image *Image) error { _, err := x.Insert(image) return err } func UpdateLocalImage(image *Image) error { _, err := x.ID(image.ID).Cols("description", "is_private", "status").Update(image) return err } func UpdateLocalImageStatus(image *Image) error { _, err := x.ID(image.ID).Cols("status").Update(image) return err } func UpdateAutoIncrementIndex() { x.Exec("SELECT setval('image_id_seq', (SELECT MAX(id) from image))") } func DeleteLocalImage(id int64) error { image := new(Image) _, err := x.ID(id).Delete(image) return err } //star or unstar Image func StarImage(userID, imageID int64, star bool) error { sess := x.NewSession() defer sess.Close() if err := sess.Begin(); err != nil { return err } if star { if isImageStaring(sess, userID, imageID) { return nil } if _, err := sess.Insert(&ImageStar{UID: userID, ImageID: imageID}); err != nil { return err } if _, err := sess.Exec("UPDATE `image` SET num_stars = num_stars + 1 WHERE id = ?", imageID); err != nil { return err } if _, err := sess.Exec("UPDATE `user` SET num_image_stars = num_image_stars + 1 WHERE id = ?", userID); err != nil { return err } } else { if !isImageStaring(sess, userID, imageID) { return nil } if _, err := sess.Delete(&ImageStar{0, userID, imageID, 0}); err != nil { return err } if _, err := sess.Exec("UPDATE `image` SET num_stars = num_stars - 1 WHERE id = ?", imageID); err != nil { return err } if _, err := sess.Exec("UPDATE `user` SET num_image_stars = num_image_stars - 1 WHERE id = ?", userID); err != nil { return err } } return sess.Commit() } func IsImageStaring(userID, datasetID int64) bool { return isImageStaring(x, userID, datasetID) } func isImageStaring(e Engine, userID, imageID int64) bool { has, _ := e.Get(&ImageStar{0, userID, imageID, 0}) return has } func RecommendImage(imageId int64, recommond bool) error { image := Image{Type: GetRecommondType(recommond)} _, err := x.ID(imageId).Cols("type").Update(image) return err } func GetRecommondType(recommond bool) int { if recommond { return RECOMMOND_TYPE } else { return NORMAL_TYPE } }