Compare commits

165 Commits
test ... main

Author SHA1 Message Date
Last
76b0c68ed3 使用 LIKE 查询游戏分类 2025-03-26 10:48:26 +08:00
ce15a541ec 没有需要同步的点赞记录 2025-03-04 05:46:30 +08:00
85efe470b9 同步点赞数据 2025-03-04 05:44:00 +08:00
e6410582d2 隐藏调试 2025-02-13 13:00:30 +08:00
a1c7d3c0d2 清理旧API 2025-01-14 04:39:02 +08:00
46c2fce7d5 清理旧API, 更改图像输出路径格式 2025-01-14 04:05:13 +08:00
3fbc9a3eae 游戏筛选:按精选 2025-01-13 14:34:12 +08:00
541c801a56 web_images.type 2025-01-06 16:23:05 +08:00
0dc2a2d144 筛取属于游戏的且非封面的图像 2025-01-04 21:13:45 +08:00
59d28e6803 修正收集阅读行为 2025-01-04 21:05:13 +08:00
bf678b7440 works 图片数 2025-01-04 19:01:27 +08:00
56013d791b games 图片数 2025-01-04 18:44:34 +08:00
36d11e7542 按游戏标签筛选图像 2025-01-04 18:39:53 +08:00
2f7182d211 数据库中筛选:按游戏标签 2025-01-04 08:45:01 +08:00
7eb3974abb 如果本地文件存在,直接输出 2024-12-31 07:07:03 +08:00
7799298282 存储到data 2024-12-31 06:57:53 +08:00
310797a6e0 从 oss 下载原图 2024-12-30 22:48:11 +08:00
b4211d801b 新增字段与索引 2024-12-29 22:32:00 +08:00
935420d43a web_article 2024-12-28 20:11:15 +08:00
ed450130da DEBUG 2024-12-28 19:45:23 +08:00
1c86dfb107 防止重复 2024-12-28 19:43:37 +08:00
4e95885a4c 去除点赞引起的重复结果 2024-12-28 18:29:16 +08:00
43d35834b5 合并查询 fan 2024-12-28 18:21:01 +08:00
41a148c449 优化颜色筛选 2024-12-28 08:15:30 +08:00
926d55e646 更混乱 2024-12-28 06:41:29 +08:00
c8b2dd01ac 数据混乱 2024-12-28 06:36:48 +08:00
6ce5885a50 DEBUG 2024-12-28 06:28:07 +08:00
155ce8574f 修正筛选 2024-12-28 05:52:21 +08:00
cb4bd0ad1f DEBUG 2024-12-28 05:44:25 +08:00
27a390f0d6 type 2024-12-28 04:40:56 +08:00
e4837a22dc 收藏类型判断: type为 string 2024-12-28 04:32:17 +08:00
06c6e70a44 榜单字段Int 2024-12-28 02:12:00 +08:00
6c42b01904 榜单字段 2024-12-28 02:07:47 +08:00
7a4747f9d3 修正点赞数 2024-12-27 21:49:33 +08:00
450822dc89 文章分类ID 2024-12-24 23:07:55 +08:00
178085305d 收藏views 2024-12-24 19:35:44 +08:00
abe1a0822f 补充字段 category_id 2024-12-24 19:19:07 +08:00
d9a719da5b 收藏夹封面 2024-12-24 18:17:26 +08:00
ace0e6213c 增加 views 2024-12-24 17:22:48 +08:00
cb9e5a5e77 after type 2024-12-21 11:27:53 +08:00
0d0b9c1659 搜索失败不再终止 2024-12-21 11:14:42 +08:00
d0fb0a0c33 增加 emoji 表情计数字段 2024-12-21 11:09:56 +08:00
a8d1748fc9 pagesize 2024-12-16 13:57:33 +08:00
eb7ea4acc3 分页参数 2024-12-16 13:56:34 +08:00
7d28cdac53 游戏补充分类ID 2024-12-16 12:53:09 +08:00
c4ec2be535 补充游戏字段 2024-12-16 12:49:33 +08:00
32101bb228 补充用户信息 2024-12-16 12:41:42 +08:00
dc86f93a28 修正地址 2024-12-16 11:54:05 +08:00
c4a66fa06d 消除调试日志 2024-12-13 08:57:28 +08:00
417df7710f 跳过注释 2024-12-13 08:34:18 +08:00
d1499a7fab GraphQLType 2024-12-12 11:31:32 +08:00
21a8560460 text_list 2024-12-06 13:14:42 +08:00
cabe9dc94e 收藏夹列表 2024-12-06 08:51:31 +08:00
6d10d4e86e 筛选条件 2024-12-06 08:25:17 +08:00
bbd9bb9af5 合并查询 text_count 2024-12-06 08:09:42 +08:00
492e792c27 game 合并查询 2024-12-06 07:46:00 +08:00
87d5a64691 游戏 作品 文章的收藏点赞 2024-12-06 05:20:09 +08:00
9f775a12c2 Fan 2024-12-03 07:01:00 +08:00
92fa9e8a2c order 2024-12-03 05:49:06 +08:00
8f5486b0cf user 2024-12-03 05:39:30 +08:00
b804dd3700 collection 2024-12-03 05:27:31 +08:00
198ad4487f DEBUG 2024-12-03 02:19:38 +08:00
b6fa4eaeae sort 2024-12-03 02:04:47 +08:00
12fcdf08b2 praise_count 2024-12-02 20:30:32 +08:00
d1d5458b28 收藏去除收藏 2024-12-02 20:01:08 +08:00
3d1a8d3884 first 2024-12-02 12:43:59 +08:00
1d78500109 collect_id 2024-12-02 11:17:39 +08:00
06070edded token 简化 2024-12-02 11:11:40 +08:00
51c8a7fe2e collect_id 2024-12-02 07:42:38 +08:00
9519ca7e65 移除参数转换 2024-12-02 07:11:06 +08:00
dc30044cf8 移除sqlx 2024-12-02 07:07:56 +08:00
e96bf3497e 补充 user 2024-12-02 06:57:26 +08:00
f4bec98b7a API分页 2024-12-02 06:53:52 +08:00
520e3bade6 修正有外部筛选的 total 2024-12-02 03:34:21 +08:00
2ebcb15fd7 清理注释 2024-11-29 06:01:10 +08:00
ec75044126 DEBUG 2024-11-29 05:58:44 +08:00
de09f12fe1 按 text 筛选支持自由排序 2024-11-29 05:49:48 +08:00
b8d32b4247 图像按点赞顺序排序 2024-11-29 05:12:54 +08:00
1c3bc5f37f PraiseCount 2024-11-29 04:54:39 +08:00
a12cad63e3 activity 2024-11-29 04:15:26 +08:00
679f36f109 如果查询了 total 字段 2024-11-29 03:32:24 +08:00
51e383fd39 images "total" 2024-11-29 03:25:08 +08:00
60626e7ca5 游戏列表支持分页 2024-11-27 21:58:26 +08:00
f817aa12aa debug DefaultValue 2024-11-27 21:07:11 +08:00
bcfde1999c works 2024-11-27 20:26:18 +08:00
a015fca246 排序条件 2024-11-27 07:28:45 +08:00
b1b030acf3 games 2024-11-27 06:46:59 +08:00
18cec4651e 統計 2024-11-21 21:46:15 +08:00
a4e6a87d59 降低笛卡爾積 2024-11-19 21:39:16 +08:00
38253f0ce5 標準 2024-11-19 21:09:25 +08:00
4b856887a0 移除多余参数 2024-11-19 03:47:46 +08:00
00b2c7eed2 移除日志 2024-11-19 03:23:24 +08:00
57cb1c74a0 双精度色彩检索 2024-11-19 02:31:20 +08:00
8b369085c0 减少传输耗时 2024-11-19 00:51:57 +08:00
8d1b13ac82 收集阅读行为 2024-11-18 15:30:25 +08:00
defb599eca OCR 搜索按上传时间倒序 2024-11-17 22:02:56 +08:00
678e05ede4 补空参 2024-11-17 19:05:14 +08:00
c1f953ce72 跳过空值 2024-11-17 18:35:47 +08:00
ac46407a30 合并join 2024-11-17 17:58:25 +08:00
a1ce51e828 合并join 2024-11-17 17:45:24 +08:00
f8493115af 筛选:兴趣推荐 2024-11-17 14:08:45 +08:00
a7845dbd4a 偏好推荐修正 2024-11-17 13:59:32 +08:00
bec2c9075b DEBUG 2024-11-17 01:27:23 +08:00
200a6b2842 分类筛选 2024-11-17 01:02:45 +08:00
0249c4e1c7 标签全文索引搜索 2024-11-17 00:31:31 +08:00
84a0b8e711 DEBUG 2024-11-16 23:39:25 +08:00
b32486305c 检索精度 precision 2024-11-16 23:10:53 +08:00
d3d68d4bb5 数据库中筛选:喜欢的截图 2024-11-15 15:15:19 +08:00
2666f9ebe3 点赞类型修正 2024-11-15 15:13:15 +08:00
8229c02374 DEBUG 2024-11-15 13:56:05 +08:00
6558e02cb2 修正复合筛选条件 2024-11-15 13:19:45 +08:00
ac67eabd29 修正点赞判断 2024-11-15 12:50:29 +08:00
c010666b8b 减少日志 2024-11-15 12:32:18 +08:00
8345dfcd5f 快速执行, 跳过错误的行 2024-11-15 12:31:44 +08:00
cc5471284e 修正色调排序 2024-11-15 11:58:54 +08:00
bddb184c78 按游戏类型筛选图像 2024-11-14 21:55:20 +08:00
8615a7b5bf 索引 2024-11-14 20:36:12 +08:00
84a2ee3bce 减少日志 2024-11-14 20:13:38 +08:00
ff087a0b16 修正字段 2024-11-14 20:03:53 +08:00
d488ef42e8 更新色彩 2024-11-14 19:43:47 +08:00
f647a04339 图像色调计算 2024-11-14 19:31:59 +08:00
3229e70023 集成颜色字段 2024-11-14 16:19:14 +08:00
4785fb9f8f 支持 颜色筛选 2024-11-14 13:35:42 +08:00
8c34739ccd 防止注入参数 2024-11-14 13:24:47 +08:00
e41c20471b TODO 2024-11-14 12:51:18 +08:00
54092a0b9a 使用命令传递启动参数 2024-11-14 11:12:57 +08:00
ca7ff275c6 收藏夹中的截图 2024-11-11 02:17:25 +08:00
a55f5f0a78 数据库中筛选:收藏夹中的截图 2024-11-11 00:32:06 +08:00
c2c14444a9 修正排序 2024-11-10 23:30:09 +08:00
ae73a99747 修正排序 2024-11-10 20:52:03 +08:00
ea868c6a8d 精确搜索 2024-11-10 20:07:23 +08:00
03b5aa6bca 跳过无数据 2024-11-10 17:59:26 +08:00
a59ff7a108 跳过无数据 2024-11-10 17:56:56 +08:00
05057b7279 补全选择器 2024-11-10 17:20:12 +08:00
450318f118 修正排序 2024-11-10 16:25:36 +08:00
840c1dd5a4 排序规则 2024-11-10 12:21:45 +08:00
241952e680 筛选指定用户点赞过的图 2024-11-10 11:09:27 +08:00
6a6e9a5283 修正筛选 2024-11-10 10:38:24 +08:00
661a124dd5 修正分页 2024-11-10 10:08:40 +08:00
b4400f00de 窗口分页 2024-11-10 09:43:57 +08:00
d5859d8126 跳过空值查询 2024-11-09 12:17:23 +08:00
43dca8caa5 elasticsearch 移除 2024-11-09 12:15:03 +08:00
b07fcb3910 elasticsearch 2024-11-09 12:11:13 +08:00
9fc57564e1 debug 2024-11-09 11:52:57 +08:00
6c5d2d20d5 数据库中筛选:游戏上线年份 2024-11-09 11:26:26 +08:00
3dc8711ae9 按设备筛选游戏截图 2024-11-09 11:08:49 +08:00
7de1958974 收藏状态 2024-11-09 10:45:43 +08:00
cdf32b6268 增加字段:用户是否点赞 2024-11-09 09:41:52 +08:00
b01c186acc 按时间筛选 2024-11-09 07:06:13 +08:00
85a71a64ff count 2024-11-09 05:55:44 +08:00
5bbb3a8797 修正 Follower 筛选 2024-11-08 17:19:17 +08:00
a196ada6e3 仅选取游戏截图 2024-11-08 17:06:46 +08:00
00abb0e504 过滤非文章图片 2024-11-08 09:18:36 +08:00
a541d2b263 修正分页截取范围 2024-11-08 09:13:38 +08:00
08266046b8 orientation 2024-11-07 07:55:31 +08:00
0fef776ece 处理 text NULL 字段 2024-11-07 05:31:30 +08:00
37156568ed 多级递归修正 2024-11-07 02:51:59 +08:00
9fa5007ac4 支持单图查询 2024-11-07 02:23:52 +08:00
cd1db1b82a 简化更新 2024-11-07 01:49:53 +08:00
7da1c994e9 修正 update 2024-11-06 02:49:54 +08:00
4eb3857ac9 更新 2024-11-06 02:40:47 +08:00
f731cd4dec 合并字段 2024-11-06 01:20:06 +08:00
1bce5ea799 多级加载 2024-11-06 01:13:15 +08:00
325264ddad 修正列表分片 2024-11-05 22:22:56 +08:00
da6913eaba 分页查询 2024-11-05 18:42:54 +08:00
22 changed files with 2778 additions and 1522 deletions

