Compare commits

..
65 Commits
Author SHA1 Message Date
ygxbnet 0602f1d21d feat(settings): 添加版本信息显示功能
部署开发环境 / deploy-dev (push) Successful in 2m3s
- 在 API 模块中添加获取版本信息的接口方法
- 在设置页面组件中引入 API 并调用版本获取方法
- 使用 onMounted 钩子在组件挂载时获取版本数据
- 将获取到的版本信息绑定到模板中显示
- 完成设置页面的初始功能实现
2026-08-23 00:17:11 +08:00
ygxbnet 54a05c27ab chore(deps): 更新项目依赖版本
部署开发环境 / deploy-dev (push) Successful in 1m32s
- 将 Element Plus 相关导入语句注释掉
- 更新 axios 从 1.16.1 到 1.19.0 版本
- 更新 element-plus 从 2.14.1 到 2.14.4 版本
- 更新 vue 从 3.5.35 到 3.5.41 版本
- 更新 vue-router 从 5.1.0 到 5.2.0 版本
- 更新 @tailwindcss/vite 和 tailwindcss 至 4.3.3 版本
- 更新 pinia 从 3.0.4 到 4.0.3 版本
- 更新 devDependencies 中多个开发工具包版本
- 在类型声明文件中添加 oxlint 和 oxfmt 的忽略注释
2026-08-22 23:57:33 +08:00
ygxbnet bce8d30108 chore(deps): 更新项目依赖版本
部署开发环境 / deploy-dev (push) Successful in 4m0s
- 将 Go 版本从 1.26 升级到 1.27
- 更新 gin 框架从 v1.10.1 到 v1.12.0
- 更新 redis 客户端从 v9.12.1 到 v9.22.0
- 更新 viper 库从 v1.20.1 到 v1.21.0
- 更新多个间接依赖包到最新版本
- 添加了新的依赖包包括 go.mongodb.org/mongo-driver/v2 和相关工具库
2026-08-20 22:31:20 +08:00
ygxbnet 8dae755df2 refactor(token): 简化循环中的条件判断逻辑
部署开发环境 / deploy-dev (push) Successful in 1m13s
- 使用 min 函数替代 if-else 条件判断
- 减少代码行数提高可读性
- 保持原有功能不变
2026-07-27 00:44:25 +08:00
ygxbnet 9105cba067 chore(build): 更新自动导入配置以优化类型定义文件位置
部署开发环境 / deploy-dev (push) Successful in 3m0s
- 将 auto-imports.d.ts 和 components.d.ts 移至 src/types/import 目录
- 修改 tsconfig.app.json 配置以排除旧的类型定义文件路径
- 在 vite.config.ts 中更新类型定义文件输出路径配置
- 重新生成组件和自动导入的类型声明文件
2026-07-17 23:48:37 +08:00
ygxbnet fe16e8b2f9 fix(token): 修复去重选项标签显示问题
部署开发环境 / deploy-dev (push) Successful in 1m53s
- 将'不用去重'修改为'不去重'以保持表述一致性
2026-07-09 00:35:02 +08:00
ygxbnet bc15b19474 feat(data): 添加数据不去重功能
部署开发环境 / deploy-dev (push) Successful in 2m6s
- 在数据控制器中引入去重对象逻辑
- 根据去重对象配置决定是否执行去重操作
- 当去重对象为 false 时不进行去重直接写入数据库
- 在管理界面令牌管理页面添加不去重选项
- 表格中显示去重对象时将 false 显示为不去重中文描述
2026-07-09 00:21:04 +08:00
ygxbnet ac72b80505 feat(admin): 添加数据格式选项自定义功能
部署开发环境 / deploy-dev (push) Successful in 1m46s
- 在TokenManageView中增加自定义数据格式选项功能
- 实现添加数据格式按钮和输入框界面
- 添加确认和取消操作按钮
- 支持动态添加新的数据格式选项到下拉列表
- 整理数据格式选项顺序保持一致性
- 添加输入验证确保选项名称不为空
2026-07-07 23:55:01 +08:00
ygxbnet 080d62621e refactor(ui): 重构UI组件样式和对话框功能
部署开发环境 / deploy-dev (push) Successful in 1m54s
- 将 AddDataDialog 组件的布局改为 flex 布局并移除自定义 CSS 类
- 更新 TokenDetailView 的整体布局结构,使用 Tailwind CSS 类替换原生样式
- 优化 TokenManageView 的表单和表格界面,调整响应式设计
- 移除 Element Plus ElContainer 组件的引用
- 合并多个编辑对话框为统一的数据编辑对话框
- 优化进度条和标签的显示样式
- 调整按钮和表单项的间距和对齐方式
2026-07-07 23:38:23 +08:00
ygxbnet 7c9bb58806 更新 web/src/views/admin/TokenManageView.vue
部署开发环境 / deploy-dev (push) Successful in 1m36s
2026-07-06 04:58:06 +00:00
ygxbnet d21a1adfb2 更新 web/src/views/admin/TokenManageView.vue
部署开发环境 / deploy-dev (push) Failing after 2m38s
2026-07-06 04:51:46 +00:00
ygxbnet ac082785f4 refactor(ui): 重构应用样式和组件结构
部署开发环境 / deploy-dev (push) Successful in 2m8s
- 移除 App.vue 中的内联样式,将 CSS 变量定义移到 main.css
- 在 HomeView.vue 中使用 Element Plus Card 组件替代自定义卡片样式
- 更新 HomeView.vue 中的统计卡片布局,使用 Space 和 Card 组件
- 添加 fade-in 动画到全局主题配置
- 在 components.d.ts 中注册 ElCard 和 ElContainer 组件
- 移除 SettingsView.vue 中的空样式标签
- 使用 Tailwind CSS 类替代原有的自定义样式类
2026-06-20 18:41:12 +08:00
ygxbnet fc640407d9 feat(admin): 实现管理员登录认证和页面权限控制
- 添加管理员密码验证登录功能
- 实现登录状态管理和用户界面显示
- 集成权限检查确保页面访问安全
- 添加登录/登出流程处理
- 重构AdminView组件结构和样式
- 集成Element Plus图标和UI组件
- 添加设置页面路由配置
- 优化Token管理页面折叠表单设计
- 移除旧的响应式布局相关代码
- 更新应用标题动态渲染逻辑
2026-06-19 17:36:50 +08:00
ygxbnet 4100a51eb8 build(vite): 集成自动导入插件优化开发体验
部署开发环境 / deploy-dev (push) Successful in 2m18s
- 移除手动导入的 Vue 和 Element Plus 相关模块
- 集成 unplugin-auto-import 插件实现自动导入
- 集成 unplugin-vue-components 插件实现组件自动注册
- 添加自动生成的类型声明文件 auto-imports.d.ts 和 components.d.ts
- 配置 Element Plus 解析器支持按需导入
- 更新 tsconfig.app.json 包含自动生成的类型文件
2026-06-15 21:50:22 +08:00
ygxbnet d5a159be05 refactor(style): 调整Tailwind CSS样式优先级
- 将main.css导入从文件开头移至Element Plus样式之后
- 在Tailwind导入后添加important关键字以提高样式优先级
- 确保全局样式在组件库样式之后正确应用
2026-06-15 21:44:32 +08:00
ygxbnet 4e0b283fa9 refactor(admin): 重构部分页面样式代码,修剪多余代码
部署开发环境 / deploy-dev (push) Successful in 1m38s
- 将AdminView中的header和main标签替换为el-header和el-main组件
- 移除自定义的导航菜单样式,使用Element Plus的导航组件
- 将管理员下拉菜单替换为el-dropdown组件
- 更新所有颜色变量为Element Plus的CSS变量
- 移除自定义按钮和表单样式,统一使用Element Plus组件
- 优化响应式布局和间距处理
2026-06-14 01:23:33 +08:00
ygxbnet ae0dddd255 refactor(router): 将管理页面移至admin文件夹
部署开发环境 / deploy-dev (push) Successful in 2m55s
2026-06-13 23:41:32 +08:00
ygxbnet 9e8db7754f feat(build): 集成 Tailwind CSS 样式框架
- 安装并配置 Tailwind CSS 和相关依赖
- 在 main.ts 中引入全局样式文件 src/assets/main.css
- 添加 @tailwindcss/vite 插件到项目依赖
- 更新 pnpm-lock.yaml 中的依赖关系和版本信息
- 修复开发环境标题设置中的缩进问题
2026-06-13 23:38:23 +08:00
ygxbnet 201ee8c0f9 refactor(api): 重构API调用方式并优化数据管理
部署开发环境 / deploy-dev (push) Successful in 2m1s
- 将原有的axios直接调用替换为统一的api模块封装
- 新增api.ts文件集中管理所有API接口方法
- 删除旧的axios.ts配置文件
- 统一API响应数据结构处理,将result改为data字段
- 优化HomeView中的Token相关状态管理和数据获取逻辑
- 重构TokenDetailView中信息获取和删除操作的API调用
- 更新TokenManageView中Token列表管理的CRUD操作实现
- 简化组件间的数据传递和状态同步机制
2026-06-03 22:09:35 +08:00
ygxbnet 58a29f2b9d refactor(后端): 重构后端API响应数据格式,统一为Code Message Data 2026-06-03 18:03:37 +08:00
ygxbnet d6d2a4ad96 refactor(前端): 优化首页刷新数据逻辑
部署开发环境 / deploy-dev (push) Successful in 1m59s
2026-06-03 17:03:15 +08:00
ygxbnet a5dbbc4163 refactor(前端): 优化首页刷新数据逻辑
部署开发环境 / deploy-dev (push) Successful in 2m2s
2026-06-03 13:55:20 +08:00
ygxbnet 026ba10bb7 refactor(token): 优化令牌详情页面的数据获取逻辑
部署开发环境 / deploy-dev (push) Successful in 2m2s
- 移除 fetchInfo 函数中的加载状态管理
- 将 loading 状态管理移到 refresh 函数中
- 为 refresh 函数添加异步支持和加载状态显示
- 保留数据获取成功后的消息提示功能
- 简化了请求处理流程并改善用户体验
2026-06-03 13:43:32 +08:00
ygxbnet a725b56ab2 chore(deps): 更新项目依赖包版本
部署开发环境 / deploy-dev (push) Successful in 2m3s
- 升级 axios 从 1.15.2 到 1.16.1
- 升级 element-plus 从 2.13.7 到 2.14.1
- 升级 vue 从 3.5.32 到 3.5.35
- 升级 vue-router 从 5.0.4 到 5.1.0
- 升级 @types/node 从 24.12.2 到 24.12.4
- 升级 @vitejs/plugin-vue 从 6.0.6 到 6.0.7
- 升级 typescript 从 ~6.0.0 到 ~6.0.3
- 升级 vite 从 8.0.8 到 8.0.16
- 升级 vite-plugin-vue-devtools 从 8.1.1 到 8.1.2
- 升级 vue-tsc 从 3.2.6 到 3.3.3
2026-06-03 13:26:10 +08:00
ygxbnet f8b062711a refactor(dialog): 更新对话框标题模板插槽名称
部署开发环境 / deploy-dev (push) Successful in 2m2s
- 将 template 标签从 #title 替换为 #header
- 保持原有标题内容和样式不变
- 符合 Element Plus 组件插槽命名规范
2026-06-03 13:12:11 +08:00
ygxbnet 1505226628 refactor(后端): 删除gin请求日志,优化代码路径
部署开发环境 / deploy-dev (push) Successful in 2m0s
2026-06-03 13:02:40 +08:00
ygxbnet f9e099bc24 feat(前端): 添加删除操作加载状态提示
部署开发环境 / deploy-dev (push) Has been cancelled
2026-06-02 23:53:03 +08:00
ygxbnet d7b15c99cf feat(前端): 添加删除操作加载状态提示
部署开发环境 / deploy-dev (push) Failing after 3m5s
2026-06-02 23:42:57 +08:00
ygxbnet 1aef5775a6 fix(数据): 修复删除数据过多而造成的redis连接超时问题
部署开发环境 / deploy-dev (push) Successful in 2m57s
2026-05-28 21:16:43 +08:00
ygxbnet cd061668f9 style(code): 统一代码格式化和导入顺序调整
部署开发环境 / deploy-dev (push) Successful in 2m54s
- 调整了多个组件中的导入语句顺序以保持一致
- 格式化了 App.vue 中的 CSS 样式规则使其更易读
- 规范化了 vite.config.ts 中的配置对象缩进
- 移除了未使用的 computed 导入以减少代码冗余
- 整理了图标组件的导入顺序使其按字母排序
- 统一了代码中的空行和缩进格式
2026-05-01 14:04:26 +08:00
ygxbnet cee1687373 fix(build): 修正版本号链接标志设置
部署开发环境 / deploy-dev (push) Successful in 1m25s
- 修正了 ldflags 中版本变量的包路径,从 global.Version 更新为 dypid/global.Version
- 确保版本信息能够正确注入到二进制文件中
2026-04-30 22:12:40 +08:00
ygxbnet 28aec98414 feat(api): 添加版本信息接口并完善构建流程
部署开发环境 / deploy-dev (push) Successful in 1m22s
- 在api路由中添加/version接口用于显示程序版本信息
- 集成全局Version变量用于版本号管理
- 修改Dockerfile支持版本号注入构建参数
- 更新开发环境部署工作流添加版本信息构建
- 更新生产环境部署工作流添加版本信息构建
- 实现在版本接口中展示程序版本和Gin框架版本
2026-04-30 22:06:21 +08:00
ygxbnet 6d379714cd feat(api): 添加测试接口
部署开发环境 / deploy-dev (push) Successful in 1m35s
- 新增 /test 接口返回 "ok"
- 集成到 API 路由组中
2026-04-30 21:46:25 +08:00
ygxbnet 8c615d01b1 style(home): 移除未使用的CSS注释
部署开发环境 / deploy-dev (push) Successful in 1m41s
- 删除了 search-box 样式中的注释掉的 justify-content 属性
2026-04-25 13:39:45 +08:00
ygxbnet d4eb597575 chore(go): 更新 Go 版本到 1.26
部署开发环境 / deploy-dev (push) Failing after 26s
- 将 go.mod 中的 Go 版本从 1.25 升级到 1.26
- 保持其他依赖项不变
2026-04-25 13:35:10 +08:00
ygxbnet f8022662aa refactor(admin): 移除未使用的Element Plus图标导入
- 从AdminView.vue中移除未使用的Delete、Clock、Document图标导入
- 从TokenManageView.vue中移除未使用的Document、Search、DocumentAdd图标导入
- 优化代码结构,减少不必要的依赖引入
- 修复AddDataDialog组件属性缩进格式问题
2026-04-25 13:23:27 +08:00
ygxbnet 1c7d71e688 feat(home): 主界面添加数据上传功能
- 集成 Upload 图标并导入 AddDataDialog 组件
- 添加显示添加数据对话框的响应式变量
- 新增成功样式按钮用于触发数据上传功能
- 在页面中添加 AddDataDialog 组件实例
- 调整搜索框布局样式以优化界面显示
2026-04-25 13:22:00 +08:00
ygxbnet 462c476141 refactor(AddDataDialog): 移除未使用的 node:timers 导入
部署开发环境 / deploy-dev (push) Successful in 1m27s
- 删除了未被引用的 timers 模块导入
- 清理了组件中无用的依赖引入
2026-04-24 17:51:02 +08:00
ygxbnet 409aac8486 feat(token): 添加数据批量上传功能
部署开发环境 / deploy-dev (push) Failing after 26s
- 新增 AddDataDialog 组件用于数据批量上传
- 在 TokenManageView 中集成数据上传对话框
- 实现按行分割数据并逐条上传的功能
- 添加上传进度显示和计数功能
- 集成 API 接口进行数据提交
- 添加上传状态控制和错误处理机制
2026-04-24 17:48:42 +08:00
ygxbnet 4ca3356214 feat(admin): 添加管理员后台布局并优化Token管理界面
- 新增AdminView.vue作为管理员后台主布局组件
- 集成导航菜单支持Token管理和详情页面切换
- 添加管理员身份标识和退出功能
- 在TokenManageView中引入DocumentAdd图标用于数据添加
- 为表格行添加数据查看对话框功能
- 调整Token管理表格列宽度以改善显示效果
- 优化移动端响应式布局适配
2026-04-24 16:51:20 +08:00
ygxbnet 37fd2c2bac build(Dockerfile): 安装pnpm并构建项目
部署开发环境 / deploy-dev (push) Successful in 1m34s
- 在Docker镜像中全局安装pnpm包管理器
- 使用pnpm安装项目依赖
- 执行pnpm构建命令生成生产版本
2026-04-24 13:24:31 +08:00
ygxbnet c7cf47c8d2 build(Dockerfile): 将npm替换为pnpm以提升构建效率
部署开发环境 / deploy-dev (push) Failing after 9s
- 使用pnpm替代npm进行依赖安装
- 保持相同的构建命令流程
- 提升包管理器的性能和磁盘空间利用效率
2026-04-24 13:21:04 +08:00
ygxbnet f774419140 chore(project): 更改包管理为 pnpm 并更新依赖项 (vite 8...)
部署开发环境 / deploy-dev (push) Successful in 2m33s
- 添加 .gitignore 文件以忽略日志、构建产物和编辑器文件
- 在开发环境中设置页面标题为"抖音数据去重 - dev"
- 移除 package.json 中的 engines 字段后重新添加并更新依赖版本
- 升级 Vue、Element Plus、TypeScript 等核心依赖到最新版本
- 更新 README.md 中的项目名称和推荐浏览器设置说明
- 修改 README.md 中的命令示例从 npm 到 pnpm
- 在 tsconfig.app.json 中启用 noUncheckedIndexedAccess 配置
- 将 tsconfig.node.json 继承的配置从 node22 更新为 node24
- 调整 tsconfig.node.json 中的模块解析和类型配置
2026-04-24 13:08:31 +08:00
ygxbnet 2786b011a8 build(docker): 使用基础 alpine 镜像替换 alpine:latest
部署开发环境 / deploy-dev (push) Successful in 2m56s
- 将 Dockerfile 中的基础镜像从 alpine:latest 改为 alpine
- 减少镜像标签依赖,使用最新基础镜像版本
2026-04-24 00:32:24 +08:00
ygxbnet 650c480416 refactor(admin): 重构管理员界面并优化用户体验并添加应用图标
部署开发环境 / deploy-dev (push) Failing after 14s
- 更新 AdminView.vue 组件结构,使用新的布局和导航设计
- 集成 Element Plus 图标组件,提升界面美观度
- 添加响应式设计支持,适配移动端设备
- 重构 HomeView.vue 组件,改进 Token 查询功能
- 实现自动刷新机制,每5秒更新 Token 信息
- 优化 TokenDetailView.vue 组件,增强数据管理功能
- 添加确认对话框,防止误删操作
- 在 App.vue 中引入全局 CSS 变量和主题系统
- 创建通用组件样式类,统一页面外观
- 优化数据加载逻辑,提升页面性能和用户体验
2026-04-23 23:51:24 +08:00
ygxbnet 819a2eb8ec refactor: 重构项目结构并优化路由组织
部署开发环境 / deploy-dev (push) Successful in 8m30s
2025-10-28 12:20:16 +08:00
ygxbnet ea9ecb770d ops: 删除构建上传工具的CI配置
部署开发环境 / deploy-dev (push) Has been cancelled
2025-10-11 12:37:36 +08:00
ygxbnet 3476d66c0d refactor: 移除tool目录及相关依赖配置
构建上传工具 / build-tool (push) Failing after 29s
部署开发环境 / deploy-dev (push) Successful in 1m7s
2025-10-11 12:35:43 +08:00
ygxbnet 1f0158dd76 refactor(tool): 优化文件处理逻辑和错误处理
构建上传工具 / build-tool (push) Successful in 54s
部署开发环境 / deploy-dev (push) Successful in 1m13s
2025-10-11 12:26:43 +08:00
ygxbnet f35fad442a refactor: 使用errgroup优化文件上传并发控制
构建上传工具 / build-tool (push) Successful in 2m24s
部署开发环境 / deploy-dev (push) Successful in 3m11s
2025-10-10 00:07:48 +08:00
ygxbnet a505f2ddc9 Merge remote-tracking branch 'origin/main'
构建上传工具 / build-tool (push) Successful in 1m27s
部署开发环境 / deploy-dev (push) Successful in 1m47s
2025-09-18 20:34:13 +08:00
ygxbnet 42cfe0dc0f feat(token): 添加Token存在性检查 2025-09-18 20:32:19 +08:00
ygxbnet 2c8e25bdf8 refactor: 更新页面标题为抖音数据去重 2025-09-17 12:21:31 +08:00
ygxbnet 0992b02880 更新 web/src/views/TokenManageView.vue
构建上传工具 / build-tool (push) Successful in 59s
部署开发环境 / deploy-dev (push) Has been cancelled
2025-09-16 06:14:45 +00:00
ygxbnet 1f8a5b8c37 更新 web/src/views/TokenManageView.vue
构建上传工具 / build-tool (push) Successful in 56s
部署开发环境 / deploy-dev (push) Failing after 44s
2025-09-16 06:04:50 +00:00
ygxbnet 795a9186a9 refactor(data): 优化数据读取和删除逻辑
构建上传工具 / build-tool (push) Failing after 2s
部署开发环境 / deploy-dev (push) Failing after 2s
2025-09-16 12:49:32 +08:00
ygxbnet 69d4c5d038 refactor(token): 优化Token状态管理和数据更新逻辑
构建上传工具 / build-tool (push) Failing after 37s
部署开发环境 / deploy-dev (push) Failing after 8s
2025-09-15 23:20:41 +08:00
ygxbnet 78d496ae72 refactor(controller): 优化Redis去重和缓存删除逻辑
构建上传工具 / build-tool (push) Successful in 56s
部署开发环境 / deploy-dev (push) Successful in 1m39s
2025-09-14 22:51:04 +08:00
ygxbnet 4c19f4b16c refactor: 优化输出格式和去重项计数方法
构建上传工具 / build-tool (push) Successful in 1m5s
部署开发环境 / deploy-dev (push) Successful in 1m18s
2025-09-12 22:52:18 +08:00
ygxbnet 1c56423ea4 refactor: 将布隆过滤器改为Cuckoo过滤器并优化代码结构
构建上传工具 / build-tool (push) Successful in 59s
部署开发环境 / deploy-dev (push) Successful in 1m36s
2025-09-12 22:26:45 +08:00
ygxbnet 5d304b6334 refactor(token): 优化表格样式和删除功能 2025-09-12 00:07:07 +08:00
ygxbnet 5e3c0762ab refactor(token): 优化Token管理界面和重构工作流 2025-09-11 22:47:08 +08:00
ygxbnet f883b0a7d8 refactor(tool): 优化文件处理逻辑和性能
构建上传工具 / build (push) Successful in 1m2s
部署开发环境 / build-and-deploy (push) Successful in 1m17s
2025-09-10 21:26:24 +08:00
ygxbnet 861ac5d365 refactor(token): 修改参数验证和删除逻辑 2025-09-10 17:03:09 +08:00
ygxbnet 1e4c2d540e build: 简化Go安装流程,移除冗余代码 2025-09-10 15:12:47 +08:00
46 changed files with 5053 additions and 4920 deletions
-44
View File
@@ -1,44 +0,0 @@
name: 构建上传工具
on: [ push ]
jobs:
build:
env:
RUNNER_TOOL_CACHE: /toolcache
runs-on: ubuntu-latest
steps:
- name: 检出代码
uses: actions/checkout@v4
- name: 安装Go(镜像)
run: |
# 使用国内镜像下载 Go
export GO_MIRROR_URL="https://mirrors.aliyun.com/golang"
wget $GO_MIRROR_URL/go1.25.1.linux-amd64.tar.gz -O go.tar.gz
# 解压并设置环境变量
sudo rm -rf /usr/local/go
sudo tar -C /usr/local -xzf go.tar.gz
echo "/usr/local/go/bin" >> $GITHUB_PATH
env:
GOROOT: /usr/local/go
# - name: 安装Go
# uses: actions/setup-go@v5
# with:
# go-version-file: "go.mod"
- name: 构建上传工具
run: |
go env -w CGO_ENABLED=0 \
&& go env -w GOARCH=amd64 \
&& go env -w GOOS=windows \
&& go mod tidy \
&& cd ./tool \
&& go build -o 上传工具.exe
- name: 上传构建文件
uses: actions/upload-artifact@v3
with:
name: 上传工具
path: tool/上传工具.exe
+6 -2
View File
@@ -2,7 +2,7 @@ name: 部署开发环境
on: [ push ] on: [ push ]
jobs: jobs:
build-and-deploy: deploy-dev:
env: env:
RUNNER_TOOL_CACHE: /toolcache RUNNER_TOOL_CACHE: /toolcache
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -11,7 +11,11 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: 构建Docker镜像 - name: 构建Docker镜像
run: docker build -t dypid:latest . run: |
set -x
git_hash=$(git rev-parse --short "$GITHUB_SHA")
build_date=$(TZ=Asia/Shanghai date +"%Y%m%d%H%M")
docker build --build-arg VERSION="dev - $build_date - $git_hash" -t dypid:latest .
- name: 导出镜像 - name: 导出镜像
run: mkdir release && docker save -o release/dypid.tar dypid:latest && docker rmi dypid:latest run: mkdir release && docker save -o release/dypid.tar dypid:latest && docker rmi dypid:latest
+6 -2
View File
@@ -3,7 +3,7 @@ on:
workflow_dispatch: workflow_dispatch:
jobs: jobs:
build-and-deploy: deploy-production:
env: env:
RUNNER_TOOL_CACHE: /toolcache RUNNER_TOOL_CACHE: /toolcache
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -12,7 +12,11 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: 构建Docker镜像 - name: 构建Docker镜像
run: docker build -t dypid:latest . run: |
set -x
git_hash=$(git rev-parse --short "$GITHUB_SHA")
build_date=$(TZ=Asia/Shanghai date +"%Y%m%d%H%M")
docker build --build-arg VERSION="release - $build_date - $git_hash" -t dypid:latest .
- name: 导出镜像 - name: 导出镜像
run: mkdir release && docker save -o release/dypid.tar dypid:latest && docker rmi dypid:latest run: mkdir release && docker save -o release/dypid.tar dypid:latest && docker rmi dypid:latest
+9 -4
View File
@@ -4,8 +4,9 @@ WORKDIR /build
COPY web . COPY web .
RUN npm install \ RUN npm install -g pnpm \
&& npm run build && pnpm install \
&& pnpm run build
FROM golang AS goBuilder FROM golang AS goBuilder
@@ -14,11 +15,15 @@ WORKDIR /build
COPY . . COPY . .
COPY --from=webBuilder /build/dist ./web/dist COPY --from=webBuilder /build/dist ./web/dist
ARG VERSION
RUN go env -w CGO_ENABLED=0 \ RUN go env -w CGO_ENABLED=0 \
&& go mod tidy \ && go mod tidy \
&& go build -o dypid && go build \
-ldflags="-s -w -X 'dypid/global.Version=$VERSION'" \
-o dypid
FROM alpine:latest FROM alpine
WORKDIR /data WORKDIR /data
-144
View File
@@ -1,144 +0,0 @@
package controller
import (
"dypid/db"
"dypid/global"
"net/http"
"github.com/gin-gonic/gin"
)
func ListTokenHandler(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"result": db.ListToken()})
}
func CreateTokenHandler(c *gin.Context) {
//解析输入数据
input := struct {
Token string `form:"token" binding:"required"`
DedupObject string `form:"dedup_object" binding:"required"`
DataFormat string `form:"data_format" binding:"required"`
Notes string `form:"notes" binding:"required"`
}{}
if err := c.ShouldBindQuery(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "参数不能为空 " + err.Error()})
return
}
//创建Token
err := db.CreateToken(input.Token, input.DedupObject, input.DataFormat, input.Notes)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
//返回
c.JSON(http.StatusOK, gin.H{"result": "ok"})
}
func UpdateTokenHandler(c *gin.Context) {
input := struct {
Token string `form:"token" binding:"required"`
DedupObject string `form:"dedup_object" binding:"required"`
DataFormat string `form:"data_format" binding:"required"`
Notes string `form:"notes" binding:"required"`
}{}
if err := c.ShouldBindQuery(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "参数不能为空 " + err.Error()})
return
}
err := db.UpdateToken(input.Token, input.DedupObject, input.DataFormat, input.Notes)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"result": "ok"})
}
func DeleteTokenHandler(c *gin.Context) {
input := struct {
Token string `form:"token" binding:"required"`
}{}
if err := c.ShouldBindQuery(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "参数不能为空 " + err.Error()})
return
}
err := db.DeleteToken(input.Token)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"result": "ok"})
}
func GetTokenInfoHandler(c *gin.Context) {
input := struct {
Token string `form:"token" binding:"required"`
}{}
if err := c.ShouldBindQuery(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Token不能为空"})
return
}
dedupObject, err := db.GetDedupObject(input.Token)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取去重对象失败 " + err.Error()})
return
}
dataFormat, err := db.GetDataFormat(input.Token)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取数据格式失败 " + err.Error()})
return
}
output := struct {
Token string `json:"token"`
DedupObject string `json:"dedup_object"`
DataFormat string `json:"data_format"`
DedupItemsNumber int64 `json:"dedup_items_number"`
CacheListNumber int64 `json:"cache_list_number"`
}{}
output.Token = input.Token
output.DedupObject = dedupObject
output.DataFormat = dataFormat
output.DedupItemsNumber = global.RDB.BFCard(global.RCtx, "dedup:"+input.Token+":"+dedupObject).Val()
output.CacheListNumber = global.RDB.LLen(global.RCtx, "list:"+input.Token).Val()
c.JSON(http.StatusOK, gin.H{"result": output})
}
func DeleteTokenInfoHandler(c *gin.Context) {
//解析输入数据
input := struct {
Token string `form:"token" binding:"required"`
DedupBF bool `form:"dedup_bf"`
CacheList bool `form:"cache_list"`
}{}
if err := c.ShouldBindQuery(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Token不能为空"})
return
}
//检查token是否存在
_, err := db.GetDedupObject(input.Token)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Token不存在" + err.Error()})
return
}
//删除去重对象
if input.DedupBF {
keys := global.RDB.Keys(global.RCtx, "dedup:"+input.Token+":*").Val()
global.RDB.Del(global.RCtx, keys...)
}
if input.CacheList {
global.RDB.Del(global.RCtx, "list:"+input.Token)
}
//输出信息
c.JSON(http.StatusOK, gin.H{"result": "ok"})
}
+36 -34
View File
@@ -1,50 +1,52 @@
module dypid module dypid
go 1.25 go 1.27
require ( require (
github.com/fsnotify/fsnotify v1.8.0 github.com/gin-contrib/cors v1.7.7
github.com/gin-contrib/cors v1.7.6 github.com/gin-gonic/gin v1.12.0
github.com/gin-gonic/gin v1.10.1 github.com/redis/go-redis/v9 v9.22.0
github.com/redis/go-redis/v9 v9.12.1 github.com/spf13/viper v1.21.0
github.com/spf13/viper v1.20.1
) )
require ( require (
github.com/bytedance/sonic v1.13.3 // indirect github.com/bytedance/gopkg v0.1.4 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect github.com/bytedance/sonic v1.15.2 // indirect
github.com/bytedance/sonic/loader v0.5.2 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect github.com/cloudwego/base64x v0.1.7 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/fsnotify/fsnotify v1.10.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect github.com/gabriel-vasile/mimetype v1.4.15 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect github.com/gin-contrib/sse v1.1.1 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.26.0 // indirect github.com/go-playground/validator/v10 v10.30.3 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-json v0.10.6 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/klauspost/cpuid/v2 v2.4.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.5.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.24 // 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.4 // indirect github.com/pelletier/go-toml/v2 v2.4.3 // indirect
github.com/sagikazarmark/locafero v0.7.0 // indirect github.com/quic-go/qpack v0.6.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect github.com/quic-go/quic-go v0.61.0 // indirect
github.com/spf13/afero v1.12.0 // indirect github.com/sagikazarmark/locafero v0.12.0 // indirect
github.com/spf13/cast v1.7.1 // indirect github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/pflag v1.0.6 // indirect github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/subosito/gotenv v1.6.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect github.com/ugorji/go/codec v1.3.2 // indirect
go.uber.org/atomic v1.9.0 // indirect go.mongodb.org/mongo-driver/v2 v2.8.0 // indirect
go.uber.org/multierr v1.9.0 // indirect go.uber.org/atomic v1.11.0 // indirect
golang.org/x/arch v0.18.0 // indirect go.yaml.in/yaml/v3 v3.0.5 // indirect
golang.org/x/crypto v0.39.0 // indirect golang.org/x/arch v0.30.0 // indirect
golang.org/x/net v0.41.0 // indirect golang.org/x/crypto v0.55.0 // indirect
golang.org/x/sys v0.35.0 // indirect golang.org/x/net v0.58.0 // indirect
golang.org/x/text v0.26.0 // indirect golang.org/x/sys v0.47.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect golang.org/x/text v0.41.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect google.golang.org/protobuf v1.36.12 // indirect
) )
+82 -77
View File
@@ -2,124 +2,129 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0= github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM=
github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/bytedance/sonic v1.15.2 h1:90H+rcF/FwLXwfB1cudOLq/je83n683Utf4Cbp0xHCo=
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= github.com/bytedance/sonic v1.15.2/go.mod h1:mT2NbXunuaEbnZ+mRIX/vYqKISmgEuHFDI4UzmKx2SA=
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= github.com/bytedance/sonic/loader v0.5.2 h1:0QtP1gevc1OZ6/H8Lb9BRZiCXd1Ftjd3OKuj1T1lBIo=
github.com/bytedance/sonic/loader v0.5.2/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 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/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= github.com/cloudwego/base64x v0.1.7 h1:NppS+Fgzg5ovhn4NkUXaDT3x9jldgH5ToMCqzBSi2zI=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/base64x v0.1.7/go.mod h1:Cu1PV9zfrSf7ET2tIbWbbEy7jO7HHJ13q4X2SQ8aWYg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
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/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/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.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= github.com/gabriel-vasile/mimetype v1.4.15 h1:05iP/CYtZ/w455R/KZM6rZ5ieAdh99UPtd+d3YzLmaI=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= github.com/gabriel-vasile/mimetype v1.4.15/go.mod h1:azpTcoLcDZRNgFou5j+APrqQx9HqVPWa6ijYQIIVswQ=
github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY= github.com/gin-contrib/cors v1.7.7 h1:Oh9joP463x7Mw72vhvJ61YQm8ODh9b04YR7vsOErD0Q=
github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk= github.com/gin-contrib/cors v1.7.7/go.mod h1:K5tW0RkzJtWSiOdikXloy8VEZlgdVNpHNw8FpjUPNrE=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= github.com/gin-contrib/sse v1.1.1 h1:uGYpNwTacv5R68bSGMapo62iLTRa9l5zxGCps4hK6ko=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-contrib/sse v1.1.1/go.mod h1:QXzuVkA0YO7o/gun03UI1Q+FTI8ZV/n5t03kIQAI89s=
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= github.com/go-playground/validator/v10 v10.30.3 h1:4MU6YkEwx7GbcPJOZxrtbu+QfF3pJLJuaYTeAH0DYy8=
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= github.com/go-playground/validator/v10 v10.30.3/go.mod h1:4Axh7oCNGcoGkqLoE4YWt6n20mcEIsPRlB7vPk3lpyc=
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
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=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
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/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.5.0 h1:pLqT2kq1zpHW/1D18QMjMpdtX7cekxqtJJjg5ANyWw0=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/leodido/go-urn v1.5.0/go.mod h1:9BORnCDhdPBJNDEX+w1bJisa8yOKYi116VeO96s4ifE=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
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/redis/go-redis/v9 v9.12.1 h1:k5iquqv27aBtnTm2tIkROUDp8JBXhXZIVu1InSgvovg= github.com/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4TzM7MFjy0=
github.com/redis/go-redis/v9 v9.12.1/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= github.com/quic-go/go-ossfuzz-seeds v0.1.0/go.mod h1:3IOHRbJIc+L6YKMwfDtJAM9Vj9k0YY4muhuyUYk5tbk=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= github.com/quic-go/quic-go v0.61.0 h1:ui88A53s8MSVYLC56en0KQ17HARk+9986Dn0SBfKNvA=
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= github.com/quic-go/quic-go v0.61.0/go.mod h1:9So2anK4Tp22URSQq00k+Vo2PNkle96ycDPDHL4s9vs=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/redis/go-redis/v9 v9.22.0 h1:laDvpYXTJtZLloinw1fA5Kqd6HAEH2XKxOkG/PDq2F0=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/redis/go-redis/v9 v9.22.0/go.mod h1:y2g0Wj8rQvuK0ELM+oxSudcLtC09JScs98I/X9gRWY4=
github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
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.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= github.com/ugorji/go/codec v1.3.2 h1:zkEASHHyEClGeURfgNT9PJZVfAbs9oEX9QXggwWNJbc=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= github.com/ugorji/go/codec v1.3.2/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= go.mongodb.org/mongo-driver/v2 v2.8.0 h1:CxWDGQYY8QQwNjAl/aq2sfWakdnWZynnqJ9F4DhHbP8=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= go.mongodb.org/mongo-driver/v2 v2.8.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/arch v0.30.0 h1:sB9h+1gRGa2+LauFSV0tm8bK1J2yo1bx6/Uyi/P6DTU=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/arch v0.30.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
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-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
@@ -1,8 +1,9 @@
package controller package controller
import ( import (
"dypid/db" "dypid/internal/db"
"dypid/global" "dypid/internal/global"
"dypid/internal/model"
"fmt" "fmt"
"net/http" "net/http"
"strings" "strings"
@@ -12,6 +13,7 @@ import (
) )
func ReadDataHandler(c *gin.Context) { func ReadDataHandler(c *gin.Context) {
//解析输入数据
input := struct { input := struct {
Token string `form:"token" binding:"required"` Token string `form:"token" binding:"required"`
}{} }{}
@@ -20,17 +22,17 @@ func ReadDataHandler(c *gin.Context) {
return return
} }
lLen := global.RDB.LLen(global.RCtx, fmt.Sprintf("list:%s", input.Token)) retData := global.RDB.LPop(global.RCtx, fmt.Sprintf("list:%s", input.Token)).Val()
if lLen.Val() == 0 { if retData == "null" {
c.JSON(http.StatusOK, gin.H{"result": "数据库没有数据"}) c.JSON(http.StatusOK, gin.H{"result": "数据库没有数据"})
return return
} }
retData := global.RDB.BLPop(global.RCtx, 0, fmt.Sprintf("list:%s", input.Token)).Val()[1]
c.String(http.StatusOK, retData) c.String(http.StatusOK, retData)
} }
func WriteDataHandler(c *gin.Context) { func WriteDataHandler(c *gin.Context) {
//解析输入数据
input := struct { input := struct {
Token string `form:"token" binding:"required"` Token string `form:"token" binding:"required"`
Data string `form:"data" binding:"required"` Data string `form:"data" binding:"required"`
@@ -40,11 +42,26 @@ func WriteDataHandler(c *gin.Context) {
return return
} }
//获取去重对象
dedupObject, err := db.GetDedupObject(input.Token) dedupObject, err := db.GetDedupObject(input.Token)
if err != nil { if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return return
} }
//判断数据是否需要去重
//如果数据不需要去重则直接写入数据库
if dedupObject == "false" {
global.RDB.LPush(global.RCtx, fmt.Sprintf("list:%s", input.Token), input.Data)
c.JSON(http.StatusOK, model.APIResponse{
Code: 200,
Message: "数据写入成功",
})
return
}
//数据要去重,下面函数会执行
//获取数据索引
dataIndex, err := getDataIndex(input.Token) dataIndex, err := getDataIndex(input.Token)
if err != nil { if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
@@ -52,7 +69,7 @@ func WriteDataHandler(c *gin.Context) {
} }
dedupValue := strings.Split(input.Data, "----")[dataIndex[dedupObject]] dedupValue := strings.Split(input.Data, "----")[dataIndex[dedupObject]]
err = createBF(fmt.Sprintf("dedup:%s:%s", input.Token, dedupObject), 0.01, 100000000) err = createCF(fmt.Sprintf("dedup:%s:%s", input.Token, dedupObject), 100_000_000)
if err != nil && err.Error() != "ERR item exists" { if err != nil && err.Error() != "ERR item exists" {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return return
@@ -60,11 +77,12 @@ func WriteDataHandler(c *gin.Context) {
luaScript := ` luaScript := `
local dedupKey = KEYS[1] -- KEYS[1]: 去重键 (dedup:token:object) local dedupKey = KEYS[1] -- KEYS[1]: 去重键 (dedup:token:object)
local listKey = KEYS[2] -- KEYS[2]: 列表键 (list:token) local listKey = KEYS[2] -- KEYS[2]: 列表键 (list:token)
local deleteListKey = KEYS[3] -- KEYS[3]: 删除列表键 (delete-list:token)
local dedupValue = ARGV[1] -- ARGV[1]: 去重值 local dedupValue = ARGV[1] -- ARGV[1]: 去重值
local rawData = ARGV[2] -- ARGV[2]: 原始数据 local rawData = ARGV[2] -- ARGV[2]: 原始数据
-- 检查布隆过滤器中是否已存在该值 -- 检查布隆过滤器中是否已存在该值
local exists = redis.call('BF.EXISTS', dedupKey, dedupValue) local exists = redis.call('CF.EXISTS', dedupKey, dedupValue)
-- 如果已存在返回已去重标记 -- 如果已存在返回已去重标记
if exists == 1 then if exists == 1 then
@@ -72,10 +90,12 @@ if exists == 1 then
end end
-- 添加到布隆过滤器 -- 添加到布隆过滤器
redis.call('BF.ADD', dedupKey, dedupValue) redis.call('CF.ADD', dedupKey, dedupValue)
-- 添加到列表 -- 添加到列表
redis.call('LPUSH', listKey, rawData) redis.call('LPUSH', listKey, rawData)
redis.call('LPUSH', deleteListKey, dedupValue)
-- 返回成功结果 -- 返回成功结果
return "ok" return "ok"
` `
@@ -85,6 +105,7 @@ return "ok"
[]string{ []string{
fmt.Sprintf("dedup:%s:%s", input.Token, dedupObject), fmt.Sprintf("dedup:%s:%s", input.Token, dedupObject),
fmt.Sprintf("list:%s", input.Token), fmt.Sprintf("list:%s", input.Token),
fmt.Sprintf("delete-list:%s", input.Token),
}, },
dedupValue, dedupValue,
input.Data, input.Data,
@@ -102,8 +123,8 @@ return "ok"
c.JSON(http.StatusInternalServerError, gin.H{"error": "WriteDataHandler 错误"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "WriteDataHandler 错误"})
} }
func createBF(bloomFilter string, errorRate float64, capacity int64) error { func createCF(bloomFilter string, capacity int64) error {
_, err := global.RDB.BFReserve(global.RCtx, bloomFilter, errorRate, capacity).Result() _, err := global.RDB.CFReserveBucketSize(global.RCtx, bloomFilter, capacity, 6).Result()
return err return err
} }
+336
View File
@@ -0,0 +1,336 @@
package controller
import (
"dypid/internal/db"
"dypid/internal/global"
"dypid/internal/model"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/redis/go-redis/v9"
)
func ListTokenHandler(c *gin.Context) {
c.JSON(http.StatusOK, model.APIResponse{
Code: 200,
Message: "获取所有token成功",
Data: db.ListToken(),
})
}
func CreateTokenHandler(c *gin.Context) {
//解析输入数据
input := struct {
Token string `form:"token" binding:"required"`
DedupObject string `form:"dedup_object" binding:"required"`
DataFormat string `form:"data_format" binding:"required"`
Notes string `form:"notes"`
}{}
if err := c.ShouldBindQuery(&input); err != nil {
c.JSON(http.StatusBadRequest, model.APIResponse{
Code: 400,
Message: "参数不能为空 " + err.Error(),
})
return
}
//检查Token是否存在
if db.CheckToken(input.Token) {
c.JSON(http.StatusBadRequest, model.APIResponse{
Code: 400,
Message: "创建Token失败,Token已经存在,请勿重复创建",
})
return
}
//创建Token
err := db.CreateToken(input.Token, input.DedupObject, input.DataFormat, input.Notes)
if err != nil {
c.JSON(http.StatusInternalServerError, model.APIResponse{
Code: 500,
Message: err.Error(),
})
return
}
//返回
c.JSON(http.StatusOK, model.APIResponse{
Code: 200,
Message: "创建Token成功",
})
}
func UpdateTokenHandler(c *gin.Context) {
input := struct {
Token string `form:"token" binding:"required"`
DedupObject string `form:"dedup_object" binding:"required"`
DataFormat string `form:"data_format" binding:"required"`
Notes string `form:"notes"`
}{}
if err := c.ShouldBindQuery(&input); err != nil {
c.JSON(http.StatusBadRequest, model.APIResponse{
Code: 400,
Message: "参数不能为空 " + err.Error(),
})
return
}
//检查Token是否存在
if !db.CheckToken(input.Token) {
c.JSON(http.StatusBadRequest, model.APIResponse{
Code: 400,
Message: "更改失败,Token不存在",
})
return
}
err := db.UpdateToken(input.Token, input.DedupObject, input.DataFormat, input.Notes)
if err != nil {
c.JSON(http.StatusInternalServerError, model.APIResponse{
Code: 500,
Message: err.Error(),
})
return
}
c.JSON(http.StatusOK, model.APIResponse{
Code: 200,
Message: "更改Token成功",
})
}
func DeleteTokenHandler(c *gin.Context) {
input := struct {
Token string `form:"token" binding:"required"`
}{}
if err := c.ShouldBindQuery(&input); err != nil {
c.JSON(http.StatusBadRequest, model.APIResponse{
Code: 400,
Message: "参数不能为空 " + err.Error(),
})
return
}
//检查Token是否存在
if !db.CheckToken(input.Token) {
c.JSON(http.StatusBadRequest, model.APIResponse{
Code: 400,
Message: "删除Token失败,Token不存在",
})
return
}
err := db.DeleteToken(input.Token)
if err != nil {
c.JSON(http.StatusInternalServerError, model.APIResponse{
Code: 500,
Message: err.Error(),
})
return
}
c.JSON(http.StatusOK, model.APIResponse{
Code: 200,
Message: "删除Token成功",
})
}
func GetTokenInfoHandler(c *gin.Context) {
input := struct {
Token string `form:"token" binding:"required"`
}{}
if err := c.ShouldBindQuery(&input); err != nil {
c.JSON(http.StatusBadRequest, model.APIResponse{
Code: 400,
Message: "参数不能为空 " + err.Error(),
})
return
}
//检查Token是否存在
if !db.CheckToken(input.Token) {
c.JSON(http.StatusBadRequest, model.APIResponse{
Code: 400,
Message: "获取信息失败,Token不存在",
})
return
}
dedupObject, err := db.GetDedupObject(input.Token)
if err != nil {
c.JSON(http.StatusInternalServerError, model.APIResponse{
Code: 500,
Message: "获取去重对象失败 " + err.Error(),
})
return
}
dataFormat, err := db.GetDataFormat(input.Token)
if err != nil {
c.JSON(http.StatusInternalServerError, model.APIResponse{
Code: 500,
Message: "获取数据格式失败 " + err.Error(),
})
return
}
output := struct {
Token string `json:"token"`
DedupObject string `json:"dedup_object"`
DataFormat string `json:"data_format"`
DedupItemsNumber int64 `json:"dedup_items_number"`
CacheListNumber int64 `json:"cache_list_number"`
}{
Token: input.Token,
DedupObject: dedupObject,
DataFormat: dataFormat,
DedupItemsNumber: global.RDB.CFInfo(global.RCtx, "dedup:"+input.Token+":"+dedupObject).Val().NumItemsInserted,
CacheListNumber: global.RDB.LLen(global.RCtx, "list:"+input.Token).Val(),
}
c.JSON(http.StatusOK, model.APIResponse{
Code: 200,
Message: "获取Token信息成功",
Data: output,
})
}
func DeleteTokenInfoHandler(c *gin.Context) {
//解析输入数据
input := struct {
Token string `form:"token" binding:"required"`
DedupBF string `form:"dedup_bf"`
CacheList string `form:"cache_list"`
BothNumber string `form:"both_number"`
}{}
if err := c.ShouldBindQuery(&input); err != nil {
c.JSON(http.StatusBadRequest, model.APIResponse{
Code: 400,
Message: "参数不能为空 " + err.Error(),
})
return
}
//检查Token是否存在
if !db.CheckToken(input.Token) {
c.JSON(http.StatusBadRequest, model.APIResponse{
Code: 400,
Message: "删除Token信息失败,Token不存在",
})
return
}
//获取去重对象
dedupObject, err := db.GetDedupObject(input.Token)
if err != nil {
c.JSON(http.StatusBadRequest, model.APIResponse{
Code: 400,
Message: "获取去重对象失败 " + err.Error(),
})
return
}
//删除去重对象
switch input.DedupBF {
case "":
case "all":
keys := global.RDB.Keys(global.RCtx, "dedup:"+input.Token+":*").Val()
global.RDB.Del(global.RCtx, keys...)
global.RDB.Del(global.RCtx, "delete-list:"+input.Token)
default:
num, err := strconv.Atoi(input.DedupBF)
if err != nil {
c.JSON(http.StatusBadRequest, model.APIResponse{
Code: 400,
Message: "dedup_bf数量设置错误 " + err.Error(),
})
return
}
for i := num; i > 0; i -= 20_0000 {
a := 0
a = min(i, 20_0000)
//获得要删的值(列表)
result := global.RDB.LRange(global.RCtx, "delete-list:"+input.Token, int64(-a), -1).Val()
//用事件批量删除布隆过滤器中值
_, err = global.RDB.TxPipelined(global.RCtx, func(pipe redis.Pipeliner) error {
for _, s := range result {
pipe.CFDel(global.RCtx, "dedup:"+input.Token+":"+dedupObject, s)
}
return nil
})
if err != nil {
c.JSON(http.StatusInternalServerError, model.APIResponse{
Code: 500,
Message: "删除去重对象失败 " + err.Error(),
})
return
}
//清除列表中要删的值
global.RDB.LTrim(global.RCtx, "delete-list:"+input.Token, 0, int64(-a-1))
}
}
//删除原始数据
switch input.CacheList {
case "":
case "all":
global.RDB.Del(global.RCtx, "list:"+input.Token)
default:
num, err := strconv.Atoi(input.CacheList)
if err != nil {
c.JSON(http.StatusBadRequest, model.APIResponse{
Code: 400,
Message: "cache_list数量设置错误 " + err.Error(),
})
return
}
global.RDB.LTrim(global.RCtx, "list:"+input.Token, 0, int64(-num-1))
}
//删除去重参考值和原始数据
switch input.BothNumber {
case "":
default:
num, err := strconv.Atoi(input.BothNumber)
if err != nil {
c.JSON(http.StatusBadRequest, model.APIResponse{
Code: 500,
Message: "both_number数量设置错误 " + err.Error(),
})
return
}
for i := num; i > 0; i -= 20_0000 {
a := 0
a = min(i, 20_0000)
result := global.RDB.LRange(global.RCtx, "delete-list:"+input.Token, int64(-a), -1).Val()
_, err = global.RDB.TxPipelined(global.RCtx, func(pipe redis.Pipeliner) error {
for _, s := range result {
pipe.CFDel(global.RCtx, "dedup:"+input.Token+":"+dedupObject, s)
}
pipe.LTrim(global.RCtx, "delete-list:"+input.Token, 0, int64(-a-1))
pipe.LTrim(global.RCtx, "list:"+input.Token, 0, int64(-a-1))
return nil
})
if err != nil {
c.JSON(http.StatusInternalServerError, model.APIResponse{
Code: 500,
Message: "删除Token信息失败 " + err.Error(),
})
return
}
}
}
//输出信息
c.JSON(http.StatusOK, model.APIResponse{
Code: 200,
Message: "删除Token信息成功",
})
}
+10
View File
@@ -32,6 +32,16 @@ start:
} }
} }
func CheckToken(token string) bool {
InitLocalDB()
for _, t := range localDB {
if t.Token == token {
return true
}
}
return false
}
func ListToken() []Token { func ListToken() []Token {
InitLocalDB() InitLocalDB()
return localDB return localDB
+2 -2
View File
@@ -1,8 +1,8 @@
package db package db
import ( import (
"dypid/config" "dypid/internal/config"
"dypid/global" "dypid/internal/global"
"fmt" "fmt"
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
@@ -8,3 +8,5 @@ import (
var RDB *redis.Client var RDB *redis.Client
var RCtx = context.Background() var RCtx = context.Background()
var Version = "dev"
+7
View File
@@ -0,0 +1,7 @@
package model
type APIResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data any `json:"data"`
}
+51
View File
@@ -0,0 +1,51 @@
package service
import (
"dypid/internal/controller"
"dypid/internal/global"
"embed"
"fmt"
"io/fs"
"net/http"
"github.com/gin-gonic/gin"
)
func RegAPIService(r *gin.Engine) {
g := r.Group("/api") //初始化路由组 /api/xxxx
{
g.GET("/test", func(context *gin.Context) {
context.String(http.StatusOK, "ok")
})
g.GET("/version", func(context *gin.Context) {
context.String(http.StatusOK, fmt.Sprintf("程序版本: %s\nGin: %s", global.Version, gin.Version))
})
}
{
g.GET("/token", controller.ListTokenHandler) //获取token列表
g.POST("/token", controller.CreateTokenHandler) //创建token
g.PUT("/token", controller.UpdateTokenHandler) //更新token
g.DELETE("/token", controller.DeleteTokenHandler) //删除token
g.GET("/token/info", controller.GetTokenInfoHandler) //获取token信息
g.DELETE("/token/info", controller.DeleteTokenInfoHandler) //删除token数据库
}
{
g.GET("/data", controller.ReadDataHandler) //获取数据
g.POST("/data", controller.WriteDataHandler) //写入数据
}
}
func RegWebService(r *gin.Engine, webFiles embed.FS) {
assets, _ := fs.Sub(webFiles, "web/dist/assets")
r.StaticFS("/assets", http.FS(assets))
icon, _ := fs.ReadFile(webFiles, "web/dist/favicon.ico")
r.GET("/favicon.ico", func(c *gin.Context) {
c.Data(200, "image/x-icon", icon)
})
indexHtml, _ := fs.ReadFile(webFiles, "web/dist/index.html")
r.NoRoute(func(c *gin.Context) {
c.Data(200, "text/html; charset=utf-8", indexHtml)
})
}
+15 -35
View File
@@ -1,57 +1,37 @@
package main package main
import ( import (
"dypid/config" "dypid/internal/config"
"dypid/controller" "dypid/internal/db"
"dypid/db" "dypid/internal/service"
"embed" "embed"
"fmt" "fmt"
"io/fs"
"net/http"
"github.com/gin-contrib/cors" "github.com/gin-contrib/cors"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
//go:embed web/dist/* //go:embed web/dist/*
var webDir embed.FS var webFiles embed.FS
func main() { func main() {
config.InitConfig() config.InitConfig()
db.InitRedis() db.InitRedis()
db.InitLocalDB() db.InitLocalDB()
//初始化一个http服务对象 //初始化一个http服务对象
gin.SetMode(config.APPConfig.RunMode) gin.SetMode(config.APPConfig.RunMode)
r := gin.Default() r := gin.New()
//跨域设置 if config.APPConfig.RunMode == "debug" {
r.Use(cors.Default()) r.Use(gin.Logger()) //日志处理
//Vue网站服务
assets, _ := fs.Sub(webDir, "web/dist/assets")
r.StaticFS("/assets", http.FS(assets))
icon, _ := fs.ReadFile(webDir, "web/dist/favicon.ico")
r.GET("/favicon.ico", func(c *gin.Context) {
c.Data(200, "image/x-icon", icon)
})
indexHtml, _ := fs.ReadFile(webDir, "web/dist/index.html")
r.NoRoute(func(c *gin.Context) {
c.Data(200, "text/html; charset=utf-8", indexHtml)
})
//API接口
g := r.Group("/api") //初始化路由组 /api/xxxx
{
g.GET("/token", controller.ListTokenHandler) //获取token列表
g.POST("/token", controller.CreateTokenHandler) //创建token
g.PUT("/token", controller.UpdateTokenHandler) //更新token
g.DELETE("/token", controller.DeleteTokenHandler) //删除token
g.GET("/token/info", controller.GetTokenInfoHandler) //获取token信息
g.DELETE("/token/info", controller.DeleteTokenInfoHandler) //删除token数据库
}
{
g.GET("/data", controller.ReadDataHandler) //获取数据
g.POST("/data", controller.WriteDataHandler) //写入数据
} }
r.Use(gin.Recovery()) //错误处理
r.Use(cors.Default()) //跨域设置
//注册网页服务(Vue
service.RegWebService(r, webFiles)
//注册API接口
service.RegAPIService(r)
// 监听并在 0.0.0.0:8080 上启动服务 // 监听并在 0.0.0.0:8080 上启动服务
fmt.Printf("服务器正在运行:http://%s\n", config.APPConfig.Host) fmt.Printf("服务器正在运行:http://%s\n", config.APPConfig.Host)
-2
View File
@@ -1,2 +0,0 @@
./upload
config.toml
-170
View File
@@ -1,170 +0,0 @@
package main
import (
"bufio"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/fsnotify/fsnotify"
"github.com/spf13/viper"
)
var httpClient = &http.Client{
Transport: &http.Transport{
MaxIdleConns: 200,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 30 * time.Second,
},
Timeout: 30 * time.Second,
}
func main() {
//程序配置
viper.SetDefault("url", "http://localhost:8080")
viper.SetDefault("token", "")
viper.SetDefault("thread-count", 10)
//设置配置文件名和路径 ./config.toml
viper.AddConfigPath(".")
viper.SetConfigName("config")
viper.SetConfigType("toml")
viper.SafeWriteConfig() //安全写入默认配置
//读取配置文件
if err := viper.ReadInConfig(); err != nil {
fmt.Errorf("无法读取配置文件: %w", err)
}
viper.WatchConfig()
viper.OnConfigChange(func(e fsnotify.Event) {
if err := viper.ReadInConfig(); err != nil {
fmt.Errorf("无法读取配置文件: %w", err)
}
})
//检测./upload
fmt.Println("程序启动成功,正在检测txt文件")
//os.Mkdir("./upload", os.ModePerm)
for {
files, err := getTxtFiles("./")
if err != nil {
fmt.Println(err)
return
}
if files != nil {
wg := sync.WaitGroup{}
for _, filePath := range files {
fmt.Println("正在上传文件:", filePath)
wg.Add(1)
go func() {
processFile(filePath)
os.Remove(filePath)
wg.Done()
}()
}
wg.Wait()
}
time.Sleep(2 * time.Second)
}
}
func uploadDataToServer(data string) error {
params := url.Values{}
params.Set("token", viper.GetString("token"))
params.Set("data", data)
resp, err := httpClient.Post(viper.GetString("url")+"/api/data?"+params.Encode(), "application/x-www-form-urlencoded", strings.NewReader(""))
if err != nil {
return err
}
if resp != nil {
_, _ = io.Copy(io.Discard, resp.Body)
}
return err
}
// 获取目录中的所有txt文件
func getTxtFiles(dir string) ([]string, error) {
var txtFiles []string
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// 只处理普通文件,跳过目录
if !info.Mode().IsRegular() {
return nil
}
// 检查文件扩展名是否为.txt
if strings.ToLower(filepath.Ext(path)) == ".txt" {
txtFiles = append(txtFiles, path)
}
return nil
})
return txtFiles, err
}
func processFile(filePath string) {
var wg sync.WaitGroup
// 打开文件
file, err := os.Open(filePath)
if err != nil {
fmt.Printf("无法打开文件 %s: %v\n", filePath, err)
return
}
defer file.Close()
// 创建行通道
lines := make(chan string, 100)
// 创建10个worker处理文件上传
for i := 0; i < viper.GetInt("thread-count"); i++ {
wg.Add(1)
go func(workerID int) {
defer wg.Done()
processLines(lines, workerID, filePath)
}(i)
}
// 读取文件并发送到通道
scanner := bufio.NewScanner(file)
lineCount := 0
for scanner.Scan() {
line := scanner.Text()
lines <- line
lineCount++
if lineCount%10000 == 0 {
fmt.Printf("文件【%s】处理进度:%v%%\n", filePath, float64(lineCount)/40000*100)
}
}
close(lines)
wg.Wait()
if err := scanner.Err(); err != nil {
fmt.Printf("读取文件 %s 错误: %v\n", filePath, err)
}
fmt.Printf("文件【%s】处理完成,共处理 %d 行数据\n", filePath, lineCount)
}
func processLines(lines <-chan string, workerID int, filePath string) {
for line := range lines {
// 跳过空行
if strings.TrimSpace(line) == "" {
continue
}
// 上传数据
if err := uploadDataToServer(line); err != nil {
fmt.Printf("Worker %d (文件 %s): 上传失败: %v\n", workerID, filePath, err)
}
}
}
+12 -3
View File
@@ -14,9 +14,6 @@ dist-ssr
coverage coverage
*.local *.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files # Editor directories and files
.vscode/* .vscode/*
!.vscode/extensions.json !.vscode/extensions.json
@@ -28,3 +25,15 @@ coverage
*.sw? *.sw?
*.tsbuildinfo *.tsbuildinfo
.eslintcache
# Cypress
/cypress/videos/
/cypress/screenshots/
# Vitest
__screenshots__/
# Vite
*.timestamp-*-*.mjs
+17 -5
View File
@@ -4,11 +4,23 @@ This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup ## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur). [VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (
and disable Vetur).
## Recommended Browser Setup
- Chromium-based browsers (Chrome, Edge, Brave, etc.):
- [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd)
- [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters)
- Firefox:
- [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/)
- [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/)
## Type Support for `.vue` Imports in TS ## Type Support for `.vue` Imports in TS
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types. TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for
type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the
TypeScript language service aware of `.vue` types.
## Customize configuration ## Customize configuration
@@ -17,17 +29,17 @@ See [Vite Configuration Reference](https://vite.dev/config/).
## Project Setup ## Project Setup
```sh ```sh
npm install pnpm install
``` ```
### Compile and Hot-Reload for Development ### Compile and Hot-Reload for Development
```sh ```sh
npm run dev pnpm dev
``` ```
### Type-Check, Compile and Minify for Production ### Type-Check, Compile and Minify for Production
```sh ```sh
npm run build pnpm build
``` ```
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/favicon.ico"> <link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>dypid</title> <title>抖音数据去重</title>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
-3804
View File
File diff suppressed because it is too large Load Diff
+23 -18
View File
@@ -3,9 +3,6 @@
"version": "0.0.0", "version": "0.0.0",
"private": true, "private": true,
"type": "module", "type": "module",
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "run-p type-check \"build-only {@}\" --", "build": "run-p type-check \"build-only {@}\" --",
@@ -14,22 +11,30 @@
"type-check": "vue-tsc --build" "type-check": "vue-tsc --build"
}, },
"dependencies": { "dependencies": {
"axios": "^1.11.0", "@element-plus/icons-vue": "^2.3.2",
"element-plus": "^2.11.1", "@tailwindcss/vite": "^4.3.3",
"pinia": "^3.0.3", "axios": "^1.19.0",
"pinia-plugin-persistedstate": "^4.5.0", "element-plus": "^2.14.5",
"vue": "^3.5.18", "pinia": "^4.0.3",
"vue-router": "^4.5.1" "pinia-plugin-persistedstate": "^4.7.1",
"tailwindcss": "^4.3.3",
"vue": "^3.5.41",
"vue-router": "^5.2.0"
}, },
"devDependencies": { "devDependencies": {
"@tsconfig/node22": "^22.0.2", "@tsconfig/node24": "^24.0.5",
"@types/node": "^22.16.5", "@types/node": "^24.13.3",
"@vitejs/plugin-vue": "^6.0.1", "@vitejs/plugin-vue": "^6.0.8",
"@vue/tsconfig": "^0.7.0", "@vue/tsconfig": "^0.9.1",
"npm-run-all2": "^8.0.4", "npm-run-all2": "^9.0.3",
"typescript": "~5.8.0", "typescript": "~6.0.3",
"vite": "^7.0.6", "unplugin-auto-import": "^21.1.0",
"vite-plugin-vue-devtools": "^8.0.0", "unplugin-vue-components": "^32.1.0",
"vue-tsc": "^3.0.4" "vite": "^8.2.2",
"vite-plugin-vue-devtools": "^8.2.1",
"vue-tsc": "^3.3.11"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
} }
} }
+2968
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

+4 -10
View File
@@ -1,15 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
const themeMode = ref('light')
</script> </script>
<template> <template>
<router-view></router-view> <div :class="themeMode" class="min-h-screen bg-(--bg-page) max-w-full">
<router-view></router-view>
</div>
</template> </template>
<style scoped>
html, body, #app {
height: 100%;
margin: 0;
padding: 0;
}
</style>
+53
View File
@@ -0,0 +1,53 @@
import axios from "axios";
const axiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
})
export default {
getVersion:() =>
axiosInstance.get('/api/version'),
getTokenList: () =>
axiosInstance.get('/api/token'),
createToken: (params: {
token: string
dedup_object: string
data_format: string
notes: string
}) =>
axiosInstance.post('/api/token', {}, {params}),
updateToken: (params: {
token: string
dedup_object?: string
data_format?: string
notes?: string
}) =>
axiosInstance.put('/api/token', {}, {params}),
deleteToken: (params: {
token: string
}) =>
axiosInstance.delete('/api/token', {params}),
getTokenInfo: (params: {
token: string
}) =>
axiosInstance.get('/api/token/info', {params}),
deleteTokenInfo: (params: {
token: string
dedup_bf?: string
cache_list?: string
both_number?: string
}) =>
axiosInstance.delete('/api/token/info', {params}),
uploadData: (params: {
data: string
token: string
}) =>
axiosInstance.post('/api/data', {}, {params}),
}
+23
View File
@@ -0,0 +1,23 @@
@import "tailwindcss" important;
@theme {
--animate-fade-in: fade-in 0.3s ease;
--bg-page: #f5f7fa;
--bg-card: #ffffff;
--bg-header: #ffffff;
}
@keyframes fade-in {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
:root {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
-7
View File
@@ -1,7 +0,0 @@
import axios from 'axios';
const instance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
});
export default instance;
+114
View File
@@ -0,0 +1,114 @@
<script setup lang="ts">
import api from "@/api"
import {Upload} from '@element-plus/icons-vue'
const props = defineProps<{
modelValue: boolean
token?: string
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void
}>()
const inputData = ref('')
const uploading = ref(false)
const uploadedCount = ref(0)
const totalCount = computed(() => {
const lines = inputData.value.split('\n').filter(line => line.trim())
return lines.length
})
const close = () => {
emit('update:modelValue', false)
inputData.value = ''
uploadedCount.value = 0
}
const handleUpload = async () => {
if (!inputData.value.trim()) {
ElMessage.warning('请输入数据')
return
}
uploading.value = true
uploadedCount.value = 0
const lines = inputData.value.split('\n').filter(line => line.trim())
const total = lines.length
uploadedCount.value = 0
for (const line of lines) {
try {
await api.uploadData({data: line, token: props.token || ''})
uploadedCount.value++
} catch (error: any) {
ElMessage.error(error.response?.data?.error || '上传失败')
uploading.value = false
return
}
}
uploading.value = false
ElMessage({message: '已上传成功 ' + uploadedCount.value + ' 条数据', type: 'success'})
setTimeout(() => {
close()
}, 2000)
}
</script>
<template>
<el-dialog
:model-value="modelValue"
@update:model-value="emit('update:modelValue', $event)"
width="700px"
:close-on-click-modal="false"
@close="close"
>
<template #header>
添加数据&nbsp;
<el-tag type="primary" effect="plain">{{ props.token }}</el-tag>
</template>
<div class="flex flex-col gap-4">
<el-input
v-model="inputData"
type="textarea"
:rows="18"
placeholder="请输入数据,每行一条"
:disabled="uploading"
class="textarea-input"
/>
<div class="flex justify-between items-center gap-4">
<div class="flex-1 flex items-center gap-3">
<el-progress
:percentage="totalCount > 0 ? Math.round((uploadedCount / totalCount) * 100) : 0"
:show-text="false"
class="flex-1"
/>
<span class="text-sm text-[var(--el-text-color-secondary)] whitespace-nowrap">已上传 {{ uploadedCount }}/{{ totalCount }} </span>
</div>
<el-button
type="primary"
:loading="uploading"
@click="handleUpload"
>
<el-icon v-if="!uploading">
<Upload/>
</el-icon>
上传
</el-button>
</div>
</div>
</el-dialog>
</template>
<style scoped>
.textarea-input :deep(.el-textarea__inner) {
overflow-x: auto;
white-space: pre;
word-break: keep-all;
}
</style>
+20 -7
View File
@@ -1,16 +1,29 @@
import {createApp} from 'vue' import {createApp} from 'vue'
import {createPinia} from 'pinia' import {createPinia} from 'pinia'
import persistedState from 'pinia-plugin-persistedstate'; import persistedState from 'pinia-plugin-persistedstate';
import App from './App.vue' import App from './App.vue'
import router from './router' import router from './router'
import ElementPlus from 'element-plus' // import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css' // import 'element-plus/dist/index.css'
import './assets/main.css'
const pinia = createPinia()
pinia.use(persistedState)
const app = createApp(App) const app = createApp(App)
app.use(pinia)
app.use(createPinia().use(persistedState))
app.use(router) app.use(router)
app.use(ElementPlus) // app.use(ElementPlus)
app.mount('#app') app.mount('#app')
router.beforeEach((to, from) => {
var postfix: string = ''
if (import.meta.env.DEV) {
postfix = '-dev'
}
if (to.meta.title) {
document.title = to.meta.title + postfix
}
})
+10 -2
View File
@@ -1,20 +1,23 @@
import {createRouter, createWebHistory} from 'vue-router' import {createRouter, createWebHistory} from 'vue-router'
import TokenManageView from '@/views/TokenManageView.vue' import TokenManageView from '@/views/admin/TokenManageView.vue'
import TokenDetailView from '@/views/TokenDetailView.vue' import TokenDetailView from '@/views/admin/TokenDetailView.vue'
import AdminView from '@/views/AdminView.vue' import AdminView from '@/views/AdminView.vue'
import HomeView from '@/views/HomeView.vue' import HomeView from '@/views/HomeView.vue'
import SettingsView from '@/views/admin/SettingsView.vue'
const routes = [ const routes = [
{ {
path: '/', path: '/',
name: "Home", name: "Home",
component: HomeView, component: HomeView,
meta: {title: "抖音数据去重",}
}, },
{ {
path: '/haha', path: '/haha',
name: "Admin", name: "Admin",
component: AdminView, component: AdminView,
meta: {title: "抖音数据去重 | 管理后台",},
children: [ children: [
{ {
path: '', path: '',
@@ -26,6 +29,11 @@ const routes = [
name: "TokenDetail", name: "TokenDetail",
component: TokenDetailView component: TokenDetailView
}, },
{
path: "settings",
name: "AdminSettings",
component: SettingsView,
}
], ],
}, },
+1 -5
View File
@@ -1,14 +1,10 @@
import {ref, computed} from 'vue' import {ref} from 'vue'
import {defineStore} from 'pinia' import {defineStore} from 'pinia'
export const useCounterStore = defineStore('counter', () => { export const useCounterStore = defineStore('counter', () => {
const homeToken = ref("")
const token = ref("") const token = ref("")
const isAdmin = ref(false) const isAdmin = ref(false)
return {token, isAdmin} return {token, isAdmin}
}, { }, {
persist: true persist: true
+82
View File
@@ -0,0 +1,82 @@
/* eslint-disable */
/* prettier-ignore */
/* oxlint-disable */
/* oxfmt-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
const EffectScope: typeof import('vue').EffectScope
const ElMessage: typeof import('element-plus/es').ElMessage
const ElMessageBox: typeof import('element-plus/es').ElMessageBox
const computed: typeof import('vue').computed
const createApp: typeof import('vue').createApp
const customRef: typeof import('vue').customRef
const defineAsyncComponent: typeof import('vue').defineAsyncComponent
const defineComponent: typeof import('vue').defineComponent
const effectScope: typeof import('vue').effectScope
const getCurrentInstance: typeof import('vue').getCurrentInstance
const getCurrentScope: typeof import('vue').getCurrentScope
const getCurrentWatcher: typeof import('vue').getCurrentWatcher
const h: typeof import('vue').h
const inject: typeof import('vue').inject
const isProxy: typeof import('vue').isProxy
const isReactive: typeof import('vue').isReactive
const isReadonly: typeof import('vue').isReadonly
const isRef: typeof import('vue').isRef
const isShallow: typeof import('vue').isShallow
const markRaw: typeof import('vue').markRaw
const nextTick: typeof import('vue').nextTick
const onActivated: typeof import('vue').onActivated
const onBeforeMount: typeof import('vue').onBeforeMount
const onBeforeRouteLeave: typeof import('vue-router').onBeforeRouteLeave
const onBeforeRouteUpdate: typeof import('vue-router').onBeforeRouteUpdate
const onBeforeUnmount: typeof import('vue').onBeforeUnmount
const onBeforeUpdate: typeof import('vue').onBeforeUpdate
const onDeactivated: typeof import('vue').onDeactivated
const onErrorCaptured: typeof import('vue').onErrorCaptured
const onMounted: typeof import('vue').onMounted
const onRenderTracked: typeof import('vue').onRenderTracked
const onRenderTriggered: typeof import('vue').onRenderTriggered
const onScopeDispose: typeof import('vue').onScopeDispose
const onServerPrefetch: typeof import('vue').onServerPrefetch
const onUnmounted: typeof import('vue').onUnmounted
const onUpdated: typeof import('vue').onUpdated
const onWatcherCleanup: typeof import('vue').onWatcherCleanup
const provide: typeof import('vue').provide
const reactive: typeof import('vue').reactive
const readonly: typeof import('vue').readonly
const ref: typeof import('vue').ref
const resolveComponent: typeof import('vue').resolveComponent
const shallowReactive: typeof import('vue').shallowReactive
const shallowReadonly: typeof import('vue').shallowReadonly
const shallowRef: typeof import('vue').shallowRef
const toRaw: typeof import('vue').toRaw
const toRef: typeof import('vue').toRef
const toRefs: typeof import('vue').toRefs
const toValue: typeof import('vue').toValue
const triggerRef: typeof import('vue').triggerRef
const unref: typeof import('vue').unref
const useAttrs: typeof import('vue').useAttrs
const useCssModule: typeof import('vue').useCssModule
const useCssVars: typeof import('vue').useCssVars
const useId: typeof import('vue').useId
const useLink: typeof import('vue-router').useLink
const useModel: typeof import('vue').useModel
const useRoute: typeof import('vue-router').useRoute
const useRouter: typeof import('vue-router').useRouter
const useSlots: typeof import('vue').useSlots
const useTemplateRef: typeof import('vue').useTemplateRef
const watch: typeof import('vue').watch
const watchEffect: typeof import('vue').watchEffect
const watchPostEffect: typeof import('vue').watchPostEffect
const watchSyncEffect: typeof import('vue').watchSyncEffect
}
// for type re-export
declare global {
// @ts-ignore
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue')
}
+43
View File
@@ -0,0 +1,43 @@
/* eslint-disable */
// @ts-nocheck
// biome-ignore lint: disable
// oxlint-disable
// ------
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
AddDataDialog: typeof import('./../../components/AddDataDialog.vue')['default']
ElButton: typeof import('element-plus/es')['ElButton']
ElCard: typeof import('element-plus/es')['ElCard']
ElCollapse: typeof import('element-plus/es')['ElCollapse']
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElDivider: typeof import('element-plus/es')['ElDivider']
ElDropdown: typeof import('element-plus/es')['ElDropdown']
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
ElHeader: typeof import('element-plus/es')['ElHeader']
ElIcon: typeof import('element-plus/es')['ElIcon']
ElInput: typeof import('element-plus/es')['ElInput']
ElMain: typeof import('element-plus/es')['ElMain']
ElOption: typeof import('element-plus/es')['ElOption']
ElPopconfirm: typeof import('element-plus/es')['ElPopconfirm']
ElProgress: typeof import('element-plus/es')['ElProgress']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElSpace: typeof import('element-plus/es')['ElSpace']
ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTag: typeof import('element-plus/es')['ElTag']
ElText: typeof import('element-plus/es')['ElText']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}
export interface GlobalDirectives {
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
}
}
+140 -34
View File
@@ -1,50 +1,156 @@
<script setup lang="ts"> <script setup lang="ts">
import {ref, watch} from 'vue' import {useCounterStore} from "@/stores/counter.ts"
import {useRoute, useRouter} from "vue-router"; import {CloseBold, Document, Edit, Lock, Setting, User} from '@element-plus/icons-vue'
import {useCounterStore} from "@/stores/counter.ts";
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
const store = useCounterStore()
const activeIndex = ref(route.name?.toString() || "TokenList") const activeIndex = ref(route.name?.toString() || "TokenManage")
const _userName = ref("未登录")
const _isAdmin = ref(store.isAdmin)
const inputPassWord = ref('')
watch(route, (newRoute) => { if (_isAdmin.value) {
activeIndex.value = newRoute.name?.toString() || "TokenList" _userName.value = "管理员"
} else {
_userName.value = "未登录"
}
watch(_isAdmin, (newValue) => {
store.isAdmin = newValue
if (newValue) {
_userName.value = "管理员"
} else {
_userName.value = "未登录"
}
}) })
const handleSelect = (key: string) => { watch(route, (newRoute) => {
router.push({ activeIndex.value = newRoute.name?.toString() || "TokenManage"
name: key })
})
const handleSelect = (name: string) => {
router.push({name: name})
}
const logout = () => {
_isAdmin.value = false
}
const checkPassword = () => {
if (inputPassWord.value === "haha") {
ElMessage({message: '登录成功', type: 'success'})
_isAdmin.value = true
} else {
ElMessage.error('密码错误')
}
}
const navItemClass = (name: string) => {
return activeIndex.value === name
? 'bg-[var(--el-color-primary)] text-white'
: 'text-[var(--el-text-color-regular)] hover:bg-[var(--bg-page)] hover:text-[var(--el-text-color-primary)]'
} }
</script> </script>
<template> <template>
<el-container> <div class="min-h-screen flex flex-col bg-(--bg-page)">
<el-header> <el-header
<el-menu class="flex items-center justify-between bg-(--bg-header) border-b border-(--el-border-color) sticky top-0 z-50 max-md:flex-wrap max-md:h-auto max-md:px-4 max-md:py-3">
:default-active="activeIndex" <div class="flex items-center">
class="el-menu-demo" <div class="flex items-center gap-2.5 text-(--el-color-primary) font-semibold text-lg">DYPID 管理后台</div>
mode="horizontal" </div>
@select="handleSelect"
>
<el-menu-item index="TokenManage">管理Token</el-menu-item>
<el-menu-item index="TokenDetail">Token详细信息</el-menu-item>
<el-menu-item v-if="useCounterStore().isAdmin">
<el-button type="danger" plain @click="useCounterStore().isAdmin=false">退出管理员</el-button>
</el-menu-item>
</el-menu>
<nav class="flex gap-2 max-md:order-3 max-md:w-full max-md:mt-3 max-md:overflow-x-auto max-md:pb-1">
<div
class="flex items-center gap-1.5 px-4 py-2 rounded-lg cursor-pointer text-sm transition-all duration-200 max-md:px-3 max-md:py-2 max-md:text-xs"
:class="navItemClass('TokenManage')"
@click="handleSelect('TokenManage')"
>
<el-icon>
<Edit/>
</el-icon>
<span>管理 Token</span>
</div>
<div
class="flex items-center gap-1.5 px-4 py-2 rounded-lg cursor-pointer text-sm transition-all duration-200 max-md:px-3 max-md:py-2 max-md:text-xs"
:class="navItemClass('TokenDetail')"
@click="handleSelect('TokenDetail')"
>
<el-icon>
<Document/>
</el-icon>
<span>Token 详情</span>
</div>
</nav>
<div class="flex items-center">
<el-button v-if="_isAdmin" circle link
@click="handleSelect('AdminSettings')"
>
<el-icon size="16">
<Setting/>
</el-icon>
</el-button>
<el-divider direction="vertical"/>
<el-dropdown :disabled="!_isAdmin">
<el-button :icon="User" link>{{ _userName }}</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="logout()">
<el-text type="danger">
<el-icon>
<CloseBold/>
</el-icon>
退出登录
</el-text>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</el-header> </el-header>
<el-container> <el-main>
<el-main> <!-- 登录页面 -->
<router-view></router-view> <div v-if="!_isAdmin" class="flex justify-center items-center min-h-[60vh]">
</el-main> <div
</el-container> class="w-full max-w-100 text-center py-12 px-10 bg-(--bg-card) rounded-2xl shadow-[0_4px_24px_rgba(0,0,0,0.08)]">
</el-container> <div class="mb-6">
<el-icon size="48" color="#409eff">
<Lock/>
</el-icon>
</div>
<div class="text-2xl font-semibold text-(--el-text-color-primary) mb-2">管理员登录</div>
<div class="text-sm text-(--el-text-color-secondary) mb-8">请输入管理员密码访问管理后台</div>
<el-input
v-model="inputPassWord"
type="password"
placeholder="请输入管理员密码"
size="large"
show-password
class="mb-4"
@keyup.enter="checkPassword"
>
<template #prefix>
<el-icon>
<Lock/>
</el-icon>
</template>
</el-input>
<el-button type="primary" size="large" class="w-full mt-2" @click="checkPassword">
登录
</el-button>
</div>
</div>
<router-view v-else v-slot="{ Component }">
<component :is="Component"/>
</router-view>
</el-main>
</div>
</template> </template>
<style scoped>
</style>
+222 -65
View File
@@ -1,77 +1,234 @@
<script setup lang="ts"> <script setup lang="ts">
import {ref} from 'vue' import api from "@/api"
import axios from "@/axios.ts"; import {DataAnalysis, Document, Key, Plus, Search, Warning} from '@element-plus/icons-vue'
import {useRoute} from "vue-router"
import {ElMessage} from "element-plus";
const token = ref(useRoute().query.token) const route = useRoute()
const input = ref(useRoute().query.token) const inputToken = ref(route.query.token as string || '')
const result = ref() const result = ref<any>(null)
const loading = ref(false)
const lastUpdate = ref('')
const showAddDataDialog = ref(false)
const updateTime = () => {
const now = new Date()
lastUpdate.value = now.toLocaleTimeString()
}
const fetchInfo = async () => {
if (!inputToken.value) return
try {
const res = await api.getTokenInfo({token: inputToken.value})
result.value = res.data.data
updateTime()
} catch {
result.value = null
ElMessage({message: 'Token输入错误', type: 'error', duration: 2000})
}
}
const refresh = async () => {
loading.value = true
try {
const res = await api.getTokenInfo({token: inputToken.value})
result.value = res.data.data
updateTime()
ElMessage({message: '查询成功', type: 'success', duration: 1500})
} catch {
result.value = null
ElMessage({message: 'Token输入错误', type: 'error', duration: 2000})
}
loading.value = false
}
const inputChange = () => { const inputChange = () => {
if (input.value != null && input.value != '') { if (inputToken.value) fetchInfo()
axios.get('/api/token/info', {
params: {
token: input.value
}
}).then(res => {
result.value = res.data.result
token.value = input.value
ElMessage({
message: '更改成功',
type: 'success',
})
}).catch(error => {
ElMessage({
message: 'Token输入错误',
type: 'error',
})
})
}
} }
const getInfo = () => { let timer: number
if (token.value != null && token.value != '') { onMounted(() => {
axios.get('/api/token/info', { fetchInfo()
params: { timer = window.setInterval(fetchInfo, 5000)
token: token.value })
}
}).then(res => {
result.value = res.data.result
})
}
}
getInfo() onUnmounted(() => {
setInterval(getInfo, 5000) clearInterval(timer)
})
</script> </script>
<template> <template>
<b>当前Token</b> <div class="p-6 h-dvh">
<el-input <div class="bg-white rounded-lg p-6 shadow-[0_2px_12px_rgba(0,0,0,0.08)] h-full">
v-model="input" <div class="text-center mb-4">
style="width: 240px" <div class="text-xl font-semibold text-(--el-text-color-primary) mb-5 pb-3 border-b border-(--el-border-color)">
placeholder="输入Token" Token 信息查询
clearable </div>
@change="inputChange" <div class="text-(--el-text-color-secondary) mt-2">输入 Token 以查看去重和缓存信息</div>
/> </div>
<el-divider/> <div class="flex justify-center mb-8">
<b>Token信息每5秒刷新</b> <el-input placeholder="请输入要查询数据的Token..." size="large" clearable
<el-button type="primary" plain @click="getInfo">手动刷新</el-button> class="max-w-120"
<el-descriptions v-model="inputToken" @change="inputChange"
direction="vertical" >
:column="4" <template #prepend>
border Token
> </template>
<el-descriptions-item label="去重对象">{{ result?.dedup_object }}</el-descriptions-item> </el-input>
<el-descriptions-item label="上传数据格式">{{ result?.data_format }}</el-descriptions-item>
<el-descriptions-item label="去重记录值">{{ result?.dedup_items_number }}</el-descriptions-item> <el-button type="primary" size="large"
<el-descriptions-item label="Redis中数据条数">{{ result?.cache_list_number }}</el-descriptions-item> class="ml-3"
</el-descriptions> @click="refresh" :loading="loading" :icon="Search"
>
查询
</el-button>
</div>
<el-space direction="vertical"
class="w-full animate-fade-in"
v-if="result"
>
<el-card class="p-6">
<template #header>
<el-text>Token 信息</el-text>
<el-divider direction="vertical"/>
<el-tag type="primary" effect="plain">{{ inputToken }}</el-tag>
</template>
<el-space direction="vertical">
<el-space>
<div
class="flex items-center gap-4 p-5 bg-(--bg-page) rounded-xl transition-all duration-200
hover:-translate-y-0.5 hover:shadow-[0_4px_16px_rgba(0,0,0,0.1)]
min-w-100"
>
<div
class="w-14 h-14 rounded-xl flex items-center justify-center shrink-0"
style="background: #409eff20; color: #409eff"
>
<el-icon size="24">
<DataAnalysis/>
</el-icon>
</div>
<div class="flex-1">
<div class="text-xs text-(--el-text-color-secondary) mb-1">
去重对象
</div>
<div
class="text-lg font-semibold text-(--el-text-color-primary) overflow-hidden text-ellipsis whitespace-nowrap"
>
{{ result['dedup_object'] || '-' }}
</div>
</div>
</div>
<div
class="flex items-center gap-4 p-5 bg-(--bg-page) rounded-xl transition-all duration-200
hover:-translate-y-0.5 hover:shadow-[0_4px_16px_rgba(0,0,0,0.1)]
min-w-100"
>
<div
class="w-14 h-14 rounded-xl flex items-center justify-center shrink-0"
style="background: #67c23a20; color: #67c23a"
>
<el-icon size="24">
<Document/>
</el-icon>
</div>
<div class="flex-1">
<div class="text-xs text-(--el-text-color-secondary) mb-1">
上传数据格式
</div>
<div
class="text-lg font-semibold text-(--el-text-color-primary) overflow-hidden text-ellipsis whitespace-nowrap"
>
{{ result['data_format'] || '-' }}
</div>
</div>
</div>
</el-space>
<el-space>
<div
class="flex items-center gap-4 p-5 bg-(--bg-page) rounded-xl transition-all duration-200
hover:-translate-y-0.5 hover:shadow-[0_4px_16px_rgba(0,0,0,0.1)]
min-w-100"
>
<div
class="w-14 h-14 rounded-xl flex items-center justify-center shrink-0"
style="background: #e6a23c20; color: #e6a23c"
>
<el-icon size="24">
<Key/>
</el-icon>
</div>
<div class="flex-1">
<div class="text-xs text-(--el-text-color-secondary) mb-1">
去重记录值
</div>
<div
class="text-lg font-semibold text-(--el-text-color-primary) overflow-hidden text-ellipsis whitespace-nowrap"
>
{{ result['dedup_items_number'] || '-' }}
</div>
</div>
</div>
<div
class="flex items-center gap-4 p-5 bg-(--bg-page) rounded-xl transition-all duration-200
hover:-translate-y-0.5 hover:shadow-[0_4px_16px_rgba(0,0,0,0.1)]
min-w-100"
>
<div
class="w-14 h-14 rounded-xl flex items-center justify-center shrink-0"
style="background: #90939920; color: #909399"
>
<el-icon size="24">
<Warning/>
</el-icon>
</div>
<div class="flex-1">
<div class="text-xs text-(--el-text-color-secondary) mb-1">
Redis中数据条数
</div>
<div
class="text-lg font-semibold text-(--el-text-color-primary) overflow-hidden text-ellipsis whitespace-nowrap"
>
{{ result['cache_list_number'] || '-' }}
</div>
</div>
</div>
</el-space>
</el-space>
<el-text v-if="lastUpdate" type="info"
class="block w-full mt-5 text-center"
>
每5秒更新一次数据 最后更新: {{ lastUpdate }}
</el-text>
<template #footer>
<el-text>操作</el-text>
<el-divider direction="vertical"/>
<el-button type="primary" size=""
class=""
@click="showAddDataDialog = true" :icon="Plus"
>
添加数据
</el-button>
</template>
</el-card>
</el-space>
<el-space v-else direction="vertical"
class="flex p-16"
>
<el-icon size="64" color="#dcdfe6">
<Search/>
</el-icon>
<el-text type="info" size="large" class="">请输入 Token 进行查询</el-text>
</el-space>
</div>
</div>
<AddDataDialog v-model="showAddDataDialog" :token="inputToken"/>
</template> </template>
<style scoped>
</style>
-109
View File
@@ -1,109 +0,0 @@
<script setup lang="ts">
import {useCounterStore} from "@/stores/counter.ts";
import {ref, watch} from 'vue'
import axios from "@/axios.ts";
import {useRoute} from "vue-router"
const route = useRoute()
const result = ref()
const value = ref('')
const getInfo = () => {
if (value.value != '') {
axios.get('/api/token/info', {
params: {
token: value.value
}
}).then(res => {
if (res.status == 200) {
result.value = res.data.result
}
})
}
}
const deleteDedup = () => {
axios.delete('/api/token/info', {
params: {
token: value.value,
dedup_bf: true
}
}).then(res => {
getInfo()
})
}
const deleteRedis = () => {
axios.delete('/api/token/info', {
params: {
token: value.value,
cache_list: true
}
}).then(res => {
getInfo()
})
}
getInfo()
setInterval(getInfo, 5000)
watch(value, (newValue) => {
getInfo()
})
interface optionsType {
value: string
}
const options = ref([] as optionsType[])
value.value = useCounterStore().token
axios.get('/api/token').then(res => {
if (res.status == 200) {
res.data.result.forEach((item: any) => {
options.value.push({"value": item.token})
})
}
})
</script>
<template>
<div v-if="!useCounterStore().isAdmin">
<el-alert title="您没有权限访问此页面" type="error" center show-icon/>
</div>
<div v-if="useCounterStore().isAdmin">
<b>当前Token</b>
<el-select v-model="value" placeholder="选择Token" style="width: 240px">
<el-option
v-for="item in options"
:key="item.value"
:value="item.value"
/>
</el-select>
<el-divider/>
<b>Token信息每5秒刷新</b>
<el-button type="primary" plain @click="getInfo">手动刷新</el-button>
<el-descriptions
direction="vertical"
:column="4"
border
>
<el-descriptions-item label="去重对象">{{ result?.dedup_object }}</el-descriptions-item>
<el-descriptions-item label="上传数据格式">{{ result?.data_format }}</el-descriptions-item>
<el-descriptions-item label="去重记录值">{{ result?.dedup_items_number }}</el-descriptions-item>
<el-descriptions-item label="Redis中数据条数">{{ result?.cache_list_number }}</el-descriptions-item>
</el-descriptions>
<p><b>管理</b></p>
<el-button type="danger" @click="deleteDedup">删除去重记录值</el-button>
<el-button type="danger" @click="deleteRedis">删除Redis数据</el-button>
</div>
</template>
<style scoped>
</style>
-301
View File
@@ -1,301 +0,0 @@
<script setup lang="ts">
import {ref} from "vue";
import axios from "@/axios.ts";
import {ElMessage, ElMessageBox} from 'element-plus'
import {useCounterStore} from "@/stores/counter.ts";
import {useRouter} from "vue-router";
const tableData = ref([])
axios.get("/api/token").then(res => {
tableData.value = res.data.result
})
const input = ref('')
const value = ref('')
const options = [
{
value: 'uid',
},
{
value: 'secid',
},
{
value: 'pid',
},
{
value: 'comment_id',
},
{
value: 'dyid',
}
]
const dataFormat = ref('')
const dataFormatOptions = [
{
value: 'uid----secid----pid----comment_id',
}, {
value: 'uid----secid',
}
]
const addToken = () => {
axios.post('/api/token', {}, {
params: {
token: input.value,
dedup_object: value.value,
data_format: dataFormat.value,
notes: inputNotes.value,
}
}).then(response => {
if (response.data.result == "ok") {
ElMessage({
message: '添加成功',
type: 'success',
})
axios.get('/api/token').then(res => {
tableData.value = res.data.result
})
}
}).catch(error => {
ElMessage.error(error.response?.data?.error)
})
}
const router = useRouter()
const viewDetails = (row: any) => {
useCounterStore().token = row.token
router.push({
name: "TokenDetail",
})
}
var rowOut: any
const dedupObjectVisible = ref(false)
const dialogDedupObjectVisible = (row: any) => {
rowOut = row
dedupObjectVisible.value = true
}
const updateDedupObject = () => {
dedupObjectVisible.value = false
axios.put('/api/token', {}, {
params: {
token: rowOut.token,
dedup_object: value.value,
data_format: rowOut.data_format,
notes: rowOut.notes,
}
}).then(res => {
if (res.data.result == "ok") {
ElMessage({
message: '更改成功',
type: 'success',
})
axios.get('/api/token').then(res => {
tableData.value = res.data.result
})
}
}).catch(error => {
ElMessage.error(error.response?.data?.error)
})
}
const dataFormatVisible = ref(false)
const dialogDataFormatVisible = (row: any) => {
rowOut = row
dataFormatVisible.value = true
}
const updateDataFormat = () => {
dataFormatVisible.value = false
axios.put('/api/token', {}, {
params: {
token: rowOut.token,
dedup_object: rowOut.dedup_object,
data_format: dataFormat.value,
notes: rowOut.notes,
}
}).then(res => {
if (res.data.result == "ok") {
ElMessage({
message: '更改成功',
type: 'success',
})
axios.get('/api/token').then(res => {
tableData.value = res.data.result
})
}
}).catch(error => {
ElMessage.error(error.response?.data?.error)
})
}
const inputNotes = ref("")
const NotesVisible = ref(false)
const dialogNotesVisible = (row: any) => {
rowOut = row
NotesVisible.value = true
}
const updateNotes = () => {
NotesVisible.value = false
axios.put('/api/token', {}, {
params: {
token: rowOut.token,
dedup_object: rowOut.dedup_object,
data_format: rowOut.data_format,
notes: inputNotes.value
}
}).then(res => {
if (res.data.result == "ok") {
ElMessage({
message: '更改成功',
type: 'success',
})
axios.get('/api/token').then(res => {
tableData.value = res.data.result
})
}
}).catch(error => {
ElMessage.error(error.response?.data?.error)
})
}
const deleteToken = (row: any) => {
axios.delete('/api/token', {
params: {
token: row.token
}
}).then(res => {
if (res.data.result == "ok") {
ElMessage({
message: '删除成功',
type: 'success',
})
axios.get('/api/token').then(res => {
tableData.value = res.data.result
})
}
}).catch(error => {
ElMessage.error(error.response?.data?.error)
})
}
const inputPassWord = ref('')
const checkPassword = () => {
if (inputPassWord.value == "haha") {
ElMessage({
message: '密码正确',
type: 'success',
})
useCounterStore().isAdmin = true
} else {
ElMessage.error('密码错误')
}
}
</script>
<template>
<!--非管理员-->
<div v-if="!useCounterStore().isAdmin" style="margin: auto auto; max-width: 400px">
<el-alert title="您没有权限访问此页面,输入管理员密码" type="error" :closable="false" show-icon/>
<p></p>
<el-input v-model="inputPassWord" style="width: 400px" placeholder="请输入管理员密码" @change="checkPassword"/>
<p></p>
<el-button type="primary" @click="checkPassword">确认</el-button>
</div>
<!--管理员-->
<div v-if="useCounterStore().isAdmin">
<!--添加Token-->
<el-input v-model="input" style="width: 150px" placeholder="请输入Token名称"/>
<el-select v-model="value" placeholder="选择去重对象" style="width: 150px">
<el-option
v-for="item in options"
:key="item.value"
:value="item.value"
/>
</el-select>
<el-select v-model="dataFormat" placeholder="选择数据格式" style="width: 280px">
<el-option
v-for="item in dataFormatOptions"
:key="item.value"
:value="item.value"
/>
</el-select>
<el-input v-model="inputNotes" style="width: 200px" placeholder="请输入备注"/>
<el-button type="primary" @click="addToken">添加Token</el-button>
<!--Token列表-->
<el-table :data="tableData" style="width: 100%">
<el-table-column prop="token" label="Token" width="150"/>
<el-table-column prop="dedup_object" label="去重对象" width="150"/>
<el-table-column prop="data_format" label="上传数据格式" width="280"/>
<el-table-column prop="notes" label="备注" width="200"/>
<el-table-column label="操作">
<template #default="scope">
<el-button @click="viewDetails(scope.row)">查看详细</el-button>
<el-button @click="dialogDedupObjectVisible(scope.row)" type="primary">更改去重对象</el-button>
<el-button @click="dialogDataFormatVisible(scope.row)" type="primary">更改数据格式</el-button>
<el-button @click="dialogNotesVisible(scope.row)" type="primary">更改备注</el-button>
<el-popconfirm
width="180"
title="确认删除此Token吗"
confirm-button-text="确认"
cancel-button-text="取消"
@confirm="deleteToken(scope.row)"
>
<template #reference>
<el-button type="danger">删除此Token</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<el-dialog v-model="dedupObjectVisible" title="更改去重对象" width="400">
<el-select v-model="value" placeholder="选择去重对象" style="width: 200px">
<el-option
v-for="item in options"
:key="item.value"
:value="item.value"
/>
</el-select>
<template #footer>
<el-button type="primary" @click="updateDedupObject">
确定
</el-button>
</template>
</el-dialog>
<el-dialog v-model="dataFormatVisible" title="更改数据格式" width="400">
<el-select v-model="dataFormat" placeholder="选择数据格式" style="width: 280px">
<el-option
v-for="item in dataFormatOptions"
:key="item.value"
:value="item.value"
/>
</el-select>
<template #footer>
<el-button type="primary" @click="updateDataFormat">
确定
</el-button>
</template>
</el-dialog>
<el-dialog v-model="NotesVisible" title="更改备注" width="400">
<el-input v-model="inputNotes" style="width: 200px" placeholder="请输入备注"/>
<template #footer>
<el-button type="primary" @click="updateNotes">
确定
</el-button>
</template>
</el-dialog>
</div>
</template>
<style scoped>
</style>
+16
View File
@@ -0,0 +1,16 @@
<script setup lang="ts">
import api from "@/api.ts";
const version = ref('')
onMounted(() => {
api.getVersion().then((response) => {
version.value = response.data
})
})
</script>
<template>
{{ version }}
</template>
+316
View File
@@ -0,0 +1,316 @@
<script setup lang="ts">
import {useCounterStore} from "@/stores/counter.ts"
import api from "@/api"
import {DataAnalysis, Delete, Document, InfoFilled, Key, Refresh, Search, Warning} from '@element-plus/icons-vue'
const store = useCounterStore()
const result = ref<any>(null)
const value = ref('')
const options = ref<string[]>([])
const loading = ref(false)
const lastUpdate = ref('')
const deleteSpecifyDataVisible = ref(false)
const inputSpecifyData = ref('')
const deleteSpecifyDedupVisible = ref(false)
const inputSpecifyDedup = ref('')
const deleteSpecifyRawVisible = ref(false)
const inputSpecifyRaw = ref('')
const isLoading = ref(false)
const fetchInfo = async () => {
if (!value.value) return
const res = await api.getTokenInfo({token: value.value})
if (res.status === 200) {
result.value = res.data.data
lastUpdate.value = new Date().toLocaleTimeString()
}
}
const fetchTokens = async () => {
try {
const res = await api.getTokenList()
if (res.status === 200) {
options.value = res.data.data?.map((item: any) => item.token) || []
}
} catch (error) {
console.error('Failed to fetch tokens:', error)
}
}
const refresh = async () => {
loading.value = true
await fetchInfo()
loading.value = false
ElMessage({message: '刷新成功', type: 'success', duration: 1500})
}
const deleteDedup = () => {
ElMessageBox.confirm('确定要删除全部去重参考值吗?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(async () => {
await api.deleteTokenInfo({token: value.value, dedup_bf: "all"})
ElMessage({message: '删除成功', type: 'success'})
await fetchInfo()
}).catch(() => {
})
}
const deleteRedis = () => {
ElMessageBox.confirm('确定要删除全部原始数据吗?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(async () => {
await api.deleteTokenInfo({token: value.value, cache_list: "all"})
ElMessage({message: '删除成功', type: 'success'})
await fetchInfo()
}).catch(() => {
})
}
const deleteSpecifyDedup = async () => {
isLoading.value = true
await api.deleteTokenInfo({token: value.value, dedup_bf: inputSpecifyDedup.value})
isLoading.value = false
ElMessage({message: '删除成功', type: 'success'})
deleteSpecifyDedupVisible.value = false
inputSpecifyDedup.value = ''
await fetchInfo()
}
const deleteSpecifyRaw = async () => {
isLoading.value = true
await api.deleteTokenInfo({token: value.value, cache_list: inputSpecifyRaw.value})
isLoading.value = false
ElMessage({message: '删除成功', type: 'success'})
deleteSpecifyRawVisible.value = false
inputSpecifyRaw.value = ''
await fetchInfo()
}
const deleteSpecifyData = async () => {
isLoading.value = true
await api.deleteTokenInfo({token: value.value, both_number: inputSpecifyData.value})
isLoading.value = false
ElMessage({message: '删除成功', type: 'success'})
deleteSpecifyDataVisible.value = false
inputSpecifyData.value = ''
await fetchInfo()
}
let timer: number
onMounted(() => {
fetchTokens()
if (store.token) {
value.value = store.token
}
})
watch(value, (newValue) => {
store.token = newValue
if (newValue) fetchInfo()
})
onMounted(() => {
if (value.value) fetchInfo()
timer = window.setInterval(fetchInfo, 5000)
})
onUnmounted(() => {
clearInterval(timer)
})
const statCards = [
{label: '去重对象', key: 'dedup_object', icon: DataAnalysis, color: '#409eff'},
{label: '上传数据格式', key: 'data_format', icon: Document, color: '#67c23a'},
{label: '去重参考值数量', key: 'dedup_items_number', icon: Key, color: '#e6a23c'},
{label: '原始数据数量', key: 'cache_list_number', icon: Warning, color: '#909399'},
]
</script>
<template>
<div class="mx-auto w-full">
<!-- 管理页面 -->
<div class="bg-white rounded-lg p-6 shadow-[0_2px_12px_rgba(0,0,0,0.08)]">
<div class="text-xl font-semibold text-(--el-text-color-primary) mb-5 pb-3 border-b border-(--el-border-color)">
Token 详情
</div>
<!-- 选择 Token -->
<div class="flex items-center gap-3 mb-6 flex-wrap max-md:flex-col max-md:items-stretch">
<el-select v-model="value" placeholder="选择 Token" clearable style="width: 100%; max-width: 300px">
<el-option v-for="item in options" :key="item" :value="item" :label="item"/>
</el-select>
<el-button type="primary" @click="refresh" :loading="loading">
<el-icon>
<Refresh/>
</el-icon>
刷新
</el-button>
<span v-if="lastUpdate" class="text-xs text-(--el-text-color-secondary)">上次更新: {{ lastUpdate }}</span>
</div>
<!-- Token 信息 -->
<div v-if="result && value" class="animate-fade-in">
<div class="text-base font-semibold text-(--el-text-color-primary) mt-5 mb-4">Token 信息</div>
<div class="grid grid-cols-4 gap-4 mb-8 max-xl:grid-cols-2 max-md:grid-cols-1">
<div v-for="card in statCards" :key="card.key"
class="flex items-center gap-4 p-5 bg-(--bg-page) rounded-xl transition-all duration-200 hover:-translate-y-0.5 hover:shadow-[0_4px_16px_rgba(0,0,0,0.1)]">
<div class="w-14 h-14 rounded-xl flex items-center justify-center shrink-0"
:style="{ background: card.color + '20', color: card.color }">
<el-icon size="24">
<component :is="card.icon"/>
</el-icon>
</div>
<div class="flex-1 min-w-0">
<div class="text-xs text-(--el-text-color-secondary) mb-1">{{ card.label }}</div>
<div class="text-lg font-semibold text-(--el-text-color-primary)">{{ result[card.key] || '-' }}</div>
</div>
</div>
</div>
<!-- 管理操作 -->
<div class="mt-8">
<div class="text-base font-semibold text-(--el-text-color-primary) mt-5 mb-4">数据管理</div>
<div class="grid grid-cols-3 gap-4 max-xl:grid-cols-2 max-md:grid-cols-1">
<div
class="flex items-center gap-4 p-5 bg-(--bg-page) rounded-xl transition-all duration-200 hover:-translate-y-0.5 hover:shadow-[0_4px_16px_rgba(0,0,0,0.1)] max-md:flex-col max-md:text-center">
<div
class="w-14 h-14 rounded-xl flex items-center justify-center shrink-0 bg-(--el-color-primary) text-white">
<el-icon size="28">
<Delete/>
</el-icon>
</div>
<div class="flex-1 min-w-0">
<div class="text-base font-semibold text-(--el-text-color-primary) mb-1">删除全部去重参考值</div>
<div class="text-xs text-(--el-text-color-secondary)">清除该 Token 的所有去重布隆过滤器</div>
</div>
<el-button type="danger" @click="deleteDedup">执行</el-button>
</div>
<div
class="flex items-center gap-4 p-5 bg-(--bg-page) rounded-xl transition-all duration-200 hover:-translate-y-0.5 hover:shadow-[0_4px_16px_rgba(0,0,0,0.1)] max-md:flex-col max-md:text-center">
<div
class="w-14 h-14 rounded-xl flex items-center justify-center shrink-0 bg-(--el-color-primary) text-white">
<el-icon size="28">
<Delete/>
</el-icon>
</div>
<div class="flex-1 min-w-0">
<div class="text-base font-semibold text-(--el-text-color-primary) mb-1">删除全部原始数据</div>
<div class="text-xs text-(--el-text-color-secondary)">清除该 Token 的所有原始缓存数据</div>
</div>
<el-button type="danger" @click="deleteRedis">执行</el-button>
</div>
</div>
<div class="text-sm font-semibold text-(--el-text-color-secondary) mt-6 mb-4">按数量删除</div>
<div class="grid grid-cols-3 gap-4 max-xl:grid-cols-2 max-md:grid-cols-1">
<div
class="flex items-center gap-4 p-5 bg-(--bg-page) rounded-xl transition-all duration-200 hover:-translate-y-0.5 hover:shadow-[0_4px_16px_rgba(0,0,0,0.1)] max-md:flex-col max-md:text-center">
<div
class="w-14 h-14 rounded-xl flex items-center justify-center shrink-0 bg-(--el-color-warning) text-white">
<el-icon size="28">
<Search/>
</el-icon>
</div>
<div class="flex-1 min-w-0">
<div class="text-base font-semibold text-(--el-text-color-primary) mb-1">删除指定数量去重参考值</div>
<div class="text-xs text-(--el-text-color-secondary)">指定要删除的去重参考值数量</div>
</div>
<el-button type="warning" @click="deleteSpecifyDedupVisible = true">指定</el-button>
</div>
<div
class="flex items-center gap-4 p-5 bg-(--bg-page) rounded-xl transition-all duration-200 hover:-translate-y-0.5 hover:shadow-[0_4px_16px_rgba(0,0,0,0.1)] max-md:flex-col max-md:text-center">
<div
class="w-14 h-14 rounded-xl flex items-center justify-center shrink-0 bg-(--el-color-warning) text-white">
<el-icon size="28">
<Search/>
</el-icon>
</div>
<div class="flex-1 min-w-0">
<div class="text-base font-semibold text-(--el-text-color-primary) mb-1">删除指定数量原始数据</div>
<div class="text-xs text-(--el-text-color-secondary)">指定要删除的原始数据数量</div>
</div>
<el-button type="warning" @click="deleteSpecifyRawVisible = true">指定</el-button>
</div>
<div
class="flex items-center gap-4 p-5 bg-(--bg-page) rounded-xl transition-all duration-200 hover:-translate-y-0.5 hover:shadow-[0_4px_16px_rgba(0,0,0,0.1)] max-md:flex-col max-md:text-center">
<div
class="w-14 h-14 rounded-xl flex items-center justify-center shrink-0 bg-(--el-color-warning) text-white">
<el-icon size="28">
<Search/>
</el-icon>
</div>
<div class="flex-1 min-w-0">
<div class="text-base font-semibold text-(--el-text-color-primary) mb-1">删除指定数量数据</div>
<div class="text-xs text-(--el-text-color-secondary)">同时删除去重参考值和原始数据</div>
</div>
<el-button type="warning" @click="deleteSpecifyDataVisible = true">指定</el-button>
</div>
</div>
</div>
</div>
<!-- 空状态 -->
<div v-else-if="value" class="flex flex-col items-center p-16 text-(--el-text-color-secondary)">
<el-icon size="64" color="#dcdfe6">
<InfoFilled/>
</el-icon>
<div class="mt-4 text-[15px]">暂无数据</div>
</div>
<!-- 未选择 -->
<div v-else class="flex flex-col items-center p-16 text-(--el-text-color-secondary)">
<el-icon size="64" color="#dcdfe6">
<Key/>
</el-icon>
<div class="mt-4 text-[15px]">请选择 Token 查看详情</div>
</div>
</div>
<!-- 删除确认对话框 -->
<el-dialog v-model="deleteSpecifyDedupVisible" title="删除指定数量去重参考值" width="400">
<el-input v-model="inputSpecifyDedup" placeholder="请输入删除数量" type="number"/>
<template #footer>
<el-button @click="deleteSpecifyDedupVisible = false">取消</el-button>
<el-button type="primary" @click="deleteSpecifyDedup" :loading="isLoading">确定</el-button>
</template>
</el-dialog>
<el-dialog v-model="deleteSpecifyRawVisible" title="删除指定数量原始数据" width="400">
<el-input v-model="inputSpecifyRaw" placeholder="请输入删除数量" type="number"/>
<template #footer>
<el-button @click="deleteSpecifyRawVisible = false">取消</el-button>
<el-button type="primary" @click="deleteSpecifyRaw" :loading="isLoading">确定</el-button>
</template>
</el-dialog>
<el-dialog v-model="deleteSpecifyDataVisible" title="删除指定数量数据" width="400">
<el-input v-model="inputSpecifyData" placeholder="请输入删除数量" type="number"/>
<template #footer>
<el-button @click="deleteSpecifyDataVisible = false">取消</el-button>
<el-button type="primary" @click="deleteSpecifyData" :loading="isLoading">确定</el-button>
</template>
</el-dialog>
</div>
</template>
+336
View File
@@ -0,0 +1,336 @@
<script setup lang="ts">
import api from "@/api"
import {useCounterStore} from "@/stores/counter.ts"
import {Delete, Edit, Key, Memo, Plus, View} from '@element-plus/icons-vue'
const store = useCounterStore()
const router = useRouter()
const tableData = ref<any[]>([])
const loading = ref(false)
const input = ref('')
const inputDedupObject = ref('')
const inputDataFormat = ref('')
const inputNotes = ref('')
const activeNames = ref(['1'])
const rowOut = ref<any>(null)
const addDataDialogVisible = ref(false)
const dataDialogVisible = ref(false)
const isAdding = ref(false)
const optionName = ref('')
const onAddOption = () => {
isAdding.value = true
}
const onConfirm = () => {
if (optionName.value) {
dataFormatOptions.push({
label: optionName.value,
value: optionName.value,
})
clear()
}
}
const clear = () => {
optionName.value = ''
isAdding.value = false
}
const options = [
{value: 'false', label: '不去重'},
{value: 'uid', label: 'uid'},
{value: 'secid', label: 'secid'},
{value: 'pid', label: 'pid'},
{value: 'comment_id', label: 'comment_id'},
{value: 'dyid', label: 'dyid'},
{value: "弹幕ID", label: "弹幕ID"}
]
const dataFormatOptions = [
{value: 'uid----secid----pid----comment_id', label: 'uid----secid----pid----comment_id'},
{value: "uid----pid----弹幕ID", label: "uid----pid----弹幕ID"},
{value: 'uid----secid', label: 'uid----secid'},
{value: 'dyid', label: 'dyid'}
]
const fetchTokens = async () => {
loading.value = true
try {
const res = await api.getTokenList()
tableData.value = res.data.data || []
} finally {
loading.value = false
}
}
const addToken = async () => {
if (!input.value || !inputDedupObject.value || !inputDataFormat.value) {
ElMessage.warning('请填写完整信息')
return
}
try {
await api.createToken({
token: input.value,
dedup_object: inputDedupObject.value,
data_format: inputDataFormat.value,
notes: inputNotes.value,
})
ElMessage({message: '添加成功', type: 'success'})
input.value = ''
inputDedupObject.value = ''
inputDataFormat.value = ''
inputNotes.value = ''
fetchTokens()
} catch (error: any) {
ElMessage.error('添加失败' + error.response?.data?.error)
}
}
const viewDetails = (row: any) => {
store.token = row.token
router.push({name: "TokenDetail"})
}
const dialogDataADDVisible = (row: any) => {
rowOut.value = row
addDataDialogVisible.value = true
}
const displayDataVisible = (row: any) => {
rowOut.value = row
inputDedupObject.value = row.dedup_object
inputDataFormat.value = row.data_format
inputNotes.value = row.notes
dataDialogVisible.value = true
}
const updateData = async () => {
try {
await api.updateToken({
token: rowOut.value.token,
dedup_object: inputDedupObject.value,
data_format: inputDataFormat.value,
notes: inputNotes.value,
})
} catch (error: any) {
ElMessage.error('更新失败' + error.response?.data?.error)
dataDialogVisible.value = false
return
}
ElMessage({message: '更新成功', type: 'success'})
fetchTokens()
dataDialogVisible.value = false
}
const deleteToken = async (row: any) => {
try {
await api.deleteToken({token: row.token})
ElMessage({message: '删除成功', type: 'success'})
fetchTokens()
} catch (error: any) {
ElMessage.error('删除失败' + error.response?.data?.error)
}
}
onMounted(() => {
if (store.isAdmin) {
fetchTokens()
}
})
</script>
<template>
<div class="mx-auto w-full bg-white">
<!-- 管理页面 -->
<div class="rounded-lg p-6 shadow-[0_2px_12px_rgba(0,0,0,0.08)]">
<div class="text-xl font-semibold text-(--el-text-color-primary) pb-3 border-b border-(--el-border-color)">
Token 管理
</div>
<!-- 添加表单 -->
<el-collapse
class="rounded-xl"
v-model="activeNames"
>
<el-collapse-item title="添加新 Token" name="1"
class="flex flex-wrap gap-3 items-center max-md:flex-col max-md:items-stretch"
>
<el-input v-model="input" placeholder="Token 名称" clearable class="w-55 max-md:w-full">
<template #prefix>
<el-icon>
<Key/>
</el-icon>
</template>
</el-input>
<el-select v-model="inputDedupObject" placeholder="去重对象" clearable class="w-70 max-md:w-full">
<el-option v-for="item in options" :key="item.value" :value="item.value" :label="item.label"/>
</el-select>
<el-select v-model="inputDataFormat" placeholder="数据格式" clearable class="w-70 max-md:w-full">
<template #footer>
<el-button v-if="!isAdding" text bg size="small" @click="onAddOption">
添加数据格式
</el-button>
<template v-else>
<el-input
v-model="optionName"
placeholder="输入数据格式(以----分割)"
size="small"
/>
<el-button type="primary" size="small" @click="onConfirm">添加</el-button>
<el-button size="small" @click="clear">取消</el-button>
</template>
</template>
<el-option v-for="item in dataFormatOptions" :key="item.value" :value="item.value"
:label="item.label"
/>
</el-select>
<el-input v-model="inputNotes" placeholder="备注(可选)" clearable class="w-55 max-md:w-full">
<template #prefix>
<el-icon>
<Memo/>
</el-icon>
</template>
</el-input>
<el-button type="primary" @click="addToken" class="max-md:w-full">
<el-icon>
<Plus/>
</el-icon>
添加
</el-button>
</el-collapse-item>
</el-collapse>
<!-- Token 列表 -->
<div>
<div class="flex justify-between items-center">
<span class="text-base font-semibold text-(--el-text-color-primary) mt-5 mb-4">Token 列表</span>
<span class="text-xs text-(--el-text-color-secondary)"> {{ tableData.length }} </span>
</div>
<el-table empty-text="没有数据" stripe style="width: 100%"
:data="tableData" v-loading="loading">
<el-table-column prop="token" label="Token" min-width="100" show-overflow-tooltip>
<template #default="{ row }">
<div class="flex items-center gap-2">
<el-icon>
<Key/>
</el-icon>
<span>{{ row.token }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="dedup_object" label="去重对象" width="200">
<template #default="{ row }">
<el-tag type="primary" effect="plain">
{{ row.dedup_object == "false" ? "不去重" : row.dedup_object }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="data_format" label="数据格式" min-width="200" show-overflow-tooltip>
<template #default="{ row }">
<div class="text-xs break-all">{{ row.data_format }}</div>
</template>
</el-table-column>
<el-table-column prop="notes" label="备注" min-width="150" show-overflow-tooltip>
<template #default="{ row }">
<span class="text-(--el-text-color-secondary) overflow-hidden text-ellipsis whitespace-nowrap">
{{ row.notes || '-' }}
</span>
</template>
</el-table-column>
<el-table-column label="操作" width="380" fixed="right">
<template #default="{ row }">
<div class="flex gap-1 flex-wrap max-md:flex-col">
<el-button size="small" text @click="viewDetails(row)">
<el-icon>
<View/>
</el-icon>
详情
</el-button>
<el-button size="small" text @click="dialogDataADDVisible(row)">
<el-icon>
<Plus/>
</el-icon>
数据
</el-button>
<el-button type="primary" size="small" text @click="displayDataVisible(row)">
<el-icon>
<Edit/>
</el-icon>
编辑
</el-button>
<el-popconfirm
title="确认删除此 Token 吗?"
confirm-button-text="确认"
cancel-button-text="取消"
@confirm="deleteToken(row)"
>
<template #reference>
<el-button type="danger" size="small" text>
<el-icon>
<Delete/>
</el-icon>
删除
</el-button>
</template>
</el-popconfirm>
</div>
</template>
</el-table-column>
</el-table>
</div>
</div>
</div>
<!-- 编辑对话框 -->
<el-dialog width="400"
v-model="dataDialogVisible"
>
<template #header>
编辑Token
<el-tag type="primary" effect="plain">{{ rowOut?.token }}</el-tag>
</template>
<div class="flex flex-col gap-3">
<el-text class="mr-auto">去重对象</el-text>
<el-select v-model="inputDedupObject" placeholder="选择去重对象">
<el-option v-for="item in options" :key="item.value" :value="item.value" :label="item.label"/>
</el-select>
<el-text class="mr-auto">数据格式</el-text>
<el-select v-model="inputDataFormat" placeholder="选择数据格式">
<el-option v-for="item in dataFormatOptions" :key="item.value" :value="item.value" :label="item.label"/>
</el-select>
<el-text class="mr-auto">备注</el-text>
<el-input v-model="inputNotes" placeholder="请输入备注"/>
</div>
<template #footer>
<el-button @click="dataDialogVisible = false">取消</el-button>
<el-button type="primary" @click="updateData">确定</el-button>
</template>
</el-dialog>
<AddDataDialog
v-model="addDataDialogVisible"
:token="rowOut?.token"
/>
</template>
+18 -6
View File
@@ -1,12 +1,24 @@
{ {
"extends": "@vue/tsconfig/tsconfig.dom.json", "extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"], "include": [
"exclude": ["src/**/__tests__/*"], "env.d.ts",
"src/**/*",
"src/**/*.vue",
],
"exclude": [
"src/**/__tests__/*"
],
"compilerOptions": { "compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", // Extra safety for array and object lookups, but may have false positives.
"noUncheckedIndexedAccess": true,
// Path mapping for cleaner imports.
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": [
} "./src/*"
]
},
// `vue-tsc --build` produces a .tsbuildinfo file for incremental type-checking.
// Specified here to keep it out of the root directory.
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo"
} }
} }
+14 -7
View File
@@ -1,19 +1,26 @@
// TSConfig for modules that run in Node.js environment via either transpilation or type-stripping.
{ {
"extends": "@tsconfig/node22/tsconfig.json", "extends": "@tsconfig/node24/tsconfig.json",
"include": [ "include": [
"vite.config.*", "vite.config.*",
"vitest.config.*", "vitest.config.*",
"cypress.config.*", "cypress.config.*",
"nightwatch.conf.*",
"playwright.config.*", "playwright.config.*",
"eslint.config.*" "eslint.config.*"
], ],
"compilerOptions": { "compilerOptions": {
// Most tools use transpilation instead of Node.js's native type-stripping.
// Bundler mode provides a smoother developer experience.
"module": "preserve",
"moduleResolution": "bundler",
// Include Node.js types and avoid accidentally including other `@types/*` packages.
"types": [
"node"
],
// Disable emitting output during `vue-tsc --build`, which is used for type-checking only.
"noEmit": true, "noEmit": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", // `vue-tsc --build` produces a .tsbuildinfo file for incremental type-checking.
// Specified here to keep it out of the root directory.
"module": "ESNext", "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo"
"moduleResolution": "Bundler",
"types": ["node"]
} }
} }
+27 -10
View File
@@ -1,18 +1,35 @@
import { fileURLToPath, URL } from 'node:url' import {fileURLToPath, URL} from 'node:url'
import { defineConfig } from 'vite' import {defineConfig} from 'vite'
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools' import vueDevTools from 'vite-plugin-vue-devtools'
import tailwindcss from '@tailwindcss/vite'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import {ElementPlusResolver} from 'unplugin-vue-components/resolvers'
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
vue(), vue(),
vueDevTools(), vueDevTools(),
], tailwindcss(),
resolve: { AutoImport({
alias: { imports: [
'@': fileURLToPath(new URL('./src', import.meta.url)) 'vue',
'vue-router',
],
resolvers: [ElementPlusResolver()],
dts: 'src/types/import/auto-imports.d.ts',
}),
Components({
resolvers: [ElementPlusResolver()],
dts: 'src/types/import/components.d.ts',
}),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
}, },
},
}) })