diff --git a/README.md b/README.md index bea6492..e20217c 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,30 @@ - [x] 提供流媒体服务 - [x] 点击播放之前不加载视频(减少流量消耗) - [x] 使用封面图片替代加载视屏第一帧 +- [x] GraphQL 风格API + - [ ] 列表翻页 + + +GraphQL +```javascript +const query = ` +query ($id: Int!) { + article(id: $id) { + id + title + content + author { + id + name + } + } +} +` + + + + +``` 通过流媒体服务降低视频文件加载消耗及防止恶意刷流量 diff --git a/api/graphql.go b/api/graphql.go index 3fa5bb0..565259f 100644 --- a/api/graphql.go +++ b/api/graphql.go @@ -9,6 +9,7 @@ import ( "github.com/graphql-go/graphql" "github.com/graphql-go/graphql/language/ast" "github.com/jmoiron/sqlx" + "github.com/mitchellh/mapstructure" ) func NewSchema(config Config) (graphql.Schema, error) { @@ -52,7 +53,7 @@ func NewSchema(config Config) (graphql.Schema, error) { }, }) - // 图像中的文字提取 [{"text": "角色选择", "confidence": 0.8484202027320862, "coordinate": [[666.0, 66.0], [908.0, 81.0], [903.0, 174.0], [661.0, 160.0]]} + // 图像中的文字提取 text := graphql.NewObject(graphql.ObjectConfig{ Name: "Text", Fields: graphql.Fields{ @@ -172,26 +173,38 @@ func NewSchema(config Config) (graphql.Schema, error) { }, }), Args: graphql.FieldConfigArgument{ - "id": &graphql.ArgumentConfig{Type: graphql.Int}, - "width": &graphql.ArgumentConfig{Type: graphql.Int}, - "height": &graphql.ArgumentConfig{Type: graphql.Int}, + "id": &graphql.ArgumentConfig{Type: graphql.Int, Description: "筛选图像中指定ID的"}, + "width": &graphql.ArgumentConfig{Type: graphql.Int, Description: "筛选图像中指定宽度的"}, + "height": &graphql.ArgumentConfig{Type: graphql.Int, Description: "筛选图像中指定高度的"}, "content": &graphql.ArgumentConfig{Type: graphql.String}, "remark": &graphql.ArgumentConfig{Type: graphql.String}, "description": &graphql.ArgumentConfig{Type: graphql.String}, "tags": &graphql.ArgumentConfig{Type: graphql.String}, "rank": &graphql.ArgumentConfig{Type: graphql.String}, - "text": &graphql.ArgumentConfig{Type: graphql.String}, // 查找图像中的文字 + "text": &graphql.ArgumentConfig{Type: graphql.String, Description: "筛选图像中含有指定文字的"}, "comment_num": &graphql.ArgumentConfig{Type: graphql.Int}, "praise_count": &graphql.ArgumentConfig{Type: graphql.Int}, "collect_count": &graphql.ArgumentConfig{Type: graphql.Int}, - "article_id": &graphql.ArgumentConfig{Type: graphql.Int}, - "user_id": &graphql.ArgumentConfig{Type: graphql.Int}, + "article_id": &graphql.ArgumentConfig{Type: graphql.Int, Description: "筛选图像中属于指定文章ID的"}, + "user_id": &graphql.ArgumentConfig{Type: graphql.Int, Description: "筛选图像中属于指定用户ID的"}, "create_time": &graphql.ArgumentConfig{Type: graphql.DateTime}, "update_time": &graphql.ArgumentConfig{Type: graphql.DateTime}, - "first": &graphql.ArgumentConfig{Type: graphql.Int, DefaultValue: 10}, // 翻页参数 - "after": &graphql.ArgumentConfig{Type: graphql.String, DefaultValue: "0"}, // 翻页参数 + "first": &graphql.ArgumentConfig{Type: graphql.Int, Description: "翻页参数(傳回清單中的前n個元素)"}, + "last": &graphql.ArgumentConfig{Type: graphql.Int, Description: "翻页参数(傳回清單中的最後n個元素)"}, + "after": &graphql.ArgumentConfig{Type: graphql.Int, Description: "翻页参数(傳回清單中指定遊標之後的元素)"}, + "before": &graphql.ArgumentConfig{Type: graphql.Int, Description: "翻页参数(傳回清單中指定遊標之前的元素)"}, }, Resolve: func(p graphql.ResolveParams) (interface{}, error) { + // 定义参数结构体 + var args struct { + First int + Last int + After int + Before int + Text string + } + mapstructure.Decode(p.Args, &args) + // 返回字段 var fields []string requestedFields := p.Info.FieldASTs[0].SelectionSet.Selections @@ -220,8 +233,7 @@ func NewSchema(config Config) (graphql.Schema, error) { } } } - first := p.Args["first"] - after := p.Args["after"] + fields_str := strings.Join(fields, ",") // 参数到 SQL 格式字符串的映射 @@ -252,10 +264,11 @@ func NewSchema(config Config) (graphql.Schema, error) { } // 特殊处理 text 参数 - if p.Args["text"] != nil { - id_list := models.ElasticsearchSearch(p.Args["text"].(string)).GetIDList() + var id_list []string + if args.Text != "" { + fmt.Println("args:", args) + id_list = models.ElasticsearchSearch(args.Text).GetIDList(args.First, args.Last, args.After, args.Before) id_list_str := strings.Trim(strings.Join(strings.Fields(fmt.Sprint(id_list)), ","), "[]") - fmt.Println("id_list_str:", id_list_str) if id_list_str == "" { return map[string]interface{}{ "list": []Image{}, @@ -263,14 +276,15 @@ func NewSchema(config Config) (graphql.Schema, error) { "total": 0, }, nil } - where = append(where, fmt.Sprintf("id IN (%s)", id_list_str)) + where = append(where, fmt.Sprintf("id IN (%s) LIMIT %d", id_list_str, len(id_list))) } where_str := strings.Join(where, " AND ") // 执行查询 var query strings.Builder - query.WriteString(fmt.Sprintf("SELECT %s FROM web_images WHERE %s LIMIT %d OFFSET %s", fields_str, where_str, first, after)) + query.WriteString(fmt.Sprintf("SELECT %s FROM web_images WHERE %s", fields_str, where_str)) + fmt.Println("query:", query.String()) var images ImageList if err := connection.Select(&images, query.String()); err != nil { @@ -278,6 +292,11 @@ func NewSchema(config Config) (graphql.Schema, error) { return nil, err } + // 按照 id_list 的顺序重新排序 + if len(id_list) > 0 { + images.SortByIDList(id_list) + } + // 获取用户信息(如果图像列表不为空且请求字段中包含user) if len(images) > 0 && strings.Contains(fields_str, "user") { user_ids_str := images.ToAllUserID().ToString() diff --git a/api/struct.go b/api/struct.go index f603b43..7976eee 100644 --- a/api/struct.go +++ b/api/struct.go @@ -3,6 +3,7 @@ package api import ( "encoding/json" "fmt" + "strconv" "strings" "time" ) @@ -16,6 +17,20 @@ func (ids IDS) ToString() (str string) { type ImageList []Image +// 按照ID排序 +func (image *ImageList) SortByIDList(id_list []string) { + var sortedImageList ImageList + for _, id := range id_list { + id_number, _ := strconv.Atoi(id) + for _, image := range *image { + if image.ID == id_number { + sortedImageList = append(sortedImageList, image) + } + } + } + *image = sortedImageList +} + // 取到所有的文章ID, 去除重复 func (images *ImageList) ToAllArticleID() (uniqueIds IDS) { article_ids := make(map[int]bool) diff --git a/bin/main.go b/bin/main.go index e4149da..c5ca05d 100644 --- a/bin/main.go +++ b/bin/main.go @@ -18,7 +18,7 @@ import ( "git.satori.love/gameui/webp/api" "git.satori.love/gameui/webp/models" _ "github.com/go-sql-driver/mysql" - "github.com/graphql-go/graphql" + "github.com/graphql-go/handler" "github.com/milvus-io/milvus-sdk-go/v2/entity" "github.com/spf13/viper" ) @@ -210,19 +210,24 @@ func main() { w.Write([]byte("Hello World!")) }) - http.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) { - defer LogComponent(time.Now().UnixNano(), r) // 最后打印日志 - query := r.URL.Query().Get("query") - params := graphql.Params{Schema: schema, RequestString: query} - result := graphql.Do(params) - if len(result.Errors) > 0 { - fmt.Printf("failed to execute graphql operation, errors: %+v", result.Errors) - http.Error(w, result.Errors[0].Error(), 500) - return - } - rJSON, _ := json.MarshalIndent(result.Data, "", " ") - w.Write(rJSON) - }) + http.Handle("/graphql", handler.New(&handler.Config{ + Schema: &schema, + Pretty: true, + })) + + //http.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) { + // defer LogComponent(time.Now().UnixNano(), r) // 最后打印日志 + // query := r.URL.Query().Get("query") + // params := graphql.Params{Schema: schema, RequestString: query} + // result := graphql.Do(params) + // if len(result.Errors) > 0 { + // fmt.Printf("failed to execute graphql operation, errors: %+v", result.Errors) + // http.Error(w, result.Errors[0].Error(), 500) + // return + // } + // rJSON, _ := json.MarshalIndent(result.Data, "", " ") + // w.Write(rJSON) + //}) http.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) { defer LogComponent(time.Now().UnixNano(), r) // 最后打印日志 @@ -320,7 +325,7 @@ func main() { // 如果是查询 text, 直接从 Elasticsearch 返回结果 var text_ids []string if text := QueryConditions("text"); len(text) > 0 { - text_ids := models.ElasticsearchSearch(strings.Join(text, " ")).GetIDList() + text_ids := models.ElasticsearchSearch(strings.Join(text, " ")).GetIDList(0, 0, 0, 0) if len(text_ids) > 0 { conditions.WriteString(fmt.Sprintf(" WHERE id IN (%s)", strings.Trim(strings.Replace(fmt.Sprint(text_ids), " ", ",", -1), "[]"))) } else { diff --git a/go.mod b/go.mod index 0ab9286..b9d04fb 100644 --- a/go.mod +++ b/go.mod @@ -28,6 +28,7 @@ require ( github.com/elastic/elastic-transport-go/v8 v8.3.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/golang/protobuf v1.5.2 // indirect + github.com/graphql-go/handler v0.2.3 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect diff --git a/go.sum b/go.sum index 20f3e03..c2746a6 100644 --- a/go.sum +++ b/go.sum @@ -172,6 +172,8 @@ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORR github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/graphql-go/graphql v0.8.1 h1:p7/Ou/WpmulocJeEx7wjQy611rtXGQaAcXGqanuMMgc= github.com/graphql-go/graphql v0.8.1/go.mod h1:nKiHzRM0qopJEwCITUuIsxk9PlVlwIiiI8pnJEhordQ= +github.com/graphql-go/handler v0.2.3 h1:CANh8WPnl5M9uA25c2GBhPqJhE53Fg0Iue/fRNla71E= +github.com/graphql-go/handler v0.2.3/go.mod h1:leLF6RpV5uZMN1CdImAxuiayrYYhOk33bZciaUGaXeU= github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= diff --git a/models/elasticsearch.go b/models/elasticsearch.go index 569a1bc..f4a79c6 100644 --- a/models/elasticsearch.go +++ b/models/elasticsearch.go @@ -5,6 +5,7 @@ import ( "context" "crypto/tls" "encoding/json" + "fmt" "log" "net/http" @@ -55,10 +56,49 @@ type SearchData struct { } // 获取搜索结果的 ID 列表 -func (sd SearchData) GetIDList() (id_list []string) { +func (sd SearchData) GetIDList(first, last, after, before int) (id_list []string) { for _, hit := range sd.Hits.Hits { id_list = append(id_list, hit.ID) } + + // 如果 after 不为 0, 从这个ID开始向后取切片 + if after != 0 { + after_str := fmt.Sprint(after) + for i, id := range id_list { + if id == after_str { + id_list = id_list[i+1:] + break + } + } + } + + // 如果 before 不为 0, 从这个ID开始向前取切片 + if before != 0 { + before_str := fmt.Sprint(before) + for i, id := range id_list { + if id == before_str { + id_list = id_list[:i] + break + } + } + } + + // 如果 first 不为 0, 取切片的前 first 个元素 + if first != 0 { + if first > len(id_list) { + first = len(id_list) + } + id_list = id_list[:first] + } + + // 如果 last 不为 0, 取切片的后 last 个元素 + if last != 0 { + if last > len(id_list) { + last = len(id_list) + } + id_list = id_list[len(id_list)-last:] + } + return id_list } @@ -80,13 +120,14 @@ func ElasticsearchSearch(text string) (r SearchData) { es := elasticsearch_init() - // 执行查询 + // 执行查询(最大返回200条) res, err := es.Search( es.Search.WithContext(context.Background()), es.Search.WithIndex("web_images"), es.Search.WithBody(&buf), es.Search.WithTrackTotalHits(true), es.Search.WithPretty(), + es.Search.WithSize(200), ) if err != nil { log.Printf("Error getting response: %s", err)