35
.gitignore vendored
View File

@@ -1,31 +1,4 @@
# ---> Go data
# If you prefer the allow list template instead of the deny list, see community template: dist
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore tmp
# oss
# 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

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"bitoAI.codeCompletion.enableAutoCompletion": false
}

View File

@@ -1,29 +1,33 @@
export HOST=ai
# 运行本地开发环境(使用SSH隧道代理端口) # 运行本地开发环境(使用SSH隧道代理端口)
dev: dev:
@if ! go list -m github.com/air-verse/air@latest >/dev/null 2>&1; then \ @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..."; \ echo "github.com/air-verse/air@latest is not installed. Installing..."; \
go get github.com/air-verse/air@latest; \ go get github.com/air-verse/air@latest; \
fi; 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; \ sleep 1; \
go run github.com/air-verse/air@latest --build.cmd "go build -o ./data/ bin/main.go" --build.bin "./data/main"; \ go run github.com/air-verse/air@latest --build.cmd "go build -o ./data/ bin/main.go" --build.bin "./data/main"; \
wait 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: build:
go mod tidy go mod tidy
go build -o dist/main bin/main.go go build -o dist/main bin/main.go
# 更新部署到服务器 # 更新部署到服务器(生产和测试)
update: build update: build
host="root@main" scp dist/main $(HOST):~/webp/main_new
ssh $host "mv ~/webp/main ~/webp/main_old" ssh ${HOST} "mv ~/webp/main ~/webp/main_old"
scp dist/main $host:~/webp/main ssh ${HOST} "mv ~/webp/main_new ~/webp/main"
ssh ${HOST} "rm ~/webp/main_old"
rm -rf dist rm -rf dist
ssh $host "systemctl restart webp"
ssh $host "rm ~/webp/main_old"
# 设为系统服务和日志轮转 # 设为系统服务和设置日志轮转
service: service:
sudo cp webp.service /etc/systemd/system/webp.service sudo cp webp.service /etc/systemd/system/webp.service
sudo systemctl enable webp sudo systemctl enable webp
@@ -37,3 +41,7 @@ gorse:
export GORSE_DASHBOARD_USER="gorse" export GORSE_DASHBOARD_USER="gorse"
export GORSE_DASHBOARD_PASS="gorse" export GORSE_DASHBOARD_PASS="gorse"
curl -fsSL https://gorse.io/playground | bash curl -fsSL https://gorse.io/playground | bash
# 安装搜图服务 python embedding
reverse:
git clone

