Compare commits
165 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
76b0c68ed3 | ||
ce15a541ec | |||
85efe470b9 | |||
e6410582d2 | |||
a1c7d3c0d2 | |||
46c2fce7d5 | |||
3fbc9a3eae | |||
541c801a56 | |||
0dc2a2d144 | |||
59d28e6803 | |||
bf678b7440 | |||
56013d791b | |||
36d11e7542 | |||
2f7182d211 | |||
7eb3974abb | |||
7799298282 | |||
310797a6e0 | |||
b4211d801b | |||
935420d43a | |||
ed450130da | |||
1c86dfb107 | |||
4e95885a4c | |||
43d35834b5 | |||
41a148c449 | |||
926d55e646 | |||
c8b2dd01ac | |||
6ce5885a50 | |||
155ce8574f | |||
cb4bd0ad1f | |||
27a390f0d6 | |||
e4837a22dc | |||
06c6e70a44 | |||
6c42b01904 | |||
7a4747f9d3 | |||
450822dc89 | |||
178085305d | |||
abe1a0822f | |||
d9a719da5b | |||
ace0e6213c | |||
cb9e5a5e77 | |||
0d0b9c1659 | |||
d0fb0a0c33 | |||
a8d1748fc9 | |||
eb7ea4acc3 | |||
7d28cdac53 | |||
c4ec2be535 | |||
32101bb228 | |||
dc86f93a28 | |||
c4a66fa06d | |||
417df7710f | |||
d1499a7fab | |||
21a8560460 | |||
cabe9dc94e | |||
6d10d4e86e | |||
bbd9bb9af5 | |||
492e792c27 | |||
87d5a64691 | |||
9f775a12c2 | |||
92fa9e8a2c | |||
8f5486b0cf | |||
b804dd3700 | |||
198ad4487f | |||
b6fa4eaeae | |||
12fcdf08b2 | |||
d1d5458b28 | |||
3d1a8d3884 | |||
1d78500109 | |||
06070edded | |||
51c8a7fe2e | |||
9519ca7e65 | |||
dc30044cf8 | |||
e96bf3497e | |||
f4bec98b7a | |||
520e3bade6 | |||
2ebcb15fd7 | |||
ec75044126 | |||
de09f12fe1 | |||
b8d32b4247 | |||
1c3bc5f37f | |||
a12cad63e3 | |||
679f36f109 | |||
51e383fd39 | |||
60626e7ca5 | |||
f817aa12aa | |||
bcfde1999c | |||
a015fca246 | |||
b1b030acf3 | |||
18cec4651e | |||
a4e6a87d59 | |||
38253f0ce5 | |||
4b856887a0 | |||
00b2c7eed2 | |||
57cb1c74a0 | |||
8b369085c0 | |||
8d1b13ac82 | |||
defb599eca | |||
678e05ede4 | |||
c1f953ce72 | |||
ac46407a30 | |||
a1ce51e828 | |||
f8493115af | |||
a7845dbd4a | |||
bec2c9075b | |||
200a6b2842 | |||
0249c4e1c7 | |||
84a0b8e711 | |||
b32486305c | |||
d3d68d4bb5 | |||
2666f9ebe3 | |||
8229c02374 | |||
6558e02cb2 | |||
ac67eabd29 | |||
c010666b8b | |||
8345dfcd5f | |||
cc5471284e | |||
bddb184c78 | |||
8615a7b5bf | |||
84a2ee3bce | |||
ff087a0b16 | |||
d488ef42e8 | |||
f647a04339 | |||
3229e70023 | |||
4785fb9f8f | |||
8c34739ccd | |||
e41c20471b | |||
54092a0b9a | |||
ca7ff275c6 | |||
a55f5f0a78 | |||
c2c14444a9 | |||
ae73a99747 | |||
ea868c6a8d | |||
03b5aa6bca | |||
a59ff7a108 | |||
05057b7279 | |||
450318f118 | |||
840c1dd5a4 | |||
241952e680 | |||
6a6e9a5283 | |||
661a124dd5 | |||
b4400f00de | |||
d5859d8126 | |||
43dca8caa5 | |||
b07fcb3910 | |||
9fc57564e1 | |||
6c5d2d20d5 | |||
3dc8711ae9 | |||
7de1958974 | |||
cdf32b6268 | |||
b01c186acc | |||
85a71a64ff | |||
5bbb3a8797 | |||
a196ada6e3 | |||
00abb0e504 | |||
a541d2b263 | |||
08266046b8 | |||
0fef776ece | |||
37156568ed | |||
9fa5007ac4 | |||
cd1db1b82a | |||
7da1c994e9 | |||
4eb3857ac9 | |||
f731cd4dec | |||
1bce5ea799 | |||
325264ddad | |||
da6913eaba |
35
.gitignore
vendored
35
.gitignore
vendored
@@ -1,31 +1,4 @@
|
||||
# ---> Go
|
||||
# If you prefer the allow list template instead of the deny list, see community template:
|
||||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||
#
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
|
||||
data/
|
||||
dist/
|
||||
venv/
|
||||
tmp/
|
||||
.env
|
||||
.env.example
|
||||
main
|
||||
.vscode
|
||||
data
|
||||
dist
|
||||
tmp
|
||||
oss
|
||||
|
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"bitoAI.codeCompletion.enableAutoCompletion": false
|
||||
}
|
24
Makefile
24
Makefile
@@ -1,29 +1,33 @@
|
||||
export HOST=ai
|
||||
|
||||
# 运行本地开发环境(使用SSH隧道代理端口)
|
||||
dev:
|
||||
@if ! go list -m github.com/air-verse/air@latest >/dev/null 2>&1; then \
|
||||
echo "github.com/air-verse/air@latest is not installed. Installing..."; \
|
||||
go get github.com/air-verse/air@latest; \
|
||||
fi;
|
||||
@ssh -NCPf main -L 3306:localhost:3306 -L 19530:localhost:19530 & \
|
||||
@ssh -NCPf ${HOST} -L 3306:localhost:3306 -L 19530:localhost:19530 & \
|
||||
sleep 1; \
|
||||
go run github.com/air-verse/air@latest --build.cmd "go build -o ./data/ bin/main.go" --build.bin "./data/main"; \
|
||||
wait
|
||||
|
||||
link:
|
||||
ssh -NCPf ai -L 3306:localhost:3306 -L 19530:localhost:19530 -L 4080:localhost:4080 -L 5002:localhost:5002 -L 4306:localhost:4306 -L 8088:localhost:8088 -L 8087:localhost:8087 -L 6005:localhost:6005
|
||||
|
||||
# 编译项目
|
||||
build:
|
||||
go mod tidy
|
||||
go build -o dist/main bin/main.go
|
||||
|
||||
# 更新部署到服务器
|
||||
# 更新部署到服务器(生产和测试)
|
||||
update: build
|
||||
host="root@main"
|
||||
ssh $host "mv ~/webp/main ~/webp/main_old"
|
||||
scp dist/main $host:~/webp/main
|
||||
scp dist/main $(HOST):~/webp/main_new
|
||||
ssh ${HOST} "mv ~/webp/main ~/webp/main_old"
|
||||
ssh ${HOST} "mv ~/webp/main_new ~/webp/main"
|
||||
ssh ${HOST} "rm ~/webp/main_old"
|
||||
rm -rf dist
|
||||
ssh $host "systemctl restart webp"
|
||||
ssh $host "rm ~/webp/main_old"
|
||||
|
||||
# 设为系统服务和日志轮转
|
||||
# 设为系统服务和设置日志轮转
|
||||
service:
|
||||
sudo cp webp.service /etc/systemd/system/webp.service
|
||||
sudo systemctl enable webp
|
||||
@@ -37,3 +41,7 @@ gorse:
|
||||
export GORSE_DASHBOARD_USER="gorse"
|
||||
export GORSE_DASHBOARD_PASS="gorse"
|
||||
curl -fsSL https://gorse.io/playground | bash
|
||||
|
||||
# 安装搜图服务 python embedding
|
||||
reverse:
|
||||
git clone
|
104
README.md
104
README.md
@@ -1,59 +1,83 @@
|
||||
# webp
|
||||
- [x] 相似图像推荐(迁移)
|
||||
- [x] 以图搜图(迁移)
|
||||
- [x] 标签筛选(补充筛选条件)
|
||||
- [x] WEBP 缩略图
|
||||
|
||||
- [ ] 原始图像
|
||||
- [ ] 缩略缓存
|
||||
# 数据迁移
|
||||
- [ ] CDN 回源直接到服务器
|
||||
- [ ] 静态原始图像
|
||||
- [ ] 异地备份
|
||||
- [ ] 视频流媒体
|
||||
- [ ] 上传图像接口
|
||||
|
||||
- [ ] 不同类型的用途(文章, 截图, 头像)
|
||||
- [ ] 增量备份
|
||||
# 偏好推荐
|
||||
- [ ] 收集用户访问记录点赞记录
|
||||
- [ ] 收集用户推荐反馈
|
||||
- [ ] 调整偏好推荐
|
||||
|
||||
cdn(img.gameui.net) -> main -> img
|
||||
/articles/{id}/{hash}
|
||||
/users/{id}/{hash}
|
||||
# OCR
|
||||
- [ ] 脚本统计各关键词总量
|
||||
- [x] 筛选条件支持多选 颜色,风格 | 类型,主题,功能,材质图案(多选逗号分隔)
|
||||
- [x] 按颜色筛选, 周期性脚本自动补全三色
|
||||
|
||||
UPDATE web_images SET description = '' WHERE description IS NULL;
|
||||
ALTER TABLE web_images MODIFY description VARCHAR(255) DEFAULT '';
|
||||
|
||||
- [ ] 解析 cdn.gameui.net 到 cdn 服务, 并将 cdn 回源指向 def.gameui.net 其解析到新服务器
|
||||
- [ ] 支持带参数下载指定尺寸图像
|
||||
# 熱門統計
|
||||
- [ ] 從caddy日誌收集用戶訪問目標數據時間
|
||||
- [ ] 計算指標作爲API提供輸出
|
||||
- [ ] 數據與指標統覽後臺
|
||||
- [x] 游戏 作品 文章 收藏夹 分别添加喜欢数点赞数是否喜欢是否点赞字段
|
||||
|
||||
|
||||
- [x] 提供webp生成服务
|
||||
- [x] 提供流媒体服务
|
||||
- [x] 点击播放之前不加载视频(减少流量消耗)
|
||||
- [x] 使用封面图片替代加载视屏第一帧
|
||||
- [x] GraphQL 风格API
|
||||
- [ ] 列表翻页
|
||||
- [ ] OCR 查询支持
|
||||
|
||||
```sql
|
||||
-- 添加列
|
||||
ALTER TABLE web_images ADD COLUMN color_0_r TINYINT UNSIGNED;
|
||||
ALTER TABLE web_images ADD COLUMN color_0_g TINYINT UNSIGNED;
|
||||
ALTER TABLE web_images ADD COLUMN color_0_b TINYINT UNSIGNED;
|
||||
ALTER TABLE web_images ADD COLUMN color_1_r TINYINT UNSIGNED;
|
||||
ALTER TABLE web_images ADD COLUMN color_1_g TINYINT UNSIGNED;
|
||||
ALTER TABLE web_images ADD COLUMN color_1_b TINYINT UNSIGNED;
|
||||
|
||||
-- 为 web_images 表的 color 设置复合索引用于筛选
|
||||
CREATE INDEX idx_color_0 ON web_images (color_0_r, color_0_g, color_0_b);
|
||||
CREATE INDEX idx_color_1 ON web_images (color_1_r, color_1_g, color_1_b);
|
||||
|
||||
-- 全文索引
|
||||
CREATE FULLTEXT INDEX idx_images_desc ON web_images (images_desc);
|
||||
CREATE FULLTEXT INDEX idx_tags ON web_images (tags);
|
||||
CREATE FULLTEXT INDEX idx_tags ON web_article (tags);
|
||||
|
||||
-- -- 同步收藏
|
||||
-- ALTER TABLE web_praise ADD COLUMN gorse BOOLEAN DEFAULT FALSE;
|
||||
-- ALTER TABLE web_praise ADD COLUMN gorse BOOLEAN DEFAULT FALSE;
|
||||
|
||||
|
||||
```javascript
|
||||
// GET 查询id为1的用户列表, 要求列表中每项只返回 id, 用户名, 头像, 以及符合筛选条件的总数 total
|
||||
const query = `/api?query={users(id:1){total,list{id,user_name,avatar}}}`
|
||||
fetch(query).then(res => res.json()).then(data => {
|
||||
console.log(data)
|
||||
})
|
||||
-- 排序内存
|
||||
SET GLOBAL sort_buffer_size = 268435456; -- 设置为 256MB (268435456 字节)
|
||||
|
||||
// GET 查询 user_id 为 2 的图像列表, 并且包含 user 的部分信息, 以及符合筛选条件的总数 total
|
||||
const query = `/api?query={images(user_id:2){total,list{id,content,user{id,user_name,avatar}}}}`
|
||||
fetch(query).then(res => res.json()).then(data => {
|
||||
console.log(data)
|
||||
})
|
||||
-- 降序索引
|
||||
CREATE INDEX idx_id_desc ON web_images (id DESC);
|
||||
|
||||
// GET 查询所有图像的前2个(第一页)
|
||||
const query = `/api?query={images(first:2){total,list{id,content,user:{id,user_name,avatar}}}}`
|
||||
fetch(query).then(res => res.json()).then(data => {
|
||||
console.log(data)
|
||||
})
|
||||
-- 复合筛选降序索引 article_category_top_id
|
||||
CREATE INDEX idx_acti_type_id_desc ON web_images (article_category_top_id, type, id DESC);
|
||||
|
||||
-- -- 为 web_images 表的 day_rank 行设置倒序索引用于排序
|
||||
-- CREATE INDEX idx_day_rank_desc ON web_images (day_rank DESC);
|
||||
|
||||
// GET 查询所有图像的指定id之后的前2个(翻页)
|
||||
const query = `/api?query={images(after:2,first:2){total,list{id,content,user{id,user_name,avatar}}}}`
|
||||
fetch(query).then(res => res.json()).then(data => {
|
||||
console.log(data)
|
||||
})
|
||||
-- 为 web_images 表创建倒排复合索引
|
||||
CREATE INDEX idx_article_dayrank ON web_images(article_category_top_id, day_rank DESC);
|
||||
CREATE INDEX idx_article_weekrank ON web_images(article_category_top_id, week_rank DESC);
|
||||
CREATE INDEX idx_article_monthrank ON web_images(article_category_top_id, month_rank DESC);
|
||||
CREATE INDEX idx_article_yearrank ON web_images(article_category_top_id, year_rank DESC);
|
||||
CREATE INDEX idx_article_aeonrank ON web_images(article_category_top_id, aeon_rank DESC);
|
||||
|
||||
```
|
||||
|
||||
```bash
|
||||
# 使用 pm2 启动服务
|
||||
pm2 start ./main --name=main-6002 --watch=./main -- --config=./data/config_test.yaml
|
||||
```
|
||||
|
||||
### 流媒体
|
||||
通过流媒体服务降低视频文件加载消耗及防止恶意刷流量
|
||||
对视频地址添加有效期, 过期需由服务器重新提供token认证观众身份
|
||||
|
276
api/article.go
Normal file
276
api/article.go
Normal file
@@ -0,0 +1,276 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/doug-martin/goqu/v9"
|
||||
"github.com/graphql-go/graphql"
|
||||
"github.com/thoas/go-funk"
|
||||
)
|
||||
|
||||
type Article struct {
|
||||
ID int `json:"id" db:"id" gorm:"primaryKey"`
|
||||
Title string `json:"title" db:"title"`
|
||||
Orientation string `json:"orientation" db:"orientation"`
|
||||
Device string `json:"device" db:"device"`
|
||||
Era string `json:"era" db:"era"`
|
||||
Tags string `json:"tags" db:"tags"`
|
||||
CategoryID int `json:"category_id"`
|
||||
UserId int `json:"user_id" db:"user_id"`
|
||||
User User `json:"user" gorm:"foreignKey:UserId"`
|
||||
Collect bool `json:"collect" gorm:"column:is_collect"`
|
||||
Praise bool `json:"praise" gorm:"column:is_praise"`
|
||||
CollectCount int `json:"collect_count" gorm:"column:collect_num"`
|
||||
PraiseCount int `json:"praise_count" gorm:"column:praise"`
|
||||
CreateTime time.Time `json:"create_time" db:"create_time"`
|
||||
UpdateTime time.Time `json:"update_time" db:"update_time"`
|
||||
Emoji1 int `json:"emoji1"`
|
||||
Emoji2 int `json:"emoji2"`
|
||||
Emoji3 int `json:"emoji3"`
|
||||
Emoji4 int `json:"emoji4"`
|
||||
Emoji5 int `json:"emoji5"`
|
||||
Views int `json:"views"`
|
||||
}
|
||||
|
||||
func (Article) TableName() string {
|
||||
return "web_article"
|
||||
}
|
||||
|
||||
var articleType = graphql.NewObject(graphql.ObjectConfig{
|
||||
Name: "Article",
|
||||
Description: "文章",
|
||||
Fields: graphql.Fields{
|
||||
"id": &graphql.Field{Type: graphql.Int, Description: "ID"},
|
||||
"title": &graphql.Field{Type: graphql.String, Description: "标题"},
|
||||
"orientation": &graphql.Field{Type: graphql.String, Description: "方向"},
|
||||
"device": &graphql.Field{Type: graphql.String, Description: "设备"},
|
||||
"era": &graphql.Field{Type: graphql.String, Description: "游戏上线年份"},
|
||||
"tags": &graphql.Field{Type: graphql.String, Description: "标签"},
|
||||
"category_id": &graphql.Field{Type: graphql.Int, Description: "分类ID"},
|
||||
"user": &graphql.Field{Type: userType, Description: "所属用户"},
|
||||
"create_time": &graphql.Field{Type: graphql.DateTime, Description: "创建时间"},
|
||||
"update_time": &graphql.Field{Type: graphql.DateTime, Description: "更新时间"},
|
||||
"praise": &graphql.Field{Type: graphql.Boolean, Description: "当前用户是否点赞"},
|
||||
"collect": &graphql.Field{Type: graphql.Boolean, Description: "当前用户是否收藏"},
|
||||
"praise_count": &graphql.Field{Type: graphql.Int, Description: "点赞数"},
|
||||
"collect_count": &graphql.Field{Type: graphql.Int, Description: "收藏数"},
|
||||
"emoji1": &graphql.Field{Type: graphql.Int, Description: "表情1数量"},
|
||||
"emoji2": &graphql.Field{Type: graphql.Int, Description: "表情2数量"},
|
||||
"emoji3": &graphql.Field{Type: graphql.Int, Description: "表情3数量"},
|
||||
"emoji4": &graphql.Field{Type: graphql.Int, Description: "表情4数量"},
|
||||
"emoji5": &graphql.Field{Type: graphql.Int, Description: "表情5数量"},
|
||||
"views": &graphql.Field{Type: graphql.Int, Description: "浏览量"},
|
||||
},
|
||||
})
|
||||
|
||||
var ArticleItem = &graphql.Field{
|
||||
Name: "article",
|
||||
Description: "单篇文章",
|
||||
Type: articleType,
|
||||
Args: graphql.FieldConfigArgument{
|
||||
"id": &graphql.ArgumentConfig{Type: graphql.Int, Description: "根据ID获取文章"},
|
||||
},
|
||||
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
||||
article := Article{ID: p.Args["id"].(int)}
|
||||
if err := db.First(&article).Error; err != nil {
|
||||
log.Println("获取文章失败", err)
|
||||
return nil, err
|
||||
}
|
||||
return article, nil
|
||||
},
|
||||
}
|
||||
|
||||
var ArticleItems = &graphql.Field{
|
||||
Name: "articles",
|
||||
Description: "文章列表",
|
||||
Type: graphql.NewObject(graphql.ObjectConfig{
|
||||
Name: "ArticleConnection",
|
||||
Description: "条件筛选文章列表",
|
||||
Fields: graphql.Fields{
|
||||
"list": &graphql.Field{Type: graphql.NewList(articleType), Description: "文章列表"},
|
||||
"total": &graphql.Field{Type: graphql.Int, Description: "文章总数"},
|
||||
},
|
||||
}),
|
||||
Args: graphql.FieldConfigArgument{
|
||||
"id": &graphql.ArgumentConfig{Type: graphql.Int, Description: "筛选文章中指定ID的"},
|
||||
"title": &graphql.ArgumentConfig{Type: graphql.String, Description: "筛选文章中含有指定标题的"},
|
||||
"tags": &graphql.ArgumentConfig{Type: graphql.String, Description: "筛选文章中含有指定标签的"},
|
||||
"create_time": &graphql.ArgumentConfig{Type: graphql.DateTime, Description: "筛选文章中创建时间等于指定值的"},
|
||||
"update_time": &graphql.ArgumentConfig{Type: graphql.DateTime, Description: "筛选文章中更新时间等于指定值的"},
|
||||
"sort": &graphql.ArgumentConfig{Type: graphql.String, Description: "按指定字段排序", DefaultValue: "id"},
|
||||
"order": &graphql.ArgumentConfig{Type: orderType, Description: "排序类型(升序或降序)", DefaultValue: "ASC"},
|
||||
"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 articles []Article
|
||||
var total int
|
||||
var err error
|
||||
|
||||
// 文章的顶级ID为9
|
||||
var query = goqu.Dialect("mysql").From("web_article").Where(goqu.Ex{"category_id": 10})
|
||||
|
||||
// 筛选条件
|
||||
for _, format := range []string{"id", "style", "device", "orientation", "era", "category_id", "tags"} {
|
||||
if p.Args[format] != nil {
|
||||
query = query.Where(goqu.C(format).Eq(p.Args[format]))
|
||||
}
|
||||
}
|
||||
|
||||
// 按指定用户点赞筛选
|
||||
if p.Args["praise"] != nil {
|
||||
query = query.Join(goqu.T("web_praise"), goqu.On(
|
||||
goqu.I("web_article.id").Eq(goqu.I("web_praise.praise_id")),
|
||||
goqu.I("web_praise.user_id").Eq(p.Args["praise"]),
|
||||
goqu.I("web_praise.type").Eq(0),
|
||||
))
|
||||
}
|
||||
|
||||
// 按指定用户收藏筛选
|
||||
if p.Args["collect"] != nil {
|
||||
query = query.Join(goqu.T("web_collect"), goqu.On(
|
||||
goqu.I("web_article.id").Eq(goqu.I("web_collect.collect_id")),
|
||||
goqu.I("web_collect.user_id").Eq(p.Args["collect"]),
|
||||
goqu.I("web_collect.type").Eq(0),
|
||||
))
|
||||
}
|
||||
|
||||
// 如果查询了 total 字段
|
||||
if existField(p.Info.FieldASTs[0].SelectionSet.Selections, "total") {
|
||||
sql, _, _ := query.ToSQL()
|
||||
sql = strings.Replace(sql, "SELECT *", "SELECT COUNT(*)", 1)
|
||||
if err := db.Raw(sql).Scan(&total).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有外部排序则使用指定排序(正则sort只能是字母数字下划线)
|
||||
if p.Args["text"] == nil && p.Args["similar"] == nil && p.Args["interest"] == nil {
|
||||
sort := regexp.MustCompile(`[^a-zA-Z0-9_]`).ReplaceAllString(p.Args["sort"].(string), "")
|
||||
query = query.Select("web_article.id", goqu.L(
|
||||
fmt.Sprintf("ROW_NUMBER() OVER(ORDER BY web_article.%s %s)", sort, p.Args["order"]),
|
||||
).As("row_num"))
|
||||
} else {
|
||||
// 排序条件
|
||||
if p.Args["sort"] != nil {
|
||||
if p.Args["order"].(string) == "ASC" {
|
||||
query = query.Order(goqu.C(p.Args["sort"].(string)).Asc())
|
||||
}
|
||||
if p.Args["order"].(string) == "DESC" {
|
||||
query = query.Order(goqu.C(p.Args["sort"].(string)).Desc())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 取所有数据的前N条
|
||||
sql, _, _ := query.ToSQL()
|
||||
|
||||
// 遊標截取篩選結果集的前N条
|
||||
var cursor string
|
||||
if p.Args["after"] != nil {
|
||||
cursor = fmt.Sprintf(`WHERE row_num > (SELECT row_num FROM RankedArticles WHERE RankedArticles.id = %d)`, p.Args["after"].(int))
|
||||
}
|
||||
|
||||
var limit int = 10
|
||||
if p.Args["first"] != nil {
|
||||
limit = p.Args["first"].(int)
|
||||
} else if p.Args["last"] != nil {
|
||||
limit = p.Args["last"].(int)
|
||||
}
|
||||
|
||||
// 字段选择
|
||||
var user_id = p.Context.Value("user_id").(int)
|
||||
var fields = ListItem(p.Info.FieldASTs[0].SelectionSet.Selections)
|
||||
var praise, collect, text_count string
|
||||
if funk.Contains(fields, "praise") {
|
||||
praise = fmt.Sprintf(",CASE WHEN EXISTS (SELECT 1 FROM web_praise WHERE web_praise.praise_id = web_article.id AND web_praise.user_id = %d AND web_praise.type = 0) THEN TRUE ELSE FALSE END AS is_praise", user_id)
|
||||
}
|
||||
if funk.Contains(fields, "collect") {
|
||||
collect = fmt.Sprintf(",CASE WHEN EXISTS (SELECT 1 FROM web_collect WHERE web_collect.collect_id = web_article.id AND web_collect.user_id = %d AND web_collect.type = 0) THEN TRUE ELSE FALSE END AS is_collect", user_id)
|
||||
}
|
||||
if funk.Contains(fields, "text_count") {
|
||||
text_count = ",(SELECT COUNT(*) FROM web_images wi WHERE wi.article_id = web_article.id AND wi.text != '') AS text_count"
|
||||
}
|
||||
sql = fmt.Sprintf(`
|
||||
WITH RankedArticles AS (%s)
|
||||
SELECT web_article.* %s %s %s FROM web_article INNER JOIN(
|
||||
SELECT id, row_num FROM RankedArticles %s
|
||||
) AS LimitedRanked ON LimitedRanked.id = web_article.id
|
||||
ORDER BY LimitedRanked.row_num ASC LIMIT %d
|
||||
`, sql, praise, collect, text_count, cursor, limit)
|
||||
//fmt.Println(sql)
|
||||
|
||||
if err := db.Raw(sql).Scan(&articles).Error; err != nil {
|
||||
fmt.Println("获取文章列表失败", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var items = ListItem(p.Info.FieldASTs[0].SelectionSet.Selections)
|
||||
if funk.Contains(items, "views") {
|
||||
type ApiResponse struct {
|
||||
ID int `json:"id"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
// 0. 收集要查询的 ID
|
||||
var ids []int
|
||||
for x := range articles {
|
||||
ids = append(ids, articles[x].ID)
|
||||
}
|
||||
idx := strings.Trim(strings.Replace(fmt.Sprint(ids), " ", ",", -1), "[]")
|
||||
|
||||
// 1. 发送 GET 请求
|
||||
resp, err := http.Get("http://localhost:6005/api/get_views/文章?ids=" + idx)
|
||||
if err != nil {
|
||||
fmt.Println("Error making GET request:", err)
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 2. 检查 HTTP 状态码
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
fmt.Printf("Request failed with status code: %d\n", resp.StatusCode)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 3. 读取响应体
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
fmt.Println("Error reading response body:", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 4. 解析 JSON 数据到结构体
|
||||
var data []ApiResponse
|
||||
err = json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
fmt.Println("Error unmarshalling JSON:", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 5. 赋值到数据集
|
||||
for _, item := range data {
|
||||
for i := range articles {
|
||||
if articles[i].ID == item.ID {
|
||||
articles[i].Views = item.Count
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"list": articles,
|
||||
"total": total,
|
||||
}, err
|
||||
},
|
||||
}
|
286
api/collect.go
Normal file
286
api/collect.go
Normal file
@@ -0,0 +1,286 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/doug-martin/goqu/v9"
|
||||
"github.com/graphql-go/graphql"
|
||||
"github.com/thoas/go-funk"
|
||||
)
|
||||
|
||||
type Cover struct {
|
||||
ID int `json:"id"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type Collection struct {
|
||||
ID int `json:"id" gorm:"primaryKey"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
Type string `json:"type"`
|
||||
Thumbnail string `json:"thumbnail"`
|
||||
Num int `json:"num"`
|
||||
Fans int `json:"fans"`
|
||||
UserId int `json:"user_id"`
|
||||
ArticleId int `json:"article_id"`
|
||||
CreateTime time.Time `json:"create_time"`
|
||||
UpdateTime time.Time `json:"update_time"`
|
||||
User User `json:"user" gorm:"foreignKey:UserId;references:ID"`
|
||||
Fan bool `json:"praise" gorm:"-"`
|
||||
Covers []Cover `json:"covers" gorm:"-"`
|
||||
Views int `json:"views"`
|
||||
}
|
||||
|
||||
func (Collection) TableName() string {
|
||||
return "web_member_explorer"
|
||||
}
|
||||
|
||||
var CollectionItems = &graphql.Field{
|
||||
Name: "collections",
|
||||
Description: "收藏列表",
|
||||
Type: graphql.NewObject(graphql.ObjectConfig{
|
||||
Name: "CollectConnection",
|
||||
Description: "条件筛选收藏夹列表",
|
||||
Fields: graphql.Fields{
|
||||
"list": &graphql.Field{Type: graphql.NewList(graphql.NewObject(graphql.ObjectConfig{
|
||||
Name: "collection",
|
||||
Description: "收藏夹",
|
||||
Fields: graphql.Fields{
|
||||
"id": &graphql.Field{Type: graphql.Int, Description: "ID"},
|
||||
"type": &graphql.Field{Type: graphql.Int, Description: "类型"},
|
||||
"title": &graphql.Field{Type: graphql.String, Description: "标题"},
|
||||
"content": &graphql.Field{Type: graphql.String, Description: "内容"},
|
||||
"thumbnail": &graphql.Field{Type: graphql.String, Description: "缩略图"},
|
||||
"num": &graphql.Field{Type: graphql.Int, Description: "收藏数量"},
|
||||
"create_time": &graphql.Field{Type: graphql.DateTime, Description: "创建时间"},
|
||||
"update_time": &graphql.Field{Type: graphql.DateTime, Description: "更新时间"},
|
||||
"fans": &graphql.Field{Type: graphql.Int, Description: "关注数"},
|
||||
"fan": &graphql.Field{Type: graphql.Boolean, Description: "当前用户是否关注"},
|
||||
"user": &graphql.Field{Type: userType, Description: "用户"},
|
||||
"covers": &graphql.Field{
|
||||
Type: graphql.NewList(graphql.NewObject(graphql.ObjectConfig{
|
||||
Name: "cover",
|
||||
Description: "封面",
|
||||
Fields: graphql.Fields{
|
||||
"id": &graphql.Field{Type: graphql.Int, Description: "ID"},
|
||||
"type": &graphql.Field{Type: graphql.String, Description: "类型"},
|
||||
},
|
||||
})),
|
||||
Description: "封面集",
|
||||
},
|
||||
"views": &graphql.Field{Type: graphql.Int, Description: "浏览量"},
|
||||
},
|
||||
})), Description: "收藏夹列表"},
|
||||
"total": &graphql.Field{Type: graphql.Int, Description: "收藏夹总数"},
|
||||
},
|
||||
}),
|
||||
Args: graphql.FieldConfigArgument{
|
||||
"id": &graphql.ArgumentConfig{Type: graphql.Int, Description: "筛选收藏中指定ID的"},
|
||||
"title": &graphql.ArgumentConfig{Type: graphql.String, Description: "筛选收藏中含有指定标题的"},
|
||||
"type": &graphql.ArgumentConfig{Type: graphql.String, Description: "筛选收藏中含有指定类型的"},
|
||||
"create_time": &graphql.ArgumentConfig{Type: graphql.DateTime, Description: "筛选收藏中创建时间等于指定值的"},
|
||||
"update_time": &graphql.ArgumentConfig{Type: graphql.DateTime, Description: "筛选收藏中更新时间等于指定值的"},
|
||||
"sort": &graphql.ArgumentConfig{Type: graphql.String, Description: "按指定字段排序", DefaultValue: "id"},
|
||||
"order": &graphql.ArgumentConfig{Type: orderType, Description: "排序类型(升序或降序)", DefaultValue: "ASC"},
|
||||
"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 collects []Collection
|
||||
var total int
|
||||
var limit int = 10
|
||||
|
||||
if p.Args["first"] != nil {
|
||||
limit = p.Args["first"].(int)
|
||||
}
|
||||
|
||||
//order := clause.OrderByColumn{
|
||||
// Column: clause.Column{Name: p.Args["sort"].(string)},
|
||||
// Desc: p.Args["order"].(string) == "DESC",
|
||||
//}
|
||||
|
||||
var query = goqu.Dialect("mysql").From("web_member_explorer")
|
||||
|
||||
// 筛选条件
|
||||
for _, format := range []string{"id", "title", "type"} {
|
||||
if p.Args[format] != nil {
|
||||
query = query.Where(goqu.C(format).Eq(p.Args[format]))
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有外部排序则使用指定排序(正则sort只能是字母数字下划下)
|
||||
if p.Args["text"] == nil && p.Args["similar"] == nil && p.Args["interest"] == nil {
|
||||
sort := regexp.MustCompile(`[^a-zA-Z0-9_]`).ReplaceAllString(p.Args["sort"].(string), "")
|
||||
query = query.Select("web_member_explorer.id", goqu.L(
|
||||
fmt.Sprintf("ROW_NUMBER() OVER(ORDER BY web_member_explorer.%s %s)", sort, p.Args["order"]),
|
||||
).As("row_num"))
|
||||
} else {
|
||||
// 排序条件
|
||||
if p.Args["sort"] != nil {
|
||||
if p.Args["order"].(string) == "ASC" {
|
||||
query = query.Order(goqu.C(p.Args["sort"].(string)).Asc())
|
||||
}
|
||||
if p.Args["order"].(string) == "DESC" {
|
||||
query = query.Order(goqu.C(p.Args["sort"].(string)).Desc())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 取所有数据的前N条
|
||||
sql, _, _ := query.ToSQL()
|
||||
|
||||
// 遊標截取篩選結果集的前N条
|
||||
var cursor string
|
||||
if p.Args["after"] != nil {
|
||||
cursor = fmt.Sprintf(`WHERE row_num > (SELECT row_num FROM RankedArticles WHERE RankedArticles.id = %d)`, p.Args["after"].(int))
|
||||
}
|
||||
|
||||
// 字段选择
|
||||
var user_id = p.Context.Value("user_id").(int)
|
||||
var items = ListItem(p.Info.FieldASTs[0].SelectionSet.Selections)
|
||||
var fan string
|
||||
if funk.Contains(items, "fan") {
|
||||
fan = fmt.Sprintf(",CASE WHEN EXISTS (SELECT 1 FROM web_fans WHERE web_fans.follower_id = %d AND web_fans.blogger_id = web_member_explorer.id AND web_fans.type = 3) THEN TRUE ELSE FALSE END AS is_praise", user_id)
|
||||
}
|
||||
|
||||
sql = fmt.Sprintf(`
|
||||
WITH RankedArticles AS (%s)
|
||||
SELECT web_member_explorer.* %s FROM web_member_explorer INNER JOIN(
|
||||
SELECT id, row_num FROM RankedArticles %s
|
||||
) AS LimitedRanked ON LimitedRanked.id = web_member_explorer.id
|
||||
ORDER BY LimitedRanked.row_num ASC LIMIT %d
|
||||
`, sql, fan, cursor, limit)
|
||||
|
||||
//fmt.Println(sql)
|
||||
|
||||
if err := db.Raw(sql).Scan(&collects).Error; err != nil {
|
||||
fmt.Println("获取游戏列表失败", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if funk.Contains(items, "user") {
|
||||
var ids []int
|
||||
for _, collect := range collects {
|
||||
ids = append(ids, collect.UserId)
|
||||
}
|
||||
ids = funk.UniqInt(ids)
|
||||
|
||||
var users []User
|
||||
if err := db.Table("web_member").Where("id in (?)", ids).Find(&users).Error; err != nil {
|
||||
fmt.Println("获取用户信息失败", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for index, game := range collects {
|
||||
for _, user := range users {
|
||||
if game.UserId == user.ID {
|
||||
collects[index].User = user
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//if err := db.Limit(limit).Preload("User").Order(order).Find(&collects).Error; err != nil {
|
||||
// fmt.Println(err.Error())
|
||||
// return nil, err
|
||||
//}
|
||||
|
||||
//if funk.Contains(items, "fan") {
|
||||
// var user_id = p.Context.Value("user_id").(int)
|
||||
// for index, item := range collects {
|
||||
// var total int64
|
||||
// if err := db.Table("web_fans").Where("follower_id = ? AND blogger_id = ? AND type = 3", user_id, item.ID).Count(&total).Error; err != nil {
|
||||
// fmt.Println(index, err.Error())
|
||||
// return nil, err
|
||||
// }
|
||||
// collects[index].Fan = total > 0
|
||||
// }
|
||||
//}
|
||||
|
||||
for index, item := range collects {
|
||||
var data []Cover
|
||||
var t string = "article"
|
||||
var s string = "collect_id AS id"
|
||||
if item.Type == "0" {
|
||||
t = "image"
|
||||
s = "image_id AS id"
|
||||
}
|
||||
|
||||
if err := db.Table("web_collect").Select(s).Limit(3).Where("explorer_id = ?", item.ID).Find(&data).Error; err != nil {
|
||||
fmt.Println("获取封面ID失败", err)
|
||||
}
|
||||
|
||||
for i := range data {
|
||||
data[i].Type = t
|
||||
}
|
||||
collects[index].Covers = data
|
||||
}
|
||||
|
||||
if funk.Contains(items, "views") {
|
||||
type ApiResponse struct {
|
||||
ID int `json:"id"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
// 0. 收集要查询的 ID
|
||||
var ids []int
|
||||
for x := range collects {
|
||||
ids = append(ids, collects[x].ID)
|
||||
}
|
||||
idx := strings.Trim(strings.Replace(fmt.Sprint(ids), " ", ",", -1), "[]")
|
||||
|
||||
// 1. 发送 GET 请求
|
||||
resp, err := http.Get("http://localhost:6005/api/get_views/收藏?ids=" + idx)
|
||||
if err != nil {
|
||||
fmt.Println("Error making GET request:", err)
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 2. 检查 HTTP 状态码
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
fmt.Printf("Request failed with status code: %d\n", resp.StatusCode)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 3. 读取响应体
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
fmt.Println("Error reading response body:", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 4. 解析 JSON 数据到结构体
|
||||
var data []ApiResponse
|
||||
err = json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
fmt.Println("Error unmarshalling JSON:", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 5. 赋值到数据集
|
||||
for _, item := range data {
|
||||
for i := range collects {
|
||||
if collects[i].ID == item.ID {
|
||||
collects[i].Views = item.Count
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"list": collects,
|
||||
"total": total,
|
||||
}, nil
|
||||
},
|
||||
}
|
321
api/game.go
Normal file
321
api/game.go
Normal file
@@ -0,0 +1,321 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/doug-martin/goqu/v9"
|
||||
"github.com/graphql-go/graphql"
|
||||
"github.com/thoas/go-funk"
|
||||
)
|
||||
|
||||
type Game struct {
|
||||
ID int `json:"id" gorm:"primaryKey"`
|
||||
Title string `json:"title"`
|
||||
Era string `json:"era"`
|
||||
Style string `json:"style"`
|
||||
Device string `json:"device"`
|
||||
Orientation string `json:"orientation"`
|
||||
Tags string `json:"tags"`
|
||||
Rank string `json:"rank"`
|
||||
UserId int `json:"user_id"`
|
||||
Content string `json:"content"`
|
||||
CategoryID int `json:"category_id"`
|
||||
Image string `json:"image"`
|
||||
Images string `json:"images"`
|
||||
User User `json:"user" gorm:"foreignKey:UserId"`
|
||||
Collect bool `json:"collect" gorm:"column:is_collect"`
|
||||
Praise bool `json:"praise" gorm:"column:is_praise"`
|
||||
CollectCount int `json:"collect_count" gorm:"column:collect_num"`
|
||||
PraiseCount int `json:"praise_count" gorm:"column:praise"`
|
||||
CommentCount int `json:"comment_count" gorm:"column:comment_num"`
|
||||
ImageCount int `json:"image_count" gorm:"column:total_image_count"`
|
||||
TextCount int `json:"text_count" gorm:"column:text_count"`
|
||||
TextList []string `json:"text_list" gorm:"-"`
|
||||
CreateTime time.Time `json:"create_time"`
|
||||
UpdateTime time.Time `json:"update_time"`
|
||||
Emoji1 int `json:"emoji1"`
|
||||
Emoji2 int `json:"emoji2"`
|
||||
Emoji3 int `json:"emoji3"`
|
||||
Emoji4 int `json:"emoji4"`
|
||||
Emoji5 int `json:"emoji5"`
|
||||
Views int `json:"views"`
|
||||
}
|
||||
|
||||
func (Game) TableName() string {
|
||||
return "web_article"
|
||||
}
|
||||
|
||||
var gameType = graphql.NewObject(graphql.ObjectConfig{
|
||||
Name: "Game",
|
||||
Description: "游戏",
|
||||
Fields: graphql.Fields{
|
||||
"id": &graphql.Field{Type: graphql.Int, Description: "游戏ID"},
|
||||
"title": &graphql.Field{Type: graphql.String, Description: "游戏标题"},
|
||||
"era": &graphql.Field{Type: graphql.String, Description: "游戏上线年份"},
|
||||
"tags": &graphql.Field{Type: graphql.String, Description: "游戏标签"},
|
||||
"rank": &graphql.Field{Type: graphql.String, Description: "游戏精选"},
|
||||
"style": &graphql.Field{Type: graphql.String, Description: "游戏风格"},
|
||||
"device": &graphql.Field{Type: graphql.String, Description: "游戏平台"},
|
||||
"orientation": &graphql.Field{Type: graphql.String, Description: "屏幕方向"},
|
||||
"category_id": &graphql.Field{Type: graphql.Int, Description: "分类ID"},
|
||||
"user": &graphql.Field{Type: userType, Description: "所属用户"},
|
||||
"create_time": &graphql.Field{Type: graphql.DateTime, Description: "游戏创建时间"},
|
||||
"update_time": &graphql.Field{Type: graphql.DateTime, Description: "游戏更新时间"},
|
||||
"praise": &graphql.Field{Type: graphql.Boolean, Description: "当前用户是否点赞"},
|
||||
"collect": &graphql.Field{Type: graphql.Boolean, Description: "当前用户是否收藏"},
|
||||
"praise_count": &graphql.Field{Type: graphql.Int, Description: "点赞数"},
|
||||
"collect_count": &graphql.Field{Type: graphql.Int, Description: "收藏数"},
|
||||
"image_count": &graphql.Field{Type: graphql.Int, Description: "图片数"},
|
||||
"text_count": &graphql.Field{Type: graphql.Int, Description: "文字数量"},
|
||||
"text_list": &graphql.Field{Type: graphql.NewList(graphql.String), Description: "文字列表"},
|
||||
"emoji1": &graphql.Field{Type: graphql.Int, Description: "表情1数量"},
|
||||
"emoji2": &graphql.Field{Type: graphql.Int, Description: "表情2数量"},
|
||||
"emoji3": &graphql.Field{Type: graphql.Int, Description: "表情3数量"},
|
||||
"emoji4": &graphql.Field{Type: graphql.Int, Description: "表情4数量"},
|
||||
"emoji5": &graphql.Field{Type: graphql.Int, Description: "表情5数量"},
|
||||
"views": &graphql.Field{Type: graphql.Int, Description: "浏览量"},
|
||||
},
|
||||
})
|
||||
|
||||
var GameItems = &graphql.Field{
|
||||
Name: "games",
|
||||
Description: "游戏列表",
|
||||
Type: graphql.NewObject(graphql.ObjectConfig{
|
||||
Name: "GameConnection",
|
||||
Description: "条件筛选游戏列表",
|
||||
Fields: graphql.Fields{
|
||||
"list": &graphql.Field{Type: graphql.NewList(gameType), Description: "游戏列表"},
|
||||
"total": &graphql.Field{Type: graphql.Int, Description: "游戏总数"},
|
||||
},
|
||||
}),
|
||||
Args: graphql.FieldConfigArgument{
|
||||
"collect": &graphql.ArgumentConfig{Type: graphql.Int, Description: "按指定用户收藏筛选"},
|
||||
"praise": &graphql.ArgumentConfig{Type: graphql.Int, Description: "按指定用户点赞筛选"},
|
||||
"era": &graphql.ArgumentConfig{Type: graphql.String, Description: "按年份筛选游戏"},
|
||||
"tags": &graphql.ArgumentConfig{Type: graphql.String, Description: "按标签筛选游戏"},
|
||||
"style": &graphql.ArgumentConfig{Type: graphql.String, Description: "按风格筛选游戏"},
|
||||
"device": &graphql.ArgumentConfig{Type: graphql.String, Description: "按平台筛选游戏"},
|
||||
"orientation": &graphql.ArgumentConfig{Type: graphql.String, Description: "按屏幕方向筛选游戏"},
|
||||
"category_id": &graphql.ArgumentConfig{Type: graphql.Int, Description: "按分类ID筛选游戏"},
|
||||
"user_id": &graphql.ArgumentConfig{Type: graphql.Int, Description: "按用户ID筛选游戏"},
|
||||
"id": &graphql.ArgumentConfig{Type: graphql.Int, Description: "筛选游戏中指定ID的"},
|
||||
"title": &graphql.ArgumentConfig{Type: graphql.String, Description: "筛选游戏中含有指定标题的"},
|
||||
"rank": &graphql.ArgumentConfig{Type: graphql.String, Description: "筛选游戏中含有指定排名的"},
|
||||
"create_time": &graphql.ArgumentConfig{Type: graphql.DateTime, Description: "按创建时间筛选游戏"},
|
||||
"update_time": &graphql.ArgumentConfig{Type: graphql.DateTime, Description: "按修改时间筛选游戏"},
|
||||
"sort": &graphql.ArgumentConfig{Type: graphql.String, Description: "按指定字段排序游戏", DefaultValue: "id"},
|
||||
"order": &graphql.ArgumentConfig{Type: orderType, Description: "排序类型(升序或降序)", DefaultValue: "ASC"},
|
||||
"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 games []Game
|
||||
var total int
|
||||
var err error
|
||||
|
||||
// 游戏的顶级ID为 22
|
||||
var query = goqu.Dialect("mysql").From("web_article").Where(goqu.Ex{"category_top_id": 22})
|
||||
|
||||
// 筛选条件
|
||||
for _, format := range []string{"id", "title", "style", "device", "orientation", "era", "category_id", "tags", "rank"} {
|
||||
if p.Args[format] != nil {
|
||||
query = query.Where(goqu.C(format).Eq(p.Args[format]))
|
||||
}
|
||||
}
|
||||
|
||||
// 按指定用户点赞筛选
|
||||
if p.Args["praise"] != nil {
|
||||
query = query.Join(goqu.T("web_praise"), goqu.On(
|
||||
goqu.I("web_article.id").Eq(goqu.I("web_praise.praise_id")),
|
||||
goqu.I("web_praise.user_id").Eq(p.Args["praise"]),
|
||||
goqu.I("web_praise.type").Eq(0),
|
||||
))
|
||||
}
|
||||
|
||||
// 按指定用户收藏筛选
|
||||
if p.Args["collect"] != nil {
|
||||
query = query.Join(goqu.T("web_collect"), goqu.On(
|
||||
goqu.I("web_article.id").Eq(goqu.I("web_collect.collect_id")),
|
||||
goqu.I("web_collect.user_id").Eq(p.Args["collect"]),
|
||||
goqu.I("web_collect.type").Eq(0),
|
||||
))
|
||||
}
|
||||
|
||||
// 如果查询了 total 字段
|
||||
if existField(p.Info.FieldASTs[0].SelectionSet.Selections, "total") {
|
||||
sql, _, _ := query.ToSQL()
|
||||
sql = strings.Replace(sql, "SELECT *", "SELECT COUNT(*)", 1)
|
||||
if err := db.Raw(sql).Scan(&total).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有外部排序则使用指定排序(正则sort只能是字母数字下划下)
|
||||
if p.Args["text"] == nil && p.Args["similar"] == nil && p.Args["interest"] == nil {
|
||||
sort := regexp.MustCompile(`[^a-zA-Z0-9_]`).ReplaceAllString(p.Args["sort"].(string), "")
|
||||
query = query.Select("web_article.id", goqu.L(
|
||||
fmt.Sprintf("ROW_NUMBER() OVER(ORDER BY web_article.%s %s)", sort, p.Args["order"]),
|
||||
).As("row_num"))
|
||||
} else {
|
||||
// 排序条件
|
||||
if p.Args["sort"] != nil {
|
||||
if p.Args["order"].(string) == "ASC" {
|
||||
query = query.Order(goqu.C(p.Args["sort"].(string)).Asc())
|
||||
}
|
||||
if p.Args["order"].(string) == "DESC" {
|
||||
query = query.Order(goqu.C(p.Args["sort"].(string)).Desc())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 取所有数据的前N条
|
||||
sql, _, _ := query.ToSQL()
|
||||
|
||||
// 遊標截取篩選結果集的前N条
|
||||
var cursor string
|
||||
if p.Args["after"] != nil {
|
||||
cursor = fmt.Sprintf(`WHERE row_num > (SELECT row_num FROM RankedArticles WHERE RankedArticles.id = %d)`, p.Args["after"].(int))
|
||||
}
|
||||
|
||||
var limit int = 10
|
||||
if p.Args["first"] != nil {
|
||||
limit = p.Args["first"].(int)
|
||||
} else if p.Args["last"] != nil {
|
||||
limit = p.Args["last"].(int)
|
||||
}
|
||||
|
||||
// 字段选择
|
||||
var user_id = p.Context.Value("user_id").(int)
|
||||
var fields = ListItem(p.Info.FieldASTs[0].SelectionSet.Selections)
|
||||
var praise, collect, text_count string
|
||||
if funk.Contains(fields, "praise") {
|
||||
praise = fmt.Sprintf(",CASE WHEN EXISTS (SELECT 1 FROM web_praise WHERE web_praise.praise_id = web_article.id AND web_praise.user_id = %d AND web_praise.type = 0) THEN TRUE ELSE FALSE END AS is_praise", user_id)
|
||||
}
|
||||
if funk.Contains(fields, "collect") {
|
||||
collect = fmt.Sprintf(",CASE WHEN EXISTS (SELECT 1 FROM web_collect WHERE web_collect.collect_id = web_article.id AND web_collect.user_id = %d AND web_collect.type = 0) THEN TRUE ELSE FALSE END AS is_collect", user_id)
|
||||
}
|
||||
|
||||
sql = fmt.Sprintf(`
|
||||
WITH RankedArticles AS (%s)
|
||||
SELECT web_article.* %s %s %s FROM web_article INNER JOIN(
|
||||
SELECT id, row_num FROM RankedArticles %s
|
||||
) AS LimitedRanked ON LimitedRanked.id = web_article.id
|
||||
ORDER BY LimitedRanked.row_num ASC LIMIT %d
|
||||
`, sql, praise, collect, text_count, cursor, limit)
|
||||
|
||||
//fmt.Println(sql)
|
||||
|
||||
if err := db.Raw(sql).Scan(&games).Error; err != nil {
|
||||
fmt.Println("获取游戏列表失败", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if funk.Contains(fields, "user") {
|
||||
var ids []int
|
||||
for _, game := range games {
|
||||
ids = append(ids, game.UserId)
|
||||
}
|
||||
ids = funk.UniqInt(ids)
|
||||
|
||||
var users []User
|
||||
if err := db.Table("web_member").Where("id in (?)", ids).Find(&users).Error; err != nil {
|
||||
fmt.Println("获取用户信息失败", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for index, game := range games {
|
||||
for _, user := range users {
|
||||
if game.UserId == user.ID {
|
||||
games[index].User = user
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if funk.Contains(fields, "text_list") || funk.Contains(fields, "text_count") {
|
||||
fmt.Println("获取游戏文字列表")
|
||||
for index, item := range games {
|
||||
var images []Image
|
||||
if err := db.Table("web_images").Where("article_id", item.ID).Find(&images).Error; err != nil {
|
||||
fmt.Println("获取游戏图片列表失败", err)
|
||||
return nil, err
|
||||
}
|
||||
for _, image := range images {
|
||||
for _, text := range image.Text {
|
||||
item.TextList = append(item.TextList, text.Text)
|
||||
}
|
||||
}
|
||||
games[index].TextList = funk.UniqString(item.TextList)
|
||||
games[index].TextCount = len(item.TextList)
|
||||
}
|
||||
}
|
||||
|
||||
var items = ListItem(p.Info.FieldASTs[0].SelectionSet.Selections)
|
||||
if funk.Contains(items, "views") {
|
||||
type ApiResponse struct {
|
||||
ID int `json:"id"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
// 0. 收集要查询的 ID
|
||||
var ids []int
|
||||
for x := range games {
|
||||
ids = append(ids, games[x].ID)
|
||||
}
|
||||
idx := strings.Trim(strings.Replace(fmt.Sprint(ids), " ", ",", -1), "[]")
|
||||
|
||||
// 1. 发送 GET 请求
|
||||
resp, err := http.Get("http://localhost:6005/api/get_views/文章?ids=" + idx)
|
||||
if err != nil {
|
||||
fmt.Println("Error making GET request:", err)
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 2. 检查 HTTP 状态码
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
fmt.Printf("Request failed with status code: %d\n", resp.StatusCode)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 3. 读取响应体
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
fmt.Println("Error reading response body:", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 4. 解析 JSON 数据到结构体
|
||||
var data []ApiResponse
|
||||
err = json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
fmt.Println("Error unmarshalling JSON:", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 5. 赋值到数据集
|
||||
for _, item := range data {
|
||||
for i := range games {
|
||||
if games[i].ID == item.ID {
|
||||
games[i].Views = item.Count
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"list": games,
|
||||
"total": total,
|
||||
}, err
|
||||
},
|
||||
}
|
119
api/gorse.go
Normal file
119
api/gorse.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/zhenghaoz/gorse/client"
|
||||
)
|
||||
|
||||
var gorse *client.GorseClient
|
||||
|
||||
func GorseInit(host string, port int) {
|
||||
gorse = client.NewGorseClient(fmt.Sprintf("http://%s:%d", host, port), "")
|
||||
}
|
||||
|
||||
// 同步图片数据
|
||||
func PutImages(page int) error {
|
||||
var ctx context.Context = context.Background()
|
||||
|
||||
for i := 0; i < page; i++ {
|
||||
var items []client.Item
|
||||
var data []Image
|
||||
|
||||
if err := db.Table("web_images").Select("id", "tags", "create_time").Limit(100).Offset(i * 100).Scan(&data).Error; err != nil {
|
||||
fmt.Println("获取图像记录失败", err)
|
||||
return err
|
||||
}
|
||||
|
||||
for _, item := range data {
|
||||
fmt.Println(item.ID, item.Tags, item.CreateTime)
|
||||
items = append(items, client.Item{
|
||||
ItemId: fmt.Sprintf("%d", item.ID),
|
||||
Timestamp: item.CreateTime.Format("2006-01-02 15:04:05"),
|
||||
Labels: strings.Split(item.Tags, ", "),
|
||||
Comment: fmt.Sprintf("%d", item.Content),
|
||||
})
|
||||
}
|
||||
|
||||
if _, err := gorse.InsertItems(ctx, items); err != nil {
|
||||
fmt.Println("写入图像记录失败", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("写入图像记录结束..", page*100)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 同步点赞数据
|
||||
func PutPraises() {
|
||||
var ctx context.Context = context.Background()
|
||||
|
||||
for {
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
var feedbacks []client.Feedback
|
||||
var data []struct {
|
||||
ID int
|
||||
PraiseID int
|
||||
UserID int
|
||||
CreateTime time.Time
|
||||
}
|
||||
|
||||
if err := db.Table("web_praise").Select("id", "praise_id", "user_id", "create_time").Where("gorse = false").Where("type = ?", 4).Limit(100).Scan(&data).Error; err != nil {
|
||||
fmt.Println("获取图像点赞记录失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(data) == 0 {
|
||||
fmt.Println("没有需要同步的点赞记录")
|
||||
time.Sleep(60 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
var ids []int
|
||||
for _, item := range data {
|
||||
feedbacks = append(feedbacks, client.Feedback{
|
||||
FeedbackType: "like",
|
||||
UserId: fmt.Sprintf("%d", item.UserID),
|
||||
ItemId: fmt.Sprintf("%d", item.PraiseID),
|
||||
Timestamp: item.CreateTime.Format("2006-01-02 15:04:05"),
|
||||
})
|
||||
ids = append(ids, item.ID)
|
||||
}
|
||||
|
||||
if err := db.Table("web_praise").Where("id in (?)", ids).Update("gorse", true).Error; err != nil {
|
||||
fmt.Println("更新点赞记录失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := gorse.InsertFeedback(ctx, feedbacks); err != nil {
|
||||
fmt.Println("写入点赞记录失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("写入点赞记录结束", len(ids))
|
||||
}
|
||||
}
|
||||
|
||||
// 同步收藏数据
|
||||
|
||||
// 获取推荐ID
|
||||
func GetRecommend(user_id int, categorys []string) ([]int, error) {
|
||||
var ctx context.Context = context.Background()
|
||||
var ids []int
|
||||
data, err := gorse.GetItemRecommend(ctx, fmt.Sprintf("%d", user_id), categorys, "read", "0", 100, 0)
|
||||
if err != nil {
|
||||
fmt.Println("获取推荐失败", err)
|
||||
return ids, err
|
||||
}
|
||||
for _, item := range data {
|
||||
id, _ := strconv.Atoi(item)
|
||||
ids = append(ids, id)
|
||||
}
|
||||
return ids, nil
|
||||
}
|
725
api/graphql.go
725
api/graphql.go
@@ -2,24 +2,169 @@ package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"log"
|
||||
"os"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.satori.love/gameui/webp/models"
|
||||
_ "github.com/doug-martin/goqu/v9/dialect/mysql"
|
||||
"github.com/graphql-go/graphql"
|
||||
"github.com/graphql-go/graphql/language/ast"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/thoas/go-funk"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var db *gorm.DB
|
||||
var err error
|
||||
|
||||
func InitDefault(config *viper.Viper) {
|
||||
if db, err = gorm.Open(mysql.Open(fmt.Sprintf(
|
||||
"%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
|
||||
config.GetString("mysql.user"),
|
||||
config.GetString("mysql.password"),
|
||||
config.GetString("mysql.host"),
|
||||
config.GetInt("mysql.port"),
|
||||
config.GetString("mysql.database"),
|
||||
)), &gorm.Config{}); err != nil {
|
||||
log.Fatal("failed to connect to database:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func ParseToken(token string) (user_id int) {
|
||||
if err := db.Table("web_auth").Select("user_id").Where("token = ?", token).Scan(&user_id).Error; err != nil {
|
||||
fmt.Println("token解析失败", err)
|
||||
}
|
||||
return user_id
|
||||
}
|
||||
|
||||
// 定时检查补全颜色字段
|
||||
func CheckColorNullRows(offset int) {
|
||||
for {
|
||||
time.Sleep(60 * time.Second)
|
||||
var list []struct {
|
||||
ID int
|
||||
Content string
|
||||
}
|
||||
|
||||
//fmt.Println("跳过的行数:", offset)
|
||||
if err := db.Table("web_images").Select("id", "content").Where("article_category_top_id = 22").Where("color_0_r IS NULL").Offset(offset).Limit(100).Scan(&list).Error; err != nil {
|
||||
fmt.Println("定时检查补全颜色字段查询失败", err)
|
||||
continue
|
||||
}
|
||||
for index, item := range list {
|
||||
matches := regexp.MustCompile(`^https?://image\.gameuiux\.cn/(.*)`).FindStringSubmatch(item.Content)
|
||||
if len(matches) < 2 {
|
||||
fmt.Println("转换路径失败", index, item.ID, item.Content)
|
||||
continue
|
||||
}
|
||||
// 打开图像文件
|
||||
filepath := "data/oss/" + matches[1]
|
||||
file, err := os.Open(filepath)
|
||||
if err != nil {
|
||||
fmt.Println("打开文件失败", index, item.ID, item.Content, err)
|
||||
offset++
|
||||
continue
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// 解码 JPEG 图像
|
||||
img, _, err := image.Decode(file)
|
||||
if err != nil {
|
||||
fmt.Println("解码图像失败", index, item.ID, item.Content, err)
|
||||
continue
|
||||
}
|
||||
|
||||
k := 8
|
||||
centers, labels := KMeans(extractColors(img), k)
|
||||
|
||||
// 将聚类中心和颜色数量结合,并按颜色数量降序排序
|
||||
type cluster struct {
|
||||
center RGB
|
||||
count int
|
||||
}
|
||||
clusters := make([]cluster, k)
|
||||
for i := 0; i < k; i++ {
|
||||
clusters[i] = cluster{center: centers[i], count: 0}
|
||||
}
|
||||
// 统计每个聚类的颜色数量
|
||||
for _, label := range labels {
|
||||
clusters[label].count++
|
||||
}
|
||||
// 按颜色数量降序排序
|
||||
sort.Slice(clusters, func(i, j int) bool {
|
||||
return clusters[i].count > clusters[j].count
|
||||
})
|
||||
// 返回排序后的聚类中心
|
||||
sortedCenters := make([]RGB, k)
|
||||
for i, c := range clusters {
|
||||
sortedCenters[i] = c.center
|
||||
}
|
||||
|
||||
//fmt.Println("聚类后的颜色数量:", clusters)
|
||||
|
||||
if err := db.Table("web_images").Where("id = ?", item.ID).Updates(map[string]interface{}{
|
||||
"color_0_r": sortedCenters[0].R,
|
||||
"color_0_g": sortedCenters[0].G,
|
||||
"color_0_b": sortedCenters[0].B,
|
||||
"color_1_r": sortedCenters[1].R,
|
||||
"color_1_g": sortedCenters[1].G,
|
||||
"color_1_b": sortedCenters[1].B,
|
||||
}).Error; err != nil {
|
||||
fmt.Println("更新颜色字段失败", index, item.ID, item.Content, err)
|
||||
continue
|
||||
}
|
||||
fmt.Println("更新颜色索引:", item.ID, filepath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取 List 中的所有字段名
|
||||
func ListItem(requestedFields []ast.Selection) (data []string) {
|
||||
for _, field := range requestedFields {
|
||||
fieldAST, _ := field.(*ast.Field)
|
||||
if fieldAST.Name.Value == "list" {
|
||||
for _, field := range fieldAST.SelectionSet.Selections {
|
||||
data = append(data, field.(*ast.Field).Name.Value)
|
||||
}
|
||||
}
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// 获取所有字段名
|
||||
func LoadItem(requestedFields []ast.Selection) (data []string) {
|
||||
var items = []string{"user", "article"}
|
||||
for _, field := range requestedFields {
|
||||
fieldAST, _ := field.(*ast.Field)
|
||||
if funk.Contains(items, fieldAST.Name.Value) {
|
||||
name := fieldAST.Name.Value
|
||||
name = strings.ToUpper(string(name[0])) + name[1:]
|
||||
data = append(data, name)
|
||||
for _, str := range LoadItem(fieldAST.SelectionSet.Selections) {
|
||||
data = append(data, name+"."+str)
|
||||
}
|
||||
}
|
||||
if fieldAST.Name.Value == "list" {
|
||||
for _, str := range LoadItem(fieldAST.SelectionSet.Selections) {
|
||||
data = append(data, str)
|
||||
}
|
||||
}
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// 自动生成 GraphQL 类型的函数
|
||||
func generateGraphQLType(model interface{}) (*graphql.Object, error) {
|
||||
func GraphQLType(model interface{}) *graphql.Object {
|
||||
modelType := reflect.TypeOf(model)
|
||||
if modelType.Kind() != reflect.Struct {
|
||||
return nil, fmt.Errorf("model must be a struct")
|
||||
fmt.Println("输入的类型必须是结构体")
|
||||
return nil
|
||||
}
|
||||
|
||||
fields := graphql.Fields{}
|
||||
@@ -45,570 +190,54 @@ func generateGraphQLType(model interface{}) (*graphql.Object, error) {
|
||||
return graphql.NewObject(graphql.ObjectConfig{
|
||||
Name: modelType.Name(),
|
||||
Fields: fields,
|
||||
}), nil
|
||||
}
|
||||
|
||||
func NewSchema(config Config) (graphql.Schema, error) {
|
||||
|
||||
db, err := gorm.Open(mysql.Open(fmt.Sprintf(
|
||||
"%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
|
||||
config.Mysql.UserName,
|
||||
config.Mysql.Password,
|
||||
config.Mysql.Host,
|
||||
config.Mysql.Port,
|
||||
config.Mysql.Database,
|
||||
)), &gorm.Config{})
|
||||
if err != nil {
|
||||
log.Fatal("failed to connect to database:", err)
|
||||
}
|
||||
|
||||
// 打开数据库连接
|
||||
connection, err := sqlx.Connect("mysql", fmt.Sprintf(
|
||||
"%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
|
||||
config.Mysql.UserName,
|
||||
config.Mysql.Password,
|
||||
config.Mysql.Host,
|
||||
config.Mysql.Port,
|
||||
config.Mysql.Database,
|
||||
))
|
||||
if err != nil {
|
||||
log.Fatalln("连接数据库失败", err)
|
||||
}
|
||||
|
||||
var user, article, text *graphql.Object
|
||||
|
||||
// 用户的可选字段
|
||||
user = graphql.NewObject(graphql.ObjectConfig{
|
||||
Name: "User",
|
||||
Description: "用户信息",
|
||||
Fields: graphql.Fields{
|
||||
"id": &graphql.Field{Type: graphql.Int, Description: "用户ID"},
|
||||
"user_name": &graphql.Field{Type: graphql.String, Description: "用户名"},
|
||||
"avatar": &graphql.Field{Type: graphql.String, Description: "用户头像"},
|
||||
"rank": &graphql.Field{Type: graphql.String, Description: "用户等级"},
|
||||
"price": &graphql.Field{Type: graphql.Float, Description: "用户金币"},
|
||||
"create_time": &graphql.Field{Type: graphql.DateTime, Description: "用户创建时间"},
|
||||
"update_time": &graphql.Field{Type: graphql.DateTime, Description: "用户更新时间"},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 文章的可选字段
|
||||
article = graphql.NewObject(graphql.ObjectConfig{
|
||||
Name: "Article",
|
||||
Description: "文章",
|
||||
Fields: graphql.Fields{
|
||||
"id": &graphql.Field{Type: graphql.Int, Description: "文章ID"},
|
||||
"title": &graphql.Field{Type: graphql.String, Description: "文章标题"},
|
||||
"tags": &graphql.Field{Type: graphql.String, Description: "文章标签"},
|
||||
"user": &graphql.Field{Type: user, Description: "文章所属用户"},
|
||||
"create_time": &graphql.Field{Type: graphql.DateTime, Description: "文章创建时间"},
|
||||
"update_time": &graphql.Field{Type: graphql.DateTime, Description: "文章更新时间"},
|
||||
},
|
||||
})
|
||||
|
||||
category := graphql.NewObject(graphql.ObjectConfig{
|
||||
Name: "Category",
|
||||
Description: "分类",
|
||||
Fields: graphql.Fields{
|
||||
"id": &graphql.Field{Type: graphql.Int, Description: "分类ID"},
|
||||
"title": &graphql.Field{Type: graphql.String, Description: "分类标题"},
|
||||
"keyword": &graphql.Field{Type: graphql.String, Description: "分类关键词"},
|
||||
"parent_id": &graphql.Field{Type: graphql.Int, Description: "分类父级ID"},
|
||||
"create_time": &graphql.Field{Type: graphql.DateTime, Description: "分类创建时间"},
|
||||
"update_time": &graphql.Field{Type: graphql.DateTime, Description: "分类更新时间"},
|
||||
"status": &graphql.Field{Type: graphql.Int, Description: "分类状态"},
|
||||
"content": &graphql.Field{Type: graphql.String, Description: "分类内容"},
|
||||
"sort": &graphql.Field{Type: graphql.Int, Description: "分类排序"},
|
||||
"image": &graphql.Field{Type: graphql.String, Description: "分类图片"},
|
||||
"image_num": &graphql.Field{Type: graphql.Int, Description: "分类图片数量"},
|
||||
"article_num": &graphql.Field{Type: graphql.Int, Description: "分类文章数量"},
|
||||
},
|
||||
})
|
||||
fmt.Println(category)
|
||||
|
||||
// 图像中的文字提取
|
||||
text = graphql.NewObject(graphql.ObjectConfig{
|
||||
Name: "Text",
|
||||
Description: "图像中的文字提取",
|
||||
Fields: graphql.Fields{
|
||||
"text": &graphql.Field{Type: graphql.String, Description: "文字内容"},
|
||||
"confidence": &graphql.Field{Type: graphql.Float, Description: "置信度"},
|
||||
"coordinate": &graphql.Field{Type: &graphql.List{OfType: graphql.NewList(graphql.Float)}, Description: "文字坐标"},
|
||||
},
|
||||
})
|
||||
|
||||
// 图像的可选字段
|
||||
image := graphql.NewObject(graphql.ObjectConfig{
|
||||
Name: "Image",
|
||||
Description: "图像",
|
||||
Fields: graphql.Fields{
|
||||
"id": &graphql.Field{Type: graphql.Int, Description: "图像ID"},
|
||||
"width": &graphql.Field{Type: graphql.Int, Description: "图像宽度"},
|
||||
"height": &graphql.Field{Type: graphql.Int, Description: "图像高度"},
|
||||
"content": &graphql.Field{Type: graphql.String, Description: "图像内容"},
|
||||
"remark": &graphql.Field{Type: graphql.String, Description: "图像备注"},
|
||||
"description": &graphql.Field{Type: graphql.String, Description: "图像描述"},
|
||||
"tags": &graphql.Field{Type: graphql.String, Description: "图像标签"},
|
||||
"rank": &graphql.Field{Type: graphql.String, Description: "图像等级"},
|
||||
"comment_num": &graphql.Field{Type: graphql.Int, Description: "评论数"},
|
||||
"article_category_top_id": &graphql.Field{Type: graphql.Int, Description: "文章分类顶级ID"},
|
||||
"praise_count": &graphql.Field{Type: graphql.Int, Description: "点赞数"},
|
||||
"collect_count": &graphql.Field{Type: graphql.Int, Description: "收藏数"},
|
||||
"create_time": &graphql.Field{Type: graphql.DateTime, Description: "图像创建时间"},
|
||||
"update_time": &graphql.Field{Type: graphql.DateTime, Description: "图像更新时间"},
|
||||
"article": &graphql.Field{Type: article, Description: "图像所属文章"},
|
||||
"text": &graphql.Field{
|
||||
Type: graphql.NewList(text),
|
||||
Description: "图像中的文字",
|
||||
Args: graphql.FieldConfigArgument{
|
||||
"text": &graphql.ArgumentConfig{Type: graphql.String, Description: "筛选含有指定文字的列"},
|
||||
},
|
||||
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
||||
if p.Args["text"] != nil {
|
||||
var texts TextList
|
||||
for _, text := range p.Source.(Image).Text {
|
||||
fmt.Println("san", text.Text)
|
||||
if strings.Contains(text.Text, p.Args["text"].(string)) {
|
||||
texts = append(texts, text)
|
||||
// 判断指定字段是否存在
|
||||
func existField(selections []ast.Selection, name string) bool {
|
||||
for _, field := range selections {
|
||||
if f, ok := field.(*ast.Field); ok && f.Name.Value == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return texts, nil
|
||||
return false
|
||||
}
|
||||
return p.Source.(Image).Text, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
image.AddFieldConfig("user", &graphql.Field{Type: user, Description: "图像所属用户"})
|
||||
image.AddFieldConfig("similars", &graphql.Field{Type: graphql.NewList(image), Description: "相似的图像", Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
||||
return []Image{}, nil
|
||||
}})
|
||||
|
||||
// 将 list 中的字段提取出来用于查询
|
||||
get_fields := func(requestedFields []ast.Selection) (fields []string) {
|
||||
func get_fields(requestedFields []ast.Selection) (fields []string) {
|
||||
for _, field := range requestedFields {
|
||||
fieldAST, ok := field.(*ast.Field)
|
||||
if ok && fieldAST.Name.Value == "list" {
|
||||
fieldAST, _ := field.(*ast.Field)
|
||||
if fieldAST.Name.Value == "list" {
|
||||
for _, field := range fieldAST.SelectionSet.Selections {
|
||||
fieldAST, ok := field.(*ast.Field)
|
||||
if ok {
|
||||
if fieldAST.Name.Value == "user" {
|
||||
fields = append(fields, "user_id")
|
||||
continue
|
||||
}
|
||||
if fieldAST.Name.Value == "article" {
|
||||
fields = append(fields, "article_id")
|
||||
continue
|
||||
}
|
||||
if fieldAST.Name.Value == "similars" {
|
||||
// 跳过自定义字段
|
||||
fieldAST, _ := field.(*ast.Field)
|
||||
if fieldAST.Name.Value == "text_count" {
|
||||
continue
|
||||
}
|
||||
fields = append(fields, fieldAST.Name.Value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
schema, err := graphql.NewSchema(graphql.SchemaConfig{Query: graphql.NewObject(graphql.ObjectConfig{Name: "RootQuery", Fields: graphql.Fields{
|
||||
"categorys": &graphql.Field{
|
||||
Name: "categorys",
|
||||
Description: "分类列表",
|
||||
Type: graphql.NewObject(graphql.ObjectConfig{
|
||||
Name: "CategoryConnection",
|
||||
Description: "条件筛选分类列表",
|
||||
Fields: graphql.Fields{
|
||||
"list": &graphql.Field{Type: graphql.NewList(category), Description: "分类列表"},
|
||||
"total": &graphql.Field{Type: graphql.Int, Description: "分类总数"},
|
||||
var orderType = graphql.NewEnum(graphql.EnumConfig{
|
||||
Name: "OrderType",
|
||||
Description: "排序类型",
|
||||
Values: graphql.EnumValueConfigMap{
|
||||
"ASC": &graphql.EnumValueConfig{
|
||||
Value: "ASC",
|
||||
Description: "升序",
|
||||
},
|
||||
}),
|
||||
Args: graphql.FieldConfigArgument{
|
||||
"id": &graphql.ArgumentConfig{Type: graphql.Int, Description: "筛选分类中指定ID的"},
|
||||
"title": &graphql.ArgumentConfig{Type: graphql.String, Description: "筛选分类中含有指定标题的"},
|
||||
"keyword": &graphql.ArgumentConfig{Type: graphql.String, Description: "筛选分类中含有指定关键词的"},
|
||||
"parent_id": &graphql.ArgumentConfig{Type: graphql.Int, Description: "筛选分类中含有指定父级ID的"},
|
||||
"create_time": &graphql.ArgumentConfig{Type: graphql.DateTime, Description: "筛选分类中创建时间等于指定值的"},
|
||||
"update_time": &graphql.ArgumentConfig{Type: graphql.DateTime, Description: "筛选分类中更新时间等于指定值的"},
|
||||
"first": &graphql.ArgumentConfig{Type: graphql.Int, Description: "翻页参数(傳回清單中的前n個元素)"},
|
||||
"last": &graphql.ArgumentConfig{Type: graphql.Int, Description: "翻页参数(傳回清單中的最後n個元素)"},
|
||||
"after": &graphql.ArgumentConfig{Type: graphql.String, Description: "翻页参数(傳回清單中指定遊標之後的元素)"},
|
||||
"before": &graphql.ArgumentConfig{Type: graphql.String, Description: "翻页参数(傳回清單中指定遊標之前的元素)"},
|
||||
},
|
||||
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
||||
var categorys []Category
|
||||
var total int
|
||||
var err error
|
||||
|
||||
// 获取筛选条件
|
||||
var arg struct {
|
||||
ID int
|
||||
Title string
|
||||
Keyword string
|
||||
ParentID int
|
||||
First int
|
||||
Last int
|
||||
After string
|
||||
Before string
|
||||
}
|
||||
mapstructure.Decode(p.Args, &arg)
|
||||
|
||||
var limit int = 10
|
||||
if arg.First != 0 {
|
||||
limit = arg.First
|
||||
} else if arg.Last != 0 {
|
||||
limit = arg.Last
|
||||
}
|
||||
|
||||
if err := db.Limit(limit).Where("id > 0").Find(&categorys).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"list": categorys,
|
||||
"total": total,
|
||||
}, err
|
||||
"DESC": &graphql.EnumValueConfig{
|
||||
Value: "DESC",
|
||||
Description: "降序",
|
||||
},
|
||||
},
|
||||
"users": &graphql.Field{
|
||||
Name: "users",
|
||||
Description: "用户列表",
|
||||
Type: graphql.NewObject(graphql.ObjectConfig{
|
||||
Name: "UserConnection",
|
||||
Description: "条件筛选用户列表",
|
||||
Fields: graphql.Fields{
|
||||
"list": &graphql.Field{Type: graphql.NewList(user), Description: "用户列表"},
|
||||
"total": &graphql.Field{Type: graphql.Int, Description: "用户总数"},
|
||||
},
|
||||
}),
|
||||
Args: graphql.FieldConfigArgument{
|
||||
"id": &graphql.ArgumentConfig{Type: graphql.Int, Description: "筛选用户中指定ID的"},
|
||||
"user_name": &graphql.ArgumentConfig{Type: graphql.String, Description: "筛选用户中含有指定用户名的"},
|
||||
"avatar": &graphql.ArgumentConfig{Type: graphql.String, Description: "筛选用户中含有指定头像的"},
|
||||
"rank": &graphql.ArgumentConfig{Type: graphql.String, Description: "筛选用户中含有指定等级的"},
|
||||
"create_time": &graphql.ArgumentConfig{Type: graphql.DateTime, Description: "筛选用户中创建时间等于指定值的"},
|
||||
"update_time": &graphql.ArgumentConfig{Type: graphql.DateTime, Description: "筛选用户中更新时间等于指定值的"},
|
||||
"text": &graphql.ArgumentConfig{Type: graphql.String, Description: "筛选图像中含有指定文字的"},
|
||||
"first": &graphql.ArgumentConfig{Type: graphql.Int, Description: "翻页参数(傳回清單中的前n個元素)"},
|
||||
"last": &graphql.ArgumentConfig{Type: graphql.Int, Description: "翻页参数(傳回清單中的最後n個元素)"},
|
||||
"after": &graphql.ArgumentConfig{Type: graphql.String, Description: "翻页参数(傳回清單中指定遊標之後的元素)"},
|
||||
"before": &graphql.ArgumentConfig{Type: graphql.String, Description: "翻页参数(傳回清單中指定遊標之前的元素)"},
|
||||
},
|
||||
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
||||
var where []string
|
||||
if p.Args["id"] != nil {
|
||||
where = append(where, fmt.Sprintf("id=%d", p.Args["id"]))
|
||||
}
|
||||
if p.Args["user_name"] != nil {
|
||||
where = append(where, fmt.Sprintf("user_name='%s'", p.Args["user_name"]))
|
||||
}
|
||||
// 筛选条件
|
||||
where_str := strings.Join(where, " AND ")
|
||||
if where_str != "" {
|
||||
where_str = "WHERE " + where_str
|
||||
}
|
||||
var query strings.Builder
|
||||
var users []User
|
||||
var total int
|
||||
fields := strings.Join(get_fields(p.Info.FieldASTs[0].SelectionSet.Selections), ",")
|
||||
query.WriteString(fmt.Sprintf("SELECT %s FROM web_member %s LIMIT %d OFFSET %d", fields, where_str, 10, 0))
|
||||
if err := connection.Select(&users, query.String()); err != nil {
|
||||
fmt.Println("获取用户列表失败", err)
|
||||
return nil, err
|
||||
}
|
||||
if len(users) > 0 {
|
||||
query.Reset()
|
||||
query.WriteString(fmt.Sprintf("SELECT COUNT(*) FROM web_member %s", where_str))
|
||||
if err := connection.Get(&total, query.String()); err != nil {
|
||||
fmt.Println("获取用户总数失败", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"list": users,
|
||||
"total": total,
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
"images": &graphql.Field{
|
||||
Name: "images",
|
||||
Description: "图像列表",
|
||||
Type: graphql.NewObject(graphql.ObjectConfig{
|
||||
Name: "ImageConnection",
|
||||
Description: "条件筛选图像列表",
|
||||
Fields: graphql.Fields{
|
||||
"list": &graphql.Field{Type: graphql.NewList(image), Description: "图像列表"},
|
||||
"total": &graphql.Field{Type: graphql.Int, Description: "图像总数"},
|
||||
},
|
||||
}),
|
||||
Args: graphql.FieldConfigArgument{
|
||||
"preference": &graphql.ArgumentConfig{Type: graphql.String, Description: "使用浏览记录获取的偏好推荐图像"},
|
||||
"similar": &graphql.ArgumentConfig{Type: graphql.Int, Description: "获取与指定ID图像相似的图像"},
|
||||
"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, Description: "筛选图像中含有指定内容的"},
|
||||
"remark": &graphql.ArgumentConfig{Type: graphql.String, Description: "筛选图像中含有指定备注的"},
|
||||
"description": &graphql.ArgumentConfig{Type: graphql.String, Description: "筛选图像中含有指定描述的"},
|
||||
"tags": &graphql.ArgumentConfig{Type: graphql.String, Description: "筛选图像中含有指定标签的"},
|
||||
"rank": &graphql.ArgumentConfig{Type: graphql.String, Description: "筛选图像中含有指定等级的"},
|
||||
"text": &graphql.ArgumentConfig{Type: graphql.String, Description: "筛选图像中含有指定文字的"},
|
||||
"comment_num": &graphql.ArgumentConfig{Type: graphql.Int, Description: "筛选图像中评论数等于指定值的"},
|
||||
"praise_count": &graphql.ArgumentConfig{Type: graphql.Int, Description: "筛选图像中点赞数等于指定值的"},
|
||||
"collect_count": &graphql.ArgumentConfig{Type: graphql.Int, Description: "筛选图像中收藏数等于指定值的"},
|
||||
"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, Description: "筛选图像中创建时间等于指定值的"},
|
||||
"update_time": &graphql.ArgumentConfig{Type: graphql.DateTime, Description: "筛选图像中更新时间等于指定值的"},
|
||||
"first": &graphql.ArgumentConfig{Type: graphql.Int, Description: "翻页参数(傳回清單中的前n個元素)"},
|
||||
"last": &graphql.ArgumentConfig{Type: graphql.Int, Description: "翻页参数(傳回清單中的最後n個元素)"},
|
||||
"after": &graphql.ArgumentConfig{Type: graphql.String, Description: "翻页参数(傳回清單中指定遊標之後的元素)"},
|
||||
"before": &graphql.ArgumentConfig{Type: graphql.String, Description: "翻页参数(傳回清單中指定遊標之前的元素)"},
|
||||
},
|
||||
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
||||
// 定义参数结构体
|
||||
var args struct {
|
||||
First int
|
||||
Last int
|
||||
After string
|
||||
Before string
|
||||
Text string
|
||||
Preference string
|
||||
Similar int
|
||||
}
|
||||
mapstructure.Decode(p.Args, &args)
|
||||
|
||||
// 参数到 SQL 格式字符串的映射
|
||||
var argToSQLFormat = map[string]string{
|
||||
"id": "id=%d",
|
||||
"width": "width=%d",
|
||||
"height": "height=%d",
|
||||
"content": "content='%s'",
|
||||
"remark": "remark='%s'",
|
||||
"description": "description='%s'",
|
||||
"tags": "tags='%s'",
|
||||
"rank": "rank='%s'",
|
||||
"comment_num": "comment_num=%d",
|
||||
"praise_count": "praise_count=%d",
|
||||
"collect_count": "collect_count=%d",
|
||||
"article_id": "article_id=%d",
|
||||
"user_id": "user_id=%d",
|
||||
"create_time": "create_time='%s'",
|
||||
"update_time": "update_time='%s'",
|
||||
}
|
||||
|
||||
// 筛选条件
|
||||
var where []string
|
||||
var order []string
|
||||
for arg, format := range argToSQLFormat {
|
||||
if p.Args[arg] != nil {
|
||||
where = append(where, fmt.Sprintf(format, p.Args[arg]))
|
||||
}
|
||||
}
|
||||
|
||||
var id_list []string
|
||||
|
||||
// 特殊处理 preference 参数
|
||||
if args.Preference != "" {
|
||||
// 去除空格并拆分以逗号分割的ID
|
||||
id_list = strings.Split(strings.ReplaceAll(args.Preference, " ", ""), ",")
|
||||
// 使用这一组 id 推荐
|
||||
fmt.Println("preference:", args.Preference)
|
||||
}
|
||||
|
||||
// 特殊处理 similar 参数
|
||||
if args.Similar != 0 {
|
||||
id_list := models.GetSimilarImagesIdList(args.Similar, 200)
|
||||
ids_str := strings.Trim(strings.Join(strings.Fields(fmt.Sprint(id_list)), ","), "[]")
|
||||
if ids_str == "" {
|
||||
return map[string]interface{}{"list": []Image{}, "total": 0}, nil
|
||||
}
|
||||
where = append(where, fmt.Sprintf("id IN (%s)", ids_str))
|
||||
order = append(order, fmt.Sprintf("ORDER BY FIELD(id,%s)", ids_str))
|
||||
}
|
||||
|
||||
// 特殊处理 text 参数
|
||||
if args.Text != "" {
|
||||
resp, err := models.ZincSearch(map[string]interface{}{
|
||||
"query": map[string]interface{}{
|
||||
"bool": map[string]interface{}{
|
||||
"must": []map[string]interface{}{
|
||||
{
|
||||
"query_string": map[string]string{"query": "text:" + args.Text},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"sort": []string{
|
||||
"_score",
|
||||
},
|
||||
"from": 0,
|
||||
"size": 200,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Println("ZincSearch 获取图像列表失败", err)
|
||||
return nil, err
|
||||
}
|
||||
id_list = resp.ToIDList(args.First, args.Last, args.After, args.Before)
|
||||
id_list_str := strings.Trim(strings.Join(strings.Fields(fmt.Sprint(id_list)), ","), "[]")
|
||||
if id_list_str == "" {
|
||||
return map[string]interface{}{
|
||||
"list": []Image{},
|
||||
"total": 0,
|
||||
}, nil
|
||||
}
|
||||
where = append(where, fmt.Sprintf("id IN (%s)", id_list_str))
|
||||
|
||||
type Cache struct {
|
||||
time time.Time
|
||||
ids []string
|
||||
}
|
||||
|
||||
where_str := strings.Join(where, " AND ")
|
||||
order_str := strings.Join(order, "")
|
||||
|
||||
if where_str != "" {
|
||||
where_str = "WHERE " + where_str
|
||||
}
|
||||
|
||||
// 处理翻页参数
|
||||
var limit, offset int
|
||||
if args.First == 0 && args.Last == 0 {
|
||||
limit = 10
|
||||
offset = 0
|
||||
}
|
||||
|
||||
if args.First != 0 {
|
||||
limit = args.First
|
||||
offset = 0
|
||||
}
|
||||
|
||||
if args.Last != 0 {
|
||||
limit = args.Last
|
||||
offset = len(id_list) - limit
|
||||
}
|
||||
|
||||
// 执行查询
|
||||
var query strings.Builder
|
||||
fields := strings.Join(get_fields(p.Info.FieldASTs[0].SelectionSet.Selections), ",")
|
||||
query.WriteString(fmt.Sprintf("SELECT %s FROM web_images %s %s LIMIT %d OFFSET %d", fields, where_str, order_str, limit, offset))
|
||||
|
||||
var images ImageList
|
||||
var q = query.String()
|
||||
if err := connection.Select(&images, q); err != nil {
|
||||
fmt.Println("获取图像列表失败", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取用户信息(如果图像列表不为空且请求字段中包含user)
|
||||
if len(images) > 0 && strings.Contains(fields, "user") {
|
||||
user_ids_str := images.ToAllUserID().ToString()
|
||||
var users []User
|
||||
if err := connection.Select(&users, fmt.Sprintf("SELECT id,user_name,avatar,rank,create_time,update_time FROM web_member WHERE id IN (%s)", user_ids_str)); err != nil {
|
||||
fmt.Println("获取用户列表失败", err)
|
||||
return nil, err
|
||||
}
|
||||
// 将用户信息与图像信息关联
|
||||
images.SetUser(users)
|
||||
}
|
||||
|
||||
// 获取文章信息(如果图像列表不为空且请求字段中包含article)
|
||||
if len(images) > 0 && strings.Contains(fields, "article") {
|
||||
article_ids_str := images.ToAllArticleID().ToString()
|
||||
var articles []Article
|
||||
if err := connection.Select(&articles, fmt.Sprintf("SELECT id,title,tags,create_time,update_time FROM web_article WHERE id IN (%s)", article_ids_str)); err != nil {
|
||||
fmt.Println("获取文章列表失败", err)
|
||||
return nil, err
|
||||
}
|
||||
// 将文章信息与图像信息关联
|
||||
images.SetArticle(articles)
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"list": images,
|
||||
"total": 0,
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
"articles": &graphql.Field{
|
||||
Name: "Articles",
|
||||
Description: "文章列表",
|
||||
Type: graphql.NewObject(graphql.ObjectConfig{
|
||||
Name: "ArticleConnection",
|
||||
Description: "条件筛选文章列表",
|
||||
Fields: graphql.Fields{
|
||||
"list": &graphql.Field{Type: graphql.NewList(article), Description: "文章列表"},
|
||||
"total": &graphql.Field{Type: graphql.Int, Description: "文章总数"},
|
||||
},
|
||||
}),
|
||||
Args: graphql.FieldConfigArgument{
|
||||
"id": &graphql.ArgumentConfig{Type: graphql.Int, Description: "筛选文章中指定ID的"},
|
||||
"title": &graphql.ArgumentConfig{Type: graphql.String, Description: "筛选文章中含有指定标题的"},
|
||||
"tags": &graphql.ArgumentConfig{Type: graphql.String, Description: "筛选文章中含有指定标签的"},
|
||||
"create_time": &graphql.ArgumentConfig{Type: graphql.DateTime, Description: "筛选文章中创建时间等于指定值的"},
|
||||
"update_time": &graphql.ArgumentConfig{Type: graphql.DateTime, Description: "筛选文章中更新时间等于指定值的"},
|
||||
},
|
||||
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
||||
first := 10 // p.Args["first"]
|
||||
after := 0 // p.Args["after"]
|
||||
fields := strings.Join(get_fields(p.Info.FieldASTs[0].SelectionSet.Selections), ",")
|
||||
var where []string
|
||||
if p.Args["id"] != nil {
|
||||
where = append(where, fmt.Sprintf("id=%d", p.Args["id"]))
|
||||
}
|
||||
if p.Args["title"] != nil {
|
||||
where = append(where, fmt.Sprintf("title='%s'", p.Args["title"]))
|
||||
}
|
||||
// 筛选条件
|
||||
where_str := strings.Join(where, " AND ")
|
||||
if where_str != "" {
|
||||
where_str = "WHERE " + where_str
|
||||
}
|
||||
|
||||
var query strings.Builder
|
||||
query.WriteString(fmt.Sprintf("SELECT %s FROM web_article %s LIMIT %d OFFSET %d", fields, where_str, first, after))
|
||||
// 返回翻页信息
|
||||
var articles []Article
|
||||
if err := connection.Select(&articles, query.String()); err != nil {
|
||||
fmt.Println("获取文章列表失败", err)
|
||||
return nil, err
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"list": articles,
|
||||
"total": 0,
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
"tags": &graphql.Field{
|
||||
Name: "tags",
|
||||
Description: "标签列表",
|
||||
Type: graphql.NewObject(graphql.ObjectConfig{
|
||||
Name: "TagConnection",
|
||||
Description: "条件筛选标签列表",
|
||||
Fields: graphql.Fields{
|
||||
"list": &graphql.Field{Type: graphql.NewList(graphql.String), Description: "标签列表"},
|
||||
"total": &graphql.Field{Type: graphql.Int, Description: "标签总数"},
|
||||
},
|
||||
}),
|
||||
Args: graphql.FieldConfigArgument{
|
||||
"first": &graphql.ArgumentConfig{Type: graphql.Int, Description: "翻页参数(傳回清單中的前n個元素)"},
|
||||
"last": &graphql.ArgumentConfig{Type: graphql.Int, Description: "翻页参数(傳回清單中的最後n個元素)"},
|
||||
"after": &graphql.ArgumentConfig{Type: graphql.String, Description: "翻页参数(傳回清單中指定遊標之後的元素)"},
|
||||
"before": &graphql.ArgumentConfig{Type: graphql.String, Description: "翻页参数(傳回清單中指定遊標之前的元素)"},
|
||||
},
|
||||
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
||||
var tags []string
|
||||
if err := connection.Select(&tags, "SELECT DISTINCT tags FROM web_images LIMIT 10"); err != nil {
|
||||
fmt.Println("获取标签列表失败", err)
|
||||
return nil, err
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"list": tags,
|
||||
"total": 0,
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
}})})
|
||||
if err != nil {
|
||||
return schema, err
|
||||
}
|
||||
return schema, nil
|
||||
}
|
||||
var cache map[string]Cache = make(map[string]Cache)
|
||||
|
719
api/image.go
Normal file
719
api/image.go
Normal file
@@ -0,0 +1,719 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.satori.love/gameui/webp/models"
|
||||
"github.com/doug-martin/goqu/v9"
|
||||
"github.com/graphql-go/graphql"
|
||||
"github.com/thoas/go-funk"
|
||||
"github.com/zhenghaoz/gorse/client"
|
||||
)
|
||||
|
||||
type Image struct {
|
||||
ID int `json:"id" gorm:"primaryKey"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
Content string `json:"content"`
|
||||
Remark string `json:"remark"`
|
||||
Description string `json:"description"`
|
||||
Tags string `json:"tags"`
|
||||
Rank string `json:"rank"`
|
||||
CommentNum int `json:"comment_num"`
|
||||
PraiseCount int `json:"praise_count" gorm:"column:praise"`
|
||||
CollectCount int `json:"collect_count"`
|
||||
ArticleID int `json:"article_id"`
|
||||
UserID int `json:"user_id"`
|
||||
CreateTime time.Time `json:"create_time"`
|
||||
UpdateTime time.Time `json:"update_time"`
|
||||
Text TextList `json:"text" gorm:"type:json"`
|
||||
User User `json:"user" gorm:"foreignKey:UserID"`
|
||||
Article Article `json:"article" gorm:"foreignKey:ArticleID"`
|
||||
Activity bool `json:"activity"`
|
||||
Emoji1 int `json:"emoji1"`
|
||||
Emoji2 int `json:"emoji2"`
|
||||
Emoji3 int `json:"emoji3"`
|
||||
Emoji4 int `json:"emoji4"`
|
||||
Emoji5 int `json:"emoji5"`
|
||||
Views int `json:"views"`
|
||||
DayRank int `json:"day_rank"`
|
||||
WeekRank int `json:"week_rank"`
|
||||
MonthRank int `json:"month_rank"`
|
||||
YearRank int `json:"year_rank"`
|
||||
AeonRank int `json:"aeon_rank"`
|
||||
}
|
||||
|
||||
func (Image) TableName() string {
|
||||
return "web_images"
|
||||
}
|
||||
|
||||
type TextList []struct {
|
||||
Text string `json:"text"`
|
||||
Confidence float64 `json:"confidence"`
|
||||
Coordinate [][]float64 `json:"coordinate"`
|
||||
}
|
||||
|
||||
func (a *TextList) Scan(value interface{}) error {
|
||||
// 如果数据库中的值为NULL,则返回nil
|
||||
if value == nil || len(value.([]byte)) == 0 {
|
||||
*a = TextList{}
|
||||
return nil
|
||||
}
|
||||
return json.Unmarshal(value.([]byte), a)
|
||||
}
|
||||
|
||||
func (a TextList) Value() (driver.Value, error) {
|
||||
return json.Marshal(a)
|
||||
}
|
||||
|
||||
// 图像的可选字段
|
||||
var imageType = graphql.NewObject(graphql.ObjectConfig{
|
||||
Name: "Image",
|
||||
Description: "图像",
|
||||
Fields: graphql.Fields{
|
||||
"id": &graphql.Field{Type: graphql.Int, Description: "图像ID"},
|
||||
"width": &graphql.Field{Type: graphql.Int, Description: "图像宽度"},
|
||||
"height": &graphql.Field{Type: graphql.Int, Description: "图像高度"},
|
||||
"content": &graphql.Field{Type: graphql.String, Description: "图像内容"},
|
||||
"remark": &graphql.Field{Type: graphql.String, Description: "图像备注"},
|
||||
"description": &graphql.Field{Type: graphql.String, Description: "图像描述"},
|
||||
"tags": &graphql.Field{Type: graphql.String, Description: "图像标签"},
|
||||
"rank": &graphql.Field{Type: graphql.String, Description: "图像等级"},
|
||||
"comment_num": &graphql.Field{Type: graphql.Int, Description: "评论数"},
|
||||
"article_category_top_id": &graphql.Field{Type: graphql.Int, Description: "文章分类顶级ID"},
|
||||
"praise_count": &graphql.Field{Type: graphql.Int, Description: "点赞数"},
|
||||
"collect_count": &graphql.Field{Type: graphql.Int, Description: "收藏数"},
|
||||
"create_time": &graphql.Field{Type: graphql.DateTime, Description: "图像创建时间"},
|
||||
"update_time": &graphql.Field{Type: graphql.DateTime, Description: "图像更新时间"},
|
||||
"activity": &graphql.Field{Type: graphql.Boolean, Description: "图像活动"},
|
||||
"article": &graphql.Field{Type: articleType, Description: "图像所属文章"},
|
||||
"praise": &graphql.Field{Type: graphql.Boolean, Description: "当前用户是否点赞", Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
||||
var user_id = p.Context.Value("user_id").(int)
|
||||
if user_id != 0 {
|
||||
var praise int64
|
||||
if err := db.Table("web_praise").Where("user_id = ?", user_id).Where("praise_id = ?", p.Source.(Image).ID).Where("type = ?", 4).Count(&praise).Error; err != nil {
|
||||
fmt.Println(user_id, p.Source.(Image).ID, praise, "E", err)
|
||||
return false, nil
|
||||
}
|
||||
if praise > 0 {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}},
|
||||
"collect": &graphql.Field{Type: graphql.Boolean, Description: "当前用户是否收藏", Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
||||
var user_id = p.Context.Value("user_id").(int)
|
||||
if user_id != 0 {
|
||||
var collect int64
|
||||
if err := db.Table("web_collect").Where("user_id = ?", user_id).Where("image_id = ?", p.Source.(Image).ID).Where("type = ?", 1).Count(&collect).Error; err != nil {
|
||||
return false, nil
|
||||
}
|
||||
if collect > 0 {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}},
|
||||
"collect_id": &graphql.Field{Type: graphql.Int, Description: "当前用户收藏ID", Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
||||
var user_id = p.Context.Value("user_id").(int)
|
||||
if user_id != 0 {
|
||||
var collect_id int
|
||||
if err := db.Table("web_collect").Where("user_id = ?", user_id).Where("image_id = ?", p.Source.(Image).ID).Where("type = ?", 1).Select("id").Scan(&collect_id).Error; err != nil {
|
||||
fmt.Println(user_id, p.Source.(Image).ID, collect_id, "E", err)
|
||||
return nil, nil
|
||||
}
|
||||
return collect_id, nil
|
||||
}
|
||||
return nil, nil
|
||||
}},
|
||||
"text": &graphql.Field{
|
||||
Type: graphql.NewList(graphql.NewObject(graphql.ObjectConfig{
|
||||
Name: "Text",
|
||||
Description: "图像中的文字提取",
|
||||
Fields: graphql.Fields{
|
||||
"text": &graphql.Field{Type: graphql.String, Description: "文字内容"},
|
||||
"confidence": &graphql.Field{Type: graphql.Float, Description: "置信度"},
|
||||
"coordinate": &graphql.Field{Type: &graphql.List{OfType: graphql.NewList(graphql.Float)}, Description: "文字坐标"},
|
||||
},
|
||||
})),
|
||||
Description: "图像中的文字",
|
||||
Args: graphql.FieldConfigArgument{
|
||||
"text": &graphql.ArgumentConfig{Type: graphql.String, Description: "筛选含有指定文字的列"},
|
||||
},
|
||||
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
||||
if p.Args["text"] != nil {
|
||||
var texts TextList
|
||||
for _, text := range p.Source.(Image).Text {
|
||||
if strings.Contains(text.Text, p.Args["text"].(string)) {
|
||||
texts = append(texts, text)
|
||||
}
|
||||
}
|
||||
return texts, nil
|
||||
}
|
||||
return p.Source.(Image).Text, nil
|
||||
},
|
||||
},
|
||||
"user": &graphql.Field{Type: userType, Description: "图像所属用户"},
|
||||
"emoji1": &graphql.Field{Type: graphql.Int, Description: "表情1数量"},
|
||||
"emoji2": &graphql.Field{Type: graphql.Int, Description: "表情2数量"},
|
||||
"emoji3": &graphql.Field{Type: graphql.Int, Description: "表情3数量"},
|
||||
"emoji4": &graphql.Field{Type: graphql.Int, Description: "表情4数量"},
|
||||
"emoji5": &graphql.Field{Type: graphql.Int, Description: "表情5数量"},
|
||||
"views": &graphql.Field{Type: graphql.Int, Description: "浏览量"},
|
||||
"day_rank": &graphql.Field{Type: graphql.Int, Description: "日榜"},
|
||||
"week_rank": &graphql.Field{Type: graphql.Int, Description: "周榜"},
|
||||
"month_rank": &graphql.Field{Type: graphql.Int, Description: "月榜"},
|
||||
"year_rank": &graphql.Field{Type: graphql.Int, Description: "年榜"},
|
||||
"aeon_rank": &graphql.Field{Type: graphql.Int, Description: "总榜"},
|
||||
},
|
||||
})
|
||||
|
||||
//image.AddFieldConfig("user", &graphql.Field{Type: userType, Description: "图像所属用户"})
|
||||
//image.AddFieldConfig("similars", &graphql.Field{Type: graphql.NewList(image), Description: "相似的图像", Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
||||
// return []Image{}, nil
|
||||
//}})
|
||||
|
||||
var ImageItem = &graphql.Field{
|
||||
Name: "image",
|
||||
Description: "单张图片",
|
||||
Type: imageType,
|
||||
Args: graphql.FieldConfigArgument{
|
||||
"id": &graphql.ArgumentConfig{Type: graphql.Int, Description: "根据ID获取图片"},
|
||||
},
|
||||
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
||||
img := Image{ID: p.Args["id"].(int)}
|
||||
query := db.Limit(1)
|
||||
for index, item := range LoadItem(p.Info.FieldASTs[0].SelectionSet.Selections) {
|
||||
fmt.Println(index, item)
|
||||
query = query.Preload(item)
|
||||
}
|
||||
if err := query.First(&img).Error; err != nil {
|
||||
log.Println("获取图片失败", err)
|
||||
return nil, err
|
||||
}
|
||||
return img, nil
|
||||
},
|
||||
}
|
||||
|
||||
var ImageItems = &graphql.Field{
|
||||
Name: "images",
|
||||
Description: "图像列表",
|
||||
Type: graphql.NewObject(graphql.ObjectConfig{
|
||||
Name: "ImageConnection",
|
||||
Description: "条件筛选图像列表",
|
||||
Fields: graphql.Fields{
|
||||
"list": &graphql.Field{Type: graphql.NewList(imageType), Description: "图像列表"},
|
||||
"total": &graphql.Field{Type: graphql.Int, Description: "图像总数"},
|
||||
},
|
||||
}),
|
||||
Args: graphql.FieldConfigArgument{
|
||||
"activity": &graphql.ArgumentConfig{Type: graphql.Boolean, Description: "按是否活动筛选图像"},
|
||||
"style": &graphql.ArgumentConfig{Type: graphql.String, Description: "按游戏风格筛选图像"},
|
||||
"device": &graphql.ArgumentConfig{Type: graphql.String, Description: "按游戏平台筛选图像"},
|
||||
"orientation": &graphql.ArgumentConfig{Type: graphql.String, Description: "按游戏版式筛选图像"},
|
||||
"era": &graphql.ArgumentConfig{Type: graphql.String, Description: "按游戏年份筛选图像"},
|
||||
"category_id": &graphql.ArgumentConfig{Type: graphql.String, Description: "按游戏类型筛选图像(支持多选)"},
|
||||
"tags": &graphql.ArgumentConfig{Type: graphql.String, Description: "按游戏标签筛选图像(支持多选)"},
|
||||
"images_desc": &graphql.ArgumentConfig{Type: graphql.String, Description: "按游戏归类筛选图像(支持多选)"},
|
||||
"article_tags": &graphql.ArgumentConfig{Type: graphql.String, Description: "按文章标签筛选图像(支持多选)"},
|
||||
"color": &graphql.ArgumentConfig{Type: graphql.String, Description: "按主色调筛选图像, 使用十六进制颜色代码(#FF1414)"},
|
||||
"explorer_id": &graphql.ArgumentConfig{Type: graphql.Int, Description: "筛选图像中指定收藏夹的"},
|
||||
"collect_id": &graphql.ArgumentConfig{Type: graphql.Int, Description: "筛选图像中指定收藏夹的"},
|
||||
"collect": &graphql.ArgumentConfig{Type: graphql.Int, Description: "筛选图像中指定用户收藏过的"},
|
||||
"praise": &graphql.ArgumentConfig{Type: graphql.Int, Description: "筛选图像中指定用户点赞过的"},
|
||||
"follower": &graphql.ArgumentConfig{Type: graphql.Int, Description: "获取指定ID用户的关注列表发布的图像"},
|
||||
"interest": &graphql.ArgumentConfig{Type: graphql.Int, Description: "获取指定ID用户的兴趣推荐图像"},
|
||||
"similar": &graphql.ArgumentConfig{Type: graphql.Int, Description: "获取与指定ID图像相似的图像"},
|
||||
"id": &graphql.ArgumentConfig{Type: graphql.Int, Description: "获取指定ID的图像"},
|
||||
"width": &graphql.ArgumentConfig{Type: graphql.Int, Description: "筛选图像中指定宽度的"},
|
||||
"height": &graphql.ArgumentConfig{Type: graphql.Int, Description: "筛选图像中指定高度的"},
|
||||
"comment_num": &graphql.ArgumentConfig{Type: graphql.Int, Description: "筛选图像中评论数等于指定值的"},
|
||||
"praise_count": &graphql.ArgumentConfig{Type: graphql.Int, Description: "筛选图像中点赞数等于指定值的"},
|
||||
"collect_count": &graphql.ArgumentConfig{Type: graphql.Int, Description: "筛选图像中收藏数等于指定值的"},
|
||||
"article_id": &graphql.ArgumentConfig{Type: graphql.Int, Description: "筛选图像中属于指定文章ID的"},
|
||||
"user_id": &graphql.ArgumentConfig{Type: graphql.Int, Description: "筛选图像中属于指定用户ID的"},
|
||||
"sort": &graphql.ArgumentConfig{Type: graphql.String, Description: "排序方法", DefaultValue: "id"},
|
||||
"content": &graphql.ArgumentConfig{Type: graphql.String, Description: "筛选图像中含有指定内容的"},
|
||||
"remark": &graphql.ArgumentConfig{Type: graphql.String, Description: "筛选图像中含有指定备注的"},
|
||||
"description": &graphql.ArgumentConfig{Type: graphql.String, Description: "筛选图像中含有指定描述的"},
|
||||
"rank": &graphql.ArgumentConfig{Type: graphql.String, Description: "筛选图像中含有指定排名的"},
|
||||
"text": &graphql.ArgumentConfig{Type: graphql.String, Description: "筛选图像中含有指定文字的"},
|
||||
"create_time": &graphql.ArgumentConfig{Type: graphql.String, Description: "筛选图像中创建时间等于指定值的"},
|
||||
"update_time": &graphql.ArgumentConfig{Type: graphql.String, Description: "筛选图像中更新时间等于指定值的"},
|
||||
"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: "翻页参数(傳回清單中指定遊標之前的元素)"},
|
||||
"order": &graphql.ArgumentConfig{Type: orderType, Description: "排序方向", DefaultValue: "ASC"},
|
||||
},
|
||||
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
||||
var total int
|
||||
var images []Image
|
||||
var query = goqu.Dialect("mysql").From("web_images")
|
||||
|
||||
// 参数映射
|
||||
var argFormat = []string{"id", "width", "height", "content", "remark", "description", "rank", "comment_num", "praise_count", "collect_count", "article_id", "user_id"}
|
||||
|
||||
// 筛选条件
|
||||
for _, format := range argFormat {
|
||||
if p.Args[format] != nil {
|
||||
fmt.Println(format, p.Args[format])
|
||||
query = query.Where(goqu.Ex{fmt.Sprintf("web_images.%s", format): p.Args[format]})
|
||||
}
|
||||
}
|
||||
|
||||
// 筛选:提取文字
|
||||
if p.Args["text"] != nil {
|
||||
resp, err := models.ZincSearch(map[string]interface{}{
|
||||
"query": map[string]interface{}{
|
||||
"bool": map[string]interface{}{
|
||||
"must": []map[string]interface{}{
|
||||
{
|
||||
"match_phrase": map[string]string{"text": p.Args["text"].(string)},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"_source": false,
|
||||
"sort": []string{"_score"},
|
||||
"size": 200000,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Println("ZincSearch 获取图像列表失败", err)
|
||||
return nil, err
|
||||
}
|
||||
var item []int
|
||||
for _, hit := range resp.Hits.Hits {
|
||||
num, _ := strconv.Atoi(hit.ID)
|
||||
item = append(item, num)
|
||||
}
|
||||
if len(item) == 0 {
|
||||
return map[string]interface{}{"list": []Image{}, "total": 0}, nil
|
||||
}
|
||||
query = query.Where(goqu.Ex{"web_images.id": goqu.Op{"in": item}})
|
||||
total = len(item)
|
||||
}
|
||||
|
||||
// 筛选:相似图像
|
||||
if p.Args["similar"] != nil {
|
||||
var item []int
|
||||
for _, id := range models.GetSimilarImagesIdList(p.Args["similar"].(int), 200) {
|
||||
item = append(item, int(id))
|
||||
}
|
||||
|
||||
if len(item) == 0 {
|
||||
return map[string]interface{}{"list": []Image{}, "total": 0}, nil
|
||||
}
|
||||
|
||||
query = query.Where(goqu.Ex{"web_images.id": goqu.Op{"in": item}}).Select("web_images.id", goqu.L(
|
||||
fmt.Sprintf("ROW_NUMBER() OVER(ORDER BY FIELD(%s, %s))", "web_images.id", regexp.MustCompile(`[\[\]]`).ReplaceAllString(strings.Join(strings.Fields(fmt.Sprint(item)), ", "), "")),
|
||||
).As("row_num"))
|
||||
total = len(item)
|
||||
|
||||
// 收集阅读行为
|
||||
if p.Context.Value("user_id") != nil && p.Args["after"] == nil {
|
||||
ctx := context.Background()
|
||||
user_id := p.Context.Value("user_id").(int)
|
||||
|
||||
feedbacks := []client.Feedback{{
|
||||
FeedbackType: "read",
|
||||
UserId: strconv.Itoa(user_id),
|
||||
ItemId: strconv.Itoa(p.Args["similar"].(int)),
|
||||
Timestamp: time.Now().Format("2006-01-02 15:04:05"),
|
||||
}}
|
||||
|
||||
if _, err := gorse.InsertFeedback(ctx, feedbacks); err != nil {
|
||||
fmt.Println("写入阅读记录失败", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 筛选:兴趣推荐
|
||||
if p.Args["interest"] != nil {
|
||||
fmt.Println("interest:", p.Args["interest"])
|
||||
user_id := p.Args["interest"].(int)
|
||||
fmt.Println("interest1:", user_id)
|
||||
list, err := GetRecommend(user_id, []string{})
|
||||
fmt.Println("interest2:", list, err)
|
||||
if err != nil {
|
||||
fmt.Println("GetRecommend 获取兴趣推荐失败", err)
|
||||
return map[string]interface{}{"list": []Image{}, "total": 0}, nil
|
||||
}
|
||||
if len(list) == 0 {
|
||||
return map[string]interface{}{"list": []Image{}, "total": 0}, nil
|
||||
}
|
||||
fmt.Println("Interest:", user_id, list)
|
||||
query = query.Where(goqu.Ex{"web_images.id": goqu.Op{"in": list}}).Select("web_images.id", goqu.L(
|
||||
fmt.Sprintf("ROW_NUMBER() OVER(ORDER BY FIELD(%s, %s))", "web_images.id", regexp.MustCompile(`[\[\]]`).ReplaceAllString(strings.Join(strings.Fields(fmt.Sprint(list)), ", "), "")),
|
||||
).As("row_num"))
|
||||
total = len(list)
|
||||
}
|
||||
|
||||
// 筛选:时间段
|
||||
applyTimeCondition := func(name string, str string) {
|
||||
parts := strings.Split(str, "-")
|
||||
query = query.Where(goqu.Ex{fmt.Sprintf("YEAR(%s)", name): parts[0]})
|
||||
|
||||
if len(parts) > 1 {
|
||||
query = query.Where(goqu.Ex{fmt.Sprintf("MONTH(%s)", name): parts[1]})
|
||||
}
|
||||
if len(parts) > 2 {
|
||||
query = query.Where(goqu.Ex{fmt.Sprintf("DAY(%s)", name): parts[2]})
|
||||
}
|
||||
}
|
||||
|
||||
// 数据库中筛选:创建时间段
|
||||
if p.Args["create_time"] != nil {
|
||||
applyTimeCondition("create_time", p.Args["create_time"].(string))
|
||||
}
|
||||
|
||||
// 数据库中筛选:更新时间段
|
||||
if p.Args["update_time"] != nil {
|
||||
applyTimeCondition("update_time", p.Args["update_time"].(string))
|
||||
}
|
||||
|
||||
// 数据库中筛选:按是否活动
|
||||
if activity, ok := p.Args["activity"].(bool); ok {
|
||||
query = query.Where(goqu.Ex{"web_images.activity": map[bool]string{true: "1", false: "0"}[activity]})
|
||||
}
|
||||
|
||||
// 数据库中筛选:按游戏标签
|
||||
if p.Args["tags"] != nil {
|
||||
tags := strings.Split(strings.ReplaceAll(p.Args["tags"].(string), " ", ""), ",")
|
||||
var conditions []string
|
||||
for _, tag := range tags {
|
||||
conditions = append(conditions, fmt.Sprintf("FIND_IN_SET('%s', web_article.tags)", tag))
|
||||
}
|
||||
query = query.Join(goqu.T("web_article"), goqu.On(
|
||||
goqu.I("web_images.article_id").Eq(goqu.I("web_article.id")),
|
||||
goqu.L(strings.Join(conditions, " AND ")),
|
||||
))
|
||||
}
|
||||
|
||||
// 数据库中筛选:按游戏分类
|
||||
if p.Args["images_desc"] != nil {
|
||||
tags := strings.Split(strings.ReplaceAll(p.Args["images_desc"].(string), " ", ""), ",")
|
||||
for _, tag := range tags {
|
||||
//query = query.Where(goqu.L("MATCH(web_images.images_desc) AGAINST (? IN NATURAL LANGUAGE MODE)", tag))
|
||||
query = query.Where(goqu.L(fmt.Sprintf("web_images.images_desc LIKE '%%%s%%'", tag)))
|
||||
}
|
||||
}
|
||||
|
||||
// 数据库中筛选:喜欢的截图
|
||||
if p.Args["praise"] != nil {
|
||||
query = query.Join(goqu.T("web_praise"), goqu.On(
|
||||
goqu.I("web_images.id").Eq(goqu.I("web_praise.praise_id")),
|
||||
goqu.I("web_praise.user_id").Eq(p.Args["praise"]),
|
||||
goqu.I("web_praise.type").Eq(4),
|
||||
))
|
||||
}
|
||||
|
||||
var collect_params []goqu.Expression = []goqu.Expression{
|
||||
goqu.I("web_images.id").Eq(goqu.I("web_collect.image_id")),
|
||||
}
|
||||
|
||||
// 数据库中筛选:用户收藏的截图
|
||||
if p.Args["collect"] != nil {
|
||||
collect_params = append(collect_params, goqu.I("web_collect.user_id").Eq(p.Args["collect"]))
|
||||
}
|
||||
|
||||
// 数据库中筛选:收藏夹中的截图
|
||||
if p.Args["collect_id"] != nil {
|
||||
collect_params = append(collect_params, goqu.I("web_collect.collect_id").Eq(p.Args["collect_id"]))
|
||||
}
|
||||
|
||||
// 数据库中筛选:收藏夹中的截图
|
||||
if p.Args["explorer_id"] != nil {
|
||||
collect_params = append(collect_params, goqu.I("web_collect.explorer_id").Eq(p.Args["explorer_id"]))
|
||||
}
|
||||
|
||||
if len(collect_params) > 1 {
|
||||
query = query.Join(goqu.T("web_collect"), goqu.On(collect_params...))
|
||||
}
|
||||
|
||||
var conditions []goqu.Expression = []goqu.Expression{
|
||||
goqu.I("web_images.article_id").Eq(goqu.I("web_article.id")),
|
||||
}
|
||||
|
||||
// 按游戏风格筛选图像
|
||||
if p.Args["style"] != nil {
|
||||
conditions = append(conditions, goqu.I("web_article.style").Eq(p.Args["style"]))
|
||||
}
|
||||
|
||||
// 按游戏平台筛选图像
|
||||
if p.Args["device"] != nil {
|
||||
conditions = append(conditions, goqu.I("web_article.device").Eq(p.Args["device"]))
|
||||
}
|
||||
|
||||
// 按游戏版式筛选图像
|
||||
if p.Args["orientation"] != nil {
|
||||
conditions = append(conditions, goqu.I("web_article.orientation").Eq(p.Args["orientation"]))
|
||||
}
|
||||
|
||||
//// 数据库中筛选:按游戏分类
|
||||
//if p.Args["images_desc"] != nil {
|
||||
// tags := strings.Split(strings.ReplaceAll(p.Args["images_desc"].(string), " ", ""), ",")
|
||||
// for _, tag := range tags {
|
||||
// //query = query.Where(goqu.L("MATCH(web_images.images_desc) AGAINST (? IN NATURAL LANGUAGE MODE)", tag))
|
||||
// //query = query.Where(goqu.L(fmt.Sprintf("web_images.images_desc LIKE '%%%s%%'", tag)))
|
||||
// conditions = append(conditions, goqu.L(fmt.Sprintf("web_article.images_desc LIKE '%%%s%%'", tag)))
|
||||
// }
|
||||
//}
|
||||
|
||||
// 按游戏年份筛选图像
|
||||
if p.Args["era"] != nil {
|
||||
conditions = append(conditions, goqu.I("web_article.era").Eq(p.Args["era"]))
|
||||
}
|
||||
|
||||
// 按游戏类型筛选图像(逗号分割且去除空格)
|
||||
if p.Args["category_id"] != nil {
|
||||
category_ids := strings.Split(strings.ReplaceAll(p.Args["category_id"].(string), " ", ""), ",")
|
||||
conditions = append(conditions, goqu.I("web_article.category_id").In(category_ids))
|
||||
}
|
||||
|
||||
// 按游戏标签筛选图像
|
||||
if p.Args["article_tags"] != nil {
|
||||
tags := strings.Split(strings.ReplaceAll(p.Args["article_tags"].(string), " ", ""), ", ")
|
||||
for _, tag := range tags {
|
||||
//conditions = append(conditions, goqu.L("MATCH(web_article.tags) AGAINST (? IN NATURAL LANGUAGE MODE)", tag))
|
||||
conditions = append(conditions, goqu.L("FIND_IN_SET(?, web_article.tags)", tag))
|
||||
}
|
||||
}
|
||||
|
||||
if len(conditions) > 1 {
|
||||
query = query.Join(goqu.T("web_article"), goqu.On(conditions...))
|
||||
}
|
||||
|
||||
// 数据库中筛选:按关注列表
|
||||
if p.Args["follower"] != nil {
|
||||
query = query.Join(goqu.T("web_fans"), goqu.On(
|
||||
goqu.I("web_images.user_id").Eq(goqu.I("web_fans.blogger_id")),
|
||||
goqu.I("web_fans.follower_id").Eq(p.Args["follower"]),
|
||||
))
|
||||
}
|
||||
|
||||
// 数据库中筛选: 按图像主色调颜色筛选
|
||||
if p.Args["color"] != nil {
|
||||
// hexToRGB 将十六进制颜色转换为 RGB
|
||||
hexToRGB := func(hex string) (int, int, int, error) {
|
||||
// 去掉 # 号
|
||||
if strings.HasPrefix(hex, "#") {
|
||||
hex = hex[1:]
|
||||
}
|
||||
// 检查是否是有效的十六进制颜色
|
||||
if len(hex) != 6 {
|
||||
return 0, 0, 0, fmt.Errorf("invalid hex color")
|
||||
}
|
||||
// 解析红色、绿色、蓝色通道的十六进制值
|
||||
r, err := strconv.ParseInt(hex[:2], 16, 0)
|
||||
if err != nil {
|
||||
return 0, 0, 0, err
|
||||
}
|
||||
g, err := strconv.ParseInt(hex[2:4], 16, 0)
|
||||
if err != nil {
|
||||
return 0, 0, 0, err
|
||||
}
|
||||
b, err := strconv.ParseInt(hex[4:], 16, 0)
|
||||
if err != nil {
|
||||
return 0, 0, 0, err
|
||||
}
|
||||
return int(r), int(g), int(b), nil
|
||||
}
|
||||
// 逗号分割且去除空格
|
||||
colors := strings.Split(strings.ReplaceAll(p.Args["color"].(string), " ", ""), ",")
|
||||
for index, color := range colors {
|
||||
|
||||
var precision int = 10
|
||||
if strings.Contains(color, ":") {
|
||||
re := regexp.MustCompile(`^#([0-9a-fA-F]{6}):(\d+)$`)
|
||||
matches := re.FindStringSubmatch(color)
|
||||
num, err := strconv.Atoi(matches[2])
|
||||
if err != nil {
|
||||
fmt.Println("数字精度转换失败:", err)
|
||||
return nil, err
|
||||
}
|
||||
color = "#" + matches[1]
|
||||
precision = num
|
||||
}
|
||||
|
||||
r, g, b, err := hexToRGB(color)
|
||||
if err != nil {
|
||||
fmt.Println("hexToRGB", index, err)
|
||||
return nil, err
|
||||
}
|
||||
fmt.Println(color, r, g, b, precision)
|
||||
|
||||
query1, _, _ := goqu.Dialect("mysql").From("web_images").Select("id").Where(
|
||||
goqu.And(
|
||||
goqu.L(fmt.Sprintf("color_%d_r", 0)).Gt(r-precision),
|
||||
goqu.L(fmt.Sprintf("color_%d_r", 0)).Lt(r+precision),
|
||||
goqu.L(fmt.Sprintf("color_%d_g", 0)).Gt(g-precision),
|
||||
goqu.L(fmt.Sprintf("color_%d_g", 0)).Lt(g+precision),
|
||||
goqu.L(fmt.Sprintf("color_%d_b", 0)).Gt(b-precision),
|
||||
goqu.L(fmt.Sprintf("color_%d_b", 0)).Lt(b+precision),
|
||||
),
|
||||
).ToSQL()
|
||||
|
||||
query2, _, _ := goqu.Dialect("mysql").From("web_images").Select("id").Where(
|
||||
goqu.And(
|
||||
goqu.L(fmt.Sprintf("color_%d_r", 1)).Gt(r-precision),
|
||||
goqu.L(fmt.Sprintf("color_%d_r", 1)).Lt(r+precision),
|
||||
goqu.L(fmt.Sprintf("color_%d_g", 1)).Gt(g-precision),
|
||||
goqu.L(fmt.Sprintf("color_%d_g", 1)).Lt(g+precision),
|
||||
goqu.L(fmt.Sprintf("color_%d_b", 1)).Gt(b-precision),
|
||||
goqu.L(fmt.Sprintf("color_%d_b", 1)).Lt(b+precision),
|
||||
),
|
||||
).ToSQL()
|
||||
|
||||
query = query.Join(
|
||||
goqu.L(fmt.Sprintf("(%s UNION %s) AS w", query1, query2)),
|
||||
goqu.On(goqu.Ex{"web_images.id": goqu.I("w.id")}),
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有外部排序则使用指定排序(正则sort只能是字母数字下划线)
|
||||
if p.Args["similar"] == nil && p.Args["interest"] == nil && p.Args["praise"] == nil {
|
||||
// 如果查询了 total 字段
|
||||
if existField(p.Info.FieldASTs[0].SelectionSet.Selections, "total") {
|
||||
sql, _, _ := query.ToSQL()
|
||||
sql = strings.Replace(sql, "SELECT *", "SELECT COUNT(*)", 1)
|
||||
if err := db.Raw(sql).Scan(&total).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
query = query.Select("web_images.id", goqu.L(fmt.Sprintf(
|
||||
"ROW_NUMBER() OVER(ORDER BY web_images.%s %s)",
|
||||
regexp.MustCompile(`[^a-zA-Z0-9_]`).ReplaceAllString(p.Args["sort"].(string), ""),
|
||||
p.Args["order"].(string),
|
||||
)).As("row_num"))
|
||||
}
|
||||
|
||||
// 图像按点赞顺序排序 p.Args["sort"].(string) == "praise_time"
|
||||
if p.Args["praise"] != nil {
|
||||
query = query.Select("web_images.id", goqu.L(
|
||||
fmt.Sprintf("ROW_NUMBER() OVER(ORDER BY web_praise.%s %s)", "id", p.Args["order"].(string)),
|
||||
).As("row_num"))
|
||||
}
|
||||
|
||||
// 取所有数据的前N条(筛取属于游戏的且非封面的图像)
|
||||
sql, _, _ := query.Where(goqu.Ex{"article_category_top_id": 22}).Where(goqu.Ex{"web_images.type": 0}).ToSQL()
|
||||
|
||||
// 遊標截取篩選結果集的前N条
|
||||
var cursor string
|
||||
if p.Args["after"] != nil {
|
||||
cursor = fmt.Sprintf(`WHERE row_num > (SELECT row_num FROM RankedArticles WHERE RankedArticles.id = %d)`, p.Args["after"].(int))
|
||||
}
|
||||
|
||||
var limit int = 10
|
||||
if p.Args["first"] != nil {
|
||||
limit = p.Args["first"].(int)
|
||||
} else if p.Args["last"] != nil {
|
||||
limit = p.Args["last"].(int)
|
||||
}
|
||||
|
||||
sql = fmt.Sprintf(`
|
||||
WITH RankedArticles AS (%s)
|
||||
SELECT web_images.* FROM web_images INNER JOIN(
|
||||
SELECT id, row_num FROM RankedArticles %s
|
||||
) AS LimitedRanked ON LimitedRanked.id = web_images.id
|
||||
ORDER BY LimitedRanked.row_num ASC LIMIT %d
|
||||
`, sql, cursor, limit)
|
||||
//fmt.Println(sql)
|
||||
|
||||
if err := db.Raw(sql).Scan(&images).Error; err != nil {
|
||||
fmt.Println("获取图像列表失败", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ids []int
|
||||
for _, item := range images {
|
||||
ids = append(ids, item.ID)
|
||||
}
|
||||
|
||||
var find = db.Where("web_images.id IN ?", ids).Select("*", "CASE WHEN activity = '1' THEN TRUE ELSE FALSE END")
|
||||
for _, item := range LoadItem(p.Info.FieldASTs[0].SelectionSet.Selections) {
|
||||
find = find.Preload(item)
|
||||
}
|
||||
|
||||
if len(ids) == 0 {
|
||||
return map[string]interface{}{"list": []Image{}, "total": 0}, nil
|
||||
}
|
||||
|
||||
orderClause := fmt.Sprintf("FIELD(id, %s)", regexp.MustCompile(`[\[\]]`).ReplaceAllString(strings.Join(strings.Fields(fmt.Sprint(ids)), ","), ""))
|
||||
if err := find.Order(orderClause).Find(&images).Error; err != nil {
|
||||
fmt.Println("获取图像列表失败", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var items = ListItem(p.Info.FieldASTs[0].SelectionSet.Selections)
|
||||
if funk.Contains(items, "views") {
|
||||
type ApiResponse struct {
|
||||
ID int `json:"id"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
// 0. 收集要查询的 ID
|
||||
var ids []int
|
||||
for x := range images {
|
||||
ids = append(ids, images[x].ID)
|
||||
}
|
||||
idx := strings.Trim(strings.Replace(fmt.Sprint(ids), " ", ",", -1), "[]")
|
||||
|
||||
// 1. 发送 GET 请求
|
||||
resp, err := http.Get("http://localhost:6005/api/get_views/截图?ids=" + idx)
|
||||
if err != nil {
|
||||
fmt.Println("Error making GET request:", err)
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 2. 检查 HTTP 状态码
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
fmt.Printf("Request failed with status code: %d\n", resp.StatusCode)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 3. 读取响应体
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
fmt.Println("Error reading response body:", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 4. 解析 JSON 数据到结构体
|
||||
var data []ApiResponse
|
||||
err = json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
fmt.Println("Error unmarshalling JSON:", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 5. 赋值到数据集
|
||||
for _, item := range data {
|
||||
for i := range images {
|
||||
if images[i].ID == item.ID {
|
||||
images[i].Views = item.Count
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"list": images,
|
||||
"total": total,
|
||||
}, nil
|
||||
},
|
||||
}
|
137
api/kMeans.go
Normal file
137
api/kMeans.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"math"
|
||||
"math/rand"
|
||||
"os"
|
||||
)
|
||||
|
||||
// 定义一个结构来表示 RGB 颜色
|
||||
type RGB struct {
|
||||
R, G, B int
|
||||
}
|
||||
|
||||
// 根据每个聚类中心包含的颜色数量进行排序
|
||||
type cluster struct {
|
||||
center RGB
|
||||
count int
|
||||
}
|
||||
|
||||
// 计算两个 RGB 颜色的欧氏距离
|
||||
func distance(c1, c2 RGB) float64 {
|
||||
return math.Sqrt(float64((c1.R-c2.R)*(c1.R-c2.R) + (c1.G-c2.G)*(c1.G-c2.G) + (c1.B-c2.B)*(c1.B-c2.B)))
|
||||
}
|
||||
|
||||
// 对图像颜色进行 KMeans 聚类
|
||||
func KMeans(colors []RGB, k int) ([]RGB, []int) {
|
||||
// 随机选择初始中心
|
||||
var centers []RGB
|
||||
for i := 0; i < k; i++ {
|
||||
centers = append(centers, colors[rand.Intn(len(colors))])
|
||||
}
|
||||
|
||||
// 聚类算法
|
||||
labels := make([]int, len(colors))
|
||||
for iter := 0; iter < 10; iter++ { // 设置最大迭代次数为 10
|
||||
// 为每个颜色分配最近的聚类中心
|
||||
for i, color := range colors {
|
||||
minDist := distance(color, centers[0])
|
||||
labels[i] = 0
|
||||
for j := 1; j < k; j++ {
|
||||
dist := distance(color, centers[j])
|
||||
if dist < minDist {
|
||||
minDist = dist
|
||||
labels[i] = j
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 计算新的聚类中心
|
||||
newCenters := make([]RGB, k)
|
||||
counts := make([]int, k)
|
||||
for i, label := range labels {
|
||||
newCenters[label].R += colors[i].R
|
||||
newCenters[label].G += colors[i].G
|
||||
newCenters[label].B += colors[i].B
|
||||
counts[label]++
|
||||
}
|
||||
|
||||
// 计算平均值作为新的中心
|
||||
for i := 0; i < k; i++ {
|
||||
if counts[i] > 0 {
|
||||
newCenters[i].R /= counts[i]
|
||||
newCenters[i].G /= counts[i]
|
||||
newCenters[i].B /= counts[i]
|
||||
}
|
||||
}
|
||||
|
||||
// 如果中心没有变化,停止迭代
|
||||
changed := false
|
||||
for i := 0; i < k; i++ {
|
||||
if newCenters[i] != centers[i] {
|
||||
changed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !changed {
|
||||
break
|
||||
}
|
||||
centers = newCenters
|
||||
}
|
||||
|
||||
return centers, labels
|
||||
}
|
||||
|
||||
// 加载图像并提取颜色
|
||||
func extractColors(img image.Image) (colors []RGB) {
|
||||
bounds := img.Bounds()
|
||||
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
||||
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
||||
r, g, b, _ := img.At(x, y).RGBA()
|
||||
colors = append(colors, RGB{int(r >> 8), int(g >> 8), int(b >> 8)})
|
||||
}
|
||||
}
|
||||
return colors
|
||||
}
|
||||
|
||||
// 将聚类后的颜色应用到图像上
|
||||
func recolorImage(img image.Image, centers []RGB, labels []int) image.Image {
|
||||
bounds := img.Bounds()
|
||||
newImage := image.NewRGBA(bounds)
|
||||
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
||||
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
||||
label := labels[(y-bounds.Min.Y)*bounds.Max.X+(x-bounds.Min.X)]
|
||||
// 将 RGB 颜色转换为 RGBA 类型
|
||||
newColor := color.RGBA{
|
||||
R: uint8(centers[label].R),
|
||||
G: uint8(centers[label].G),
|
||||
B: uint8(centers[label].B),
|
||||
A: 255, // 不透明
|
||||
}
|
||||
// 设置新图像像素颜色
|
||||
newImage.Set(x, y, newColor)
|
||||
}
|
||||
}
|
||||
return newImage
|
||||
}
|
||||
|
||||
func ImageToColors(str string) (colors []RGB, err error) {
|
||||
// 打开图像文件
|
||||
file, err := os.Open(str)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// 解码 JPEG 图像
|
||||
img, _, err := image.Decode(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 提取图像颜色
|
||||
centers, _ := KMeans(extractColors(img), 8)
|
||||
return centers, nil
|
||||
}
|
71
api/search.go
Normal file
71
api/search.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/graphql-go/graphql"
|
||||
)
|
||||
|
||||
type Search struct {
|
||||
Name string `json:"text"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
var SearchItem = graphql.NewObject(graphql.ObjectConfig{
|
||||
Name: "Search",
|
||||
Description: "搜索",
|
||||
Fields: graphql.Fields{
|
||||
"name": &graphql.Field{Type: graphql.String, Description: "搜索词"},
|
||||
},
|
||||
})
|
||||
|
||||
var SearchItems = &graphql.Field{
|
||||
Name: "Searchs",
|
||||
Description: "搜索词列表",
|
||||
Type: graphql.NewObject(graphql.ObjectConfig{
|
||||
Name: "SearchConnection",
|
||||
Description: "搜索词列表",
|
||||
Fields: graphql.Fields{
|
||||
"list": &graphql.Field{Type: graphql.NewList(SearchItem), Description: "搜索词列表"},
|
||||
},
|
||||
}),
|
||||
Args: graphql.FieldConfigArgument{
|
||||
"name": &graphql.ArgumentConfig{Type: graphql.String, Description: "按指定字符筛选"},
|
||||
"first": &graphql.ArgumentConfig{Type: graphql.Int, Description: "翻页参数(傳回清單中的前n個元素)", DefaultValue: 10},
|
||||
},
|
||||
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
||||
var searchs []Search
|
||||
|
||||
// 发送 GET 请求
|
||||
resp, err := http.Get(fmt.Sprintf("http://localhost:6005/api/get_search/hot?pagesize=%d", p.Args["first"]))
|
||||
if err != nil {
|
||||
log.Println("Failed to fetch data: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 读取响应体
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Println("Failed to read response body: %v", err)
|
||||
}
|
||||
|
||||
// 检查 HTTP 状态码
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
log.Println("HTTP request failed with status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// 解码 JSON 数据
|
||||
err = json.Unmarshal(body, &searchs)
|
||||
if err != nil {
|
||||
log.Println("Failed to parse JSON: %v", err)
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"list": searchs,
|
||||
}, err
|
||||
},
|
||||
}
|
159
api/struct.go
159
api/struct.go
@@ -1,159 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type IDS []int
|
||||
|
||||
// 合并为以逗号分隔的字符串
|
||||
func (ids IDS) ToString() (str string) {
|
||||
return strings.Trim(strings.Join(strings.Fields(fmt.Sprint(ids)), ","), "[]")
|
||||
}
|
||||
|
||||
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)
|
||||
for _, image := range *images {
|
||||
article_ids[image.ArticleID] = true
|
||||
}
|
||||
for id := range article_ids {
|
||||
uniqueIds = append(uniqueIds, id)
|
||||
}
|
||||
return uniqueIds
|
||||
}
|
||||
|
||||
// 取到所有的用户ID, 去除重复
|
||||
func (images *ImageList) ToAllUserID() (uniqueIds IDS) {
|
||||
user_ids := make(map[int]bool)
|
||||
for _, image := range *images {
|
||||
user_ids[image.UserID] = true
|
||||
}
|
||||
for id := range user_ids {
|
||||
uniqueIds = append(uniqueIds, id)
|
||||
}
|
||||
return uniqueIds
|
||||
}
|
||||
|
||||
// 为每个图像设置用户信息
|
||||
func (images *ImageList) SetUser(userList []User) {
|
||||
for i, image := range *images {
|
||||
for _, user := range userList {
|
||||
if image.UserID == user.ID {
|
||||
(*images)[i].User = user
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 为每个图像设置文章信息
|
||||
func (images *ImageList) SetArticle(articleList []Article) {
|
||||
for i, image := range *images {
|
||||
for _, article := range articleList {
|
||||
if image.ArticleID == article.ID {
|
||||
(*images)[i].Article = article
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Image struct {
|
||||
ID int `json:"id" db:"id"`
|
||||
Width int `json:"width" db:"width"`
|
||||
Height int `json:"height" db:"height"`
|
||||
Content string `json:"content" db:"content"`
|
||||
Remark string `json:"remark" db:"remark"`
|
||||
Description string `json:"description" db:"description"`
|
||||
Tags string `json:"tags" db:"tags"`
|
||||
Rank string `json:"rank" db:"rank"`
|
||||
CommentNum int `json:"comment_num" db:"comment_num"`
|
||||
PraiseCount int `json:"praise_count" db:"praise_count"`
|
||||
CollectCount int `json:"collect_count" db:"collect_count"`
|
||||
ArticleID int `json:"article_id" db:"article_id"`
|
||||
UserID int `json:"user_id" db:"user_id"`
|
||||
CreateTime time.Time `json:"create_time" db:"create_time"`
|
||||
UpdateTime time.Time `json:"update_time" db:"update_time"`
|
||||
Text TextList `json:"text" db:"text"`
|
||||
User User `json:"user" db:"-"`
|
||||
Article Article `json:"article" db:"-"`
|
||||
}
|
||||
|
||||
type TextList []struct {
|
||||
Text string `json:"text"`
|
||||
Confidence float64 `json:"confidence"`
|
||||
Coordinate [][]float64 `json:"coordinate"`
|
||||
}
|
||||
|
||||
func (a *TextList) Scan(value interface{}) error {
|
||||
return json.Unmarshal(value.([]byte), a)
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID int `json:"id" db:"id"`
|
||||
UserName *string `json:"user_name" db:"user_name"`
|
||||
Avatar *string `json:"avatar" db:"avatar"`
|
||||
Rank *string `json:"rank" db:"rank"`
|
||||
CreateTime time.Time `json:"create_time" db:"create_time"`
|
||||
UpdateTime time.Time `json:"update_time" db:"update_time"`
|
||||
}
|
||||
|
||||
type Article struct {
|
||||
ID int `json:"id" db:"id"`
|
||||
Title string `json:"title" db:"title"`
|
||||
Tags string `json:"tags" db:"tags"`
|
||||
CreateTime time.Time `json:"create_time" db:"create_time"`
|
||||
UpdateTime time.Time `json:"update_time" db:"update_time"`
|
||||
}
|
||||
|
||||
type Category struct {
|
||||
ID int `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Keyword string `json:"keyword"`
|
||||
ParentID int `json:"parent_id"`
|
||||
Status int `json:"status"`
|
||||
Content string `json:"content"`
|
||||
Sort int `json:"sort"`
|
||||
Image string `json:"image"`
|
||||
ImageNum int `json:"image_num"`
|
||||
ArticleNum int `json:"article_num"`
|
||||
CreateTime time.Time `json:"create_time"`
|
||||
UpdateTime time.Time `json:"update_time"`
|
||||
}
|
||||
|
||||
// TableName 方法用于自定义表名
|
||||
func (Category) TableName() string {
|
||||
return "web_article_category"
|
||||
}
|
||||
|
||||
// 输入配置
|
||||
type ConfigMysql struct {
|
||||
Host string
|
||||
Port int
|
||||
Database string
|
||||
UserName string
|
||||
Password string
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Mysql ConfigMysql
|
||||
}
|
93
api/user.go
Normal file
93
api/user.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/graphql-go/graphql"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID int `json:"id" db:"id" gorm:"primaryKey"`
|
||||
UserName string `json:"user_name" db:"user_name"`
|
||||
Avatar string `json:"avatar" db:"avatar"`
|
||||
Rank string `json:"rank" db:"rank"`
|
||||
Price int `json:"price" db:"price"`
|
||||
CreateTime time.Time `json:"create_time" db:"create_time"`
|
||||
UpdateTime time.Time `json:"update_time" db:"update_time"`
|
||||
}
|
||||
|
||||
func (User) TableName() string {
|
||||
return "web_member"
|
||||
}
|
||||
|
||||
var userType = graphql.NewObject(graphql.ObjectConfig{
|
||||
Name: "user",
|
||||
Description: "用户信息",
|
||||
Fields: graphql.Fields{
|
||||
"id": &graphql.Field{Type: graphql.Int, Description: "用户ID"},
|
||||
"user_name": &graphql.Field{Type: graphql.String, Description: "用户名"},
|
||||
"avatar": &graphql.Field{Type: graphql.String, Description: "用户头像"},
|
||||
"rank": &graphql.Field{Type: graphql.String, Description: "用户等级"},
|
||||
"price": &graphql.Field{Type: graphql.Int, Description: "用户金币"},
|
||||
"create_time": &graphql.Field{Type: graphql.DateTime, Description: "用户创建时间"},
|
||||
"update_time": &graphql.Field{Type: graphql.DateTime, Description: "用户更新时间"},
|
||||
},
|
||||
})
|
||||
|
||||
var UserItems = &graphql.Field{
|
||||
Name: "users",
|
||||
Description: "用户列表",
|
||||
Type: graphql.NewObject(graphql.ObjectConfig{
|
||||
Name: "UserConnection",
|
||||
Description: "条件筛选用户列表",
|
||||
Fields: graphql.Fields{
|
||||
"list": &graphql.Field{Type: graphql.NewList(userType), Description: "用户列表"},
|
||||
"total": &graphql.Field{Type: graphql.Int, Description: "用户总数"},
|
||||
},
|
||||
}),
|
||||
Args: graphql.FieldConfigArgument{
|
||||
"id": &graphql.ArgumentConfig{Type: graphql.Int, Description: "筛选用户中指定ID的"},
|
||||
"user_name": &graphql.ArgumentConfig{Type: graphql.String, Description: "筛选用户中含有指定用户名的"},
|
||||
"create_time": &graphql.ArgumentConfig{Type: graphql.DateTime, Description: "筛选用户中创建时间等于指定值的"},
|
||||
"update_time": &graphql.ArgumentConfig{Type: graphql.DateTime, Description: "筛选用户中更新时间等于指定值的"},
|
||||
"first": &graphql.ArgumentConfig{Type: graphql.Int, Description: "翻页参数(傳回清單中的前n個元素)"},
|
||||
"last": &graphql.ArgumentConfig{Type: graphql.Int, Description: "翻页参数(傳回清單中的最後n個元素)"},
|
||||
"after": &graphql.ArgumentConfig{Type: graphql.String, Description: "翻页参数(傳回清單中指定遊標之後的元素)"},
|
||||
"before": &graphql.ArgumentConfig{Type: graphql.String, Description: "翻页参数(傳回清單中指定遊標之前的元素)"},
|
||||
},
|
||||
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
||||
//var where []string
|
||||
//if p.Args["id"] != nil {
|
||||
// where = append(where, fmt.Sprintf("id=%d", p.Args["id"]))
|
||||
//}
|
||||
//if p.Args["user_name"] != nil {
|
||||
// where = append(where, fmt.Sprintf("user_name='%s'", p.Args["user_name"]))
|
||||
//}
|
||||
//// 筛选条件
|
||||
//where_str := strings.Join(where, " AND ")
|
||||
//if where_str != "" {
|
||||
// where_str = "WHERE " + where_str
|
||||
//}
|
||||
//var query strings.Builder
|
||||
var users []User
|
||||
var total int
|
||||
//fields := strings.Join(get_fields(p.Info.FieldASTs[0].SelectionSet.Selections), ",")
|
||||
//query.WriteString(fmt.Sprintf("SELECT %s FROM web_member %s LIMIT %d OFFSET %d", fields, where_str, 10, 0))
|
||||
//if err := connection.Select(&users, query.String()); err != nil {
|
||||
// fmt.Println("获取用户列表失败", err)
|
||||
// return nil, err
|
||||
//}
|
||||
//if len(users) > 0 {
|
||||
// query.Reset()
|
||||
// query.WriteString(fmt.Sprintf("SELECT COUNT(*) FROM web_member %s", where_str))
|
||||
// if err := connection.Get(&total, query.String()); err != nil {
|
||||
// fmt.Println("获取用户总数失败", err)
|
||||
// return nil, err
|
||||
// }
|
||||
//}
|
||||
return map[string]interface{}{
|
||||
"list": users,
|
||||
"total": total,
|
||||
}, nil
|
||||
},
|
||||
}
|
279
api/work.go
Normal file
279
api/work.go
Normal file
@@ -0,0 +1,279 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/doug-martin/goqu/v9"
|
||||
"github.com/graphql-go/graphql"
|
||||
"github.com/thoas/go-funk"
|
||||
)
|
||||
|
||||
type Work struct {
|
||||
ID int `json:"id" gorm:"primaryKey"`
|
||||
Title string `json:"title"`
|
||||
Orientation string `json:"orientation"`
|
||||
Device string `json:"device"`
|
||||
Era string `json:"era"`
|
||||
Tags string `json:"tags"`
|
||||
UserId int `json:"user_id"`
|
||||
Content string `json:"content"`
|
||||
Image string `json:"image"`
|
||||
Images string `json:"images"`
|
||||
User User `json:"user" gorm:"foreignKey:UserId"`
|
||||
Collect bool `json:"collect" gorm:"column:is_collect"`
|
||||
Praise bool `json:"praise" gorm:"column:is_praise"`
|
||||
CollectCount int `json:"collect_count" gorm:"column:collect_num"`
|
||||
PraiseCount int `json:"praise_count" gorm:"column:praise"`
|
||||
ImageCount int `json:"image_count" gorm:"column:total_image_count"`
|
||||
CreateTime time.Time `json:"create_time"`
|
||||
UpdateTime time.Time `json:"update_time"`
|
||||
Emoji1 int `json:"emoji1"`
|
||||
Emoji2 int `json:"emoji2"`
|
||||
Emoji3 int `json:"emoji3"`
|
||||
Emoji4 int `json:"emoji4"`
|
||||
Emoji5 int `json:"emoji5"`
|
||||
Views int `json:"views"`
|
||||
}
|
||||
|
||||
func (Work) TableName() string {
|
||||
return "web_article"
|
||||
}
|
||||
|
||||
var workType = graphql.NewObject(graphql.ObjectConfig{
|
||||
Name: "Work",
|
||||
Description: "作品",
|
||||
Fields: graphql.Fields{
|
||||
"id": &graphql.Field{Type: graphql.Int, Description: "作品ID"},
|
||||
"title": &graphql.Field{Type: graphql.String, Description: "作品标题"},
|
||||
"tags": &graphql.Field{Type: graphql.String, Description: "作品标签"},
|
||||
"user": &graphql.Field{Type: userType, Description: "所属用户"},
|
||||
"create_time": &graphql.Field{Type: graphql.DateTime, Description: "作品创建时间"},
|
||||
"update_time": &graphql.Field{Type: graphql.DateTime, Description: "作品更新时间"},
|
||||
"praise": &graphql.Field{Type: graphql.Boolean, Description: "当前用户是否点赞"},
|
||||
"collect": &graphql.Field{Type: graphql.Boolean, Description: "当前用户是否收藏"},
|
||||
"praise_count": &graphql.Field{Type: graphql.Int, Description: "点赞数"},
|
||||
"collect_count": &graphql.Field{Type: graphql.Int, Description: "收藏数"},
|
||||
"image_count": &graphql.Field{Type: graphql.Int, Description: "图片数"},
|
||||
"emoji1": &graphql.Field{Type: graphql.Int, Description: "表情1数量"},
|
||||
"emoji2": &graphql.Field{Type: graphql.Int, Description: "表情2数量"},
|
||||
"emoji3": &graphql.Field{Type: graphql.Int, Description: "表情3数量"},
|
||||
"emoji4": &graphql.Field{Type: graphql.Int, Description: "表情4数量"},
|
||||
"emoji5": &graphql.Field{Type: graphql.Int, Description: "表情5数量"},
|
||||
"views": &graphql.Field{Type: graphql.Int, Description: "浏览量"},
|
||||
},
|
||||
})
|
||||
|
||||
var WorkItems = &graphql.Field{
|
||||
Name: "works",
|
||||
Description: "作品列表",
|
||||
Type: graphql.NewObject(graphql.ObjectConfig{
|
||||
Name: "WorkConnection",
|
||||
Description: "条件筛选作品列表",
|
||||
Fields: graphql.Fields{
|
||||
"list": &graphql.Field{Type: graphql.NewList(workType), Description: "作品列表"},
|
||||
//"total": &graphql.Field{Type: graphql.Int, Description: "作品总数"},
|
||||
},
|
||||
}),
|
||||
Args: graphql.FieldConfigArgument{
|
||||
"id": &graphql.ArgumentConfig{Type: graphql.Int, Description: "筛选作品中指定ID的"},
|
||||
"title": &graphql.ArgumentConfig{Type: graphql.String, Description: "筛选作品中含有指定标题的"},
|
||||
"tags": &graphql.ArgumentConfig{Type: graphql.String, Description: "筛选作品中含有指定标签的"},
|
||||
"create_time": &graphql.ArgumentConfig{Type: graphql.DateTime, Description: "筛选作品中创建时间等于指定值的"},
|
||||
"update_time": &graphql.ArgumentConfig{Type: graphql.DateTime, Description: "筛选作品中更新时间等于指定值的"},
|
||||
"sort": &graphql.ArgumentConfig{Type: graphql.String, Description: "按指定字段排序游戏", DefaultValue: "id"},
|
||||
"order": &graphql.ArgumentConfig{Type: orderType, Description: "排序类型(升序或降序)", DefaultValue: "ASC"},
|
||||
"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 works []Work
|
||||
var total int
|
||||
var err error
|
||||
|
||||
var query = goqu.Dialect("mysql").From("web_article").Where(goqu.Ex{"category_top_id": 1})
|
||||
|
||||
// 筛选条件
|
||||
for _, format := range []string{"id", "tags"} {
|
||||
if p.Args[format] != nil {
|
||||
query = query.Where(goqu.C(format).Eq(p.Args[format]))
|
||||
}
|
||||
}
|
||||
|
||||
// 按指定用户点赞筛选
|
||||
if p.Args["praise"] != nil {
|
||||
query = query.Join(goqu.T("web_praise"), goqu.On(
|
||||
goqu.I("web_article.id").Eq(goqu.I("web_praise.praise_id")),
|
||||
goqu.I("web_praise.user_id").Eq(p.Args["praise"]),
|
||||
goqu.I("web_praise.type").Eq(0),
|
||||
))
|
||||
}
|
||||
|
||||
// 按指定用户收藏筛选
|
||||
if p.Args["collect"] != nil {
|
||||
query = query.Join(goqu.T("web_collect"), goqu.On(
|
||||
goqu.I("web_article.id").Eq(goqu.I("web_collect.collect_id")),
|
||||
goqu.I("web_collect.user_id").Eq(p.Args["collect"]),
|
||||
goqu.I("web_collect.type").Eq(0),
|
||||
))
|
||||
}
|
||||
|
||||
// 如果查询了 total 字段
|
||||
if existField(p.Info.FieldASTs[0].SelectionSet.Selections, "total") {
|
||||
sql, _, _ := query.ToSQL()
|
||||
sql = strings.Replace(sql, "SELECT *", "SELECT COUNT(*)", 1)
|
||||
if err := db.Raw(sql).Scan(&total).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有外部排序则使用指定排序(正则sort只能是字母数字下划下)
|
||||
if p.Args["text"] == nil && p.Args["similar"] == nil && p.Args["interest"] == nil {
|
||||
sort := regexp.MustCompile(`[^a-zA-Z0-9_]`).ReplaceAllString(p.Args["sort"].(string), "")
|
||||
query = query.Select("web_article.id", goqu.L(
|
||||
fmt.Sprintf("ROW_NUMBER() OVER(ORDER BY web_article.%s %s)", sort, p.Args["order"]),
|
||||
).As("row_num"))
|
||||
} else {
|
||||
// 排序条件
|
||||
if p.Args["sort"] != nil {
|
||||
if p.Args["order"].(string) == "ASC" {
|
||||
query = query.Order(goqu.C(p.Args["sort"].(string)).Asc())
|
||||
}
|
||||
if p.Args["order"].(string) == "DESC" {
|
||||
query = query.Order(goqu.C(p.Args["sort"].(string)).Desc())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 取所有数据的前N条
|
||||
sql, _, _ := query.ToSQL()
|
||||
|
||||
// 遊標截取篩選結果集的前N条
|
||||
var cursor string
|
||||
if p.Args["after"] != nil {
|
||||
cursor = fmt.Sprintf(`WHERE row_num > (SELECT row_num FROM RankedArticles WHERE RankedArticles.id = %d)`, p.Args["after"].(int))
|
||||
}
|
||||
|
||||
var limit int = 10
|
||||
if p.Args["first"] != nil {
|
||||
limit = p.Args["first"].(int)
|
||||
} else if p.Args["last"] != nil {
|
||||
limit = p.Args["last"].(int)
|
||||
}
|
||||
|
||||
// 字段选择
|
||||
var user_id = p.Context.Value("user_id").(int)
|
||||
var fields = ListItem(p.Info.FieldASTs[0].SelectionSet.Selections)
|
||||
var praise, collect, text_count string
|
||||
if funk.Contains(fields, "praise") {
|
||||
praise = fmt.Sprintf(",CASE WHEN EXISTS (SELECT 1 FROM web_praise WHERE web_praise.praise_id = web_article.id AND web_praise.user_id = %d AND web_praise.type = 0) THEN TRUE ELSE FALSE END AS is_praise", user_id)
|
||||
}
|
||||
if funk.Contains(fields, "collect") {
|
||||
collect = fmt.Sprintf(",CASE WHEN EXISTS (SELECT 1 FROM web_collect WHERE web_collect.collect_id = web_article.id AND web_collect.user_id = %d AND web_collect.type = 0) THEN TRUE ELSE FALSE END AS is_collect", user_id)
|
||||
}
|
||||
|
||||
sql = fmt.Sprintf(`
|
||||
WITH RankedArticles AS (%s)
|
||||
SELECT web_article.* %s %s %s FROM web_article INNER JOIN(
|
||||
SELECT id, row_num FROM RankedArticles %s
|
||||
) AS LimitedRanked ON LimitedRanked.id = web_article.id
|
||||
ORDER BY LimitedRanked.row_num ASC LIMIT %d
|
||||
`, sql, praise, collect, text_count, cursor, limit)
|
||||
|
||||
//fmt.Println(sql)
|
||||
|
||||
if err := db.Raw(sql).Scan(&works).Error; err != nil {
|
||||
fmt.Println("获取作品列表失败", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if funk.Contains(fields, "user") {
|
||||
var ids []int
|
||||
for _, game := range works {
|
||||
ids = append(ids, game.UserId)
|
||||
}
|
||||
ids = funk.UniqInt(ids)
|
||||
|
||||
var users []User
|
||||
if err := db.Table("web_member").Where("id in (?)", ids).Find(&users).Error; err != nil {
|
||||
fmt.Println("获取用户信息失败", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for index, work := range works {
|
||||
for _, user := range users {
|
||||
if work.UserId == user.ID {
|
||||
works[index].User = user
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var items = ListItem(p.Info.FieldASTs[0].SelectionSet.Selections)
|
||||
if funk.Contains(items, "views") {
|
||||
type ApiResponse struct {
|
||||
ID int `json:"id"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
// 0. 收集要查询的 ID
|
||||
var ids []int
|
||||
for x := range works {
|
||||
ids = append(ids, works[x].ID)
|
||||
}
|
||||
idx := strings.Trim(strings.Replace(fmt.Sprint(ids), " ", ",", -1), "[]")
|
||||
|
||||
// 1. 发送 GET 请求
|
||||
resp, err := http.Get("http://localhost:6005/api/get_views/文章?ids=" + idx)
|
||||
if err != nil {
|
||||
fmt.Println("Error making GET request:", err)
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 2. 检查 HTTP 状态码
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
fmt.Printf("Request failed with status code: %d\n", resp.StatusCode)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 3. 读取响应体
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
fmt.Println("Error reading response body:", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 4. 解析 JSON 数据到结构体
|
||||
var data []ApiResponse
|
||||
err = json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
fmt.Println("Error unmarshalling JSON:", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 5. 赋值到数据集
|
||||
for _, item := range data {
|
||||
for i := range works {
|
||||
if works[i].ID == item.ID {
|
||||
works[i].Views = item.Count
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"list": works,
|
||||
"total": total,
|
||||
}, err
|
||||
},
|
||||
}
|
535
bin/main.go
535
bin/main.go
@@ -2,12 +2,14 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"regexp"
|
||||
@@ -18,11 +20,9 @@ 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"
|
||||
|
||||
lru "github.com/hashicorp/golang-lru/v2"
|
||||
)
|
||||
|
||||
// string 转换为 int, 如果转换失败则返回默认值
|
||||
@@ -63,139 +63,25 @@ func LogComponent(startTime int64, r *http.Request) {
|
||||
func LogRequest(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer LogComponent(time.Now().UnixNano(), r) // 最后打印日志
|
||||
next.ServeHTTP(w, r)
|
||||
|
||||
var user_id int
|
||||
if token := r.Header.Get("token"); token != "" {
|
||||
user_id = api.ParseToken(token)
|
||||
}
|
||||
next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), "user_id", user_id)))
|
||||
})
|
||||
}
|
||||
|
||||
type Image struct {
|
||||
Id int `json:"id" db:"id"`
|
||||
Width int `json:"width" db:"width"`
|
||||
Height int `json:"height" db:"height"`
|
||||
Content string `json:"content" db:"content"`
|
||||
ArticleCategoryTopId int `json:"article_category_top_id" db:"article_category_top_id"`
|
||||
PraiseCount int `json:"praise_count" db:"praise_count"`
|
||||
CollectCount int `json:"collect_count" db:"collect_count"`
|
||||
CreateTime time.Time `json:"createTime" db:"createTime"`
|
||||
UpdateTime time.Time `json:"updateTime" db:"updateTime"`
|
||||
UserID int `json:"user_id" db:"user_id"`
|
||||
User models.User `json:"user" db:"user"`
|
||||
Article models.Article `json:"article" db:"article"`
|
||||
}
|
||||
|
||||
type Tag struct {
|
||||
Id int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
CreateTime time.Time `json:"create_time"`
|
||||
UpdateTime time.Time `json:"update_time"`
|
||||
}
|
||||
|
||||
type History struct {
|
||||
Type string `json:"type"`
|
||||
CreateTime time.Time `json:"create_time"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
type ListView struct {
|
||||
Code int `json:"code"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"pageSize"`
|
||||
Total int `json:"total"`
|
||||
Next bool `json:"next"`
|
||||
List []interface{} `json:"list"`
|
||||
}
|
||||
|
||||
var mysqlConnection models.MysqlConnection
|
||||
var milvusConnection models.MilvusConnection
|
||||
|
||||
func GetNetWorkEmbedding(id int) (embedding []float32) {
|
||||
host := viper.GetString("embedding.host")
|
||||
port := viper.GetInt("embedding.port")
|
||||
httpClient := &http.Client{}
|
||||
request, err := http.NewRequest("PUT", fmt.Sprintf("http://%s:%d/api/default/%d", host, port, id), nil)
|
||||
if err != nil {
|
||||
log.Println("请求失败1:", err)
|
||||
return
|
||||
}
|
||||
response, err := httpClient.Do(request)
|
||||
if err != nil {
|
||||
log.Println("请求失败2:", err)
|
||||
return
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
var result struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Feature []float32 `json:"feature"`
|
||||
}
|
||||
err = json.NewDecoder(response.Body).Decode(&result)
|
||||
if err != nil {
|
||||
log.Println("解析失败:", err)
|
||||
return
|
||||
}
|
||||
if result.Code != 0 {
|
||||
log.Println("请求失败3:", result.Message)
|
||||
return
|
||||
}
|
||||
return result.Feature
|
||||
}
|
||||
|
||||
var lruCache, _ = lru.New[int, []int64](100000)
|
||||
|
||||
func (image *Image) GetSimilarImagesIdList(collection_name string) (ids []int64) {
|
||||
ctx := context.Background()
|
||||
|
||||
// 先从 LRU 中查询缓存的结果, 如果缓存中有, 直接返回
|
||||
if value, ok := lruCache.Get(image.Id); ok {
|
||||
return value
|
||||
}
|
||||
|
||||
// 先从milvus中查询图片的向量
|
||||
var embedding []float32
|
||||
result, err := milvusConnection.Client.Query(ctx, collection_name, nil, fmt.Sprintf("id in [%d]", image.Id), []string{"embedding"})
|
||||
if err != nil {
|
||||
log.Println("查詢向量失敗:", err)
|
||||
embedding = GetNetWorkEmbedding(image.Id)
|
||||
} else {
|
||||
for _, item := range result {
|
||||
if item.Name() == "embedding" {
|
||||
embedding = item.FieldData().GetVectors().GetFloatVector().Data
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理向量不存在的情况
|
||||
if len(embedding) == 0 {
|
||||
log.Println("向量不存在, 也未能重新生成")
|
||||
return ids
|
||||
}
|
||||
|
||||
// 用向量查询相似图片
|
||||
topk := 200
|
||||
sp, _ := entity.NewIndexIvfFlatSearchParam(64)
|
||||
vectors := []entity.Vector{entity.FloatVector(embedding)}
|
||||
resultx, err := milvusConnection.Client.Search(ctx, collection_name, nil, "", []string{"id", "article_id"}, vectors, "embedding", entity.L2, topk, sp)
|
||||
if err != nil {
|
||||
log.Println("搜索相似失敗:", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 输出结果
|
||||
for _, item := range resultx {
|
||||
ids = item.IDs.FieldData().GetScalars().GetLongData().GetData()
|
||||
}
|
||||
|
||||
// 将结果缓存到 LRU 中
|
||||
lruCache.Add(image.Id, ids)
|
||||
|
||||
return ids
|
||||
}
|
||||
|
||||
func main() {
|
||||
runtime.GOMAXPROCS(runtime.NumCPU() - 1)
|
||||
|
||||
viper.SetConfigFile("./data/config.yaml")
|
||||
configFilePath := flag.String("config", "./data/config.yaml", "配置文件路径")
|
||||
flag.Parse()
|
||||
|
||||
viper.SetConfigFile(*configFilePath)
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
log.Println("读取配置文件失败", err)
|
||||
}
|
||||
@@ -203,6 +89,7 @@ func main() {
|
||||
|
||||
models.InitConfig(config)
|
||||
models.ZincInit()
|
||||
api.InitDefault(config)
|
||||
|
||||
mysqlConnection.Init()
|
||||
milvusConnection.Init()
|
||||
@@ -212,374 +99,67 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
// graphql Schema
|
||||
schema, err := api.NewSchema(api.Config{
|
||||
Mysql: api.ConfigMysql{
|
||||
Host: config.GetString("mysql.host"),
|
||||
Port: config.GetInt("mysql.port"),
|
||||
Database: config.GetString("mysql.database"),
|
||||
UserName: config.GetString("mysql.user"),
|
||||
Password: config.GetString("mysql.password"),
|
||||
},
|
||||
})
|
||||
if config.GetBool("oss.local") {
|
||||
fmt.Println("开启图像色调计算")
|
||||
go api.CheckColorNullRows(0)
|
||||
}
|
||||
if config.GetBool("gorse.open") {
|
||||
fmt.Println("开启用户偏好收集")
|
||||
api.GorseInit(config.GetString("gorse.host"), config.GetInt("gorse.port"))
|
||||
fmt.Println("同步点赞数据")
|
||||
go api.PutPraises()
|
||||
}
|
||||
|
||||
schema, err := graphql.NewSchema(graphql.SchemaConfig{Query: graphql.NewObject(graphql.ObjectConfig{Name: "RootQuery", Fields: graphql.Fields{
|
||||
"users": api.UserItems,
|
||||
"games": api.GameItems,
|
||||
"works": api.WorkItems,
|
||||
"collections": api.CollectionItems,
|
||||
"articles": api.ArticleItems,
|
||||
"article": api.ArticleItem,
|
||||
"images": api.ImageItems,
|
||||
"image": api.ImageItem,
|
||||
"searchs": api.SearchItems,
|
||||
}})})
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create new schema, error: %v", err)
|
||||
}
|
||||
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
defer LogComponent(time.Now().UnixNano(), r) // 最后打印日志
|
||||
w.Write([]byte("Hello World!"))
|
||||
})
|
||||
|
||||
http.Handle("/api", LogRequest(handler.New(&handler.Config{
|
||||
Schema: &schema,
|
||||
Playground: true,
|
||||
Pretty: false,
|
||||
})))
|
||||
|
||||
// 获取图片信息列表(分页)
|
||||
http.HandleFunc("/api/images", func(w http.ResponseWriter, r *http.Request) {
|
||||
// URL 格式: /image/{type}-{id}-{width}x{height}-{fit}.{format}?version
|
||||
http.HandleFunc("/image", func(w http.ResponseWriter, r *http.Request) {
|
||||
defer LogComponent(time.Now().UnixNano(), r) // 最后打印日志
|
||||
|
||||
// 私域: (自己的图片, 自己的文章, 自己的精选集, 点赞收藏精选集)
|
||||
// 条件查询(模糊搜索, 时间区间, 作者, 标签, 分类, 精选集, 状态, 置顶, 模糊权重)(权重规则:权重指数)
|
||||
// 条件筛选(交集, 并集, 差集, 子集)
|
||||
// 文字搜索支持翻页
|
||||
// 文字搜索支持与按颜色筛选混合
|
||||
|
||||
// TODO 查找含有指定文字的图像
|
||||
// TODO 查找含有指定标签的图像
|
||||
// TODO 查找含有指定特征的图像
|
||||
// TODO 查找含有指定颜色的图像(倒排索引)
|
||||
// TODO 查找含有指定分类的图像
|
||||
// 1,000,000
|
||||
|
||||
// 获取查询条件(忽略空值)
|
||||
QueryConditions := func(key string) (list []string) {
|
||||
for _, item := range strings.Split(r.URL.Query().Get(key), ",") {
|
||||
if item != "" {
|
||||
list = append(list, fmt.Sprintf("'%s'", item))
|
||||
}
|
||||
}
|
||||
return list
|
||||
}
|
||||
// 拼接基本查询条件
|
||||
var addCondition = func(conditions *strings.Builder, key, column string) {
|
||||
if values := QueryConditions(key); len(values) > 0 {
|
||||
if conditions.Len() > 0 {
|
||||
conditions.WriteString(" AND")
|
||||
} else {
|
||||
conditions.WriteString(" WHERE")
|
||||
}
|
||||
conditions.WriteString(fmt.Sprintf(" %s IN (%s)", column, strings.Join(values, ",")))
|
||||
}
|
||||
}
|
||||
var conditions strings.Builder
|
||||
// 如果是查询 text, 直接从 Elasticsearch 返回结果
|
||||
var text_ids []string
|
||||
if text := QueryConditions("text"); len(text) > 0 {
|
||||
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 {
|
||||
// 直接返回空列表
|
||||
var images ListView
|
||||
images.Page, images.PageSize = stringToInt(r.URL.Query().Get("page"), 1), stringToInt(r.URL.Query().Get("pageSize"), 20)
|
||||
images.Total = 0
|
||||
images.Next = false
|
||||
images.List = make([]interface{}, 0)
|
||||
data, _ := json.MarshalIndent(images, "", " ")
|
||||
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
|
||||
w.Write(data)
|
||||
return
|
||||
}
|
||||
if conditions.Len() > 1024 {
|
||||
log.Println("查询条件过长")
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
addCondition(&conditions, "authors", "author")
|
||||
addCondition(&conditions, "tags", "tag")
|
||||
addCondition(&conditions, "categories", "categorie")
|
||||
addCondition(&conditions, "sets", "sets")
|
||||
}
|
||||
|
||||
// 获取图片列表
|
||||
var images ListView
|
||||
var image_list []Image
|
||||
images.Page, images.PageSize = stringToInt(r.URL.Query().Get("page"), 1), stringToInt(r.URL.Query().Get("pageSize"), 20)
|
||||
|
||||
var ids []int64
|
||||
if similar := QueryConditions("similar"); len(similar) > 0 {
|
||||
id, err := strconv.Atoi(strings.Trim(similar[0], "'"))
|
||||
if err != nil {
|
||||
log.Println("strconv.Atoi failed:", err)
|
||||
return
|
||||
}
|
||||
// 如果指定以某个图片为基准的相似图片列表范围, 获取相似图片ID的列表
|
||||
ids = (&Image{Id: id}).GetSimilarImagesIdList("default")
|
||||
images.Total = len(ids)
|
||||
|
||||
// 按照分页取相应的图片ID
|
||||
if len(ids) > images.Page*images.PageSize {
|
||||
ids = ids[(images.Page-1)*images.PageSize : images.Page*images.PageSize]
|
||||
} else {
|
||||
ids = ids[(images.Page-1)*images.PageSize:]
|
||||
}
|
||||
|
||||
idsStr := make([]string, len(ids))
|
||||
for i, v := range ids {
|
||||
idsStr[i] = strconv.FormatInt(v, 10)
|
||||
}
|
||||
if len(idsStr) > 0 {
|
||||
if conditions.Len() > 0 {
|
||||
conditions.WriteString(" AND")
|
||||
} else {
|
||||
conditions.WriteString(" WHERE")
|
||||
}
|
||||
conditions.WriteString(fmt.Sprintf(" id IN (%s)", strings.Join(idsStr, ","))) // 拼接查询条件
|
||||
}
|
||||
}
|
||||
sql := fmt.Sprintf("SELECT id, width, height, content, update_time, create_time, user_id, article_id, article_category_top_id, praise_count, collect_count FROM web_images %s", conditions.String())
|
||||
rows, err := mysqlConnection.Database.Query(sql)
|
||||
if err != nil {
|
||||
log.Println("获取图片列表失败", err)
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var image Image
|
||||
rows.Scan(&image.Id, &image.Width, &image.Height, &image.Content, &image.UpdateTime, &image.CreateTime, &image.User.ID, &image.Article.Id, &image.ArticleCategoryTopId, &image.PraiseCount, &image.CollectCount)
|
||||
image.UpdateTime = image.UpdateTime.UTC()
|
||||
image.CreateTime = image.CreateTime.UTC()
|
||||
image.Content = regexp.MustCompile(`http:`).ReplaceAllString(image.Content, "https:")
|
||||
image_list = append(image_list, image)
|
||||
}
|
||||
|
||||
// 如果使用了相似图片列表范围, 需要按照相似图片ID原本的顺序重新排序, 需要注意的是, 相似图片ID列表中可能会包含不在当前页的图片ID
|
||||
if similar := QueryConditions("similar"); len(similar) > 0 {
|
||||
var image_list_sorted []Image
|
||||
for _, id := range ids {
|
||||
for _, image := range image_list {
|
||||
if image.Id == int(id) {
|
||||
image_list_sorted = append(image_list_sorted, image)
|
||||
}
|
||||
}
|
||||
}
|
||||
image_list = image_list_sorted
|
||||
}
|
||||
|
||||
// 如果使用了图像文字检索, 需要按照图像文字检索的相似度重新排序 text_ids
|
||||
if len(text_ids) > 0 {
|
||||
var image_list_sorted []Image
|
||||
for _, id := range text_ids {
|
||||
for _, image := range image_list {
|
||||
if id == strconv.Itoa(image.Id) {
|
||||
image_list_sorted = append(image_list_sorted, image)
|
||||
}
|
||||
}
|
||||
}
|
||||
image_list = image_list_sorted
|
||||
}
|
||||
|
||||
// 用户ID, 文章ID
|
||||
var user_ids []int
|
||||
var article_ids []int
|
||||
for _, image := range image_list {
|
||||
user_ids = append(user_ids, image.User.ID)
|
||||
article_ids = append(article_ids, image.Article.Id)
|
||||
}
|
||||
|
||||
// 附加用户信息
|
||||
users := models.QueryUserList(user_ids)
|
||||
for i, image := range image_list {
|
||||
for _, user := range users {
|
||||
if image.User.ID == user.ID {
|
||||
image_list[i].User = user
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 附加文章信息
|
||||
articles := models.QueryArticleList(article_ids)
|
||||
for i, image := range image_list {
|
||||
for _, article := range articles {
|
||||
if image.Article.Id == article.Id {
|
||||
image_list[i].Article = article
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 将 []Image 转换为 []interface{}
|
||||
images.List = make([]interface{}, len(image_list))
|
||||
for i, v := range image_list {
|
||||
images.List[i] = v
|
||||
}
|
||||
|
||||
// 如果不是获取相似图像固定数量, 则从mysql获取总数
|
||||
if similar := QueryConditions("similar"); len(similar) > 0 {
|
||||
// 固定数量
|
||||
} else {
|
||||
// 获取总数
|
||||
err = mysqlConnection.Database.QueryRow("SELECT COUNT(*) FROM web_images" + conditions.String()).Scan(&images.Total)
|
||||
if err != nil {
|
||||
log.Println("获取图片总数失败", err)
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 是否有下一页
|
||||
images.Next = images.Total > images.Page*images.PageSize
|
||||
|
||||
// 将对象转换为有缩进的JSON输出
|
||||
data, err := json.MarshalIndent(images, "", " ")
|
||||
if err != nil {
|
||||
log.Println("转换图片列表失败", err)
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
|
||||
w.Write(data)
|
||||
|
||||
})
|
||||
|
||||
// 获取标签列表
|
||||
http.HandleFunc("/tags", func(w http.ResponseWriter, r *http.Request) {
|
||||
defer LogComponent(time.Now().UnixNano(), r) // 最后打印日志
|
||||
|
||||
// 标签的原理
|
||||
// 1. 通过文章的 tag 字段, 获取所有的标签
|
||||
// 2. 通过标签的 id, 获取标签的名称
|
||||
|
||||
// 热门权重指数的标签排序
|
||||
// 1. 标签的权重指数 = (标签的文章数 * 标签的文章数) * 近期增幅
|
||||
// 2. 标签的近期增幅 = (标签的文章数 - 标签的文章数) / 标签的文章数
|
||||
|
||||
// 标签是一个虚拟表, ORC 提取的数据都带有多个维度的比重概率(分布概率, 对比度概率, 文字大小, 文字重量, 词频概率, 词性概率, 词长概率, 词序概率)
|
||||
// 经过规则过滤后, 用动态调参的指数计算乘积作为权重, 权重仍达到某个阈值的数据才会被视为标签
|
||||
|
||||
// 获取查询条件(忽略空值), 超级简洁写法
|
||||
QueryConditions := func(key string) (list []string) {
|
||||
if r.FormValue(key) != "" {
|
||||
list = strings.Split(r.FormValue(key), ",")
|
||||
}
|
||||
// 如果本地文件存在,直接输出
|
||||
filePath := filepath.Join("data/webp", r.URL.Path)
|
||||
if _, err := os.Stat(filePath); err == nil {
|
||||
http.ServeFile(w, r, filePath)
|
||||
return
|
||||
}
|
||||
|
||||
// 拼接查询条件, 超级简洁写法
|
||||
conditions := ""
|
||||
if authors := QueryConditions("authors"); len(authors) > 0 {
|
||||
conditions += fmt.Sprintf(" AND author IN (%s)", strings.Join(authors, ","))
|
||||
}
|
||||
if tags := QueryConditions("tags"); len(tags) > 0 {
|
||||
conditions += fmt.Sprintf(" AND tag IN (%s)", strings.Join(tags, ","))
|
||||
}
|
||||
if categories := QueryConditions("categories"); len(categories) > 0 {
|
||||
conditions += fmt.Sprintf(" AND categorie IN (%s)", strings.Join(categories, ","))
|
||||
}
|
||||
|
||||
// 获取标签列表
|
||||
var tags ListView
|
||||
tags.Page, tags.PageSize = stringToInt(r.FormValue("page"), 1), stringToInt(r.FormValue("pageSize"), 20)
|
||||
rows, err := mysqlConnection.Database.Query("SELECT id, name, update_time, create_time FROM web_tags"+conditions+" ORDER BY id DESC LIMIT ?, ?", (tags.Page-1)*tags.PageSize, tags.PageSize)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var tag Tag
|
||||
if err := rows.Scan(&tag.Id, &tag.Name, &tag.UpdateTime, &tag.CreateTime); err != nil {
|
||||
log.Println(err)
|
||||
continue
|
||||
}
|
||||
tags.List = append(tags.List, tag)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
if err := mysqlConnection.Database.QueryRow("SELECT COUNT(*) FROM web_tags" + conditions).Scan(&tags.Total); err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
// 是否有下一页
|
||||
tags.Next = tags.Total > tags.Page*tags.PageSize
|
||||
|
||||
// 将对象转换为有缩进的JSON输出
|
||||
json, err := json.MarshalIndent(tags, "", " ")
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
// 输出JSON
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(json)
|
||||
})
|
||||
|
||||
// URL 格式: /img/{type}-{id}.{format}?width=320&height=320&fit=cover
|
||||
http.HandleFunc("/img/", func(w http.ResponseWriter, r *http.Request) {
|
||||
defer LogComponent(time.Now().UnixNano(), r) // 最后打印日志
|
||||
|
||||
reg := regexp.MustCompile(`^/img/([0-9a-zA-Z]+)-([0-9a-zA-Z]+).(jpg|jpeg|png|webp)$`)
|
||||
reg := regexp.MustCompile(`^/image/([a-z]+)-([0-9]+)-([0-9]+)x([0-9]+)-([a-z]+).(jpg|jpeg|png|webp)$`)
|
||||
matches := reg.FindStringSubmatch(r.URL.Path)
|
||||
if len(matches) != 4 {
|
||||
http.Error(w, "URL 格式错误", http.StatusNotFound)
|
||||
if len(matches) != 7 {
|
||||
log.Println("URL 格式错误", matches)
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
group, id, format, width, height, fit := matches[1], matches[2], matches[3], stringToInt(r.URL.Query().Get("width"), 0), stringToInt(r.URL.Query().Get("height"), 0), r.URL.Query().Get("fit")
|
||||
group, id, width, height, fit, format := matches[1], matches[2], stringToInt(matches[3], 0), stringToInt(matches[4], 0), matches[5], matches[6]
|
||||
content, err := mysqlConnection.GetImageContent(group, id)
|
||||
if err != nil {
|
||||
log.Println("获取图片失败", format, err)
|
||||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
var img models.Image
|
||||
if err := img.Init(content); err != nil {
|
||||
log.Println("初始化图片失败", format, err)
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
data, err := img.ToWebP(width, height, fit)
|
||||
if err != nil {
|
||||
log.Println("转换图片失败", err)
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "image/webp")
|
||||
w.Header().Set("Cache-Control", "max-age=31536000")
|
||||
w.Write(data)
|
||||
})
|
||||
|
||||
// URL 格式: /webp/{type}-{id}-{version}-{width}-{height}-{fit}.{format}
|
||||
http.HandleFunc("/webp/", func(w http.ResponseWriter, r *http.Request) {
|
||||
defer LogComponent(time.Now().UnixNano(), r) // 最后打印日志
|
||||
|
||||
reg := regexp.MustCompile(`^/webp/([0-9a-zA-Z]+)-([0-9a-zA-Z]+)-([0-9a-zA-Z]+)-([0-9]+)-([0-9]+)-([a-zA-Z]+).(jpg|jpeg|png|webp)$`)
|
||||
matches := reg.FindStringSubmatch(r.URL.Path)
|
||||
if len(matches) != 8 {
|
||||
log.Println("URL 格式错误", matches)
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
group, id, version, width, height, fit, format := matches[1], matches[2], matches[3], stringToInt(matches[4], 0), stringToInt(matches[5], 0), matches[6], matches[7]
|
||||
content, err := mysqlConnection.GetImageContent(group, id)
|
||||
if err != nil {
|
||||
log.Println("获取图片失败", version, format, err)
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
var img models.Image
|
||||
if err := img.Init(content); err != nil {
|
||||
log.Println("初始化图片失败", version, format, err)
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
@@ -589,9 +169,20 @@ func main() {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err = os.MkdirAll(filepath.Dir(filePath), os.ModePerm)
|
||||
if err != nil {
|
||||
log.Println("创建文件目录失败:", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
err = os.WriteFile(filePath, data, 0644)
|
||||
if err != nil {
|
||||
log.Println("保存文件失败:", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "image/webp")
|
||||
w.Header().Set("Cache-Control", "max-age=31536000")
|
||||
w.Header().Set("Cache-Control", "max-age=604800")
|
||||
w.Write(data)
|
||||
})
|
||||
|
||||
|
45
go.mod
45
go.mod
@@ -1,26 +1,26 @@
|
||||
module git.satori.love/gameui/webp
|
||||
|
||||
go 1.23
|
||||
|
||||
toolchain go1.23.2
|
||||
go 1.23.3
|
||||
|
||||
require (
|
||||
github.com/alibabacloud-go/darabonba-openapi v0.1.18
|
||||
github.com/alibabacloud-go/tea v1.2.1
|
||||
github.com/alibabacloud-go/vod-20170321/v2 v2.16.10
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/elastic/go-elasticsearch/v8 v8.11.0
|
||||
github.com/doug-martin/goqu/v9 v9.19.0
|
||||
github.com/graphql-go/graphql v0.8.1
|
||||
github.com/graphql-go/handler v0.2.3
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7
|
||||
github.com/jmoiron/sqlx v1.3.5
|
||||
github.com/milvus-io/milvus-sdk-go/v2 v2.2.1
|
||||
github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c
|
||||
github.com/spf13/viper v1.15.0
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/thoas/go-funk v0.9.3
|
||||
github.com/zhenghaoz/gorse v0.5.0-alpha.1.0.20241116140254-a323b179f5e4
|
||||
gorm.io/driver/mysql v1.5.7
|
||||
gorm.io/gorm v1.25.12
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4 // indirect
|
||||
github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68 // indirect
|
||||
github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect
|
||||
@@ -29,47 +29,52 @@ require (
|
||||
github.com/alibabacloud-go/tea-xml v1.1.2 // indirect
|
||||
github.com/aliyun/credentials-go v1.1.2 // indirect
|
||||
github.com/clbanning/mxj/v2 v2.7.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/elastic/elastic-transport-go/v8 v8.3.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/lib/pq v1.10.9 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/milvus-io/milvus-proto/go-api v0.0.0-20230301092744-7efc6eec15fd // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.12.0 // indirect
|
||||
github.com/sizeofint/webp-animation v0.0.0-20190207194838-b631dc900de9 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
github.com/spf13/cast v1.7.0 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/subosito/gotenv v1.4.2 // indirect
|
||||
github.com/tjfoc/gmsm v1.3.2 // indirect
|
||||
golang.org/x/net v0.28.0 // indirect
|
||||
golang.org/x/sys v0.25.0 // indirect
|
||||
golang.org/x/text v0.18.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 // indirect
|
||||
go.opentelemetry.io/otel v1.31.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.31.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.31.0 // indirect
|
||||
golang.org/x/net v0.30.0 // indirect
|
||||
golang.org/x/sys v0.26.0 // indirect
|
||||
golang.org/x/text v0.19.0 // indirect
|
||||
golang.org/x/time v0.6.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240812133136-8ffd90a71988 // indirect
|
||||
google.golang.org/grpc v1.65.0 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect
|
||||
google.golang.org/grpc v1.67.1 // indirect
|
||||
google.golang.org/protobuf v1.35.1 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gorm.io/driver/mysql v1.5.7 // indirect
|
||||
gorm.io/gorm v1.25.12 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/aliyun/aliyun-oss-go-sdk v2.2.7+incompatible
|
||||
github.com/chai2010/webp v1.1.1
|
||||
github.com/go-sql-driver/mysql v1.7.1
|
||||
github.com/go-sql-driver/mysql v1.8.1
|
||||
github.com/sizeofint/gif-to-webp v0.0.0-20210224202734-e9d7ed071591
|
||||
golang.org/x/image v0.20.0 // indirect
|
||||
)
|
||||
|
84
go.sum
84
go.sum
@@ -1,5 +1,9 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4 h1:iC9YFYKDGEy3n/FtqJnOkZsene9olVspKmkX5A2YBEo=
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc=
|
||||
github.com/alibabacloud-go/darabonba-openapi v0.1.18 h1:3eUVmAr7WCJp7fgIvmCd9ZUyuwtJYbtUqJIed5eXCmk=
|
||||
@@ -31,6 +35,8 @@ github.com/aliyun/aliyun-oss-go-sdk v2.2.7+incompatible/go.mod h1:T/Aws4fEfogEE9
|
||||
github.com/aliyun/credentials-go v1.1.2 h1:qU1vwGIBb3UJ8BwunHDRFtAhS6jnQLnde/yk0+Ih2GY=
|
||||
github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chai2010/webp v1.1.1 h1:jTRmEccAJ4MGrhFOrPMpNGIJ/eybIgwKpcACsrTEapk=
|
||||
github.com/chai2010/webp v1.1.1/go.mod h1:0XVwvZWdjjdxpUEIf7b9g9VkHFnInUSYujwqTLEuldU=
|
||||
github.com/clbanning/mxj/v2 v2.5.6/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
|
||||
@@ -41,28 +47,37 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||
github.com/elastic/elastic-transport-go/v8 v8.3.0 h1:DJGxovyQLXGr62e9nDMPSxRyWION0Bh6d9eCFBriiHo=
|
||||
github.com/elastic/elastic-transport-go/v8 v8.3.0/go.mod h1:87Tcz8IVNe6rVSLdBux1o/PEItLtyabHU3naC7IoqKI=
|
||||
github.com/elastic/go-elasticsearch/v8 v8.11.0 h1:gUazf443rdYAEAD7JHX5lSXRgTkG4N4IcsV8dcWQPxM=
|
||||
github.com/elastic/go-elasticsearch/v8 v8.11.0/go.mod h1:GU1BJHO7WeamP7UhuElYwzzHtvf9SDmeVpSSy9+o6Qg=
|
||||
github.com/doug-martin/goqu/v9 v9.19.0 h1:PD7t1X3tRcUiSdc5TEyOFKujZA5gs3VSA7wxSvBx7qo=
|
||||
github.com/doug-martin/goqu/v9 v9.19.0/go.mod h1:nf0Wc2/hV3gYK9LiyqIrzBEVGlI8qW3GuDCEobC4wBQ=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
|
||||
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
@@ -90,8 +105,6 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
|
||||
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
@@ -107,12 +120,12 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.10.1/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
|
||||
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/milvus-io/milvus-proto/go-api v0.0.0-20230301092744-7efc6eec15fd h1:9ilgTEqZSdEPbJKSrRGB1TIHTaF7DqVDIwn8/azcaBk=
|
||||
github.com/milvus-io/milvus-proto/go-api v0.0.0-20230301092744-7efc6eec15fd/go.mod h1:148qnlmZ0Fdm1Fq+Mj/OW2uDoEP25g3mjh0vMGtkgmk=
|
||||
github.com/milvus-io/milvus-sdk-go/v2 v2.2.1 h1:6p/lrxZCGkw5S2p5GPWy/BUmO6mVUNfrczv98uAnhoU=
|
||||
@@ -134,6 +147,8 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
|
||||
github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
@@ -163,20 +178,35 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
|
||||
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
|
||||
github.com/thoas/go-funk v0.9.3 h1:7+nAEx3kn5ZJcnDm2Bh23N2yOtweO14bi//dvRtgLpw=
|
||||
github.com/thoas/go-funk v0.9.3/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q=
|
||||
github.com/tjfoc/gmsm v1.3.2 h1:7JVkAn5bvUJ7HtU08iW6UiD+UTmJTIToHCfeFzkcCxM=
|
||||
github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/zhenghaoz/gorse v0.5.0-alpha.1.0.20241116140254-a323b179f5e4 h1:l9J7zrPXlJjOusTqw5wh0XQNsDHZj4BbxxjaC33xT4s=
|
||||
github.com/zhenghaoz/gorse v0.5.0-alpha.1.0.20241116140254-a323b179f5e4/go.mod h1:cPJh4IEuHS4JGuv3VSN/QvUwtGl9qxM05O+2jO/MTIE=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 h1:ZIg3ZT/aQ7AfKqdwp7ECpOK6vHqquXXuyTjIO8ZdmPs=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0/go.mod h1:DQAwmETtZV00skUwgD6+0U89g80NKsJE3DCKeLLPQMI=
|
||||
go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY=
|
||||
go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE=
|
||||
go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE=
|
||||
go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY=
|
||||
go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys=
|
||||
go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
@@ -208,8 +238,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
|
||||
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
|
||||
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
|
||||
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
||||
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -233,8 +263,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
@@ -247,8 +277,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
||||
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -272,19 +302,19 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240812133136-8ffd90a71988 h1:V71AcdLZr2p8dC9dbOIMCpqi4EmRl8wUwnJzXXLmbmc=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240812133136-8ffd90a71988/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
|
||||
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
|
||||
google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
|
||||
google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
|
||||
google.golang.org/grpc/examples v0.0.0-20220617181431-3e7b97febc7f h1:rqzndB2lIQGivcXdTuY3Y9NBvr70X+y77woofSRluec=
|
||||
google.golang.org/grpc/examples v0.0.0-20220617181431-3e7b97febc7f/go.mod h1:gxndsbNG1n4TZcHGgsYEfVGnTxqfEdfiDv6/DADXX9o=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
|
||||
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
@@ -294,11 +324,11 @@ gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
|
||||
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
|
||||
gorm.io/gorm v1.25.7 h1:VsD6acwRjz2zFxGO50gPO6AkNs7KKnvfzUjHQhZDz/A=
|
||||
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
||||
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||
|
@@ -1,151 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/elastic/go-elasticsearch/v8"
|
||||
)
|
||||
|
||||
func elasticsearch_init() (es *elasticsearch.Client) {
|
||||
es, err := elasticsearch.NewClient(elasticsearch.Config{
|
||||
Addresses: []string{config.GetString("elasticsearch.host")},
|
||||
Username: config.GetString("elasticsearch.user"),
|
||||
Password: config.GetString("elasticsearch.password"),
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("Error creating the client: %s", err)
|
||||
return nil
|
||||
}
|
||||
return es
|
||||
}
|
||||
|
||||
type SearchData struct {
|
||||
_shards struct {
|
||||
failed int
|
||||
skipped int
|
||||
successful int
|
||||
total int
|
||||
}
|
||||
Hits struct {
|
||||
Hits []struct {
|
||||
ID string `json:"_id"`
|
||||
Index string `json:"_index"`
|
||||
Score float64 `json:"_score"`
|
||||
Source struct {
|
||||
Content string `json:"content"`
|
||||
} `json:"_source"`
|
||||
Type string `json:"_type"`
|
||||
} `json:"hits"`
|
||||
max_score float64
|
||||
total struct {
|
||||
relation string
|
||||
value int
|
||||
}
|
||||
} `json:"hits"`
|
||||
timed_out bool
|
||||
took int
|
||||
}
|
||||
|
||||
// 获取搜索结果的 ID 列表
|
||||
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
|
||||
}
|
||||
|
||||
// 获取搜索结果的内容列表
|
||||
func ElasticsearchSearch(text string) (r SearchData) {
|
||||
// 通过字符串构建查询
|
||||
var buf bytes.Buffer
|
||||
query := map[string]interface{}{
|
||||
"query": map[string]interface{}{
|
||||
"match": map[string]interface{}{
|
||||
"content": text,
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := json.NewEncoder(&buf).Encode(query); err != nil {
|
||||
log.Printf("Error encoding query: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
return
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
// 处理错误
|
||||
if res.IsError() {
|
||||
log.Printf("Error: %s", res.String())
|
||||
return
|
||||
}
|
||||
|
||||
// 转换返回结果
|
||||
if err := json.NewDecoder(res.Body).Decode(&r); err != nil {
|
||||
log.Printf("Error parsing the response body: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
@@ -1,16 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestElasticsearchSearch(t *testing.T) {
|
||||
// 创建一个测试用例
|
||||
expected := "植物学家 阿尔法 可复活一次 技能:召唤豌豆射手 转到设置"
|
||||
actual := ElasticsearchSearch("豌豆")
|
||||
|
||||
// 使用 assert 包中的函数来验证函数的输出
|
||||
assert.Equal(t, expected, actual.Hits.Hits[0].Source.Content)
|
||||
}
|
@@ -1,10 +1,13 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
|
||||
"github.com/chai2010/webp"
|
||||
@@ -20,18 +23,30 @@ type Image struct {
|
||||
}
|
||||
|
||||
// 初始化图片
|
||||
func (img *Image) Init(content string) (err error) {
|
||||
var body io.ReadCloser
|
||||
func (img *Image) Init(content string) error {
|
||||
|
||||
if len(regexp.MustCompile(`image.gameuiux.cn`).FindStringSubmatch(content)) > 0 {
|
||||
key := regexp.MustCompile(`^https?://image.gameuiux.cn/`).ReplaceAllString(content, "")
|
||||
filePath := filepath.Join("data/oss", key)
|
||||
|
||||
// 从OSS中读取图片
|
||||
// 先检查本地是否存在原图, 没有则从OSS下载
|
||||
if _, err := os.Stat(filePath); err != nil {
|
||||
if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil {
|
||||
log.Fatalf("创建目录失败: %v\n", err)
|
||||
}
|
||||
bucket := GetBucket("gameui-image2")
|
||||
body, err = bucket.GetObject(key)
|
||||
fmt.Println("从 OSS 下载:", key, filePath)
|
||||
if err := bucket.GetObjectToFile(key, filePath); err != nil {
|
||||
log.Println("下载文件失败:", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 从本地打开文件
|
||||
body, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
log.Println("读取图片失败", err)
|
||||
return
|
||||
log.Println("打开本地图片失败:", err)
|
||||
return err
|
||||
}
|
||||
defer body.Close()
|
||||
|
||||
@@ -42,25 +57,25 @@ func (img *Image) Init(content string) (err error) {
|
||||
|
||||
if err != nil {
|
||||
log.Println("读取图片失败", err)
|
||||
return
|
||||
return err
|
||||
}
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
// 将文件解码为 image.Image
|
||||
img.image, img.format, err = image.Decode(body)
|
||||
if err != nil {
|
||||
log.Println("解码图像失败", err)
|
||||
return
|
||||
return err
|
||||
}
|
||||
return
|
||||
return nil
|
||||
} else {
|
||||
var resp *http.Response
|
||||
log.Println("直接从网络下载图片:", content)
|
||||
resp, err = http.Get(content)
|
||||
resp, err := http.Get(content)
|
||||
if err != nil {
|
||||
log.Println("下载图片失败", err)
|
||||
return
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
@@ -70,19 +85,19 @@ func (img *Image) Init(content string) (err error) {
|
||||
img.data, err = io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Println("读取图片失败", err)
|
||||
return
|
||||
return err
|
||||
}
|
||||
println("数据长度:", len(img.data))
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
// 将文件解码为 image.Image
|
||||
img.image, img.format, err = image.Decode(resp.Body)
|
||||
if err != nil {
|
||||
log.Println("解码图像失败", err)
|
||||
return
|
||||
return nil
|
||||
}
|
||||
return
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -9,6 +9,7 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -46,30 +47,31 @@ type Response struct {
|
||||
ID string `json:"_id"`
|
||||
Score float64 `json:"_score"`
|
||||
Timestamp string `json:"@timestamp"`
|
||||
Source struct {
|
||||
Timestamp string `json:"@timestamp"`
|
||||
Athlete string `json:"Athlete"`
|
||||
City string `json:"City"`
|
||||
Country string `json:"Country"`
|
||||
Discipline string `json:"Discipline"`
|
||||
Event string `json:"Event"`
|
||||
Gender string `json:"Gender"`
|
||||
Medal string `json:"Medal"`
|
||||
Season string `json:"Season"`
|
||||
Sport string `json:"Sport"`
|
||||
Year int `json:"Year"`
|
||||
} `json:"_source"`
|
||||
//Source struct {
|
||||
// Timestamp string `json:"@timestamp"`
|
||||
// Athlete string `json:"Athlete"`
|
||||
// City string `json:"City"`
|
||||
// Country string `json:"Country"`
|
||||
// Discipline string `json:"Discipline"`
|
||||
// Event string `json:"Event"`
|
||||
// Gender string `json:"Gender"`
|
||||
// Medal string `json:"Medal"`
|
||||
// Season string `json:"Season"`
|
||||
// Sport string `json:"Sport"`
|
||||
// Year int `json:"Year"`
|
||||
//} `json:"_source"`
|
||||
} `json:"hits"`
|
||||
} `json:"hits"`
|
||||
}
|
||||
|
||||
func (res Response) ToIDList(first, last int, after, before string) (id_list []string) {
|
||||
func (res Response) ToIDList(first, last int, after, before int) (id_list []int) {
|
||||
for _, hit := range res.Hits.Hits {
|
||||
id_list = append(id_list, hit.ID)
|
||||
num, _ := strconv.Atoi(hit.ID)
|
||||
id_list = append(id_list, num)
|
||||
}
|
||||
|
||||
// 如果 after 不为 0, 从这个ID开始向后取切片
|
||||
if after != "" {
|
||||
if after != 0 {
|
||||
for i, id := range id_list {
|
||||
if id == after {
|
||||
id_list = id_list[i+1:]
|
||||
@@ -79,7 +81,7 @@ func (res Response) ToIDList(first, last int, after, before string) (id_list []s
|
||||
}
|
||||
|
||||
// 如果 before 不为 0, 从这个ID开始向前取切片
|
||||
if before != "" {
|
||||
if before != 0 {
|
||||
for i, id := range id_list {
|
||||
if id == before {
|
||||
id_list = id_list[:i]
|
||||
@@ -146,7 +148,8 @@ func ZincSearch(query map[string]interface{}) (rest Response, err error) {
|
||||
log.Println("解析响应失败", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
|
||||
return rest, err
|
||||
}
|
||||
|
||||
func ZincPut(data map[string]interface{}) (err error) {
|
||||
|
Reference in New Issue
Block a user