104
README.md
View File

@@ -1,59 +1,83 @@
# webp # webp
- [x] 相似图像推荐(迁移)
- [x] 以图搜图(迁移)
- [x] 标签筛选(补充筛选条件)
- [x] WEBP 缩略图
- [ ] 原始图像 # 数据迁移
- [ ] 缩略缓存 - [ ] CDN 回源直接到服务器
- [ ] 静态原始图像
- [ ] 异地备份
- [ ] 视频流媒体
- [ ] 上传图像接口
- [ ] 不同类型的用途(文章, 截图, 头像) # 偏好推荐
- [ ] 增量备份 - [ ] 收集用户访问记录点赞记录
- [ ] 收集用户推荐反馈
- [ ] 调整偏好推荐
cdn(img.gameui.net) -> main -> img # OCR
/articles/{id}/{hash} - [ ] 脚本统计各关键词总量
/users/{id}/{hash} - [x] 筛选条件支持多选 颜色,风格 | 类型,主题,功能,材质图案(多选逗号分隔)
- [x] 按颜色筛选, 周期性脚本自动补全三色
UPDATE web_images SET description = '' WHERE description IS NULL; # 熱門統計
ALTER TABLE web_images MODIFY description VARCHAR(255) DEFAULT ''; - [ ] 從caddy日誌收集用戶訪問目標數據時間
- [ ] 計算指標作爲API提供輸出
- [ ] 解析 cdn.gameui.net 到 cdn 服务, 并将 cdn 回源指向 def.gameui.net 其解析到新服务器 - [ ] 數據與指標統覽後臺
- [ ] 支持带参数下载指定尺寸图像 - [x] 游戏 作品 文章 收藏夹 分别添加喜欢数点赞数是否喜欢是否点赞字段
- [x] 提供webp生成服务
- [x] 提供流媒体服务 ```sql
- [x] 点击播放之前不加载视频(减少流量消耗) -- 添加列
- [x] 使用封面图片替代加载视屏第一帧 ALTER TABLE web_images ADD COLUMN color_0_r TINYINT UNSIGNED;
- [x] GraphQL 风格API ALTER TABLE web_images ADD COLUMN color_0_g TINYINT UNSIGNED;
- [ ] 列表翻页 ALTER TABLE web_images ADD COLUMN color_0_b TINYINT UNSIGNED;
- [ ] OCR 查询支持 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 SET GLOBAL sort_buffer_size = 268435456; -- 设置为 256MB (268435456 字节)
const query = `/api?query={users(id:1){total,list{id,user_name,avatar}}}`
fetch(query).then(res => res.json()).then(data => {
console.log(data)
})
// GET 查询 user_id 为 2 的图像列表, 并且包含 user 的部分信息, 以及符合筛选条件的总数 total -- 降序索引
const query = `/api?query={images(user_id:2){total,list{id,content,user{id,user_name,avatar}}}}` CREATE INDEX idx_id_desc ON web_images (id DESC);
fetch(query).then(res => res.json()).then(data => {
console.log(data)
})
// GET 查询所有图像的前2个(第一页) -- 复合筛选降序索引 article_category_top_id
const query = `/api?query={images(first:2){total,list{id,content,user:{id,user_name,avatar}}}}` CREATE INDEX idx_acti_type_id_desc ON web_images (article_category_top_id, type, id DESC);
fetch(query).then(res => res.json()).then(data => {
console.log(data)
})
-- -- 为 web_images 表的 day_rank 行设置倒序索引用于排序
-- CREATE INDEX idx_day_rank_desc ON web_images (day_rank DESC);
// GET 查询所有图像的指定id之后的前2个(翻页) -- 为 web_images 表创建倒排复合索引
const query = `/api?query={images(after:2,first:2){total,list{id,content,user{id,user_name,avatar}}}}` CREATE INDEX idx_article_dayrank ON web_images(article_category_top_id, day_rank DESC);
fetch(query).then(res => res.json()).then(data => { CREATE INDEX idx_article_weekrank ON web_images(article_category_top_id, week_rank DESC);
console.log(data) 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认证观众身份 对视频地址添加有效期, 过期需由服务器重新提供token认证观众身份

276
api/article.go Normal file
View 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
View 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
View 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
View 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
}

View File

@@ -2,24 +2,169 @@ package api
import ( import (
"fmt" "fmt"
"image"
"log" "log"
"os"
"reflect" "reflect"
"regexp"
"sort"
"strings" "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"
"github.com/graphql-go/graphql/language/ast" "github.com/graphql-go/graphql/language/ast"
"github.com/jmoiron/sqlx" "github.com/spf13/viper"
"github.com/mitchellh/mapstructure" "github.com/thoas/go-funk"
"gorm.io/driver/mysql" "gorm.io/driver/mysql"
"gorm.io/gorm" "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 类型的函数 // 自动生成 GraphQL 类型的函数
func generateGraphQLType(model interface{}) (*graphql.Object, error) { func GraphQLType(model interface{}) *graphql.Object {
modelType := reflect.TypeOf(model) modelType := reflect.TypeOf(model)
if modelType.Kind() != reflect.Struct { if modelType.Kind() != reflect.Struct {
return nil, fmt.Errorf("model must be a struct") fmt.Println("输入的类型必须是结构体")
return nil
} }
fields := graphql.Fields{} fields := graphql.Fields{}
@@ -45,570 +190,54 @@ func generateGraphQLType(model interface{}) (*graphql.Object, error) {
return graphql.NewObject(graphql.ObjectConfig{ return graphql.NewObject(graphql.ObjectConfig{
Name: modelType.Name(), Name: modelType.Name(),
Fields: fields, Fields: fields,
}), nil })
} }
func NewSchema(config Config) (graphql.Schema, error) { // 判断指定字段是否存在
func existField(selections []ast.Selection, name string) bool {
db, err := gorm.Open(mysql.Open(fmt.Sprintf( for _, field := range selections {
"%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", if f, ok := field.(*ast.Field); ok && f.Name.Value == name {
config.Mysql.UserName, return true
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)
} }
} }
return texts, nil return false
} }
return p.Source.(Image).Text, nil
},
},
},
})
image.AddFieldConfig("user", &graphql.Field{Type: user, Description: "图像所属用户"}) // 将 list 中的字段提取出来用于查询
image.AddFieldConfig("similars", &graphql.Field{Type: graphql.NewList(image), Description: "相似的图像", Resolve: func(p graphql.ResolveParams) (interface{}, error) { func get_fields(requestedFields []ast.Selection) (fields []string) {
return []Image{}, nil
}})
// 将 list 中的字段提取出来用于查询
get_fields := func(requestedFields []ast.Selection) (fields []string) {
for _, field := range requestedFields { for _, field := range requestedFields {
fieldAST, ok := field.(*ast.Field) fieldAST, _ := field.(*ast.Field)
if ok && fieldAST.Name.Value == "list" { if fieldAST.Name.Value == "list" {
for _, field := range fieldAST.SelectionSet.Selections { for _, field := range fieldAST.SelectionSet.Selections {
fieldAST, ok := field.(*ast.Field) fieldAST, _ := field.(*ast.Field)
if ok { if fieldAST.Name.Value == "text_count" {
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" {
// 跳过自定义字段
continue continue
} }
fields = append(fields, fieldAST.Name.Value) fields = append(fields, fieldAST.Name.Value)
} }
} }
} }
}
return fields 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: "分类总数"},
},
}),
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
},
},
"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))
}
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 orderType = graphql.NewEnum(graphql.EnumConfig{
Name: "OrderType",
Description: "排序类型",
Values: graphql.EnumValueConfigMap{
"ASC": &graphql.EnumValueConfig{
Value: "ASC",
Description: "升序",
},
"DESC": &graphql.EnumValueConfig{
Value: "DESC",
Description: "降序",
},
},
})
type Cache struct {
time time.Time
ids []string
}
var cache map[string]Cache = make(map[string]Cache)

719
api/image.go Normal file
View 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
View 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
View 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
},
}

View File

@@ -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
View 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
View 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
},
}

View File

@@ -2,12 +2,14 @@ package main
import ( import (
"context" "context"
"flag"
"fmt" "fmt"
"log" "log"
"net/http" "net/http"
"net/url" "net/url"
"os"
"path/filepath"
"runtime" "runtime"
"strings"
"time" "time"
"regexp" "regexp"
@@ -18,11 +20,9 @@ import (
"git.satori.love/gameui/webp/api" "git.satori.love/gameui/webp/api"
"git.satori.love/gameui/webp/models" "git.satori.love/gameui/webp/models"
_ "github.com/go-sql-driver/mysql" _ "github.com/go-sql-driver/mysql"
"github.com/graphql-go/graphql"
"github.com/graphql-go/handler" "github.com/graphql-go/handler"
"github.com/milvus-io/milvus-sdk-go/v2/entity"
"github.com/spf13/viper" "github.com/spf13/viper"
lru "github.com/hashicorp/golang-lru/v2"
) )
// string 转换为 int, 如果转换失败则返回默认值 // string 转换为 int, 如果转换失败则返回默认值
@@ -63,139 +63,25 @@ func LogComponent(startTime int64, r *http.Request) {
func LogRequest(next http.Handler) http.Handler { func LogRequest(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer LogComponent(time.Now().UnixNano(), r) // 最后打印日志 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 mysqlConnection models.MysqlConnection
var milvusConnection models.MilvusConnection 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() { func main() {
runtime.GOMAXPROCS(runtime.NumCPU() - 1) 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 { if err := viper.ReadInConfig(); err != nil {
log.Println("读取配置文件失败", err) log.Println("读取配置文件失败", err)
} }
@@ -203,6 +89,7 @@ func main() {
models.InitConfig(config) models.InitConfig(config)
models.ZincInit() models.ZincInit()
api.InitDefault(config)
mysqlConnection.Init() mysqlConnection.Init()
milvusConnection.Init() milvusConnection.Init()
@@ -212,374 +99,67 @@ func main() {
return return
} }
// graphql Schema if config.GetBool("oss.local") {
schema, err := api.NewSchema(api.Config{ fmt.Println("开启图像色调计算")
Mysql: api.ConfigMysql{ go api.CheckColorNullRows(0)
Host: config.GetString("mysql.host"), }
Port: config.GetInt("mysql.port"), if config.GetBool("gorse.open") {
Database: config.GetString("mysql.database"), fmt.Println("开启用户偏好收集")
UserName: config.GetString("mysql.user"), api.GorseInit(config.GetString("gorse.host"), config.GetInt("gorse.port"))
Password: config.GetString("mysql.password"), 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 { if err != nil {
log.Fatalf("failed to create new schema, error: %v", err) 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{ http.Handle("/api", LogRequest(handler.New(&handler.Config{
Schema: &schema, Schema: &schema,
Playground: true, Playground: true,
Pretty: false, Pretty: false,
}))) })))
// 获取图片信息列表(分页) // URL 格式: /image/{type}-{id}-{width}x{height}-{fit}.{format}?version
http.HandleFunc("/api/images", func(w http.ResponseWriter, r *http.Request) { http.HandleFunc("/image", func(w http.ResponseWriter, r *http.Request) {
defer LogComponent(time.Now().UnixNano(), r) // 最后打印日志 defer LogComponent(time.Now().UnixNano(), r) // 最后打印日志
// 私域: (自己的图片, 自己的文章, 自己的精选集, 点赞收藏精选集) // 如果本地文件存在,直接输出
// 条件查询(模糊搜索, 时间区间, 作者, 标签, 分类, 精选集, 状态, 置顶, 模糊权重)(权重规则:权重指数) filePath := filepath.Join("data/webp", r.URL.Path)
// 条件筛选(交集, 并集, 差集, 子集) if _, err := os.Stat(filePath); err == nil {
// 文字搜索支持翻页 http.ServeFile(w, r, filePath)
// 文字搜索支持与按颜色筛选混合
// 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), ",")
}
return return
} }
// 拼接查询条件, 超级简洁写法 reg := regexp.MustCompile(`^/image/([a-z]+)-([0-9]+)-([0-9]+)x([0-9]+)-([a-z]+).(jpg|jpeg|png|webp)$`)
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)$`)
matches := reg.FindStringSubmatch(r.URL.Path) matches := reg.FindStringSubmatch(r.URL.Path)
if len(matches) != 4 { if len(matches) != 7 {
http.Error(w, "URL 格式错误", http.StatusNotFound) log.Println("URL 格式错误", matches)
w.WriteHeader(http.StatusNotFound)
return 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) content, err := mysqlConnection.GetImageContent(group, id)
if err != nil { if err != nil {
log.Println("获取图片失败", format, err) log.Println("获取图片失败", format, err)
http.Error(w, err.Error(), http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
return return
} }
var img models.Image var img models.Image
if err := img.Init(content); err != nil { if err := img.Init(content); err != nil {
log.Println("初始化图片失败", format, err) 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) w.WriteHeader(http.StatusNotFound)
return return
} }
@@ -589,9 +169,20 @@ func main() {
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
return 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("Content-Type", "image/webp")
w.Header().Set("Cache-Control", "max-age=31536000") w.Header().Set("Cache-Control", "max-age=604800")
w.Write(data) w.Write(data)
}) })

45
go.mod
View File

@@ -1,26 +1,26 @@
module git.satori.love/gameui/webp module git.satori.love/gameui/webp
go 1.23 go 1.23.3
toolchain go1.23.2
require ( require (
github.com/alibabacloud-go/darabonba-openapi v0.1.18 github.com/alibabacloud-go/darabonba-openapi v0.1.18
github.com/alibabacloud-go/tea v1.2.1 github.com/alibabacloud-go/tea v1.2.1
github.com/alibabacloud-go/vod-20170321/v2 v2.16.10 github.com/alibabacloud-go/vod-20170321/v2 v2.16.10
github.com/disintegration/imaging v1.6.2 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/graphql v0.8.1
github.com/graphql-go/handler v0.2.3 github.com/graphql-go/handler v0.2.3
github.com/hashicorp/golang-lru/v2 v2.0.7 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/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/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 ( require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4 // 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/debug v0.0.0-20190504072949-9472017b5c68 // indirect
github.com/alibabacloud-go/endpoint-util v1.1.0 // 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/alibabacloud-go/tea-xml v1.1.2 // indirect
github.com/aliyun/credentials-go v1.1.2 // indirect github.com/aliyun/credentials-go v1.1.2 // indirect
github.com/clbanning/mxj/v2 v2.7.0 // indirect github.com/clbanning/mxj/v2 v2.7.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/elastic/elastic-transport-go/v8 v8.3.0 // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect
github.com/fsnotify/fsnotify v1.7.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/golang/protobuf v1.5.4 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // 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/magiconair/properties v1.8.7 // indirect
github.com/milvus-io/milvus-proto/go-api v0.0.0-20230301092744-7efc6eec15fd // 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/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // 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/rogpeppe/go-internal v1.12.0 // indirect
github.com/sizeofint/webp-animation v0.0.0-20190207194838-b631dc900de9 // indirect github.com/sizeofint/webp-animation v0.0.0-20190207194838-b631dc900de9 // indirect
github.com/spf13/afero v1.11.0 // indirect github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.7.0 // indirect github.com/spf13/cast v1.7.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // 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/subosito/gotenv v1.4.2 // indirect
github.com/tjfoc/gmsm v1.3.2 // indirect github.com/tjfoc/gmsm v1.3.2 // indirect
golang.org/x/net v0.28.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 // indirect
golang.org/x/sys v0.25.0 // indirect go.opentelemetry.io/otel v1.31.0 // indirect
golang.org/x/text v0.18.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 golang.org/x/time v0.6.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240812133136-8ffd90a71988 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect
google.golang.org/grpc v1.65.0 // indirect google.golang.org/grpc v1.67.1 // indirect
google.golang.org/protobuf v1.34.2 // indirect google.golang.org/protobuf v1.35.1 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // 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 ( require (
github.com/aliyun/aliyun-oss-go-sdk v2.2.7+incompatible github.com/aliyun/aliyun-oss-go-sdk v2.2.7+incompatible
github.com/chai2010/webp v1.1.1 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 github.com/sizeofint/gif-to-webp v0.0.0-20210224202734-e9d7ed071591
golang.org/x/image v0.20.0 // indirect golang.org/x/image v0.20.0 // indirect
) )

84
go.sum
View File

@@ -1,5 +1,9 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 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/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 h1:iC9YFYKDGEy3n/FtqJnOkZsene9olVspKmkX5A2YBEo=
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc= 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= 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 h1:qU1vwGIBb3UJ8BwunHDRFtAhS6jnQLnde/yk0+Ih2GY=
github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw= 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/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 h1:jTRmEccAJ4MGrhFOrPMpNGIJ/eybIgwKpcACsrTEapk=
github.com/chai2010/webp v1.1.1/go.mod h1:0XVwvZWdjjdxpUEIf7b9g9VkHFnInUSYujwqTLEuldU= 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= 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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= 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/doug-martin/goqu/v9 v9.19.0 h1:PD7t1X3tRcUiSdc5TEyOFKujZA5gs3VSA7wxSvBx7qo=
github.com/elastic/elastic-transport-go/v8 v8.3.0/go.mod h1:87Tcz8IVNe6rVSLdBux1o/PEItLtyabHU3naC7IoqKI= github.com/doug-martin/goqu/v9 v9.19.0/go.mod h1:nf0Wc2/hV3gYK9LiyqIrzBEVGlI8qW3GuDCEobC4wBQ=
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/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 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.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/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/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 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 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.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 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-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-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.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.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= 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/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/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/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/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 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/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 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 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.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 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 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.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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 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.10.1/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 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 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 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.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.6/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 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-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= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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/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 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 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= 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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 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.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 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 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 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= 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 h1:7JVkAn5bvUJ7HtU08iW6UiD+UTmJTIToHCfeFzkcCxM=
github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w= 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.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.30/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.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 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/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/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 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-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-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-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/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.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.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= 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.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= 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/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-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.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.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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-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.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 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.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.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.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 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 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 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= 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-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-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 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-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs=
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/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 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.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 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.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= 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.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= 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 h1:rqzndB2lIQGivcXdTuY3Y9NBvr70X+y77woofSRluec=
google.golang.org/grpc/examples v0.0.0-20220617181431-3e7b97febc7f/go.mod h1:gxndsbNG1n4TZcHGgsYEfVGnTxqfEdfiDv6/DADXX9o= 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.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 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 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-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 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/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.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= 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.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -1,10 +1,13 @@
package models package models
import ( import (
"fmt"
"image" "image"
"io" "io"
"log" "log"
"net/http" "net/http"
"os"
"path/filepath"
"regexp" "regexp"
"github.com/chai2010/webp" "github.com/chai2010/webp"
@@ -20,18 +23,30 @@ type Image struct {
} }
// 初始化图片 // 初始化图片
func (img *Image) Init(content string) (err error) { func (img *Image) Init(content string) error {
var body io.ReadCloser
if len(regexp.MustCompile(`image.gameuiux.cn`).FindStringSubmatch(content)) > 0 { if len(regexp.MustCompile(`image.gameuiux.cn`).FindStringSubmatch(content)) > 0 {
key := regexp.MustCompile(`^https?://image.gameuiux.cn/`).ReplaceAllString(content, "") 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") 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 { if err != nil {
log.Println("读取图片失败", err) log.Println("打开本地图片失败:", err)
return return err
} }
defer body.Close() defer body.Close()
@@ -42,25 +57,25 @@ func (img *Image) Init(content string) (err error) {
if err != nil { if err != nil {
log.Println("读取图片失败", err) log.Println("读取图片失败", err)
return return err
} }
return return nil
} }
// 将文件解码为 image.Image // 将文件解码为 image.Image
img.image, img.format, err = image.Decode(body) img.image, img.format, err = image.Decode(body)
if err != nil { if err != nil {
log.Println("解码图像失败", err) log.Println("解码图像失败", err)
return return err
} }
return return nil
} else { } else {
var resp *http.Response var resp *http.Response
log.Println("直接从网络下载图片:", content) log.Println("直接从网络下载图片:", content)
resp, err = http.Get(content) resp, err := http.Get(content)
if err != nil { if err != nil {
log.Println("下载图片失败", err) log.Println("下载图片失败", err)
return return err
} }
defer resp.Body.Close() defer resp.Body.Close()
@@ -70,19 +85,19 @@ func (img *Image) Init(content string) (err error) {
img.data, err = io.ReadAll(resp.Body) img.data, err = io.ReadAll(resp.Body)
if err != nil { if err != nil {
log.Println("读取图片失败", err) log.Println("读取图片失败", err)
return return err
} }
println("数据长度:", len(img.data)) println("数据长度:", len(img.data))
return return nil
} }
// 将文件解码为 image.Image // 将文件解码为 image.Image
img.image, img.format, err = image.Decode(resp.Body) img.image, img.format, err = image.Decode(resp.Body)
if err != nil { if err != nil {
log.Println("解码图像失败", err) log.Println("解码图像失败", err)
return return nil
} }
return return nil
} }
} }

View File

@@ -9,6 +9,7 @@ import (
"log" "log"
"net/http" "net/http"
"net/url" "net/url"
"strconv"
) )
var ( var (
@@ -46,30 +47,31 @@ type Response struct {
ID string `json:"_id"` ID string `json:"_id"`
Score float64 `json:"_score"` Score float64 `json:"_score"`
Timestamp string `json:"@timestamp"` Timestamp string `json:"@timestamp"`
Source struct { //Source struct {
Timestamp string `json:"@timestamp"` // Timestamp string `json:"@timestamp"`
Athlete string `json:"Athlete"` // Athlete string `json:"Athlete"`
City string `json:"City"` // City string `json:"City"`
Country string `json:"Country"` // Country string `json:"Country"`
Discipline string `json:"Discipline"` // Discipline string `json:"Discipline"`
Event string `json:"Event"` // Event string `json:"Event"`
Gender string `json:"Gender"` // Gender string `json:"Gender"`
Medal string `json:"Medal"` // Medal string `json:"Medal"`
Season string `json:"Season"` // Season string `json:"Season"`
Sport string `json:"Sport"` // Sport string `json:"Sport"`
Year int `json:"Year"` // Year int `json:"Year"`
} `json:"_source"` //} `json:"_source"`
} `json:"hits"` } `json:"hits"`
} `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 { 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开始向后取切片 // 如果 after 不为 0, 从这个ID开始向后取切片
if after != "" { if after != 0 {
for i, id := range id_list { for i, id := range id_list {
if id == after { if id == after {
id_list = id_list[i+1:] 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开始向前取切片 // 如果 before 不为 0, 从这个ID开始向前取切片
if before != "" { if before != 0 {
for i, id := range id_list { for i, id := range id_list {
if id == before { if id == before {
id_list = id_list[:i] id_list = id_list[:i]
@@ -146,7 +148,8 @@ func ZincSearch(query map[string]interface{}) (rest Response, err error) {
log.Println("解析响应失败", err) log.Println("解析响应失败", err)
return return
} }
return
return rest, err
} }
func ZincPut(data map[string]interface{}) (err error) { func ZincPut(data map[string]interface{}) (err error) {