Compare commits

...

251 Commits

Author SHA1 Message Date
shaw
93db889a10 fix: Gemini OpenCode 教程 baseURL 改为 v1beta 2026-01-13 09:52:37 +08:00
Wesley Liddick
0df7385c4e Merge pull request #226 from xilu0/main
feat(gateway): 优化 Antigravity/Gemini 思考块处理 此提交解决了思考块 (thinking blocks) 在转发过程中的兼容性问题
2026-01-13 09:39:43 +08:00
Wesley Liddick
1a3fa6411c Merge pull request #260 from IanShaw027/fix/sync-openai-gpt5-models
fix: 同步 OpenAI GPT-5 模型列表并完善参数处理
2026-01-13 09:31:00 +08:00
Wesley Liddick
64614756d1 Merge pull request #259 from cyhhao/main
fix: adjust OpenCode OpenAI example store placement
2026-01-13 09:30:26 +08:00
Wesley Liddick
bb1fd54d4d Merge pull request #257 from Edric-Li/feat/ops-fullscreen-scrollbar
feat(ops): 添加运维监控全屏模式 & 优化滚动条
2026-01-13 09:29:25 +08:00
ianshaw
d85288a6c0 Revert "fix(gateway): 修复 base_url 包含 /chat/completions 时路径拼接错误"
This reverts commit 7fdc25df3c.
2026-01-12 13:29:04 -08:00
ianshaw
3402acb606 feat(gateway): 对所有请求(包括 Codex CLI)应用模型映射
- 移除 Codex CLI 的模型映射跳过逻辑
- 添加详细的模型映射日志,包含账号名称和请求类型
- 确保所有 OpenAI 请求都能正确应用账号配置的模型映射
2026-01-12 13:23:05 -08:00
ianshaw
7fdc25df3c fix(gateway): 修复 base_url 包含 /chat/completions 时路径拼接错误
问题:
- 当账号的 base_url 配置为 https://example.com/v1/chat/completions 时
- 代码直接追加 /responses,导致路径变成 /v1/chat/completions/responses
- 上游返回 404 错误

修复:
- 在追加 /responses 前,先移除 base_url 中的 /chat/completions 后缀
- 确保最终路径为 https://example.com/v1/responses

影响范围:
- OpenAI API Key 账号的测试接口
- OpenAI API Key 账号的实际网关请求

Related-to: #231
2026-01-12 11:39:45 -08:00
ianshaw
ea699cbdc2 docs(frontend): 完善 OpenCode 配置说明
更新 API 密钥页面 OpenCode 配置提示信息:
- 补充支持 opencode.jsonc 后缀名
- 说明可使用默认 provider(openai/anthropic/google)或自定义 provider_id
- 说明 API Key 支持直接配置或通过 /connect 命令配置
- 保留"示例仅供参考,模型与选项可按需调整"的提示

配置文件路径:~/.config/opencode/opencode.json(或 opencode.jsonc)
2026-01-12 11:17:47 -08:00
ianshaw
fe6a3f4267 fix(gateway): 完善 max_output_tokens 参数处理逻辑
根据不同平台和账号类型处理 max_output_tokens 参数:
- OpenAI OAuth (Responses API): 保留 max_output_tokens(支持)
- OpenAI API Key: 删除 max_output_tokens(不支持)
- Anthropic (Claude): 转换 max_output_tokens 为 max_tokens
- Gemini: 删除 max_output_tokens(由 Gemini 专用转换处理)
- 其他平台: 删除(安全起见)

同时处理 max_completion_tokens 参数,仅在 OpenAI OAuth 时保留。

修复客户端(如 OpenCode)发送不支持参数导致上游返回 400 错误的问题。

Related-to: #231
2026-01-12 11:08:28 -08:00
ianshaw
fe8198c8cd fix(frontend): 同步 OpenAI GPT-5 系列模型列表
修复编辑账号页面 GPT-5 模型只显示 3 个的问题:
- 原来只有: gpt-5, gpt-5-mini, gpt-5-nano
- 现在添加完整的 22 个模型,包括:
  * GPT-5 系列: gpt-5, gpt-5-codex, gpt-5-chat, gpt-5-pro, gpt-5-mini, gpt-5-nano 及各时间戳版本
  * GPT-5.1 系列: gpt-5.1, gpt-5.1-codex, gpt-5.1-codex-max, gpt-5.1-codex-mini 及各版本
  * GPT-5.2 系列: gpt-5.2, gpt-5.2-codex, gpt-5.2-pro 及各版本
- 更新快捷预设按钮,新增 GPT-5.1, GPT-5.2, GPT-5.1 Codex 选项

与后端定价文件 (model_prices_and_context_window.json) 保持一致。

Fixes issue introduced in fb86002 (feat: 添加模型白名单选择器组件)
Related-to: fb86002ef9
2026-01-12 10:14:50 -08:00
cyhhao
675e61385f Merge branch 'main' of github.com:Wei-Shaw/sub2api 2026-01-12 22:36:14 +08:00
cyhhao
67acac1082 fix: adjust OpenCode OpenAI example store placement 2026-01-12 22:31:43 +08:00
Edric Li
d02e1db018 style: 优化滚动条自动隐藏效果
- 默认隐藏滚动条,悬停时显示
- 支持 Webkit (Chrome/Safari/Edge) 和 Firefox
- 滚动条样式与暗色主题适配
2026-01-12 22:10:59 +08:00
Edric Li
0da515071b feat(ops): 添加运维监控全屏模式
- 支持通过 URL 参数 ?fullscreen=1 进入全屏模式
- 全屏模式下隐藏非必要 UI 元素(选择器、按钮、提示等)
- 增大健康评分圆环和字体以提升可读性
- 支持 ESC 键退出全屏
- 添加全屏按钮的 i18n 翻译
2026-01-12 22:10:59 +08:00
xiluo
524d80ae1c feat(gateway): 优化 Antigravity/Gemini 思考块处理
此提交解决了思考块 (thinking blocks) 在转发过程中的兼容性问题。

主要变更:

1. **思考块优化 (Thinking Blocks)**:
   - 在 AntigravityGatewayService 中增加了 sanitizeThinkingBlocks 处理,强制移除思考块中不支持的 cache_control 字段(避免 Anthropic/Vertex AI 报错)
   - 实现历史思考块展平 (Flattening):将非最后一条消息中的思考块转换为普通文本块,以绕过上游对历史思考块签名的严格校验
   - 增加 cleanCacheControlFromGeminiJSON 作为最后一道防线,确保转换后的 Gemini 请求中不残留非法的 cache_control

2. **GatewayService 缓存控制优化**:
   - 更新缓存控制逻辑,跳过 thinking 块(thinking 块不支持 cache_control 字段)
   - 增加 removeCacheControlFromThinkingBlocks 函数强制清理

关联 Issue: #225
2026-01-12 13:36:59 +00:00
shaw
3b71bc3df1 feat: OpenCode 配置提示添加配置文件路径说明 2026-01-12 20:49:54 +08:00
shaw
22ef9534e0 fix: 修复反向代理下客户端 IP 获取错误 2026-01-12 20:44:38 +08:00
Wesley Liddick
c206d12d5c Merge pull request #254 from IanShaw027/feat/ops-count-tokens-filter-and-auto-refresh
feat(ops): count_tokens 错误过滤和自动刷新功能
2026-01-12 17:31:54 +08:00
IanShaw027
6ad29a470c style(ops): 移除未使用的 isAutoRefreshActive 变量 2026-01-12 17:28:25 +08:00
IanShaw027
2d45e61a9b style(ops): 修复代码格式问题以通过 golangci-lint 2026-01-12 17:18:49 +08:00
IanShaw027
b98fb013ae feat(ops): 添加自动刷新配置功能
功能特性:
- 支持配置启用/禁用自动刷新
- 可配置刷新间隔(15秒/30秒/60秒)
- 实时倒计时显示,用户可见下次刷新时间
- 手动刷新自动重置倒计时
- 页面卸载时自动清理定时器

用户体验:
- 默认禁用,用户可根据需求开启
- 与现有 OpsConcurrencyCard 5秒刷新保持一致
- 倒计时带旋转动画,视觉反馈清晰
- 配置修改后立即生效,无需刷新页面

技术实现:
- ops.ts: 添加 auto_refresh_enabled 和 auto_refresh_interval_seconds 配置
- OpsSettingsDialog.vue: 添加自动刷新配置界面
- OpsDashboard.vue: 实现主刷新逻辑和双定时器设计
- OpsDashboardHeader.vue: 倒计时显示组件

配置说明:
- auto_refresh_enabled: 是否启用(默认 false)
- auto_refresh_interval_seconds: 刷新间隔(默认 30 秒,范围 15-300 秒)
2026-01-12 17:07:07 +08:00
IanShaw027
345a965fa3 feat(ops): 添加 count_tokens 错误过滤功能
功能特性:
- 自动识别并标记 count_tokens 请求的错误
- 支持配置是否在统计中忽略 count_tokens 错误
- 错误数据完整保留,仅在统计时动态过滤

技术实现:
- ops_error_logger.go: 自动标记 count_tokens 请求
- ops_repo.go: INSERT 语句添加 is_count_tokens 字段
- ops_repo_dashboard.go: buildErrorWhere 核心过滤函数
- ops_repo_preagg.go: 预聚合统计中添加过滤
- ops_repo_trends.go: 趋势统计查询添加过滤(2 处)
- ops_settings_models.go: 添加 ignore_count_tokens_errors 配置
- ops_settings.go: 配置验证和默认值设置
- ops_port.go: 错误日志模型添加 IsCountTokens 字段

业务价值:
- count_tokens 是探测性请求,其错误不影响真实业务 SLA
- 用户可根据需求灵活控制是否计入统计
- 提升错误率、告警等运维指标的准确性

影响范围:
- Dashboard 概览统计
- 错误趋势图表
- 告警规则评估
- 预聚合指标(hourly/daily)
- 健康分数计算
2026-01-12 17:06:12 +08:00
IanShaw027
c02c120579 feat(ops): 添加 count_tokens 错误标记数据库迁移
- 新增 is_count_tokens 布尔字段到 ops_error_logs 表
- 默认值为 false
- 支持后续动态过滤统计
2026-01-12 17:06:12 +08:00
shaw
4da681f58a Merge branch 'mt21625457/main' 2026-01-12 16:20:55 +08:00
shaw
68ba866c38 fix(frontend): 修复账号管理页面分组显示和 Cookie 授权问题
- 新增 AccountGroupsCell 组件优化分组列显示(最多4个+折叠)
- 修复 Cookie 自动授权时 group_ids/notes/expires_at 字段丢失
- 修复 SettingsView 流超时配置前后端字段不一致问题
2026-01-12 16:08:44 +08:00
yangjianbo
9622347faa fix(调度): 修复 outbox 空载写入并稳固回放测试
将 outbox payload 为空时写入 NULL
避免事务因 JSON 解析错误中断
调整回放测试为预置缓存后验证 last_used 更新

测试: go test -tags=integration ./internal/repository
2026-01-12 15:46:55 +08:00
shaw
8363663ea8 fix(gateway): 修复 usage_logs 记录 IP 不正确的问题
在 nginx 反向代理场景下,使用 ip.GetClientIP() 替代 c.ClientIP()
以正确获取客户端真实 IP 地址
2026-01-12 15:37:45 +08:00
Wesley Liddick
b588ea194c Merge pull request #251 from IanShaw027/fix/ops-bugs
feat(ops): 运维看板功能增强 - 实时流量监控与指标阈值配置
2026-01-12 15:26:26 +08:00
Wesley Liddick
465ba76788 Merge pull request #250 from IanShaw027/fix/custom-error-codes-disable-scheduling
fix(gateway): 自定义错误码触发停止调度
2026-01-12 15:26:14 +08:00
shaw
cf313d5761 fix(gateway): 修复 Claude Code 客户端检测和请求信息记录
- 在 Messages 方法中调用 SetClaudeCodeClientContext 启用客户端检测
- 修复 RecordUsageInput 未传递 UserAgent 和 IPAddress 的问题
2026-01-12 15:19:40 +08:00
yangjianbo
8c1958c9ad fix(调度): 修复流超时配置并补回放测试
删除前端未支持的 timeout_seconds 字段,避免类型检查失败
新增调度 outbox 回放集成测试
调整调度默认等待超时断言

测试: make test
2026-01-12 15:13:39 +08:00
yangjianbo
2db34139f0 Merge branch 'main' of https://github.com/mt21625457/aicodex2api 2026-01-12 14:50:53 +08:00
IanShaw027
e0cccf6ed2 fix(ops): 修复Go代码格式问题 2026-01-12 14:36:32 +08:00
IanShaw027
89c1a41305 fix(ops): 修复错误日志和请求详情模态框的布局问题
- 修复 OpsErrorDetailsModal 的内容溢出问题,使用 flex 布局确保正确显示
- 修复 OpsErrorLogTable 的表格滚动问题,添加 min-h-0 确保正确的滚动行为
- 修复 OpsRequestDetailsModal 的布局问题,添加 pageSize 初始化并优化 flex 布局
- 统一使用 flex 布局模式,确保模态框内容在不同屏幕尺寸下正确显示
2026-01-12 14:31:21 +08:00
yangjianbo
202ec21bab fix(config): 提升粘性会话默认等待时长
- 默认值调整为 120s
- 同步示例配置与环境变量
2026-01-12 14:26:31 +08:00
ianshaw
6dcb27632e fix(gateway): 自定义错误码触发停止调度
- 修改 HandleUpstreamError 逻辑,启用自定义错误码时所有在列表中的错误码都会停止调度
- 添加 handleCustomErrorCode 函数处理自定义错误码的账号停用
- 前端添加 429/529 错误码的警告提示,因为这些错误码已有内置处理机制
- 更新 EditAccountModal、CreateAccountModal、BulkEditAccountModal 的错误码添加逻辑
2026-01-11 22:20:02 -08:00
yangjianbo
3141aa5144 feat(scheduler): 引入调度快照缓存与 outbox 回放
- 调度热路径优先读 Redis 快照,保留分组排序语义
- outbox 回放 + 全量重建纠偏,失败重试不推进水位
- 自动 Atlas 基线对齐并同步调度配置示例
2026-01-12 14:19:06 +08:00
IanShaw027
5443efd7d7 feat(ops): 前端集成实时流量功能
- 添加实时流量API调用方法
- 优化OpsDashboard组件代码
2026-01-12 14:18:16 +08:00
IanShaw027
62771583e7 feat(ops): 集成实时流量API接口
- 添加实时流量handler处理逻辑
- 注册实时流量路由
- 扩展ops service接口定义
2026-01-12 14:17:58 +08:00
IanShaw027
5526f122b7 feat(ops): 新增实时流量数据层
- 添加实时流量repository层实现
- 添加实时流量service层逻辑
- 定义实时流量数据模型
2026-01-12 14:17:42 +08:00
Wesley Liddick
9c144587fe Merge pull request #249 from IanShaw027/feat/stream-timeout-handling
feat(gateway): 添加流超时处理机制
2026-01-12 14:14:21 +08:00
IanShaw027
098bf5a1e8 fix(i18n): 补充缺失的英文翻译
- 添加 admin.ops.requestsTitle
- 添加 admin.ops.alertRules.manage 和 saveSuccess/deleteSuccess
- 添加 common.settings
- 添加完整的 admin.ops.settings 部分
- 添加 admin.ops.tooltips.totalRequests 和 upstreamErrors
2026-01-12 14:10:44 +08:00
Wesley Liddick
4c37ca71ee Merge pull request #247 from 7836246/fix/negative-zero-balance
fix: 修复扣款时浮点数精度导致的余额不足误判和 -0.00 显示问题
2026-01-12 14:10:41 +08:00
ianshaw
0c52809591 refactor(settings): 简化流超时配置,移除冗余字段
- 移除 TimeoutSeconds 字段,超时判定由网关配置控制
- 默认禁用流超时处理功能
2026-01-11 22:09:35 -08:00
小海
53e730f8d5 fix: 修复扣款时浮点数精度导致的余额不足误判和 -0.00 显示问题 2026-01-12 14:06:30 +08:00
IanShaw027
8e248e0853 fix(ops): 修正卡片标题翻译
- 卡片标题显示"请求"
- 卡片内部标签保持"请求数"
2026-01-12 14:05:10 +08:00
ianshaw
2a0758bdfe feat(gateway): 添加流超时处理机制
- 添加 StreamTimeoutSettings 配置结构体和系统设置
- 实现 TimeoutCounterCache Redis 计数器用于累计超时次数
- 在 RateLimitService 添加 HandleStreamTimeout 方法
- 在 gateway_service、openai_gateway_service、antigravity_gateway_service 中调用超时处理
- 添加后端 API 端点 GET/PUT /admin/settings/stream-timeout
- 添加前端配置界面到系统设置页面
- 支持配置:启用开关、超时阈值、处理方式、暂停时长、触发阈值、阈值窗口

默认配置:
- 启用:true
- 超时阈值:60秒
- 处理方式:临时不可调度
- 暂停时长:5分钟
- 触发阈值:3次
- 阈值窗口:10分钟
2026-01-11 21:54:52 -08:00
IanShaw027
f55ba3f6c1 fix(ops): 优化卡片标题和明细筛选逻辑
- 将"请求数"改为"请求"
- SLA卡片明细只显示错误请求(kind='error')
- TTFT卡片明细按延迟降序排序
2026-01-12 13:00:39 +08:00
IanShaw027
db51e65b42 chore: 添加ESLint忽略配置
- 添加.eslintignore文件
2026-01-12 11:44:34 +08:00
IanShaw027
72a2ed958b feat(ops): 看板上应用指标阈值显示
- 在OpsDashboard中加载阈值配置
- 在OpsDashboardHeader中根据阈值判断指标是否超标
- 超出阈值的指标显示为红色(SLA低于阈值也显示红色)
- 用Icon组件替换emoji表情
2026-01-12 11:44:14 +08:00
IanShaw027
d0b91a40d4 feat(ops): 添加指标阈值配置UI
- 在OpsSettingsDialog中添加指标阈值配置表单
- 在OpsRuntimeSettingsCard中添加阈值配置区域
- 添加阈值验证逻辑
- 更新国际化文本
2026-01-12 11:43:54 +08:00
IanShaw027
bd74bf7994 fix(ops): 添加brain图标替换emoji表情
- 在Icon组件中添加brain图标
- 用于替换运维诊断中的emoji表情
2026-01-12 11:43:35 +08:00
IanShaw027
f28d4b78e7 feat(ops): 前端添加指标阈值类型定义和API
- 添加OpsMetricThresholds类型定义
- 新增getMetricThresholds和updateMetricThresholds API方法
2026-01-12 11:43:15 +08:00
IanShaw027
7536dbfee5 feat(ops): 后端添加指标阈值管理API
- 新增GetMetricThresholds和UpdateMetricThresholds接口
- 支持配置SLA、延迟P99、TTFT P99、请求错误率、上游错误率阈值
- 添加参数验证逻辑
- 提供默认阈值配置
2026-01-12 11:42:56 +08:00
yangjianbo
b76cc583fb 去掉失误提交的openspec提案 2026-01-12 11:18:14 +08:00
yangjianbo
955af6b3ec fix(仪表盘): 添加聚合和回填操作的执行时间日志 2026-01-12 10:59:52 +08:00
yangjianbo
1073317a3e fix(仪表盘): 增加对数据库驱动和仓储实例的有效性检查 2026-01-12 10:53:41 +08:00
yangjianbo
839ab37d40 Merge branch 'main' of https://github.com/mt21625457/aicodex2api 2026-01-12 10:38:44 +08:00
yangjianbo
9dd0ef187d fix(仪表盘): 优化CleanupAggregates方法,逐条删除数据以提高错误处理能力 2026-01-12 10:38:42 +08:00
程序猿MT
fd8473f267 Merge branch 'Wei-Shaw:main' into main 2026-01-12 10:28:22 +08:00
shaw
cc4910dd30 fix(test): 修复聚合测试的时区边界条件问题 2026-01-12 09:23:46 +08:00
shaw
50de5d05b0 fix: 修复PR合并后的功能回退和安全问题 2026-01-12 09:14:32 +08:00
shaw
7844dc4f2d Merge PR #238: feat(ops): 实现完整的运维监控系统(vNext) 2026-01-12 08:52:17 +08:00
IanShaw027
c48795a948 fix(ci): 修复最后一批CI错误
- 修复 ops_repo_trends.go 中剩余3处 Rows.Close 未检查错误
- 修复 ops_settings.go, ops_settings_models.go, ops_trends.go 的格式化问题
2026-01-12 00:02:19 +08:00
IanShaw027
19b67e89a2 fix(ci): 修复剩余的CI错误
- 修复 ops_repo_latency_histogram_buckets.go 中另一个函数的 WriteString 未检查错误
- 修复 ops_repo_request_details.go 和 ops_repo_trends.go 中的 Rows.Close 未检查错误
- 修复 ops_alert_models.go, ops_cleanup_service.go, ops_request_details.go 的格式化问题
- 移除 ops_retry.go 中未使用的 status 字段
- 修复 maxTime 函数重复声明(将测试文件中的函数重命名为 testMaxTime)
2026-01-11 23:57:20 +08:00
IanShaw027
f017fd97c1 fix(ci): 修复所有CI失败问题
- 修复 ops_ws_handler.go 代码格式问题
- 修复所有未检查的错误返回值(Rows.Close 和 WriteString)
- 更新 .golangci.yml 排除 ops 相关服务文件的 redis 导入检查
2026-01-11 23:49:03 +08:00
IanShaw027
ce3336e3f4 fix(lint): 修复代码格式和未使用变量问题
- 修复 ops_ws_handler.go 中的代码格式和返回值
- 移除 ops_repo_latency_histogram_buckets.go 中不必要的错误检查
- 修复 api_contract_test.go 缩进并添加运维监控配置项测试
- 移除 ops_cleanup_service.go 中未使用的变量
- 添加 ops_retry.go 中缺失的 status 字段
2026-01-11 23:40:09 +08:00
IanShaw027
54c5788b86 fix(lint): 修复所有golangci-lint错误
- 修复depguard错误:为ops service文件添加redis导入例外
- 修复errcheck错误:添加错误检查和类型断言检查
- 修复gofmt错误:格式化代码
- 修复ineffassign错误:移除无效的idx++赋值
- 修复staticcheck错误:合并条件赋值
- 修复unused错误:移除未使用的字段和函数
  - ops_cleanup_service.go: entryID字段
  - ops_retry.go: status字段
  - ops_upstream_context.go: getOpsUpstreamErrors函数
2026-01-11 23:26:29 +08:00
IanShaw027
4cb7b26f03 fix: 移除未使用的os包导入 2026-01-11 23:18:00 +08:00
IanShaw027
3dfb62e996 merge: 合并main分支最新改动
解决冲突:
- backend/internal/config/config.go: 合并Ops和Dashboard配置
- backend/internal/server/api_contract_test.go: 合并handler初始化
- backend/internal/service/openai_gateway_service.go: 保留Ops错误追踪逻辑
- backend/internal/service/wire.go: 合并Ops和APIKeyAuth provider

主要合并内容:
- Dashboard缓存和预聚合功能
- API Key认证缓存优化
- Codex转换支持
- 使用日志分区表
2026-01-11 23:15:01 +08:00
IanShaw027
d5c711d081 refactor(ops): 从系统设置页面移除运维监控配置项
- 移除Ops Monitoring设置卡片及相关配置项
- 移除ops相关组件导入和展示逻辑
- 相关配置已迁移至运维监控页面统一管理
2026-01-11 23:03:21 +08:00
IanShaw027
73b62bb15c feat(ops): 增强上游错误追踪和新增定时报告服务
- 优化错误日志中间件,即使请求成功也记录上游重试/故障转移事件
- 新增OpsScheduledReportService支持定时报告功能
- 使用Redis分布式锁确保定时任务单实例执行
- 完善依赖注入配置
- 优化前端错误趋势图表展示
2026-01-11 23:00:31 +08:00
yangjianbo
18b8bd43ad fix(限流): 原子化 Redis 限流并支持故障策略
使用 Lua 脚本原子设置计数与过期,修复 TTL 缺失\n支持 fail-open/fail-close 并对优惠码验证启用 fail-close\n新增单元与集成测试覆盖关键分支\n\n测试:go test ./...
2026-01-11 22:21:05 +08:00
IanShaw027
8fffcd8091 feat(ops): 优化健康评分算法和智能诊断机制
- 采用分层加权评分(业务70% + 基础设施30%),避免重复扣分
- 新增延迟诊断(P99 > 2s critical, > 1s warning)
- 新增资源诊断(CPU/内存/DB/Redis状态)
- 调整诊断阈值(上游错误率5% critical,请求错误率3% critical)
- 为每个诊断项添加可操作建议
- 添加完整的单元测试覆盖(30+测试用例)
- 完善中英文国际化文本
2026-01-11 21:42:02 +08:00
IanShaw027
c8e3a476fc feat(ops): 限制告警规则最多显示6个,超出可滚动 2026-01-11 21:01:26 +08:00
IanShaw027
808cee9665 feat(ops): 限制并发卡片最多显示3组数据,超出可滚动 2026-01-11 20:59:27 +08:00
IanShaw027
92eafbc2a6 feat(ops): 优化运维监控界面组件功能和交互 2026-01-11 20:56:36 +08:00
IanShaw027
2548800c3f feat(ui): 增强Select组件功能和样式 2026-01-11 20:56:27 +08:00
IanShaw027
9dce8a5388 i18n(ops): 添加运维监控相关国际化文本 2026-01-11 20:56:19 +08:00
IanShaw027
76484bd5c9 chore(ops): 更新依赖注入配置 2026-01-11 20:56:10 +08:00
IanShaw027
e4ed35fe01 feat(ops): 增强告警评估和指标收集服务功能 2026-01-11 20:56:02 +08:00
IanShaw027
f5e45c1a8a refactor(ops): 优化运维监控数据仓库层查询逻辑 2026-01-11 20:55:52 +08:00
IanShaw027
a2f83ff032 test(ops): 添加告警评估服务单元测试 2026-01-11 20:55:44 +08:00
Wesley Liddick
2b2f7a6dec Merge pull request #237 from cyhhao/main
feat: 补充 OpenCode 使用密钥配置示例
2026-01-11 20:53:58 +08:00
Wesley Liddick
49c15c0d44 Merge pull request #236 from mt21625457/main
feat: 新增预聚合体系,解决 Admin Dashboard 的累计统计是 usage_logs 是全表扫描 的性能问题
2026-01-11 20:51:52 +08:00
IanShaw027
1b938b2003 feat(ops): 统一弹窗组件并优化分页设置
- OpsErrorDetailsModal和OpsRequestDetailsModal改用BaseDialog统一弹窗组件
- 分页默认值从50改为20条,减少单页数据量
- OpsAlertEventsCard表格添加sticky表头,优化滚动体验
- 移除自定义Teleport和Transition实现,使用统一组件
2026-01-11 20:41:39 +08:00
yangjianbo
5f80760a8c fix(账号管理): 修复BulkUpdateAccounts方法中的混合渠道检查逻辑缩进 2026-01-11 20:39:15 +08:00
IanShaw027
dd59e872ff feat(ops): 添加分组和账号级别监控指标
- 后端新增 GetAccountAvailability 方法获取账号可用性数据
- 添加分组可用率和限流率计算辅助函数
- 前端支持分组和账号级别的监控指标类型
- 优化警报规则指标选择器,按类别分组显示
2026-01-11 20:33:52 +08:00
cyhhao
aa1a3b9a74 fix: update OpenCode use-key examples 2026-01-11 20:29:32 +08:00
yangjianbo
32953405b1 fix(账号管理): 调度批量结果明细与刷新优化
补充批量调度返回 success_ids/failed_ids 并增加合约/单测

前端加入降级处理与部分失败提示,表格行使用稳定 key

测试: make test-frontend

测试: go test ./internal/service -run BulkUpdateAccounts -tags=unit

测试: go test ./internal/server -run APIContracts -tags=unit
2026-01-11 20:22:17 +08:00
IanShaw027
c1a3dd41dd feat(ops): 添加运维监控配置开关
- 在 .env.example 和 config.example.yaml 中添加 ops.enabled 配置项
- 默认值为 true,保持现有行为
- 当设置为 false 时,左侧栏隐藏运维监控菜单并禁用所有运维监控功能
- 修改后端 GetSettings API,让 ops_monitoring_enabled 受 config.ops.enabled 控制
- 数据清理和预聚合任务默认保持开启状态(通过运维监控设置对话框配置)
2026-01-11 20:10:08 +08:00
IanShaw027
63dc6a68df feat(ops): 隐藏查询模式选择器
- 在OpsDashboardHeader中隐藏queryMode选择器(使用v-if="false")
- 保留所有后端逻辑和前端状态管理
- auto模式逻辑:优先使用预聚合数据,不存在时回退到实时计算
- 用户界面更简洁,后端自动选择最优查询方式

相关文件:
- frontend/src/views/admin/ops/components/OpsDashboardHeader.vue
2026-01-11 19:58:38 +08:00
IanShaw027
a39316e004 feat(ops): 集成运维监控设置对话框到仪表盘
- 在OpsDashboardHeader添加设置和警报规则按钮
- 在OpsDashboard集成OpsSettingsDialog组件
- 添加警报规则弹窗展示
- 添加高级设置API类型定义
- 支持从Header快速访问设置和规则管理

相关文件:
- frontend/src/api/admin/ops.ts
- frontend/src/views/admin/ops/types.ts
- frontend/src/views/admin/ops/OpsDashboard.vue
- frontend/src/views/admin/ops/components/OpsDashboardHeader.vue
2026-01-11 19:51:37 +08:00
IanShaw027
988b4d0254 feat(ops): 添加高级设置API支持
- 新增OpsAdvancedSettings数据模型
- 支持数据保留策略配置(错误日志、分钟级指标、小时级指标)
- 支持数据聚合开关配置
- 添加GET/PUT /admin/ops/advanced-settings接口
- 添加配置校验和默认值处理

相关文件:
- backend/internal/service/ops_settings_models.go
- backend/internal/service/ops_settings.go
- backend/internal/handler/admin/ops_settings_handler.go
- backend/internal/server/routes/admin.go
- backend/internal/service/domain_constants.go
2026-01-11 19:51:18 +08:00
IanShaw027
f541636840 feat(ops): 优化警报规则和设置的成功提示信息
- 添加警报规则保存成功提示:"警报规则保存成功"
- 添加警报规则删除成功提示:"警报规则删除成功"
- 添加运维监控设置保存成功提示:"运维监控设置保存成功"
- 替换通用的"操作成功"提示为具体的业务提示
- 失败时显示后端返回的详细错误信息

相关文件:
- frontend/src/i18n/locales/zh.ts
- frontend/src/views/admin/ops/components/OpsAlertRulesCard.vue
- frontend/src/views/admin/ops/components/OpsSettingsDialog.vue
2026-01-11 19:50:43 +08:00
yangjianbo
48613558d4 fix(仪表盘): 修正分区迁移与范围测试 2026-01-11 19:01:15 +08:00
yangjianbo
4b66ee2f8f chore(测试): 清理未使用导入 2026-01-11 18:49:57 +08:00
cyhhao
abbde130ab Revert Codex OAuth fallback handling 2026-01-11 18:43:47 +08:00
yangjianbo
ccb8144557 fix(仪表盘): 修复rows.Close错误检查 2026-01-11 18:39:29 +08:00
yangjianbo
1240c78ef6 Merge branch 'test' into dev 2026-01-11 18:22:07 +08:00
程序猿MT
66c8b6f2bc Merge branch 'Wei-Shaw:main' into main 2026-01-11 18:21:42 +08:00
yangjianbo
6271a33d08 fix(仪表盘): 兼容禁用聚合与回填限制 2026-01-11 18:20:15 +08:00
yangjianbo
5364011a5b fix(仪表盘): 修正聚合时间桶与清理节流 2026-01-11 17:21:17 +08:00
yangjianbo
d78f42d2fd chore(注释): 调整仪表盘注释为中文 2026-01-11 16:02:28 +08:00
yangjianbo
1a869547d7 feat(仪表盘): 引入预聚合统计与聚合作业 2026-01-11 16:01:35 +08:00
IanShaw027
e4bc9f6fb0 feat(ops): 优化仪表盘Header响应式布局与指标展示
**响应式优化**:
- 添加flex-wrap支持窄屏时间选择器自动换行
- 当前QPS/TPS在窄屏时自动换行,避免溢出
- 时间按钮在窄屏使用更小字号和间距(9px/1.5px)
- 当前数值使用响应式字体(xl→sm:2xl)

**指标展示优化**:
1. 请求卡片:
   - 标题简化:总请求 → 请求
   - 字段调整:请求 → 请求数
   - 移除:平均延迟、平均首字延迟(避免冗余)

2. 延迟和TTFT卡片:
   - 布局:grid → flex-wrap(自适应布局)
   - 指标不换行:添加whitespace-nowrap
   - 最小宽度:min-w-[60px]保证可读性
   - 单位内联:名称、数值、单位在同一行(P95: 123 ms)
   - 自动换行:整个指标项作为整体换行

**效果**:
- 窄屏:所有元素自动适配,无溢出
- 宽屏:充分利用空间,清晰展示
- 灵活布局:根据容器宽度自动调整指标排列
2026-01-11 15:50:26 +08:00
IanShaw027
e5857161ff feat(ops): 增强错误详情弹窗与API支持
**前端改动**:
1. OpsErrorDetailModal.vue:
   - 新增上游错误详情展示功能
   - 支持查看上游错误的请求头、响应体等调试信息
   - 改进错误信息格式化与可读性

2. ops.ts API:
   - 新增getUpstreamErrors接口调用上游错误查询API

**后端配置**:
- config.go/config.yaml/deploy/config.example.yaml:
  - 更新配置支持上游错误事件记录开关
  - 添加相关配置项文档说明
2026-01-11 15:31:48 +08:00
IanShaw027
abdc4f39cb feat(ops): 恢复仪表盘脉搏动画效果
- 将静态QPS历史折线图替换为动画脉搏线
- 使用SVG animate元素实现心跳效果(2秒循环动画)
- 增强流量可视化:通过脉冲跳动直观展示流量"活跃"状态
- 恢复重构前的视觉效果与用户体验
2026-01-11 15:30:59 +08:00
IanShaw027
7ebca553ef feat(ops): 实现上游错误事件记录与查询功能
**新增功能**:
- 新建ops_upstream_error_events表存储上游服务错误详情
- 支持记录上游429/529/5xx错误的详细上下文信息
- 提供按时间范围查询上游错误事件的API

**后端改动**:
1. 模型层(ops_models.go, ops_port.go):
   - 新增UpstreamErrorEvent结构体
   - 扩展Repository接口支持上游错误事件CRUD

2. 仓储层(ops_repo.go):
   - 实现InsertUpstreamErrorEvent写入上游错误
   - 实现GetUpstreamErrorEvents按时间范围查询

3. 服务层(ops_service.go, ops_upstream_context.go):
   - ops_service: 新增GetUpstreamErrorEvents查询方法
   - ops_upstream_context: 封装上游错误上下文构建逻辑

4. Handler层(ops_error_logger.go):
   - 新增GetUpstreamErrorsHandler处理上游错误查询请求

5. Gateway层集成:
   - antigravity_gateway_service.go: 429/529错误时记录上游事件
   - gateway_service.go: OpenAI 429/5xx错误时记录
   - gemini_messages_compat_service.go: Gemini 429/5xx错误时记录
   - openai_gateway_service.go: OpenAI 429/5xx错误时记录
   - ratelimit_service.go: 429限流错误时记录

**数据记录字段**:
- request_id: 关联ops_logs主记录
- platform/model: 上游服务标识
- status_code/error_message: 错误详情
- request_headers/response_body: 调试信息(可选)
- created_at: 错误发生时间
2026-01-11 15:30:27 +08:00
IanShaw027
c2962752eb feat(ops): 添加上游错误事件数据库表
- 新建ops_upstream_error_events表存储上游服务错误详情
- 记录上游错误的请求ID、平台、模型、状态码等信息
- 支持索引优化查询性能(request_id, platform, status_code, created_at)
2026-01-11 15:29:59 +08:00
yangjianbo
ab5839b461 fix(仪表盘): 修复缓存稳定性并补充测试 2026-01-11 15:00:16 +08:00
IanShaw027
89a725a433 feat(ops): 添加QPS脉搏线图并优化指标布局
- 添加实时QPS/TPS历史数据追踪(最近60个数据点)
- 在平均QPS/TPS上方添加SVG脉搏线图(sparkline)
- 将延迟和TTFT卡片的指标布局从2列改为3列
- 恢复Max指标显示(P95/P90/P50/Avg/Max)
2026-01-11 11:49:34 +08:00
IanShaw027
645609d441 merge: 正确合并 main 分支改动
合并 origin/main 最新改动,正确保留所有配置:
- Ops 运维监控配置和功能
- LinuxDo Connect OAuth 配置
- Update 在线更新配置
- 优惠码功能
- 其他 main 分支新功能

修复之前合并时错误删除 LinuxDo 和 Update 配置的问题。
2026-01-11 11:41:10 +08:00
IanShaw027
fc4ea65936 fix: 临时保存编译错误修复
- 添加 LinuxDo 和 Update 配置(从 main 分支缺失)
- 添加 LinuxDoConnectSyntheticEmailDomain 常量
- 添加 IsClaudeCodeClient context key
- 添加 GetLinuxDoConnectOAuthConfig 方法
- 修复 BindStickySession 调用签名
- 修复前端 i18n 重复属性
- 重新生成 wire 依赖注入代码

这个提交准备被合并替换,先保存以防丢失。
2026-01-11 10:59:01 +08:00
yangjianbo
d75cd820b0 fix(认证): 订阅兑换失效认证缓存
订阅兑换后同步失效认证缓存避免授权快照滞后
补充单测覆盖订阅兑换的失效场景

测试: go test ./... -tags=unit
2026-01-11 10:55:26 +08:00
yangjianbo
cb3e08dda4 fix(认证): 补齐余额与删除场景缓存失效
为 Usage/Promo/Redeem 注入认证缓存失效逻辑
删除用户与分组前先失效认证缓存降低窗口
补充回归测试验证失效调用

测试: make test
2026-01-11 10:55:25 +08:00
yangjianbo
44a93c1922 perf(认证): 引入 API Key 认证缓存与轻量删除查询
增加 L1/L2 缓存、负缓存与单飞回源
使用 key+owner 轻量查询替代全量加载并清理旧接口
补充缓存失效与余额更新测试,修复随机抖动 lint

测试: make test
2026-01-11 10:55:25 +08:00
Wesley Liddick
9cba595fd0 Merge pull request #233 from cyhhao/main
fix(openai): 对齐 OpenCode OAuth instructions,保持 Codex CLI 透明转发
2026-01-11 10:41:57 +08:00
shaw
56fc2764e4 chore: remove accidentally committed test binary 2026-01-11 10:37:09 +08:00
Wesley Liddick
0c4f1762c9 Merge pull request #232 from Edric-Li/feat/api-key-ip-restriction
feat(settings): 首页自定义内容 & 配置注入优化
2026-01-11 10:36:01 +08:00
yangjianbo
c2c865b0cb perf(仪表盘): 增强统计缓存与隔离配置
新增仪表盘缓存开关与 TTL 配置,支持 Redis key 前缀隔离,并补充单测与校验。

测试: make test-backend
2026-01-11 10:07:03 +08:00
程序猿MT
a66d318820 Merge branch 'Wei-Shaw:main' into main 2026-01-10 23:34:23 +08:00
yangjianbo
a16f72f52e fix(认证): 订阅兑换失效认证缓存
订阅兑换后同步失效认证缓存避免授权快照滞后
补充单测覆盖订阅兑换的失效场景

测试: go test ./... -tags=unit
2026-01-10 23:14:20 +08:00
yangjianbo
99e2391b2a fix(认证): 补齐余额与删除场景缓存失效
为 Usage/Promo/Redeem 注入认证缓存失效逻辑
删除用户与分组前先失效认证缓存降低窗口
补充回归测试验证失效调用

测试: make test
2026-01-10 22:52:13 +08:00
cyhhao
80c1cdf024 fix(lint): trim unused codex helpers 2026-01-10 22:45:29 +08:00
Edric Li
0fa5a6015e feat(settings): add iframe CSP warning for home content
Add a warning message to inform admins that some websites may have
X-Frame-Options or CSP policies that prevent iframe embedding.
2026-01-10 22:35:33 +08:00
yangjianbo
9d0a4f3d68 perf(认证): 引入 API Key 认证缓存与轻量删除查询
增加 L1/L2 缓存、负缓存与单飞回源
使用 key+owner 轻量查询替代全量加载并清理旧接口
补充缓存失效与余额更新测试,修复随机抖动 lint

测试: make test
2026-01-10 22:23:51 +08:00
cyhhao
1a641392d9 Merge up/main 2026-01-10 21:57:57 +08:00
cyhhao
36b817d008 Align OAuth transform with OpenCode instructions 2026-01-10 20:53:16 +08:00
kzw200015
24d19a5f78 fix: 从codex请求参数中移除max_output_tokens (#231)
某些客户端比如 opencode 会在请求中附加 max_output_tokens,这会导致上游返回400错误
2026-01-10 19:37:04 +08:00
Edric Li
3fb4a2b0ff style: replace interface{} with any per golangci-lint rules 2026-01-10 19:08:41 +08:00
Edric Li
0772cdda0f fix: update API contract test for home_content field and fix gofmt 2026-01-10 19:01:00 +08:00
Edric Li
f6f072cb9a Merge branch 'main' into feat/api-key-ip-restriction 2026-01-10 18:49:50 +08:00
Edric Li
5265b12cc7 feat(settings): add home content customization and config injection
- Add home_content setting for custom homepage (HTML or iframe URL)
- Inject public settings into index.html to eliminate page flash
- Support ETag caching with automatic invalidation on settings update
- Add Vite plugin for dev mode settings injection
- Refactor HomeView to use appStore instead of local API calls
2026-01-10 18:37:44 +08:00
shaw
ff0875868e Merge PR #229: perf(网关): 粘性会话命中复用候选账号 2026-01-10 15:21:59 +08:00
yangjianbo
e79dbad602 Merge branch 'main' into test 2026-01-10 14:56:51 +08:00
yangjianbo
6a9cc13e3e fix(网关): 明确粘性命中范围并优化映射构建
仅在粘性命中时构建候选账号映射以减少开销
新增用例验证粘性账号缺失时回退负载感知选择
2026-01-10 14:51:16 +08:00
shaw
d1a6d6b1cf Merge branch 'mt21625457/main' 2026-01-10 14:44:58 +08:00
yangjianbo
7a0ca05233 perf(网关): 粘性会话命中复用候选账号
使用候选账号映射避免粘性命中时额外的 GetByID 查询
新增单测确保粘性命中不触发 GetByID 且提前返回
2026-01-10 14:39:33 +08:00
shaw
15884f368d Merge branch 'longgexx/main' 2026-01-10 14:16:13 +08:00
shaw
b03fb9c2f6 fix: remove accidentally committed test binary and restore .gitignore
- Remove backend/repository.test (62MB macOS arm64 test binary)
- Restore *.test pattern in .gitignore to prevent future accidents
2026-01-10 14:16:06 +08:00
shaw
3d4984133e chore: 删除误提交的 Go 测试二进制并更新 .gitignore 2026-01-10 14:03:41 +08:00
IanShaw027
13ae0ce7b0 refactor(migration): 重命名 ops 迁移文件避免编号冲突
将 030_ops_monitoring_vnext.sql 重命名为 033_ops_monitoring_vnext.sql
以避免与主分支的 030_add_account_expires_at.sql 冲突。
2026-01-10 13:26:01 +08:00
IanShaw027
3a67002cfe merge: 合并主分支改动并保留 ops 监控实现
合并 main 分支的最新改动到 ops 监控分支。
冲突解决策略:保留当前分支的 ops 相关改动,接受主分支的其他改动。

保留的 ops 改动:
- 运维监控配置和依赖注入
- 运维监控 API 处理器和中间件
- 运维监控服务层和数据访问层
- 运维监控前端界面和状态管理

接受的主分支改动:
- Linux DO OAuth 集成
- 账号过期功能
- IP 地址限制功能
- 用量统计优化
- 其他 bug 修复和功能改进
2026-01-10 13:24:40 +08:00
long
9f4d4e5adf feat: 实现注册优惠码功能
- 支持创建/编辑/删除优惠码,设置赠送金额和使用限制
  - 注册页面实时验证优惠码并显示赠送金额
  - 支持 URL 参数自动填充 (?promo=CODE)
  - 添加优惠码验证接口速率限制
  - 使用数据库行锁防止并发超限
  - 新增后台优惠码管理页面,支持复制注册链接
2026-01-10 13:23:03 +08:00
long
d2fc14fb97 feat: 实现注册优惠码功能
- 支持创建/编辑/删除优惠码,设置赠送金额和使用限制
  - 注册页面实时验证优惠码并显示赠送金额
  - 支持 URL 参数自动填充 (?promo=CODE)
  - 添加优惠码验证接口速率限制
  - 使用数据库行锁防止并发超限
  - 新增后台优惠码管理页面,支持复制注册链接
2026-01-10 13:14:35 +08:00
yangjianbo
3730819857 chore(合并): 修复合并冲突并保留分组上下文优化
解决 GroupRepository 接口签名更新导致的测试失败

保留 ctxkey.Group Hydrated 覆盖逻辑相关测试

测试: make test-backend
2026-01-10 10:00:49 +08:00
yangjianbo
297f08c683 Merge branch 'test' into dev 2026-01-10 09:39:02 +08:00
yangjianbo
61f556745a Merge branch 'main' of https://github.com/mt21625457/aicodex2api 2026-01-10 09:28:30 +08:00
yangjianbo
435f693892 test(分组): 增加无效上下文覆盖回归测试
补充 GatewayService 与 APIKey 中间件对无效 ctxkey.Group 的覆盖行为测试

测试: make test-backend
2026-01-10 09:27:47 +08:00
yangjianbo
72f78f8a56 fix(分组): 强化上下文分组可信校验
- 引入 Hydrated 标记限制复用来源

- 无效上下文分组允许被新值覆盖自愈

- 更新相关单测覆盖
2026-01-10 08:40:27 +08:00
yangjianbo
2597fe78ba fix(分组): 防止降级环并校验上下文分组
- 增加降级链路环检测并拦截配置

- 仅复用合法分组上下文并必要时回退查询

- 标注 GetByIDLite 轻量语义并补充测试
2026-01-10 07:56:50 +08:00
cyhhao
eb06006d6c Make Codex CLI passthrough 2026-01-10 03:12:56 +08:00
IanShaw027
c48dc097ff feat(运维监控): 重构仪表板布局和增强数据展示
主要改动:
- 重构仪表板为左右布局(5:7比例)
- 左侧:健康评分 + 实时信息(当前/峰值/平均 QPS/TPS)
- 右侧:6个卡片展示详细指标(3列x2行)
  - 总请求:请求数、Token数、平均QPS/TPS、平均延迟/TTFT
  - SLA:百分比、异常数、进度条
  - 延迟:P99/P95/P90/P50/Avg/Max(带颜色编码)
  - TTFT:P99/P95/P90/P50/Avg/Max(带颜色编码)
  - 请求错误:错误率、错误数、业务限制数
  - 上游错误:错误率、错误数(排除429/529)、429/529数
- 添加延迟/TTFT颜色编码(<500ms绿色,<1s黄色,<2s橙色,≥2s红色)
- 添加实时窗口选择器(1min/5min/30min/1h)
- 优化时间段选择器标签("近5分钟"等)
- 完善中英文i18n翻译
- 数据库:添加Redis连接池字段(redis_conn_total, redis_conn_idle)
2026-01-10 02:17:38 +08:00
IanShaw027
585257d340 feat(运维监控): 增强监控功能和健康评分系统
后端改进:
- 新增健康评分计算服务(ops_health_score.go)
- 添加分布式锁支持(ops_advisory_lock.go)
- 优化指标采集和聚合逻辑
- 新增运维指标采集间隔配置(60-3600秒)
- 移除未使用的WebSocket查询token认证中间件
- 改进清理服务和告警评估逻辑

前端改进:
- 简化OpsDashboard组件结构
- 完善国际化文本(中英文)
- 新增运维监控相关API类型定义
- 添加运维指标采集间隔设置界面
- 优化错误详情模态框

测试:
- 添加健康评分单元测试
- 更新API契约测试
2026-01-10 01:38:47 +08:00
yangjianbo
675543240e perf(网关): 复用分组上下文减少热路径查询
新增 GetByIDLite 并在网关与 Gemini 选择流程复用上下文 group,避免 COUNT 触发
更新 API key 中间件注入 group 上下文,减少重复查库
补充 gateway/gemini 中间件与仓库层回归测试

测试: make test
2026-01-09 23:01:42 +08:00
Song Siyu
7d1fe818be feat: antigravity 配额域限流 + SSE 上限 (#222)
* fix: 添加 gemini-3-flash 前缀映射支持 gemini-3-flash-preview

* feat(antigravity): 增强请求参数和注入 Antigravity 身份 system prompt

* feat: antigravity 配额域限流

* chore: 调整 SSE 单行上限到 25MB

* chore: 提升 SSE 单行上限到 40MB
2026-01-09 22:00:14 +08:00
Edric.Li
0a4641c24e feat(api-key): 添加 IP 白名单/黑名单限制功能 (#221)
* feat(api-key): add IP whitelist/blacklist restriction and usage log IP tracking

- Add IP restriction feature for API keys (whitelist/blacklist with CIDR support)
- Add IP address logging to usage logs (admin-only visibility)
- Remove billing_type column from usage logs UI (redundant)
- Use generic "Access denied" error message for security

Backend:
- New ip package with IP/CIDR validation and matching utilities
- Database migrations for ip_whitelist, ip_blacklist (api_keys) and ip_address (usage_logs)
- Middleware IP restriction check after API key validation
- Input validation for IP/CIDR patterns on create/update

Frontend:
- API key form with enable toggle for IP restriction
- Shield icon indicator in table for keys with IP restriction
- Removed billing_type filter and column from usage views

* fix: update API contract tests for ip_whitelist/ip_blacklist fields

Add ip_whitelist and ip_blacklist fields to expected JSON responses
in API contract tests to match the new API key schema.
2026-01-09 21:59:32 +08:00
Edric Li
e83f644c3f fix: update API contract tests for ip_whitelist/ip_blacklist fields
Add ip_whitelist and ip_blacklist fields to expected JSON responses
in API contract tests to match the new API key schema.
2026-01-09 21:37:07 +08:00
Edric Li
6b97a8be28 Merge branch 'main' into feat/api-key-ip-restriction 2026-01-09 21:34:28 +08:00
Edric Li
90798f14b5 feat(api-key): add IP whitelist/blacklist restriction and usage log IP tracking
- Add IP restriction feature for API keys (whitelist/blacklist with CIDR support)
- Add IP address logging to usage logs (admin-only visibility)
- Remove billing_type column from usage logs UI (redundant)
- Use generic "Access denied" error message for security

Backend:
- New ip package with IP/CIDR validation and matching utilities
- Database migrations for ip_whitelist, ip_blacklist (api_keys) and ip_address (usage_logs)
- Middleware IP restriction check after API key validation
- Input validation for IP/CIDR patterns on create/update

Frontend:
- API key form with enable toggle for IP restriction
- Shield icon indicator in table for keys with IP restriction
- Removed billing_type filter and column from usage views
2026-01-09 21:24:59 +08:00
IanShaw027
8ae75e7f6e feat(前端UI): 实现运维监控前端界面
- 新增帮助提示组件(HelpTooltip.vue)
- 更新侧边栏添加 ops 监控菜单项
- 扩展设置视图集成 ops 配置面板
- 新增 ops 监控视图目录(dashboard, alerts, realtime, settings 等)
2026-01-09 21:00:04 +08:00
IanShaw027
fc32b57798 feat(国际化): 添加运维监控多语言支持
- 添加英文翻译(en.ts)包含 ops 监控所有文案
- 添加中文翻译(zh.ts)包含 ops 监控所有文案
2026-01-09 20:59:33 +08:00
IanShaw027
337a188660 feat(前端状态): 添加运维监控状态管理和路由
- 新增 adminSettings store 管理 ops 配置状态
- 注册 adminSettings store 到全局 store
- 添加 ops 监控相关路由(dashboard, alerts, realtime, settings)
2026-01-09 20:59:02 +08:00
IanShaw027
11d063e3c4 feat(前端API): 实现运维监控 API 客户端
- 新增 ops API 客户端(ops.ts)
- 扩展 settings API 支持 ops 配置
- 更新 admin API 索引导出 ops 模块
- 扩展 API 客户端支持 WebSocket 连接
2026-01-09 20:58:33 +08:00
IanShaw027
e846458009 test(后端): 更新 API 契约测试支持 ops 监控端点
- 更新 api_contract_test.go 包含 ops 相关端点测试
2026-01-09 20:58:01 +08:00
IanShaw027
2d123a11ad feat(设置): 集成运维监控配置到系统设置
- 扩展 setting_handler 支持 ops 配置管理
- 扩展 setting_service 支持 ops 配置持久化
- 更新 settings_view 包含 ops 配置视图
2026-01-09 20:57:32 +08:00
IanShaw027
fcdf839b6b feat(网关): 集成运维监控到 API 网关处理器
- 在 gateway_handler 中添加请求监控和错误追踪
- 在 openai_gateway_handler 中集成 ops 指标采集
- 在 gemini_v1beta_handler 中集成 ops 指标采集
- 更新 handler 基类支持 ops 错误日志记录
2026-01-09 20:56:37 +08:00
IanShaw027
d55dd56fd2 feat(依赖注入): 集成运维监控依赖注入配置
- 更新 wire.go 添加 ops 服务依赖注入提供者
- 重新生成 wire_gen.go 包含完整的依赖注入图
2026-01-09 20:55:52 +08:00
IanShaw027
e0d12b46d8 feat(路由): 集成运维监控路由到服务器
- 更新路由器注册 ops 监控路由
- 添加 ops 管理路由(dashboard, alerts, realtime, settings, ws)
- 更新 gateway 路由支持请求追踪
- 集成 ops 服务到 HTTP 服务器
2026-01-09 20:55:12 +08:00
IanShaw027
f3ed95d4de feat(handler): 实现运维监控 API 处理器和中间件
- 新增 ops 错误日志记录器(ops_error_logger.go)
- 新增 ops 主处理器(ops_handler.go)
- 新增告警管理处理器(ops_alerts_handler.go)
- 新增仪表板处理器(ops_dashboard_handler.go)
- 新增实时监控处理器(ops_realtime_handler.go)
- 新增配置管理处理器(ops_settings_handler.go)
- 新增 WebSocket 处理器(ops_ws_handler.go)
- 扩展设置 DTO 支持 ops 配置
- 新增客户端请求 ID 中间件(client_request_id.go)
- 新增 WebSocket 查询令牌认证中间件(ws_query_token_auth.go)
- 更新管理员认证中间件支持 ops 路由
- 注册 handler 依赖注入
2026-01-09 20:54:26 +08:00
IanShaw027
5baa8b5673 feat(service): 实现运维监控业务逻辑层
- 新增 ops 主服务(ops_service.go)和端口定义(ops_port.go)
- 实现账号可用性检查服务(ops_account_availability.go)
- 实现数据聚合服务(ops_aggregation_service.go)
- 实现告警评估服务(ops_alert_evaluator_service.go)
- 实现告警管理服务(ops_alerts.go)
- 实现数据清理服务(ops_cleanup_service.go)
- 实现并发控制服务(ops_concurrency.go)
- 实现仪表板服务(ops_dashboard.go)
- 实现错误处理服务(ops_errors.go)
- 实现直方图服务(ops_histograms.go)
- 实现指标采集服务(ops_metrics_collector.go)
- 实现查询模式服务(ops_query_mode.go)
- 实现实时监控服务(ops_realtime.go)
- 实现请求详情服务(ops_request_details.go)
- 实现重试机制服务(ops_retry.go)
- 实现配置管理服务(ops_settings.go)
- 实现趋势分析服务(ops_trends.go)
- 实现窗口统计服务(ops_window_stats.go)
- 添加 ops 相关领域常量
- 注册 service 依赖注入
2026-01-09 20:53:44 +08:00
IanShaw027
bb5303272b feat(repository): 实现运维监控数据访问层
- 新增 ops 主仓库(ops_repo.go)
- 实现告警数据访问(ops_repo_alerts.go)
- 实现仪表板数据访问(ops_repo_dashboard.go)
- 实现直方图数据访问(ops_repo_histograms.go)
- 实现延迟直方图桶逻辑(ops_repo_latency_histogram_buckets.go)
- 新增延迟直方图桶测试(ops_repo_latency_histogram_buckets_test.go)
- 实现指标数据访问(ops_repo_metrics.go)
- 实现预聚合数据访问(ops_repo_preagg.go)
- 实现请求详情数据访问(ops_repo_request_details.go)
- 实现趋势数据访问(ops_repo_trends.go)
- 实现窗口统计数据访问(ops_repo_window_stats.go)
- 更新并发缓存支持 ops 场景
- 注册 repository 依赖注入
2026-01-09 20:52:57 +08:00
IanShaw027
d55866d375 feat(数据库): 添加运维监控数据模型和数据库迁移脚本
- 新增 ops 监控数据库迁移脚本(表结构定义)
- 定义核心数据模型(ops_models.go)
- 定义告警相关模型(ops_alert_models.go)
- 定义仪表板数据模型(ops_dashboard_models.go)
- 定义实时监控数据模型(ops_realtime_models.go)
- 定义配置相关模型(ops_settings_models.go)
- 定义趋势分析数据模型(ops_trend_models.go)
2026-01-09 20:52:17 +08:00
IanShaw027
4b9e47cec9 feat(基础设施): 添加运维监控功能的基础配置和依赖
- 更新 .gitignore 排除临时文件
- 添加 ops 监控相关配置项到 config.yaml
- 更新 Go 依赖包(go.mod/go.sum)
- 扩展 config.go 支持 ops 监控配置
- 新增上下文键定义(ClientRequestID)
2026-01-09 20:51:41 +08:00
shaw
62dc0b953b Merge branch 'fix/table-pagination-and-features' 2026-01-09 20:42:05 +08:00
IanShaw027
7c3d5cadd5 fix(admin): 代码审查修复 - 输入验证和测试完善
根据 Codex 代码审查报告,修复所有 P0 和 P1 优先级问题。

## P0 紧急修复

### 1. 修复集成测试编译错误
- 更新 group_repo_integration_test.go 中所有 ListWithFilters 调用
- 添加缺失的 search 参数(传入空字符串)
- 修复 4 处旧签名调用,避免 CI 编译失败

### 2. 添加统一的 search 参数输入验证
为所有 admin handler 添加一致的输入验证逻辑:
- group_handler.go: 添加 TrimSpace + 长度限制
- proxy_handler.go: 添加 TrimSpace + 长度限制
- redeem_handler.go: 添加 TrimSpace + 长度限制
- user_handler.go: 添加 TrimSpace + 长度限制

验证规则:
- TrimSpace() 去除首尾空格
- 最大长度 100 字符(防止 DoS 攻击)
- 超长输入自动截断

## P1 改进

### 3. 补充 search 功能的单元测试
新增 admin_service_group_test.go 中的测试:
- TestAdminService_ListGroups_WithSearch
  - search 参数正常传递到 repository 层
  - search 为空字符串时的行为
  - search 与其他过滤条件组合使用

新增 admin_service_search_test.go 文件:
- 为其他 admin API 添加 search 测试覆盖
- 统一的测试模式和断言

### 4. 补充 search 功能的集成测试
新增 group_repo_integration_test.go 测试场景:
- TestListWithFilters_Search
  - 搜索 name 字段匹配
  - 搜索 description 字段匹配
  - 搜索不存在内容(返回空)
  - 大小写不敏感测试
  - 特殊字符转义测试(%、_)
  - 与其他过滤条件组合

## 测试结果

-  编译检查通过
-  单元测试全部通过 (3/3)
-  集成测试编译通过
-  所有 service 测试通过

## 影响范围

修改文件: 8 个
代码变更: +234 行 / -8 行

## 相关 Issue

解决代码审查中的安全性和稳定性问题:
- 防止 DoS 攻击(超长搜索字符串)
- 修复测试编译错误(CI 阻塞问题)
- 提升测试覆盖率
2026-01-09 19:43:19 +08:00
shaw
f060db0b30 fix: 加固 LinuxDo OAuth 登录安全与配置校验 2026-01-09 19:32:06 +08:00
IanShaw027
5e936fbf0e feat(admin): 添加账号批量调度开关功能
- 后端:支持批量更新账号的 schedulable 字段
  - 在 BulkUpdateAccountsRequest 中添加 schedulable 参数
  - 在 AccountBulkUpdate 中添加 schedulable 字段支持
  - 更新 repository 层批量更新 SQL 逻辑
- 前端:在账号管理页面添加批量调度控制
  - 新增"批量启用调度"和"批量停止调度"按钮
  - 添加 handleBulkToggleSchedulable 处理函数
  - 显示具体的成功提示信息(包含操作账号数量)
- 国际化:添加批量调度相关中英文翻译
- 优化:添加 search 参数标准化和验证(account_handler)
2026-01-09 19:26:32 +08:00
IanShaw027
3820232241 fix(admin): 修复表格批量操作和搜索功能问题
1. 恢复账号管理批量操作栏缺失的功能按钮
   - 添加"本页全选"按钮支持批量选择当前页所有账号
   - 添加"清除已选"按钮快速清空已选账号列表
   - 在重构拆分组件时遗漏,现已恢复

2. 修复分组管理搜索功能仅搜索当前页的问题
   - 前端:移除本地过滤逻辑,改用后端搜索
   - 后端:添加 search 参数支持,搜索名称和描述字段
   - 支持不区分大小写的模糊匹配
   - 统一所有管理页面的搜索体验
2026-01-09 18:58:06 +08:00
admin
707061efac feat(admin): 添加 LinuxDO OAuth 回调地址快速设置按钮
- 在设置页面添加"使用当前站点生成并复制"按钮
- 自动填充回调地址并复制到剪贴板
- 添加中英文国际化支持

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 18:50:48 +08:00
cyhhao
7a06c4873e Fix Codex OAuth tool mapping 2026-01-09 18:35:58 +08:00
shaw
1a1e23fc76 fix(auth): 注册接口安全加固 - 默认关闭注册 2026-01-09 18:26:32 +08:00
admin
d1c2a61d19 refactor(auth): 将 Linux DO OAuth 配置迁移到系统设置
- 将 LinuxDo Connect 配置从环境变量迁移到数据库持久化
- 在管理后台系统设置中添加 LinuxDo OAuth 配置项
- 简化部署流程,无需修改 docker-compose.override.yml

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 18:26:32 +08:00
admin
152d0cdec6 feat(auth): 添加 Linux DO Connect OAuth 登录支持
- 新增 Linux DO OAuth 配置项和环境变量支持
- 实现 OAuth 授权流程和回调处理
- 前端添加 Linux DO 登录按钮和回调页面
- 支持通过 Linux DO 账号注册/登录
- 添加相关国际化文本

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 18:26:32 +08:00
IanShaw027
514f5802b5 fix(fe): 修复中优先级表格功能问题
修复的问题:

1. **搜索和筛选防抖不同步**(AccountsView.vue)
   - 问题:筛选器使用 reload(立即),搜索使用 debouncedReload(300ms延迟)
   - 修复:统一使用 debouncedReload,避免多余的API调用

2. **useTableLoader 竞态条件**(useTableLoader.ts)
   - 问题:finally 块检查 signal.aborted 而不是 controller 实例
   - 修复:检查 abortController === currentController

3. **改进错误处理**(UsersView.vue)
   - 添加详细错误消息:error.response?.data?.detail || error.message
   - 用户可以看到具体的错误原因而不是通用消息

4. **分页边界检查**(useTableLoader.ts, UsersView.vue)
   - 添加页码有效性检查:Math.max(1, Math.min(page, pagination.pages || 1))
   - 防止分页越界导致显示空表

影响范围:
- frontend/src/composables/useTableLoader.ts
- frontend/src/views/admin/AccountsView.vue
- frontend/src/views/admin/UsersView.vue

测试:✓ 前端构建测试通过
2026-01-09 17:58:21 +08:00
IanShaw027
ee9b9b3971 fix(fe): 修复表格分页和基础功能问题
修复的主要问题:

1. **分页切换失效**(AccountsView.vue)
   - 修复 useTableLoader 未解构 handlePageSizeChange 函数
   - 添加 @update:pageSize 事件绑定到 Pagination 组件

2. **内存泄漏修复**(多个文件)
   - UsersView.vue: 添加 searchTimeout 清理和 abortController.abort()
   - ProxiesView.vue: 添加 onUnmounted 钩子清理定时器
   - RedeemView.vue: 添加 onUnmounted 钩子清理定时器

3. **分页重置问题**(UsersView.vue)
   - toggleBuiltInFilter: 切换筛选器时重置 pagination.page = 1
   - toggleAttributeFilter: 切换属性筛选时重置 pagination.page = 1

影响范围:
- frontend/src/views/admin/AccountsView.vue
- frontend/src/views/admin/ProxiesView.vue
- frontend/src/views/admin/RedeemView.vue
- frontend/src/views/admin/UsersView.vue

测试:✓ 前端构建测试通过
2026-01-09 17:38:45 +08:00
shaw
27291f2e5f fix(docker): 修改 Redis 配置以支持可选的密码设置 2026-01-09 17:28:55 +08:00
yangjianbo
eeb1282f0c Merge branch 'main' of https://github.com/mt21625457/aicodex2api 2026-01-09 15:30:52 +08:00
Xu Kang
5d1badfe67 fix: add missing i18n key admin.accounts.outputCopied (#218) 2026-01-09 15:28:55 +08:00
shaw
43f104bdf7 fix(auth): 注册接口安全加固 - 默认关闭注册 2026-01-09 14:49:20 +08:00
shaw
0a9c17b9d1 Merge PR #213: feat(openai): 支持 OpenAI Responses API 标准内容格式 2026-01-09 10:40:47 +08:00
程序猿MT
799b010631 fix(auth): 修复 RefreshToken 使用过期 token 时的 nil pointer panic (#214)
* fix(auth): 修复 RefreshToken 使用过期 token 时的 nil pointer panic

问题分析:
- RefreshToken 允许过期 token 继续流程(用于无感刷新)
- 但 ValidateToken 在 token 过期时返回 nil claims
- 导致后续访问 claims.UserID 时触发 panic

修复方案:
- 修改 ValidateToken,在检测到 ErrTokenExpired 时仍然返回 claims
- jwt-go 在解析时即使遇到过期错误,token.Claims 仍会被填充
- 这样 RefreshToken 可以正常获取用户信息并生成新 token

新增测试:
- TestAuthService_ValidateToken_ExpiredReturnsClaimsWithError
- TestAuthService_RefreshToken_ExpiredTokenNoPanic

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(auth): 修复邮件验证服务未配置时可绕过验证的安全漏洞

当邮件验证开启但 emailService 未配置时,原逻辑允许用户绕过验证直接注册。
现在会返回 ErrServiceUnavailable 拒绝注册,确保配置错误不会导致安全问题。

- 在验证码检查前先检查 emailService 是否配置
- 添加日志记录帮助发现配置问题
- 新增单元测试覆盖该场景

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: yangjianbo <yangjianbo@leagsoft.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 10:37:15 +08:00
shaw
2d83941aaa feat(antigravity): 添加 URL fallback 机制 (sandbox → daily → prod) 2026-01-09 10:36:56 +08:00
yangjianbo
470abee092 fix(auth): 修复邮件验证服务未配置时可绕过验证的安全漏洞
当邮件验证开启但 emailService 未配置时,原逻辑允许用户绕过验证直接注册。
现在会返回 ErrServiceUnavailable 拒绝注册,确保配置错误不会导致安全问题。

- 在验证码检查前先检查 emailService 是否配置
- 添加日志记录帮助发现配置问题
- 新增单元测试覆盖该场景

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 09:57:42 +08:00
yangjianbo
39433f2a29 fix(auth): 修复 RefreshToken 使用过期 token 时的 nil pointer panic
问题分析:
- RefreshToken 允许过期 token 继续流程(用于无感刷新)
- 但 ValidateToken 在 token 过期时返回 nil claims
- 导致后续访问 claims.UserID 时触发 panic

修复方案:
- 修改 ValidateToken,在检测到 ErrTokenExpired 时仍然返回 claims
- jwt-go 在解析时即使遇到过期错误,token.Claims 仍会被填充
- 这样 RefreshToken 可以正常获取用户信息并生成新 token

新增测试:
- TestAuthService_ValidateToken_ExpiredReturnsClaimsWithError
- TestAuthService_RefreshToken_ExpiredTokenNoPanic

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 09:36:06 +08:00
Call White
f6a9a0a45a Merge pull request #1 from cyhhao/feat/ai-sdk-compatibility
feat(openai): add AI SDK content format compatibility for OAuth accounts
2026-01-09 00:47:44 +08:00
cyhhao
5b8d4fb047 feat(openai): add AI SDK content format compatibility for OAuth accounts
- Add normalizeInputForCodexAPI function to convert AI SDK multi-part
  content format to simplified format expected by ChatGPT Codex API
- AI SDK sends: {"content": [{"type": "input_text", "text": "..."}]}
- Codex API expects: {"content": "..."}
- Only applies to OAuth accounts (ChatGPT internal API)
- API Key accounts remain unchanged (OpenAI Platform API supports both)
2026-01-09 00:34:49 +08:00
IanShaw
afcfbb458d fix(gemini): Google One 强制使用内置 OAuth client + 自动获取 project_id + UI 优化 (#212)
* fix(gemini): Google One 强制使用内置 OAuth client + 自动获取 project_id + UI 优化

## 后端改动

### 1. Google One 强制使用内置 Gemini CLI OAuth Client
**问题**:
- Google One 之前允许使用自定义 OAuth client,导致认证流程不稳定
- 与 Code Assist 的行为不一致

**解决方案**:
- 修改 `gemini_oauth_service.go`: Google One 现在与 Code Assist 一样强制使用内置 client (L122-135)
- 更新 `gemini_oauth_client.go`: ExchangeCode 和 RefreshToken 方法支持强制内置 client (L31-44, L77-86)
- 简化 `geminicli/oauth.go`: Google One scope 选择逻辑 (L187-190)
- 标记 `geminicli/constants.go`: DefaultGoogleOneScopes 为 DEPRECATED (L30-33)
- 更新测试用例以反映新行为

**OAuth 类型对比**:
| OAuth类型 | Client来源 | Scopes | Redirect URI |
|-----------|-----------|--------|-----------------|
| code_assist | 内置 Gemini CLI | DefaultCodeAssistScopes | https://codeassist.google.com/authcode |
| google_one | 内置 Gemini CLI (新) | DefaultCodeAssistScopes | https://codeassist.google.com/authcode |
| ai_studio | 必须自定义 | DefaultAIStudioScopes | http://localhost:1455/auth/callback |

### 2. Google One 自动获取 project_id
**问题**:
- Google One 个人账号测试模型时返回 403/404 错误
- 原因:cloudaicompanion API 需要 project_id,但个人账号无需手动创建 GCP 项目

**解决方案**:
- 修改 `gemini_oauth_service.go`: OAuth 流程中自动调用 fetchProjectID
- Google 通过 LoadCodeAssist API 自动分配 project_id
- 与 Gemini CLI 行为保持一致
- 后端根据 project_id 自动选择正确的 API 端点

**影响**:
- Google One 账号现在可以正常使用(需要重新授权)
- Code Assist 和 AI Studio 账号不受影响

### 3. 修复 Gemini 测试账号无内容输出问题
**问题**:
- 测试 Gemini 账号时只显示"测试成功",没有显示 AI 响应内容
- 原因:processGeminiStream 在检查到 finishReason 时立即返回,跳过了内容提取

**解决方案**:
- 修改 `account_test_service.go`: 调整逻辑顺序,先提取内容再检查是否完成
- 确保最后一个 chunk 的内容也能被正确显示

**影响**:
- 所有 Gemini 账号类型(API Key、OAuth)的测试现在都会显示完整响应内容
- 用户可以看到流式输出效果

## 前端改动

### 1. 修复图标宽度压缩问题
**问题**:
- 账户类型选择按钮中的图标在某些情况下会被压缩变形

**解决方案**:
- 修改 `CreateAccountModal.vue`: 为所有平台图标容器添加 `shrink-0` 类
- 确保 Anthropic、OpenAI、Gemini、Antigravity 图标保持固定 8×8 尺寸 (32px × 32px)

### 2. 优化重新授权界面
**问题**:
- 重新授权时显示三个可点击的授权类型选择按钮,可能导致用户误切换到不兼容的授权方式

**解决方案**:
- 修改 `ReAuthAccountModal.vue` (admin 和普通用户版本):
  - 将可点击的授权类型选择按钮改为只读信息展示框
  - 根据账号的 `credentials.oauth_type` 动态显示对应图标和文本
  - 删除 `geminiAIStudioOAuthEnabled` 状态和 `handleSelectGeminiOAuthType` 方法
  - 防止用户误操作

## 测试验证
-  所有后端单元测试通过
-  OAuth client 选择逻辑正确
-  Google One 和 Code Assist 行为一致
-  测试账号显示完整响应内容
-  UI 图标显示正常
-  重新授权界面只读展示正确

* fix(lint): 修复 golangci-lint 错误信息格式问题

- 将错误信息改为小写开头以符合 Go 代码规范
- 修复 ST1005: error strings should not be capitalized
2026-01-08 23:47:29 +08:00
Edric Li
8f24d239af fix: update integration tests for GatewayCache groupID parameter 2026-01-08 23:25:05 +08:00
Edric Li
b7a29a4bac fix: update mock interfaces and fix gofmt issues for CI
- Update mockGatewayCacheForPlatform and mockGatewayCacheForGemini
  to match new GatewayCache interface with groupID parameter
- Fix gofmt formatting in group_handler.go and admin_service.go
2026-01-08 23:13:57 +08:00
Edric Li
a42105881f feat(groups): add Claude Code client restriction and session isolation
- Add claude_code_only field to restrict groups to Claude Code clients only
- Add fallback_group_id for non-Claude Code requests to use alternate group
- Implement ClaudeCodeValidator for User-Agent detection
- Add group-level session binding isolation (groupID in Redis key)
- Prevent cross-group sticky session pollution
- Update frontend with Claude Code restriction controls
2026-01-08 23:07:00 +08:00
Edric Li
958ffe7a8a test: fix unit tests for user_agent and proxy repo interface
- Add user_agent field to API contract test expectation
- Add ListWithFiltersAndAccountCount stub to proxyRepoStub
2026-01-08 21:44:18 +08:00
Edric Li
b46b3c5c3c Merge remote-tracking branch 'upstream/main' 2026-01-08 21:35:34 +08:00
Edric Li
fd1b14fd1d feat(home): redirect admin users to admin dashboard
When clicking "Enter Console" button on home page, admin users are now
redirected to /admin/dashboard instead of /dashboard.
2026-01-08 21:26:00 +08:00
Edric Li
eb198e5969 feat(proxies): add account count column to proxy list
Display the number of accounts bound to each proxy in the admin proxy
management page, similar to the groups list view.
2026-01-08 21:20:12 +08:00
Edric Li
70fcbd7006 feat(usage): add User-Agent column to usage logs
- Add user_agent field to UsageLog DTO and mapper
- Display User-Agent column in admin and user usage tables
- Add formatUserAgent helper to show friendly client names
- Include user_agent in Excel export
- Remove request_id column from admin usage table
2026-01-08 21:02:13 +08:00
shaw
b015a3bd8a fix(antigravity): 修复频繁出现429错误的问题 2026-01-08 20:06:32 +08:00
shaw
3fb43b91bf fix(security): 强化 usage 端点信息暴露控制 2026-01-08 17:45:31 +08:00
shaw
6e8188ed64 fix(antigravity): 修复请求频繁429的问题 2026-01-08 17:27:35 +08:00
shaw
db6f53e2c9 fix(billing): 修复客户端取消请求时计费丢失问题
检测 context.Canceled 作为客户端断开信号,返回已收集的 usage 而非错误
2026-01-08 11:25:17 +08:00
shaw
acabdc2f99 fix(i18n): correct priority description - lower value means higher priority 2026-01-08 09:56:26 +08:00
shaw
169aa4716e Merge branch 'feature/account-expires-at' into main: feat: add account expires-at field and auto-pause expired accounts 2026-01-08 09:27:57 +08:00
shaw
c0753320a0 Merge branch 'feat/usage-log-user-agent' 2026-01-08 09:16:48 +08:00
Edric Li
38d875b06f feat(update): 添加在线更新和定价数据获取的代理支持
针对国内服务器访问 GitHub 困难的问题,为在线更新和定价数据获取功能添加代理支持。

主要变更:
- 新增 update.proxy_url 配置项,支持 http/https/socks5/socks5h 协议
- 修改 GitHubReleaseClient 和 PricingRemoteClient 支持代理配置
- 更新 Wire 依赖注入,通过 Provider 函数传递配置
- 更新 Docker 配置文件,支持通过 UPDATE_PROXY_URL 环境变量设置代理

配置示例:
  update:
    proxy_url: "http://127.0.0.1:7890"

Docker 环境变量:
  UPDATE_PROXY_URL=http://host.docker.internal:7890

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 23:15:20 +08:00
Edric Li
1ada6cf768 feat(usage-log): 增加请求 User-Agent 记录
在使用记录中添加 user_agent 字段,用于记录 API 请求的 User-Agent 头信息,
便于分析客户端类型和调试。

变更内容:
- 新增数据库迁移 028_add_usage_logs_user_agent.sql
- 更新 UsageLog 模型和 Ent Schema 添加 user_agent 字段
- 更新 Repository 层的 Create 和 scanUsageLog 方法
- 更新 RecordUsageInput 结构体支持传入 UserAgent
- 更新 Claude/OpenAI/Gemini 三个网关 Handler 传递 UserAgent

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 22:49:46 +08:00
LLLLLLiulei
2b528c5f81 feat: auto-pause expired accounts 2026-01-07 16:59:35 +08:00
Xu Kang
f6dd4752e7 fix: 修复 Go 版本、包管理器和技术栈文档 (#195)
- backend/Dockerfile: Go 版本从 1.21 更新到 1.25.5(与 go.mod 一致)

- Makefile: 使用 pnpm 替代 npm(与 pnpm-lock.yaml 和 CI 一致)

- README.md/README_CN.md: 技术栈从 GORM 修正为 Ent
2026-01-07 16:35:51 +08:00
Xu Kang
b19c7875a4 fix(i18n): use correct translation key for dashboard redeem code description (#194)
Changed dashboard.addBalance to dashboard.addBalanceWithCode to match the existing translation key in locale files.
2026-01-07 15:01:07 +08:00
shaw
d99a3ef14b fix(gateway): 修复账号跨分组调度问题
问题:账号可能被调度到未分配的分组(如 simon 账号被调度到 claude_default)

根因:
- 强制平台模式下分组查询失败时回退到全平台查询
- listSchedulableAccounts 中分组为空时回退到无分组查询
- 粘性会话只检查平台匹配,未校验账号分组归属

修复:
- 移除强制平台模式的回退逻辑,分组内无账号时返回错误
- 移除 listSchedulableAccounts 的回退逻辑
- 新增 isAccountInGroup 方法用于分组校验
- 在三处粘性会话检查中增加分组归属验证
2026-01-07 10:56:52 +08:00
shaw
fc8fa83fcc fix(keys): 修复代码框第一行多余空格问题
pre 标签会原样保留内部空白字符,导致 code 标签前的模板缩进
被渲染为实际空格。将 pre/code 标签写在同一行消除此问题。
2026-01-07 10:26:24 +08:00
shaw
6dcd99468b fix(gateway): 修复 cache_control 块超限问题并优化 Claude Code 检测
问题:
- OAuth/SetupToken 账号注入 system prompt 后可能导致 cache_control
  块超过 Anthropic API 的 4 个限制
- Claude Code 检测使用精确匹配,无法识别 Agent SDK 等变体

修复:
- 新增 enforceCacheControlLimit 函数,强制执行 4 个块限制
- 优先从 messages 移除,再从 system 尾部移除(保护注入的 prompt)
- 改用前缀匹配检测 Claude Code 系统提示词,支持多种变体:
  - 标准版、Agent SDK 版、Explore Agent 版、Compact 版
2026-01-07 10:17:09 +08:00
shaw
d5ba7b80d3 fix(admin/usage): 恢复成本 Tooltip 明细并优化账号筛选
问题修复:
- 恢复 Cost Tooltip 的成本分项明细 (input_cost, output_cost, cache 成本)
- 修复 Token Tooltip 双分隔线显示问题
- 修复 Tooltip 翻译键缺失问题,新增 costDetails/tokenDetails
- 恢复 Excel 导出格式化 (aoa_to_sheet + 翻译列头)

功能优化:
- 账号筛选从前端搜索改为后端搜索,避免一次加载 1000 条数据
- 行为与用户/API Key 筛选保持一致 (debounce + 后端分页)
2026-01-07 09:35:21 +08:00
shaw
a3b81ef7bc fix(test): 补充 stubUsageLogRepo 缺失的 GetStatsWithFilters 方法 2026-01-06 22:35:22 +08:00
shaw
015974a27e feat(admin/usage): 优化管理员用量页面功能和体验
后端改进:
- 新增 GetStatsWithFilters 方法支持完整筛选条件
- Stats 端点支持 account_id, group_id, model, stream, billing_type 参数
- 统一使用 filters 结构体,移除冗余的分支逻辑

前端改进:
- 统计卡片添加"所选范围内"文字提示
- 优化总消费显示格式,清晰展示实际费用和标准计费
- Token 和费用列添加问号图标 tooltip 显示详细信息
- API Key 搜索框体验优化:点击即显示下拉选项
- 选择用户后自动加载该用户的所有 API Key
2026-01-06 22:19:07 +08:00
程序猿MT
4cf756ebe6 Merge branch 'Wei-Shaw:main' into main 2026-01-06 20:33:30 +08:00
yangjianbo
823497a2af fix(并发): 修复 wrapReleaseOnDone goroutine 泄露问题
问题描述:
- wrapReleaseOnDone 函数创建的 goroutine 会持续等待 ctx.Done()
- 即使 release() 已被调用,goroutine 仍不会退出
- 高并发场景下(1000 req/s)会产生 3000+ 个泄露 goroutine

修复方案:
- 添加 quit channel 作为退出信号
- 正常释放时 close(quit) 通知 goroutine 立即退出
- 使用 select 监听 ctx.Done() 和 quit 两个信号
- 确保 goroutine 在正常流程中及时退出

测试覆盖:
- 新增 5 个单元测试验证修复效果
- 验证 goroutine 不泄露
- 验证并发安全性和多次调用保护
- 性能影响:471.9 ns/op, 208 B/op

影响范围:
- gateway_handler.go: 每请求调用 2-4 次
- openai_gateway_handler.go: 每请求调用 2-3 次
- 修复后 goroutine 泄露数量从 3/req 降至 0

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-06 20:31:40 +08:00
yangjianbo
66fe484f0d chore: 删除依赖安全文档 2026-01-06 20:26:32 +08:00
shaw
216321aa9e Merge PR #183: 修复账号管理页面过滤器和错误提示问题 2026-01-06 19:49:18 +08:00
yangjianbo
5a52cb608c fix(前端): 修复账号管理页面平台过滤不生效的问题
添加 @update:filters 事件监听,使过滤器参数能正确同步到数据请求中。
修复了平台、类型、状态三个过滤器全部失效的问题。

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-06 19:20:05 +08:00
yangjianbo
1181b332f7 fix(前端): 修复编辑账号错误提示无法显示具体原因的问题
后端 API 返回 message 字段,但前端读取 detail 字段,导致无法显示具体错误信息。
现在优先读取 message 字段,兼容 detail 字段。

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-06 15:46:36 +08:00
yangjianbo
58b1777198 fix(ci): 修复 frontend-security job 中的 pnpm 安装顺序问题
**问题描述:**
GitHub Actions 在 frontend-security job 中报错:
"Error: Unable to locate executable file: pnpm"

**根本原因:**
setup-node@v4 在尝试使用 pnpm cache 时,pnpm 还未安装

**解决方案:**
1. 调整步骤顺序:先安装 pnpm,再设置 Node.js
2. 升级 pnpm/action-setup 从 v2 到 v4
3. 明确指定 pnpm version: 9

**修改内容:**
- 将 "Set up pnpm" 步骤移到 "Set up Node.js" 之前
- 更新 pnpm/action-setup@v2 → pnpm/action-setup@v4
- 添加 version: 9 配置

**正确的步骤顺序:**
1. Checkout 代码
2. Set up pnpm (指定版本)
3. Set up Node.js (可以使用 pnpm cache)
4. Install dependencies

相关 Issue: #174

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-06 13:42:47 +08:00
yangjianbo
0c7a58fcc7 fix(配置): 修改 URL 安全配置默认值为开发友好模式
调整以下配置的默认值以匹配 .env.example:
- allow_insecure_http: false → true (允许 HTTP URL)
- allow_private_hosts: false → true (允许本地/私有 IP)

**改动说明:**
- 默认允许 HTTP URL,方便开发测试环境使用
- 默认允许本地和私有 IP 地址
- 与 deploy/.env.example 中的推荐配置保持一致
- 更新相应的单元测试以验证新的默认值

**安全提示:**
⚠️ 这些默认值适合开发/测试环境
⚠️ 生产环境建议显式配置更严格的安全策略
⚠️ HTTP 存在明文传输风险,仅在可信网络中使用

**测试结果:**
-  所有单元测试通过
-  golangci-lint 无问题

相关文件:
- backend/internal/config/config.go:451-452
- backend/internal/config/config_test.go:83-88

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-06 12:56:29 +08:00
yangjianbo
17ae51c0a0 merge: 合并远程分支并修复代码冲突
合并了远程分支 cb72262 的功能更新,同时保留了 ESLint 修复:

**冲突解决详情:**

1. AccountTableFilters.vue
   -  保留 emit 模式修复(避免 vue/no-mutating-props 错误)
   -  添加第三个筛选器 type(账户类型)
   -  新增 antigravity 平台和 inactive 状态选项

2. UserBalanceModal.vue
   -  保留 console.error 错误日志
   -  添加输入验证(金额校验、余额不足检查)
   -  使用 appStore.showError 向用户显示友好错误

3. AccountsView.vue
   -  保留所有 console.error 错误日志(避免 no-empty 错误)
   -  使用新 API:clearRateLimit 和 setSchedulable

4. UsageView.vue
   -  添加 console.error 错误日志
   -  添加图表功能(模型分布、使用趋势)
   -  添加粒度选择(按天/按小时)
   -  保留 XLSX 动态导入优化

**测试结果:**
-  Go tests: PASS
-  golangci-lint: 0 issues
-  ESLint: 0 errors
-  TypeScript: PASS

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-06 12:50:51 +08:00
yangjianbo
4790aced15 fix(前端): 修复 ESLint 代码规范问题
- 修复 AccountTableFilters.vue 中的 vue/no-mutating-props 错误,使用 emit 模式替代直接修改 props
- 修复 TypeScript 类型错误,支持 Select 组件的 null 值类型
- 为所有空 catch 块添加错误日志,提升代码可维护性和调试能力
- 涉及文件:AccountTableFilters.vue, UserAllowedGroupsModal.vue, UserApiKeysModal.vue, UserBalanceModal.vue, AccountsView.vue, UsageView.vue, DashboardView.vue, ProfileView.vue

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-06 12:42:06 +08:00
yangjianbo
3f0017d1f1 fix(安全): 修复依赖漏洞并强化安全扫描
主要改动:
- 固定 Go 1.25.5 与 CI 校验并更新扫描流程
- 升级 quic-go、x/crypto、req 等依赖并通过 govulncheck
- 强化 JWT 校验、TLS 配置与 xlsx 动态加载
- 新增审计豁免清单与校验脚本
2026-01-06 11:36:38 +08:00
shaw
cb72262ad8 Merge PR #166: feat: 图片生成计费功能 2026-01-06 11:29:35 +08:00
song
195e227c04 merge: 合并 upstream/main 并保留本地图片计费功能 2026-01-06 10:49:26 +08:00
Yuhao Jiang
f5603b0780 fix: 修复跨时区用户日期范围查询不准确的问题
问题:当用户时区与服务器时区不同时,日期范围查询使用服务器时区解析,
导致用户看到的数据与预期不符。

修复方案:
- 前端:所有 GET 请求自动携带用户时区参数
- 后端:新增时区辅助函数,所有日期解析和默认日期范围计算都使用用户时区
- 当用户时区为空或无效时,自动回退到服务器时区

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-05 20:43:03 -06:00
shaw
752882a022 fix: Token 统计支持 M 单位并修复 lint 错误
- 用户仪表盘 Token 统计卡片支持 K/M 单位自动切换
- 更新 formatTokensK 工具函数支持百万级显示
- 修复 setup.go 中未检查返回值的 errcheck 错误
2026-01-06 10:13:12 +08:00
shaw
9731b961d0 fix: redeem 页面类型的字段翻译 2026-01-06 09:46:46 +08:00
shaw
2920409404 docs: 更新 Docker 部署文档强调 JWT_SECRET 配置重要性
- docker-compose.yml: 添加注释说明设置固定 JWT_SECRET 可防止容器重启后登录失效
- .env.example: 添加 openssl rand -hex 32 生成安全密钥的命令
2026-01-06 09:44:54 +08:00
shaw
7dbbfc22b6 fix: 移除 release 模式 JWT Secret 必填限制并支持 Docker 数据目录
- 移除 Install() 和 AutoSetupFromEnv() 中 release 模式下 JWT Secret 必填检查
- 移除 config.Validate() 中 release 模式下的 JWT 验证
- 新增 GetDataDir() 函数,自动检测数据目录:DATA_DIR 环境变量 > /app/data > 当前目录
- config.yaml 和 .installed 文件现在写入正确的数据目录
- config.Load() 添加 /app/data 到配置搜索路径

这修复了两个问题:
1. Web Setup Wizard 在 release 模式下无法完成安装
2. Docker 部署时 config.yaml 未被持久化导致每次重启重新初始化
2026-01-06 09:43:56 +08:00
shaw
aaaa68ea7f fix: 修复 CSP 策略阻止 Cloudflare Turnstile 加载的问题
在 script-src 和 frame-src 中添加 challenges.cloudflare.com 域名,
允许 Turnstile 脚本加载和 iframe 渲染。
2026-01-06 09:15:03 +08:00
shaw
af753de481 fix: 修复账号修改代理更新无效的bug 2026-01-06 09:04:01 +08:00
shaw
3956819c78 fix: 数据迁移时长增加到10分钟 2026-01-05 22:24:24 +08:00
song
6fa704d6fc fix: Antigravity 账户刷新 token 500 错误
AccountHandler.Refresh 方法缺少对 Antigravity 平台的处理分支,
导致刷新时错误地走进 Claude 刷新逻辑。
2026-01-05 18:29:37 +08:00
song
0400fcdca4 fix: 更新 API 合同测试,添加 image_count 和 image_size 字段 2026-01-05 17:31:23 +08:00
song
5b1907fe61 fix: 图片计费代码审查问题修复
- isImageGenerationModel 改为精确匹配/前缀匹配,避免误匹配
- 新增 normalizePrice 函数,支持负数清除价格配置
- 更新注释说明 Gemini API 每次请求只生成一张图片
- 添加测试用例验证不会误匹配自定义模型名
2026-01-05 17:14:06 +08:00
song
d4c2b723a5 feat: 图片生成计费功能
- 新增 Group 图片价格配置(image_price_1k/2k/4k)
- BillingService 新增 CalculateImageCost 方法
- AntigravityGatewayService 支持识别图片生成模型并按次计费
- UsageLog 新增 image_count 和 image_size 字段
- 前端分组管理支持配置图片价格(antigravity 和 gemini 平台)
- 图片计费复用通用计费能力(余额检查、扣费、倍率、订阅限额)
2026-01-05 17:07:29 +08:00
400 changed files with 68759 additions and 2197 deletions

16
.github/audit-exceptions.yml vendored Normal file
View File

@@ -0,0 +1,16 @@
version: 1
exceptions:
- package: xlsx
advisory: "GHSA-4r6h-8v6p-xvw6"
severity: high
reason: "Admin export only; switched to dynamic import to reduce exposure (CVE-2023-30533)"
mitigation: "Load only on export; restrict export permissions and data scope"
expires_on: "2026-04-05"
owner: "security@your-domain"
- package: xlsx
advisory: "GHSA-5pgg-2g8v-p4x9"
severity: high
reason: "Admin export only; switched to dynamic import to reduce exposure (CVE-2024-22363)"
mitigation: "Load only on export; restrict export permissions and data scope"
expires_on: "2026-04-05"
owner: "security@your-domain"

View File

@@ -15,8 +15,11 @@ jobs:
- uses: actions/setup-go@v5
with:
go-version-file: backend/go.mod
check-latest: true
check-latest: false
cache: true
- name: Verify Go version
run: |
go version | grep -q 'go1.25.5'
- name: Unit tests
working-directory: backend
run: make test-unit
@@ -31,8 +34,11 @@ jobs:
- uses: actions/setup-go@v5
with:
go-version-file: backend/go.mod
check-latest: true
check-latest: false
cache: true
- name: Verify Go version
run: |
go version | grep -q 'go1.25.5'
- name: golangci-lint
uses: golangci/golangci-lint-action@v9
with:

View File

@@ -109,9 +109,14 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.24'
go-version-file: backend/go.mod
check-latest: false
cache-dependency-path: backend/go.sum
- name: Verify Go version
run: |
go version | grep -q 'go1.25.5'
# Docker setup for GoReleaser
- name: Set up QEMU
uses: docker/setup-qemu-action@v3

62
.github/workflows/security-scan.yml vendored Normal file
View File

@@ -0,0 +1,62 @@
name: Security Scan
on:
push:
pull_request:
schedule:
- cron: '0 3 * * 1'
permissions:
contents: read
jobs:
backend-security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: backend/go.mod
check-latest: false
cache-dependency-path: backend/go.sum
- name: Verify Go version
run: |
go version | grep -q 'go1.25.5'
- name: Run govulncheck
working-directory: backend
run: |
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...
- name: Run gosec
working-directory: backend
run: |
go install github.com/securego/gosec/v2/cmd/gosec@latest
gosec -severity high -confidence high ./...
frontend-security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
cache-dependency-path: frontend/pnpm-lock.yaml
- name: Install dependencies
working-directory: frontend
run: pnpm install --frozen-lockfile
- name: Run pnpm audit
working-directory: frontend
run: |
pnpm audit --prod --audit-level=high --json > audit.json || true
- name: Check audit exceptions
run: |
python tools/check_pnpm_audit_exceptions.py \
--audit frontend/audit.json \
--exceptions .github/audit-exceptions.yml

4
.gitignore vendored
View File

@@ -14,6 +14,9 @@ backend/server
backend/sub2api
backend/main
# Go 测试二进制
*.test
# 测试覆盖率
*.out
coverage.html
@@ -123,3 +126,4 @@ backend/cmd/server/server
deploy/docker-compose.override.yml
.gocache/
vite.config.js
docs/*

View File

@@ -7,8 +7,8 @@
# =============================================================================
ARG NODE_IMAGE=node:24-alpine
ARG GOLANG_IMAGE=golang:1.25-alpine
ARG ALPINE_IMAGE=alpine:3.19
ARG GOLANG_IMAGE=golang:1.25.5-alpine
ARG ALPINE_IMAGE=alpine:3.20
ARG GOPROXY=https://goproxy.cn,direct
ARG GOSUMDB=sum.golang.google.cn

368
Linux DO Connect.md Normal file
View File

@@ -0,0 +1,368 @@
# Linux DO Connect
OAuthOpen Authorization是一个开放的网络授权标准目前最新版本为 OAuth 2.0。我们日常使用的第三方登录(如 Google 账号登录就采用了该标准。OAuth 允许用户授权第三方应用访问存储在其他服务提供商(如 Google上的信息无需在不同平台上重复填写注册信息。用户授权后平台可以直接访问用户的账户信息进行身份验证而用户无需向第三方应用提供密码。
目前系统已实现完整的 OAuth2 授权码code方式鉴权但界面等配套功能还在持续完善中。让我们一起打造一个更完善的共享方案。
## 基本介绍
这是一套标准的 OAuth2 鉴权系统,可以让开发者共享论坛的用户基本信息。
- 可获取字段:
| 参数 | 说明 |
| ----------------- | ------------------------------- |
| `id` | 用户唯一标识(不可变) |
| `username` | 论坛用户名 |
| `name` | 论坛用户昵称(可变) |
| `avatar_template` | 用户头像模板URL支持多种尺寸 |
| `active` | 账号活跃状态 |
| `trust_level` | 信任等级0-4 |
| `silenced` | 禁言状态 |
| `external_ids` | 外部ID关联信息 |
| `api_key` | API访问密钥 |
通过这些信息,公益网站/接口可以实现:
1. 基于 `id` 的服务频率限制
2. 基于 `trust_level` 的服务额度分配
3. 基于用户信息的滥用举报机制
## 相关端点
- Authorize 端点: `https://connect.linux.do/oauth2/authorize`
- Token 端点:`https://connect.linux.do/oauth2/token`
- 用户信息 端点:`https://connect.linux.do/api/user`
## 申请使用
- 访问 [Connect.Linux.Do](https://connect.linux.do/) 申请接入你的应用。
![linuxdoconnect_1](https://wiki.linux.do/_next/image?url=%2Flinuxdoconnect_1.png&w=1080&q=75)
- 点击 **`我的应用接入`** - **`申请新接入`**,填写相关信息。其中 **`回调地址`** 是你的应用接收用户信息的地址。
![linuxdoconnect_2](https://wiki.linux.do/_next/image?url=%2Flinuxdoconnect_2.png&w=1080&q=75)
- 申请成功后,你将获得 **`Client Id`** 和 **`Client Secret`**,这是你应用的唯一身份凭证。
![linuxdoconnect_3](https://wiki.linux.do/_next/image?url=%2Flinuxdoconnect_3.png&w=1080&q=75)
## 接入 Linux Do
JavaScript
```JavaScript
// 安装第三方请求库(或使用原生的 Fetch API本例中使用 axios
// npm install axios
// 通过 OAuth2 获取 Linux Do 用户信息的参考流程
const axios = require('axios');
const readline = require('readline');
// 配置信息(建议通过环境变量配置,避免使用硬编码)
const CLIENT_ID = '你的 Client ID';
const CLIENT_SECRET = '你的 Client Secret';
const REDIRECT_URI = '你的回调地址';
const AUTH_URL = 'https://connect.linux.do/oauth2/authorize';
const TOKEN_URL = 'https://connect.linux.do/oauth2/token';
const USER_INFO_URL = 'https://connect.linux.do/api/user';
// 第一步:生成授权 URL
function getAuthUrl() {
const params = new URLSearchParams({
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
response_type: 'code',
scope: 'user'
});
return `${AUTH_URL}?${params.toString()}`;
}
// 第二步:获取 code 参数
function getCode() {
return new Promise((resolve) => {
// 本例中使用终端输入来模拟流程,仅供本地测试
// 请在实际应用中替换为真实的处理逻辑
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
rl.question('从回调 URL 中提取出 code粘贴到此处并按回车', (answer) => {
rl.close();
resolve(answer.trim());
});
});
}
// 第三步:使用 code 参数获取访问令牌
async function getAccessToken(code) {
try {
const form = new URLSearchParams({
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
code: code,
redirect_uri: REDIRECT_URI,
grant_type: 'authorization_code'
}).toString();
const response = await axios.post(TOKEN_URL, form, {
// 提醒:需正确配置请求头,否则无法正常获取访问令牌
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': 'application/json'
}
});
return response.data;
} catch (error) {
console.error(`获取访问令牌失败:${error.response ? JSON.stringify(error.response.data) : error.message}`);
throw error;
}
}
// 第四步:使用访问令牌获取用户信息
async function getUserInfo(accessToken) {
try {
const response = await axios.get(USER_INFO_URL, {
headers: {
Authorization: `Bearer ${accessToken}`
}
});
return response.data;
} catch (error) {
console.error(`获取用户信息失败:${error.response ? JSON.stringify(error.response.data) : error.message}`);
throw error;
}
}
// 主流程
async function main() {
// 1. 生成授权 URL前端引导用户访问授权页
const authUrl = getAuthUrl();
console.log(`请访问此 URL 授权:${authUrl}
`);
// 2. 用户授权后,从回调 URL 获取 code 参数
const code = await getCode();
try {
// 3. 使用 code 参数获取访问令牌
const tokenData = await getAccessToken(code);
const accessToken = tokenData.access_token;
// 4. 使用访问令牌获取用户信息
if (accessToken) {
const userInfo = await getUserInfo(accessToken);
console.log(`
获取用户信息成功:${JSON.stringify(userInfo, null, 2)}`);
} else {
console.log(`
获取访问令牌失败:${JSON.stringify(tokenData)}`);
}
} catch (error) {
console.error('发生错误:', error);
}
}
```
Python
```python
# 安装第三方请求库,本例中使用 requests
# pip install requests
# 通过 OAuth2 获取 Linux Do 用户信息的参考流程
import requests
import json
# 配置信息(建议通过环境变量配置,避免使用硬编码)
CLIENT_ID = '你的 Client ID'
CLIENT_SECRET = '你的 Client Secret'
REDIRECT_URI = '你的回调地址'
AUTH_URL = 'https://connect.linux.do/oauth2/authorize'
TOKEN_URL = 'https://connect.linux.do/oauth2/token'
USER_INFO_URL = 'https://connect.linux.do/api/user'
# 第一步:生成授权 URL
def get_auth_url():
params = {
'client_id': CLIENT_ID,
'redirect_uri': REDIRECT_URI,
'response_type': 'code',
'scope': 'user'
}
auth_url = f"{AUTH_URL}?{'&'.join(f'{k}={v}' for k, v in params.items())}"
return auth_url
# 第二步:获取 code 参数
def get_code():
# 本例中使用终端输入来模拟流程,仅供本地测试
# 请在实际应用中替换为真实的处理逻辑
return input('从回调 URL 中提取出 code粘贴到此处并按回车').strip()
# 第三步:使用 code 参数获取访问令牌
def get_access_token(code):
try:
data = {
'client_id': CLIENT_ID,
'client_secret': CLIENT_SECRET,
'code': code,
'redirect_uri': REDIRECT_URI,
'grant_type': 'authorization_code'
}
# 提醒:需正确配置请求头,否则无法正常获取访问令牌
headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': 'application/json'
}
response = requests.post(TOKEN_URL, data=data, headers=headers)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f"获取访问令牌失败:{e}")
return None
# 第四步:使用访问令牌获取用户信息
def get_user_info(access_token):
try:
headers = {
'Authorization': f'Bearer {access_token}'
}
response = requests.get(USER_INFO_URL, headers=headers)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f"获取用户信息失败:{e}")
return None
# 主流程
if __name__ == '__main__':
# 1. 生成授权 URL前端引导用户访问授权页
auth_url = get_auth_url()
print(f'请访问此 URL 授权:{auth_url}
')
# 2. 用户授权后,从回调 URL 获取 code 参数
code = get_code()
# 3. 使用 code 参数获取访问令牌
token_data = get_access_token(code)
if token_data:
access_token = token_data.get('access_token')
# 4. 使用访问令牌获取用户信息
if access_token:
user_info = get_user_info(access_token)
if user_info:
print(f"
获取用户信息成功{json.dumps(user_info, indent=2)}")
else:
print("
获取用户信息失败")
else:
print(f"
获取访问令牌失败{json.dumps(token_data, indent=2)}")
else:
print("
获取访问令牌失败")
```
PHP
```php
// 通过 OAuth2 获取 Linux Do 用户信息的参考流程
// 配置信息
$CLIENT_ID = '你的 Client ID';
$CLIENT_SECRET = '你的 Client Secret';
$REDIRECT_URI = '你的回调地址';
$AUTH_URL = 'https://connect.linux.do/oauth2/authorize';
$TOKEN_URL = 'https://connect.linux.do/oauth2/token';
$USER_INFO_URL = 'https://connect.linux.do/api/user';
// 生成授权 URL
function getAuthUrl($clientId, $redirectUri) {
global $AUTH_URL;
return $AUTH_URL . '?' . http_build_query([
'client_id' => $clientId,
'redirect_uri' => $redirectUri,
'response_type' => 'code',
'scope' => 'user'
]);
}
// 使用 code 参数获取用户信息(合并获取令牌和获取用户信息的步骤)
function getUserInfoWithCode($code, $clientId, $clientSecret, $redirectUri) {
global $TOKEN_URL, $USER_INFO_URL;
// 1. 获取访问令牌
$ch = curl_init($TOKEN_URL);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
'client_id' => $clientId,
'client_secret' => $clientSecret,
'code' => $code,
'redirect_uri' => $redirectUri,
'grant_type' => 'authorization_code'
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/x-www-form-urlencoded',
'Accept: application/json'
]);
$tokenResponse = curl_exec($ch);
curl_close($ch);
$tokenData = json_decode($tokenResponse, true);
if (!isset($tokenData['access_token'])) {
return ['error' => '获取访问令牌失败', 'details' => $tokenData];
}
// 2. 获取用户信息
$ch = curl_init($USER_INFO_URL);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $tokenData['access_token']
]);
$userResponse = curl_exec($ch);
curl_close($ch);
return json_decode($userResponse, true);
}
// 主流程
// 1. 生成授权 URL
$authUrl = getAuthUrl($CLIENT_ID, $REDIRECT_URI);
echo "<a href='$authUrl'>使用 Linux Do 登录</a>";
// 2. 处理回调并获取用户信息
if (isset($_GET['code'])) {
$userInfo = getUserInfoWithCode(
$_GET['code'],
$CLIENT_ID,
$CLIENT_SECRET,
$REDIRECT_URI
);
if (isset($userInfo['error'])) {
echo '错误: ' . $userInfo['error'];
} else {
echo '欢迎, ' . $userInfo['name'] . '!';
// 处理用户登录逻辑...
}
}
```
## 使用说明
### 授权流程
1. 用户点击应用中的’使用 Linux Do 登录’按钮
2. 系统将用户重定向至 Linux Do 的授权页面
3. 用户完成授权后,系统自动重定向回应用并携带授权码
4. 应用使用授权码获取访问令牌
5. 使用访问令牌获取用户信息
### 安全建议
- 切勿在前端代码中暴露 Client Secret
- 对所有用户输入数据进行严格验证
- 确保使用 HTTPS 协议传输数据
- 定期更新并妥善保管 Client Secret

View File

@@ -9,7 +9,7 @@ build-backend:
# 编译前端(需要已安装依赖)
build-frontend:
@npm --prefix frontend run build
@pnpm --dir frontend run build
# 运行测试(后端 + 前端)
test: test-backend test-frontend
@@ -18,5 +18,5 @@ test-backend:
@$(MAKE) -C backend test
test-frontend:
@npm --prefix frontend run lint:check
@npm --prefix frontend run typecheck
@pnpm --dir frontend run lint:check
@pnpm --dir frontend run typecheck

View File

@@ -2,7 +2,7 @@
<div align="center">
[![Go](https://img.shields.io/badge/Go-1.21+-00ADD8.svg)](https://golang.org/)
[![Go](https://img.shields.io/badge/Go-1.25.5-00ADD8.svg)](https://golang.org/)
[![Vue](https://img.shields.io/badge/Vue-3.4+-4FC08D.svg)](https://vuejs.org/)
[![PostgreSQL](https://img.shields.io/badge/PostgreSQL-15+-336791.svg)](https://www.postgresql.org/)
[![Redis](https://img.shields.io/badge/Redis-7+-DC382D.svg)](https://redis.io/)
@@ -44,13 +44,19 @@ Sub2API is an AI API gateway platform designed to distribute and manage API quot
| Component | Technology |
|-----------|------------|
| Backend | Go 1.21+, Gin, GORM |
| Backend | Go 1.25.5, Gin, Ent |
| Frontend | Vue 3.4+, Vite 5+, TailwindCSS |
| Database | PostgreSQL 15+ |
| Cache/Queue | Redis 7+ |
---
## Documentation
- Dependency Security: `docs/dependency-security.md`
---
## Deployment
### Method 1: Script Installation (Recommended)
@@ -160,6 +166,22 @@ ADMIN_PASSWORD=your_admin_password
# Optional: Custom port
SERVER_PORT=8080
# Optional: Security configuration
# Enable URL allowlist validation (false to skip allowlist checks, only basic format validation)
SECURITY_URL_ALLOWLIST_ENABLED=false
# Allow insecure HTTP URLs when allowlist is disabled (default: false, requires https)
# ⚠️ WARNING: Enabling this allows HTTP (plaintext) URLs which can expose API keys
# Only recommended for:
# - Development/testing environments
# - Internal networks with trusted endpoints
# - When using local test servers (http://localhost)
# PRODUCTION: Keep this false or use HTTPS URLs only
SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP=false
# Allow private IP addresses for upstream/pricing/CRS (for internal deployments)
SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS=false
```
```bash
@@ -276,13 +298,48 @@ Additional security-related options are available in `config.yaml`:
- `cors.allowed_origins` for CORS allowlist
- `security.url_allowlist` for upstream/pricing/CRS host allowlists
- `security.url_allowlist.enabled` to disable URL validation (use with caution)
- `security.url_allowlist.allow_insecure_http` to allow http URLs when validation is disabled
- `security.url_allowlist.allow_insecure_http` to allow HTTP URLs when validation is disabled
- `security.url_allowlist.allow_private_hosts` to allow private/local IP addresses
- `security.response_headers.enabled` to enable configurable response header filtering (disabled uses default allowlist)
- `security.csp` to control Content-Security-Policy headers
- `billing.circuit_breaker` to fail closed on billing errors
- `server.trusted_proxies` to enable X-Forwarded-For parsing
- `turnstile.required` to require Turnstile in release mode
**⚠️ Security Warning: HTTP URL Configuration**
When `security.url_allowlist.enabled=false`, the system performs minimal URL validation by default, **rejecting HTTP URLs** and only allowing HTTPS. To allow HTTP URLs (e.g., for development or internal testing), you must explicitly set:
```yaml
security:
url_allowlist:
enabled: false # Disable allowlist checks
allow_insecure_http: true # Allow HTTP URLs (⚠️ INSECURE)
```
**Or via environment variable:**
```bash
SECURITY_URL_ALLOWLIST_ENABLED=false
SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP=true
```
**Risks of allowing HTTP:**
- API keys and data transmitted in **plaintext** (vulnerable to interception)
- Susceptible to **man-in-the-middle (MITM) attacks**
- **NOT suitable for production** environments
**When to use HTTP:**
- ✅ Development/testing with local servers (http://localhost)
- ✅ Internal networks with trusted endpoints
- ✅ Testing account connectivity before obtaining HTTPS
- ❌ Production environments (use HTTPS only)
**Example error without this setting:**
```
Invalid base URL: invalid url scheme: http
```
If you disable URL validation or response header filtering, harden your network layer:
- Enforce an egress allowlist for upstream domains/IPs
- Block private/loopback/link-local ranges

View File

@@ -2,7 +2,7 @@
<div align="center">
[![Go](https://img.shields.io/badge/Go-1.21+-00ADD8.svg)](https://golang.org/)
[![Go](https://img.shields.io/badge/Go-1.25.5-00ADD8.svg)](https://golang.org/)
[![Vue](https://img.shields.io/badge/Vue-3.4+-4FC08D.svg)](https://vuejs.org/)
[![PostgreSQL](https://img.shields.io/badge/PostgreSQL-15+-336791.svg)](https://www.postgresql.org/)
[![Redis](https://img.shields.io/badge/Redis-7+-DC382D.svg)](https://redis.io/)
@@ -44,13 +44,19 @@ Sub2API 是一个 AI API 网关平台,用于分发和管理 AI 产品订阅(
| 组件 | 技术 |
|------|------|
| 后端 | Go 1.21+, Gin, GORM |
| 后端 | Go 1.25.5, Gin, Ent |
| 前端 | Vue 3.4+, Vite 5+, TailwindCSS |
| 数据库 | PostgreSQL 15+ |
| 缓存/队列 | Redis 7+ |
---
## 文档
- 依赖安全:`docs/dependency-security.md`
---
## 部署方式
### 方式一:脚本安装(推荐)
@@ -160,6 +166,22 @@ ADMIN_PASSWORD=your_admin_password
# 可选:自定义端口
SERVER_PORT=8080
# 可选:安全配置
# 启用 URL 白名单验证false 则跳过白名单检查,仅做基本格式校验)
SECURITY_URL_ALLOWLIST_ENABLED=false
# 关闭白名单时,是否允许 http:// URL默认 false只允许 https://
# ⚠️ 警告:允许 HTTP 会暴露 API 密钥(明文传输)
# 仅建议在以下场景使用:
# - 开发/测试环境
# - 内部可信网络
# - 本地测试服务器http://localhost
# 生产环境:保持 false 或仅使用 HTTPS URL
SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP=false
# 是否允许私有 IP 地址用于上游/定价/CRS内网部署时使用
SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS=false
```
```bash
@@ -276,13 +298,48 @@ default:
- `cors.allowed_origins` 配置 CORS 白名单
- `security.url_allowlist` 配置上游/价格数据/CRS 主机白名单
- `security.url_allowlist.enabled` 可关闭 URL 校验(慎用)
- `security.url_allowlist.allow_insecure_http` 关闭校验时允许 http URL
- `security.url_allowlist.allow_insecure_http` 关闭校验时允许 HTTP URL
- `security.url_allowlist.allow_private_hosts` 允许私有/本地 IP 地址
- `security.response_headers.enabled` 可启用可配置响应头过滤(关闭时使用默认白名单)
- `security.csp` 配置 Content-Security-Policy
- `billing.circuit_breaker` 计费异常时 fail-closed
- `server.trusted_proxies` 启用可信代理解析 X-Forwarded-For
- `turnstile.required` 在 release 模式强制启用 Turnstile
**⚠️ 安全警告HTTP URL 配置**
`security.url_allowlist.enabled=false` 时,系统默认执行最小 URL 校验,**拒绝 HTTP URL**,仅允许 HTTPS。要允许 HTTP URL例如用于开发或内网测试必须显式设置
```yaml
security:
url_allowlist:
enabled: false # 禁用白名单检查
allow_insecure_http: true # 允许 HTTP URL 不安全)
```
**或通过环境变量:**
```bash
SECURITY_URL_ALLOWLIST_ENABLED=false
SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP=true
```
**允许 HTTP 的风险:**
- API 密钥和数据以**明文传输**(可被截获)
- 易受**中间人攻击 (MITM)**
- **不适合生产环境**
**适用场景:**
- ✅ 开发/测试环境的本地服务器http://localhost
- ✅ 内网可信端点
- ✅ 获取 HTTPS 前测试账号连通性
- ❌ 生产环境(仅使用 HTTPS
**未设置此项时的错误示例:**
```
Invalid base URL: invalid url scheme: http
```
如关闭 URL 校验或响应头过滤,请加强网络层防护:
- 出站访问白名单限制上游域名/IP
- 阻断私网/回环/链路本地地址

2
backend/.dockerignore Normal file
View File

@@ -0,0 +1,2 @@
.cache/
.DS_Store

View File

@@ -18,6 +18,12 @@ linters:
list-mode: original
files:
- "**/internal/service/**"
- "!**/internal/service/ops_aggregation_service.go"
- "!**/internal/service/ops_alert_evaluator_service.go"
- "!**/internal/service/ops_cleanup_service.go"
- "!**/internal/service/ops_metrics_collector.go"
- "!**/internal/service/ops_scheduled_report_service.go"
- "!**/internal/service/wire.go"
deny:
- pkg: github.com/Wei-Shaw/sub2api/internal/repository
desc: "service must not import repository"

View File

@@ -1,4 +1,4 @@
FROM golang:1.21-alpine
FROM golang:1.25.5-alpine
WORKDIR /app

View File

@@ -33,7 +33,7 @@ func main() {
}()
userRepo := repository.NewUserRepository(client, sqlDB)
authService := service.NewAuthService(userRepo, cfg, nil, nil, nil, nil)
authService := service.NewAuthService(userRepo, cfg, nil, nil, nil, nil, nil)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

View File

@@ -1 +1 @@
0.1.1
0.1.46

View File

@@ -62,7 +62,14 @@ func provideServiceBuildInfo(buildInfo handler.BuildInfo) service.BuildInfo {
func provideCleanup(
entClient *ent.Client,
rdb *redis.Client,
opsMetricsCollector *service.OpsMetricsCollector,
opsAggregation *service.OpsAggregationService,
opsAlertEvaluator *service.OpsAlertEvaluatorService,
opsCleanup *service.OpsCleanupService,
opsScheduledReport *service.OpsScheduledReportService,
schedulerSnapshot *service.SchedulerSnapshotService,
tokenRefresh *service.TokenRefreshService,
accountExpiry *service.AccountExpiryService,
pricing *service.PricingService,
emailQueue *service.EmailQueueService,
billingCache *service.BillingCacheService,
@@ -80,10 +87,50 @@ func provideCleanup(
name string
fn func() error
}{
{"OpsScheduledReportService", func() error {
if opsScheduledReport != nil {
opsScheduledReport.Stop()
}
return nil
}},
{"OpsCleanupService", func() error {
if opsCleanup != nil {
opsCleanup.Stop()
}
return nil
}},
{"OpsAlertEvaluatorService", func() error {
if opsAlertEvaluator != nil {
opsAlertEvaluator.Stop()
}
return nil
}},
{"OpsAggregationService", func() error {
if opsAggregation != nil {
opsAggregation.Stop()
}
return nil
}},
{"OpsMetricsCollector", func() error {
if opsMetricsCollector != nil {
opsMetricsCollector.Stop()
}
return nil
}},
{"SchedulerSnapshotService", func() error {
if schedulerSnapshot != nil {
schedulerSnapshot.Stop()
}
return nil
}},
{"TokenRefreshService", func() error {
tokenRefresh.Stop()
return nil
}},
{"AccountExpiryService", func() error {
accountExpiry.Stop()
return nil
}},
{"PricingService", func() error {
pricing.Stop()
return nil

View File

@@ -51,33 +51,40 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
turnstileVerifier := repository.NewTurnstileVerifier()
turnstileService := service.NewTurnstileService(settingService, turnstileVerifier)
emailQueueService := service.ProvideEmailQueueService(emailService)
authService := service.NewAuthService(userRepository, configConfig, settingService, emailService, turnstileService, emailQueueService)
userService := service.NewUserService(userRepository)
authHandler := handler.NewAuthHandler(configConfig, authService, userService)
userHandler := handler.NewUserHandler(userService)
promoCodeRepository := repository.NewPromoCodeRepository(client)
billingCache := repository.NewBillingCache(redisClient)
userSubscriptionRepository := repository.NewUserSubscriptionRepository(client)
billingCacheService := service.NewBillingCacheService(billingCache, userRepository, userSubscriptionRepository, configConfig)
apiKeyRepository := repository.NewAPIKeyRepository(client)
groupRepository := repository.NewGroupRepository(client, db)
userSubscriptionRepository := repository.NewUserSubscriptionRepository(client)
apiKeyCache := repository.NewAPIKeyCache(redisClient)
apiKeyService := service.NewAPIKeyService(apiKeyRepository, userRepository, groupRepository, userSubscriptionRepository, apiKeyCache, configConfig)
apiKeyAuthCacheInvalidator := service.ProvideAPIKeyAuthCacheInvalidator(apiKeyService)
promoService := service.NewPromoService(promoCodeRepository, userRepository, billingCacheService, client, apiKeyAuthCacheInvalidator)
authService := service.NewAuthService(userRepository, configConfig, settingService, emailService, turnstileService, emailQueueService, promoService)
userService := service.NewUserService(userRepository, apiKeyAuthCacheInvalidator)
authHandler := handler.NewAuthHandler(configConfig, authService, userService, settingService, promoService)
userHandler := handler.NewUserHandler(userService)
apiKeyHandler := handler.NewAPIKeyHandler(apiKeyService)
usageLogRepository := repository.NewUsageLogRepository(client, db)
usageService := service.NewUsageService(usageLogRepository, userRepository, client)
dashboardAggregationRepository := repository.NewDashboardAggregationRepository(db)
usageService := service.NewUsageService(usageLogRepository, userRepository, client, apiKeyAuthCacheInvalidator)
usageHandler := handler.NewUsageHandler(usageService, apiKeyService)
redeemCodeRepository := repository.NewRedeemCodeRepository(client)
billingCache := repository.NewBillingCache(redisClient)
billingCacheService := service.NewBillingCacheService(billingCache, userRepository, userSubscriptionRepository, configConfig)
subscriptionService := service.NewSubscriptionService(groupRepository, userSubscriptionRepository, billingCacheService)
redeemCache := repository.NewRedeemCache(redisClient)
redeemService := service.NewRedeemService(redeemCodeRepository, userRepository, subscriptionService, redeemCache, billingCacheService, client)
redeemService := service.NewRedeemService(redeemCodeRepository, userRepository, subscriptionService, redeemCache, billingCacheService, client, apiKeyAuthCacheInvalidator)
redeemHandler := handler.NewRedeemHandler(redeemService)
subscriptionHandler := handler.NewSubscriptionHandler(subscriptionService)
dashboardService := service.NewDashboardService(usageLogRepository)
dashboardHandler := admin.NewDashboardHandler(dashboardService)
dashboardStatsCache := repository.NewDashboardCache(redisClient, configConfig)
timingWheelService := service.ProvideTimingWheelService()
dashboardAggregationService := service.ProvideDashboardAggregationService(dashboardAggregationRepository, timingWheelService, configConfig)
dashboardService := service.NewDashboardService(usageLogRepository, dashboardAggregationRepository, dashboardStatsCache, configConfig)
dashboardHandler := admin.NewDashboardHandler(dashboardService, dashboardAggregationService)
accountRepository := repository.NewAccountRepository(client, db)
proxyRepository := repository.NewProxyRepository(client, db)
proxyExitInfoProber := repository.NewProxyExitInfoProber(configConfig)
adminService := service.NewAdminService(userRepository, groupRepository, accountRepository, proxyRepository, apiKeyRepository, redeemCodeRepository, billingCacheService, proxyExitInfoProber)
adminService := service.NewAdminService(userRepository, groupRepository, accountRepository, proxyRepository, apiKeyRepository, redeemCodeRepository, billingCacheService, proxyExitInfoProber, apiKeyAuthCacheInvalidator)
adminUserHandler := admin.NewUserHandler(adminService)
groupHandler := admin.NewGroupHandler(adminService)
claudeOAuthClient := repository.NewClaudeOAuthClient()
@@ -87,9 +94,11 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
geminiOAuthClient := repository.NewGeminiOAuthClient(configConfig)
geminiCliCodeAssistClient := repository.NewGeminiCliCodeAssistClient()
geminiOAuthService := service.NewGeminiOAuthService(proxyRepository, geminiOAuthClient, geminiCliCodeAssistClient, configConfig)
antigravityOAuthService := service.NewAntigravityOAuthService(proxyRepository)
geminiQuotaService := service.NewGeminiQuotaService(configConfig, settingRepository)
tempUnschedCache := repository.NewTempUnschedCache(redisClient)
rateLimitService := service.NewRateLimitService(accountRepository, usageLogRepository, configConfig, geminiQuotaService, tempUnschedCache)
timeoutCounterCache := repository.NewTimeoutCounterCache(redisClient)
rateLimitService := service.ProvideRateLimitService(accountRepository, usageLogRepository, configConfig, geminiQuotaService, tempUnschedCache, timeoutCounterCache, settingService)
claudeUsageFetcher := repository.NewClaudeUsageFetcher()
antigravityQuotaFetcher := service.NewAntigravityQuotaFetcher(proxyRepository)
usageCache := service.NewUsageCache()
@@ -97,24 +106,42 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
geminiTokenCache := repository.NewGeminiTokenCache(redisClient)
geminiTokenProvider := service.NewGeminiTokenProvider(accountRepository, geminiTokenCache, geminiOAuthService)
gatewayCache := repository.NewGatewayCache(redisClient)
antigravityOAuthService := service.NewAntigravityOAuthService(proxyRepository)
antigravityTokenProvider := service.NewAntigravityTokenProvider(accountRepository, geminiTokenCache, antigravityOAuthService)
httpUpstream := repository.NewHTTPUpstream(configConfig)
antigravityGatewayService := service.NewAntigravityGatewayService(accountRepository, gatewayCache, antigravityTokenProvider, rateLimitService, httpUpstream, settingService)
accountTestService := service.NewAccountTestService(accountRepository, geminiTokenProvider, antigravityGatewayService, httpUpstream, configConfig)
concurrencyCache := repository.ProvideConcurrencyCache(redisClient, configConfig)
concurrencyService := service.NewConcurrencyService(concurrencyCache)
concurrencyService := service.ProvideConcurrencyService(concurrencyCache, accountRepository, configConfig)
schedulerCache := repository.NewSchedulerCache(redisClient)
schedulerOutboxRepository := repository.NewSchedulerOutboxRepository(db)
schedulerSnapshotService := service.ProvideSchedulerSnapshotService(schedulerCache, schedulerOutboxRepository, accountRepository, groupRepository, configConfig)
crsSyncService := service.NewCRSSyncService(accountRepository, proxyRepository, oAuthService, openAIOAuthService, geminiOAuthService, configConfig)
accountHandler := admin.NewAccountHandler(adminService, oAuthService, openAIOAuthService, geminiOAuthService, rateLimitService, accountUsageService, accountTestService, concurrencyService, crsSyncService)
accountHandler := admin.NewAccountHandler(adminService, oAuthService, openAIOAuthService, geminiOAuthService, antigravityOAuthService, rateLimitService, accountUsageService, accountTestService, concurrencyService, crsSyncService)
oAuthHandler := admin.NewOAuthHandler(oAuthService)
openAIOAuthHandler := admin.NewOpenAIOAuthHandler(openAIOAuthService, adminService)
geminiOAuthHandler := admin.NewGeminiOAuthHandler(geminiOAuthService)
antigravityOAuthHandler := admin.NewAntigravityOAuthHandler(antigravityOAuthService)
proxyHandler := admin.NewProxyHandler(adminService)
adminRedeemHandler := admin.NewRedeemHandler(adminService)
settingHandler := admin.NewSettingHandler(settingService, emailService, turnstileService)
promoHandler := admin.NewPromoHandler(promoService)
opsRepository := repository.NewOpsRepository(db)
pricingRemoteClient := repository.ProvidePricingRemoteClient(configConfig)
pricingService, err := service.ProvidePricingService(configConfig, pricingRemoteClient)
if err != nil {
return nil, err
}
billingService := service.NewBillingService(configConfig, pricingService)
identityCache := repository.NewIdentityCache(redisClient)
identityService := service.NewIdentityService(identityCache)
deferredService := service.ProvideDeferredService(accountRepository, timingWheelService)
gatewayService := service.NewGatewayService(accountRepository, groupRepository, usageLogRepository, userRepository, userSubscriptionRepository, gatewayCache, configConfig, schedulerSnapshotService, concurrencyService, billingService, rateLimitService, billingCacheService, identityService, httpUpstream, deferredService)
openAIGatewayService := service.NewOpenAIGatewayService(accountRepository, usageLogRepository, userRepository, userSubscriptionRepository, gatewayCache, configConfig, schedulerSnapshotService, concurrencyService, billingService, rateLimitService, billingCacheService, httpUpstream, deferredService)
geminiMessagesCompatService := service.NewGeminiMessagesCompatService(accountRepository, groupRepository, gatewayCache, schedulerSnapshotService, geminiTokenProvider, rateLimitService, httpUpstream, antigravityGatewayService, configConfig)
opsService := service.NewOpsService(opsRepository, settingRepository, configConfig, accountRepository, concurrencyService, gatewayService, openAIGatewayService, geminiMessagesCompatService, antigravityGatewayService)
settingHandler := admin.NewSettingHandler(settingService, emailService, turnstileService, opsService)
opsHandler := admin.NewOpsHandler(opsService)
updateCache := repository.NewUpdateCache(redisClient)
gitHubReleaseClient := repository.NewGitHubReleaseClient()
gitHubReleaseClient := repository.ProvideGitHubReleaseClient(configConfig)
serviceBuildInfo := provideServiceBuildInfo(buildInfo)
updateService := service.ProvideUpdateService(updateCache, gitHubReleaseClient, serviceBuildInfo)
systemHandler := handler.ProvideSystemHandler(updateService)
@@ -124,31 +151,24 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
userAttributeValueRepository := repository.NewUserAttributeValueRepository(client)
userAttributeService := service.NewUserAttributeService(userAttributeDefinitionRepository, userAttributeValueRepository)
userAttributeHandler := admin.NewUserAttributeHandler(userAttributeService)
adminHandlers := handler.ProvideAdminHandlers(dashboardHandler, adminUserHandler, groupHandler, accountHandler, oAuthHandler, openAIOAuthHandler, geminiOAuthHandler, antigravityOAuthHandler, proxyHandler, adminRedeemHandler, settingHandler, systemHandler, adminSubscriptionHandler, adminUsageHandler, userAttributeHandler)
pricingRemoteClient := repository.NewPricingRemoteClient(configConfig)
pricingService, err := service.ProvidePricingService(configConfig, pricingRemoteClient)
if err != nil {
return nil, err
}
billingService := service.NewBillingService(configConfig, pricingService)
identityCache := repository.NewIdentityCache(redisClient)
identityService := service.NewIdentityService(identityCache)
timingWheelService := service.ProvideTimingWheelService()
deferredService := service.ProvideDeferredService(accountRepository, timingWheelService)
gatewayService := service.NewGatewayService(accountRepository, groupRepository, usageLogRepository, userRepository, userSubscriptionRepository, gatewayCache, configConfig, concurrencyService, billingService, rateLimitService, billingCacheService, identityService, httpUpstream, deferredService)
geminiMessagesCompatService := service.NewGeminiMessagesCompatService(accountRepository, groupRepository, gatewayCache, geminiTokenProvider, rateLimitService, httpUpstream, antigravityGatewayService, configConfig)
adminHandlers := handler.ProvideAdminHandlers(dashboardHandler, adminUserHandler, groupHandler, accountHandler, oAuthHandler, openAIOAuthHandler, geminiOAuthHandler, antigravityOAuthHandler, proxyHandler, adminRedeemHandler, promoHandler, settingHandler, opsHandler, systemHandler, adminSubscriptionHandler, adminUsageHandler, userAttributeHandler)
gatewayHandler := handler.NewGatewayHandler(gatewayService, geminiMessagesCompatService, antigravityGatewayService, userService, concurrencyService, billingCacheService, configConfig)
openAIGatewayService := service.NewOpenAIGatewayService(accountRepository, usageLogRepository, userRepository, userSubscriptionRepository, gatewayCache, configConfig, concurrencyService, billingService, rateLimitService, billingCacheService, httpUpstream, deferredService)
openAIGatewayHandler := handler.NewOpenAIGatewayHandler(openAIGatewayService, concurrencyService, billingCacheService, configConfig)
handlerSettingHandler := handler.ProvideSettingHandler(settingService, buildInfo)
handlers := handler.ProvideHandlers(authHandler, userHandler, apiKeyHandler, usageHandler, redeemHandler, subscriptionHandler, adminHandlers, gatewayHandler, openAIGatewayHandler, handlerSettingHandler)
jwtAuthMiddleware := middleware.NewJWTAuthMiddleware(authService, userService)
adminAuthMiddleware := middleware.NewAdminAuthMiddleware(authService, userService, settingService)
apiKeyAuthMiddleware := middleware.NewAPIKeyAuthMiddleware(apiKeyService, subscriptionService, configConfig)
engine := server.ProvideRouter(configConfig, handlers, jwtAuthMiddleware, adminAuthMiddleware, apiKeyAuthMiddleware, apiKeyService, subscriptionService)
engine := server.ProvideRouter(configConfig, handlers, jwtAuthMiddleware, adminAuthMiddleware, apiKeyAuthMiddleware, apiKeyService, subscriptionService, opsService, settingService, redisClient)
httpServer := server.ProvideHTTPServer(configConfig, engine)
opsMetricsCollector := service.ProvideOpsMetricsCollector(opsRepository, settingRepository, accountRepository, concurrencyService, db, redisClient, configConfig)
opsAggregationService := service.ProvideOpsAggregationService(opsRepository, settingRepository, db, redisClient, configConfig)
opsAlertEvaluatorService := service.ProvideOpsAlertEvaluatorService(opsService, opsRepository, emailService, redisClient, configConfig)
opsCleanupService := service.ProvideOpsCleanupService(opsRepository, db, redisClient, configConfig)
opsScheduledReportService := service.ProvideOpsScheduledReportService(opsService, userService, emailService, redisClient, configConfig)
tokenRefreshService := service.ProvideTokenRefreshService(accountRepository, oAuthService, openAIOAuthService, geminiOAuthService, antigravityOAuthService, configConfig)
v := provideCleanup(client, redisClient, tokenRefreshService, pricingService, emailQueueService, billingCacheService, oAuthService, openAIOAuthService, geminiOAuthService, antigravityOAuthService)
accountExpiryService := service.ProvideAccountExpiryService(accountRepository)
v := provideCleanup(client, redisClient, opsMetricsCollector, opsAggregationService, opsAlertEvaluatorService, opsCleanupService, opsScheduledReportService, schedulerSnapshotService, tokenRefreshService, accountExpiryService, pricingService, emailQueueService, billingCacheService, oAuthService, openAIOAuthService, geminiOAuthService, antigravityOAuthService)
application := &Application{
Server: httpServer,
Cleanup: v,
@@ -173,7 +193,14 @@ func provideServiceBuildInfo(buildInfo handler.BuildInfo) service.BuildInfo {
func provideCleanup(
entClient *ent.Client,
rdb *redis.Client,
opsMetricsCollector *service.OpsMetricsCollector,
opsAggregation *service.OpsAggregationService,
opsAlertEvaluator *service.OpsAlertEvaluatorService,
opsCleanup *service.OpsCleanupService,
opsScheduledReport *service.OpsScheduledReportService,
schedulerSnapshot *service.SchedulerSnapshotService,
tokenRefresh *service.TokenRefreshService,
accountExpiry *service.AccountExpiryService,
pricing *service.PricingService,
emailQueue *service.EmailQueueService,
billingCache *service.BillingCacheService,
@@ -190,10 +217,50 @@ func provideCleanup(
name string
fn func() error
}{
{"OpsScheduledReportService", func() error {
if opsScheduledReport != nil {
opsScheduledReport.Stop()
}
return nil
}},
{"OpsCleanupService", func() error {
if opsCleanup != nil {
opsCleanup.Stop()
}
return nil
}},
{"OpsAlertEvaluatorService", func() error {
if opsAlertEvaluator != nil {
opsAlertEvaluator.Stop()
}
return nil
}},
{"OpsAggregationService", func() error {
if opsAggregation != nil {
opsAggregation.Stop()
}
return nil
}},
{"OpsMetricsCollector", func() error {
if opsMetricsCollector != nil {
opsMetricsCollector.Stop()
}
return nil
}},
{"SchedulerSnapshotService", func() error {
if schedulerSnapshot != nil {
schedulerSnapshot.Stop()
}
return nil
}},
{"TokenRefreshService", func() error {
tokenRefresh.Stop()
return nil
}},
{"AccountExpiryService", func() error {
accountExpiry.Stop()
return nil
}},
{"PricingService", func() error {
pricing.Stop()
return nil

View File

@@ -49,6 +49,10 @@ type Account struct {
ErrorMessage *string `json:"error_message,omitempty"`
// LastUsedAt holds the value of the "last_used_at" field.
LastUsedAt *time.Time `json:"last_used_at,omitempty"`
// Account expiration time (NULL means no expiration).
ExpiresAt *time.Time `json:"expires_at,omitempty"`
// Auto pause scheduling when account expires.
AutoPauseOnExpired bool `json:"auto_pause_on_expired,omitempty"`
// Schedulable holds the value of the "schedulable" field.
Schedulable bool `json:"schedulable,omitempty"`
// RateLimitedAt holds the value of the "rate_limited_at" field.
@@ -129,13 +133,13 @@ func (*Account) scanValues(columns []string) ([]any, error) {
switch columns[i] {
case account.FieldCredentials, account.FieldExtra:
values[i] = new([]byte)
case account.FieldSchedulable:
case account.FieldAutoPauseOnExpired, account.FieldSchedulable:
values[i] = new(sql.NullBool)
case account.FieldID, account.FieldProxyID, account.FieldConcurrency, account.FieldPriority:
values[i] = new(sql.NullInt64)
case account.FieldName, account.FieldNotes, account.FieldPlatform, account.FieldType, account.FieldStatus, account.FieldErrorMessage, account.FieldSessionWindowStatus:
values[i] = new(sql.NullString)
case account.FieldCreatedAt, account.FieldUpdatedAt, account.FieldDeletedAt, account.FieldLastUsedAt, account.FieldRateLimitedAt, account.FieldRateLimitResetAt, account.FieldOverloadUntil, account.FieldSessionWindowStart, account.FieldSessionWindowEnd:
case account.FieldCreatedAt, account.FieldUpdatedAt, account.FieldDeletedAt, account.FieldLastUsedAt, account.FieldExpiresAt, account.FieldRateLimitedAt, account.FieldRateLimitResetAt, account.FieldOverloadUntil, account.FieldSessionWindowStart, account.FieldSessionWindowEnd:
values[i] = new(sql.NullTime)
default:
values[i] = new(sql.UnknownType)
@@ -257,6 +261,19 @@ func (_m *Account) assignValues(columns []string, values []any) error {
_m.LastUsedAt = new(time.Time)
*_m.LastUsedAt = value.Time
}
case account.FieldExpiresAt:
if value, ok := values[i].(*sql.NullTime); !ok {
return fmt.Errorf("unexpected type %T for field expires_at", values[i])
} else if value.Valid {
_m.ExpiresAt = new(time.Time)
*_m.ExpiresAt = value.Time
}
case account.FieldAutoPauseOnExpired:
if value, ok := values[i].(*sql.NullBool); !ok {
return fmt.Errorf("unexpected type %T for field auto_pause_on_expired", values[i])
} else if value.Valid {
_m.AutoPauseOnExpired = value.Bool
}
case account.FieldSchedulable:
if value, ok := values[i].(*sql.NullBool); !ok {
return fmt.Errorf("unexpected type %T for field schedulable", values[i])
@@ -416,6 +433,14 @@ func (_m *Account) String() string {
builder.WriteString(v.Format(time.ANSIC))
}
builder.WriteString(", ")
if v := _m.ExpiresAt; v != nil {
builder.WriteString("expires_at=")
builder.WriteString(v.Format(time.ANSIC))
}
builder.WriteString(", ")
builder.WriteString("auto_pause_on_expired=")
builder.WriteString(fmt.Sprintf("%v", _m.AutoPauseOnExpired))
builder.WriteString(", ")
builder.WriteString("schedulable=")
builder.WriteString(fmt.Sprintf("%v", _m.Schedulable))
builder.WriteString(", ")

View File

@@ -45,6 +45,10 @@ const (
FieldErrorMessage = "error_message"
// FieldLastUsedAt holds the string denoting the last_used_at field in the database.
FieldLastUsedAt = "last_used_at"
// FieldExpiresAt holds the string denoting the expires_at field in the database.
FieldExpiresAt = "expires_at"
// FieldAutoPauseOnExpired holds the string denoting the auto_pause_on_expired field in the database.
FieldAutoPauseOnExpired = "auto_pause_on_expired"
// FieldSchedulable holds the string denoting the schedulable field in the database.
FieldSchedulable = "schedulable"
// FieldRateLimitedAt holds the string denoting the rate_limited_at field in the database.
@@ -115,6 +119,8 @@ var Columns = []string{
FieldStatus,
FieldErrorMessage,
FieldLastUsedAt,
FieldExpiresAt,
FieldAutoPauseOnExpired,
FieldSchedulable,
FieldRateLimitedAt,
FieldRateLimitResetAt,
@@ -172,6 +178,8 @@ var (
DefaultStatus string
// StatusValidator is a validator for the "status" field. It is called by the builders before save.
StatusValidator func(string) error
// DefaultAutoPauseOnExpired holds the default value on creation for the "auto_pause_on_expired" field.
DefaultAutoPauseOnExpired bool
// DefaultSchedulable holds the default value on creation for the "schedulable" field.
DefaultSchedulable bool
// SessionWindowStatusValidator is a validator for the "session_window_status" field. It is called by the builders before save.
@@ -251,6 +259,16 @@ func ByLastUsedAt(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldLastUsedAt, opts...).ToFunc()
}
// ByExpiresAt orders the results by the expires_at field.
func ByExpiresAt(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldExpiresAt, opts...).ToFunc()
}
// ByAutoPauseOnExpired orders the results by the auto_pause_on_expired field.
func ByAutoPauseOnExpired(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldAutoPauseOnExpired, opts...).ToFunc()
}
// BySchedulable orders the results by the schedulable field.
func BySchedulable(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldSchedulable, opts...).ToFunc()

View File

@@ -120,6 +120,16 @@ func LastUsedAt(v time.Time) predicate.Account {
return predicate.Account(sql.FieldEQ(FieldLastUsedAt, v))
}
// ExpiresAt applies equality check predicate on the "expires_at" field. It's identical to ExpiresAtEQ.
func ExpiresAt(v time.Time) predicate.Account {
return predicate.Account(sql.FieldEQ(FieldExpiresAt, v))
}
// AutoPauseOnExpired applies equality check predicate on the "auto_pause_on_expired" field. It's identical to AutoPauseOnExpiredEQ.
func AutoPauseOnExpired(v bool) predicate.Account {
return predicate.Account(sql.FieldEQ(FieldAutoPauseOnExpired, v))
}
// Schedulable applies equality check predicate on the "schedulable" field. It's identical to SchedulableEQ.
func Schedulable(v bool) predicate.Account {
return predicate.Account(sql.FieldEQ(FieldSchedulable, v))
@@ -855,6 +865,66 @@ func LastUsedAtNotNil() predicate.Account {
return predicate.Account(sql.FieldNotNull(FieldLastUsedAt))
}
// ExpiresAtEQ applies the EQ predicate on the "expires_at" field.
func ExpiresAtEQ(v time.Time) predicate.Account {
return predicate.Account(sql.FieldEQ(FieldExpiresAt, v))
}
// ExpiresAtNEQ applies the NEQ predicate on the "expires_at" field.
func ExpiresAtNEQ(v time.Time) predicate.Account {
return predicate.Account(sql.FieldNEQ(FieldExpiresAt, v))
}
// ExpiresAtIn applies the In predicate on the "expires_at" field.
func ExpiresAtIn(vs ...time.Time) predicate.Account {
return predicate.Account(sql.FieldIn(FieldExpiresAt, vs...))
}
// ExpiresAtNotIn applies the NotIn predicate on the "expires_at" field.
func ExpiresAtNotIn(vs ...time.Time) predicate.Account {
return predicate.Account(sql.FieldNotIn(FieldExpiresAt, vs...))
}
// ExpiresAtGT applies the GT predicate on the "expires_at" field.
func ExpiresAtGT(v time.Time) predicate.Account {
return predicate.Account(sql.FieldGT(FieldExpiresAt, v))
}
// ExpiresAtGTE applies the GTE predicate on the "expires_at" field.
func ExpiresAtGTE(v time.Time) predicate.Account {
return predicate.Account(sql.FieldGTE(FieldExpiresAt, v))
}
// ExpiresAtLT applies the LT predicate on the "expires_at" field.
func ExpiresAtLT(v time.Time) predicate.Account {
return predicate.Account(sql.FieldLT(FieldExpiresAt, v))
}
// ExpiresAtLTE applies the LTE predicate on the "expires_at" field.
func ExpiresAtLTE(v time.Time) predicate.Account {
return predicate.Account(sql.FieldLTE(FieldExpiresAt, v))
}
// ExpiresAtIsNil applies the IsNil predicate on the "expires_at" field.
func ExpiresAtIsNil() predicate.Account {
return predicate.Account(sql.FieldIsNull(FieldExpiresAt))
}
// ExpiresAtNotNil applies the NotNil predicate on the "expires_at" field.
func ExpiresAtNotNil() predicate.Account {
return predicate.Account(sql.FieldNotNull(FieldExpiresAt))
}
// AutoPauseOnExpiredEQ applies the EQ predicate on the "auto_pause_on_expired" field.
func AutoPauseOnExpiredEQ(v bool) predicate.Account {
return predicate.Account(sql.FieldEQ(FieldAutoPauseOnExpired, v))
}
// AutoPauseOnExpiredNEQ applies the NEQ predicate on the "auto_pause_on_expired" field.
func AutoPauseOnExpiredNEQ(v bool) predicate.Account {
return predicate.Account(sql.FieldNEQ(FieldAutoPauseOnExpired, v))
}
// SchedulableEQ applies the EQ predicate on the "schedulable" field.
func SchedulableEQ(v bool) predicate.Account {
return predicate.Account(sql.FieldEQ(FieldSchedulable, v))

View File

@@ -195,6 +195,34 @@ func (_c *AccountCreate) SetNillableLastUsedAt(v *time.Time) *AccountCreate {
return _c
}
// SetExpiresAt sets the "expires_at" field.
func (_c *AccountCreate) SetExpiresAt(v time.Time) *AccountCreate {
_c.mutation.SetExpiresAt(v)
return _c
}
// SetNillableExpiresAt sets the "expires_at" field if the given value is not nil.
func (_c *AccountCreate) SetNillableExpiresAt(v *time.Time) *AccountCreate {
if v != nil {
_c.SetExpiresAt(*v)
}
return _c
}
// SetAutoPauseOnExpired sets the "auto_pause_on_expired" field.
func (_c *AccountCreate) SetAutoPauseOnExpired(v bool) *AccountCreate {
_c.mutation.SetAutoPauseOnExpired(v)
return _c
}
// SetNillableAutoPauseOnExpired sets the "auto_pause_on_expired" field if the given value is not nil.
func (_c *AccountCreate) SetNillableAutoPauseOnExpired(v *bool) *AccountCreate {
if v != nil {
_c.SetAutoPauseOnExpired(*v)
}
return _c
}
// SetSchedulable sets the "schedulable" field.
func (_c *AccountCreate) SetSchedulable(v bool) *AccountCreate {
_c.mutation.SetSchedulable(v)
@@ -405,6 +433,10 @@ func (_c *AccountCreate) defaults() error {
v := account.DefaultStatus
_c.mutation.SetStatus(v)
}
if _, ok := _c.mutation.AutoPauseOnExpired(); !ok {
v := account.DefaultAutoPauseOnExpired
_c.mutation.SetAutoPauseOnExpired(v)
}
if _, ok := _c.mutation.Schedulable(); !ok {
v := account.DefaultSchedulable
_c.mutation.SetSchedulable(v)
@@ -464,6 +496,9 @@ func (_c *AccountCreate) check() error {
return &ValidationError{Name: "status", err: fmt.Errorf(`ent: validator failed for field "Account.status": %w`, err)}
}
}
if _, ok := _c.mutation.AutoPauseOnExpired(); !ok {
return &ValidationError{Name: "auto_pause_on_expired", err: errors.New(`ent: missing required field "Account.auto_pause_on_expired"`)}
}
if _, ok := _c.mutation.Schedulable(); !ok {
return &ValidationError{Name: "schedulable", err: errors.New(`ent: missing required field "Account.schedulable"`)}
}
@@ -555,6 +590,14 @@ func (_c *AccountCreate) createSpec() (*Account, *sqlgraph.CreateSpec) {
_spec.SetField(account.FieldLastUsedAt, field.TypeTime, value)
_node.LastUsedAt = &value
}
if value, ok := _c.mutation.ExpiresAt(); ok {
_spec.SetField(account.FieldExpiresAt, field.TypeTime, value)
_node.ExpiresAt = &value
}
if value, ok := _c.mutation.AutoPauseOnExpired(); ok {
_spec.SetField(account.FieldAutoPauseOnExpired, field.TypeBool, value)
_node.AutoPauseOnExpired = value
}
if value, ok := _c.mutation.Schedulable(); ok {
_spec.SetField(account.FieldSchedulable, field.TypeBool, value)
_node.Schedulable = value
@@ -898,6 +941,36 @@ func (u *AccountUpsert) ClearLastUsedAt() *AccountUpsert {
return u
}
// SetExpiresAt sets the "expires_at" field.
func (u *AccountUpsert) SetExpiresAt(v time.Time) *AccountUpsert {
u.Set(account.FieldExpiresAt, v)
return u
}
// UpdateExpiresAt sets the "expires_at" field to the value that was provided on create.
func (u *AccountUpsert) UpdateExpiresAt() *AccountUpsert {
u.SetExcluded(account.FieldExpiresAt)
return u
}
// ClearExpiresAt clears the value of the "expires_at" field.
func (u *AccountUpsert) ClearExpiresAt() *AccountUpsert {
u.SetNull(account.FieldExpiresAt)
return u
}
// SetAutoPauseOnExpired sets the "auto_pause_on_expired" field.
func (u *AccountUpsert) SetAutoPauseOnExpired(v bool) *AccountUpsert {
u.Set(account.FieldAutoPauseOnExpired, v)
return u
}
// UpdateAutoPauseOnExpired sets the "auto_pause_on_expired" field to the value that was provided on create.
func (u *AccountUpsert) UpdateAutoPauseOnExpired() *AccountUpsert {
u.SetExcluded(account.FieldAutoPauseOnExpired)
return u
}
// SetSchedulable sets the "schedulable" field.
func (u *AccountUpsert) SetSchedulable(v bool) *AccountUpsert {
u.Set(account.FieldSchedulable, v)
@@ -1308,6 +1381,41 @@ func (u *AccountUpsertOne) ClearLastUsedAt() *AccountUpsertOne {
})
}
// SetExpiresAt sets the "expires_at" field.
func (u *AccountUpsertOne) SetExpiresAt(v time.Time) *AccountUpsertOne {
return u.Update(func(s *AccountUpsert) {
s.SetExpiresAt(v)
})
}
// UpdateExpiresAt sets the "expires_at" field to the value that was provided on create.
func (u *AccountUpsertOne) UpdateExpiresAt() *AccountUpsertOne {
return u.Update(func(s *AccountUpsert) {
s.UpdateExpiresAt()
})
}
// ClearExpiresAt clears the value of the "expires_at" field.
func (u *AccountUpsertOne) ClearExpiresAt() *AccountUpsertOne {
return u.Update(func(s *AccountUpsert) {
s.ClearExpiresAt()
})
}
// SetAutoPauseOnExpired sets the "auto_pause_on_expired" field.
func (u *AccountUpsertOne) SetAutoPauseOnExpired(v bool) *AccountUpsertOne {
return u.Update(func(s *AccountUpsert) {
s.SetAutoPauseOnExpired(v)
})
}
// UpdateAutoPauseOnExpired sets the "auto_pause_on_expired" field to the value that was provided on create.
func (u *AccountUpsertOne) UpdateAutoPauseOnExpired() *AccountUpsertOne {
return u.Update(func(s *AccountUpsert) {
s.UpdateAutoPauseOnExpired()
})
}
// SetSchedulable sets the "schedulable" field.
func (u *AccountUpsertOne) SetSchedulable(v bool) *AccountUpsertOne {
return u.Update(func(s *AccountUpsert) {
@@ -1904,6 +2012,41 @@ func (u *AccountUpsertBulk) ClearLastUsedAt() *AccountUpsertBulk {
})
}
// SetExpiresAt sets the "expires_at" field.
func (u *AccountUpsertBulk) SetExpiresAt(v time.Time) *AccountUpsertBulk {
return u.Update(func(s *AccountUpsert) {
s.SetExpiresAt(v)
})
}
// UpdateExpiresAt sets the "expires_at" field to the value that was provided on create.
func (u *AccountUpsertBulk) UpdateExpiresAt() *AccountUpsertBulk {
return u.Update(func(s *AccountUpsert) {
s.UpdateExpiresAt()
})
}
// ClearExpiresAt clears the value of the "expires_at" field.
func (u *AccountUpsertBulk) ClearExpiresAt() *AccountUpsertBulk {
return u.Update(func(s *AccountUpsert) {
s.ClearExpiresAt()
})
}
// SetAutoPauseOnExpired sets the "auto_pause_on_expired" field.
func (u *AccountUpsertBulk) SetAutoPauseOnExpired(v bool) *AccountUpsertBulk {
return u.Update(func(s *AccountUpsert) {
s.SetAutoPauseOnExpired(v)
})
}
// UpdateAutoPauseOnExpired sets the "auto_pause_on_expired" field to the value that was provided on create.
func (u *AccountUpsertBulk) UpdateAutoPauseOnExpired() *AccountUpsertBulk {
return u.Update(func(s *AccountUpsert) {
s.UpdateAutoPauseOnExpired()
})
}
// SetSchedulable sets the "schedulable" field.
func (u *AccountUpsertBulk) SetSchedulable(v bool) *AccountUpsertBulk {
return u.Update(func(s *AccountUpsert) {

View File

@@ -9,6 +9,7 @@ import (
"math"
"entgo.io/ent"
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field"
@@ -31,6 +32,7 @@ type AccountQuery struct {
withProxy *ProxyQuery
withUsageLogs *UsageLogQuery
withAccountGroups *AccountGroupQuery
modifiers []func(*sql.Selector)
// intermediate query (i.e. traversal path).
sql *sql.Selector
path func(context.Context) (*sql.Selector, error)
@@ -495,6 +497,9 @@ func (_q *AccountQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*Acco
node.Edges.loadedTypes = loadedTypes
return node.assignValues(columns, values)
}
if len(_q.modifiers) > 0 {
_spec.Modifiers = _q.modifiers
}
for i := range hooks {
hooks[i](ctx, _spec)
}
@@ -690,6 +695,9 @@ func (_q *AccountQuery) loadAccountGroups(ctx context.Context, query *AccountGro
func (_q *AccountQuery) sqlCount(ctx context.Context) (int, error) {
_spec := _q.querySpec()
if len(_q.modifiers) > 0 {
_spec.Modifiers = _q.modifiers
}
_spec.Node.Columns = _q.ctx.Fields
if len(_q.ctx.Fields) > 0 {
_spec.Unique = _q.ctx.Unique != nil && *_q.ctx.Unique
@@ -755,6 +763,9 @@ func (_q *AccountQuery) sqlQuery(ctx context.Context) *sql.Selector {
if _q.ctx.Unique != nil && *_q.ctx.Unique {
selector.Distinct()
}
for _, m := range _q.modifiers {
m(selector)
}
for _, p := range _q.predicates {
p(selector)
}
@@ -772,6 +783,32 @@ func (_q *AccountQuery) sqlQuery(ctx context.Context) *sql.Selector {
return selector
}
// ForUpdate locks the selected rows against concurrent updates, and prevent them from being
// updated, deleted or "selected ... for update" by other sessions, until the transaction is
// either committed or rolled-back.
func (_q *AccountQuery) ForUpdate(opts ...sql.LockOption) *AccountQuery {
if _q.driver.Dialect() == dialect.Postgres {
_q.Unique(false)
}
_q.modifiers = append(_q.modifiers, func(s *sql.Selector) {
s.ForUpdate(opts...)
})
return _q
}
// ForShare behaves similarly to ForUpdate, except that it acquires a shared mode lock
// on any rows that are read. Other sessions can read the rows, but cannot modify them
// until your transaction commits.
func (_q *AccountQuery) ForShare(opts ...sql.LockOption) *AccountQuery {
if _q.driver.Dialect() == dialect.Postgres {
_q.Unique(false)
}
_q.modifiers = append(_q.modifiers, func(s *sql.Selector) {
s.ForShare(opts...)
})
return _q
}
// AccountGroupBy is the group-by builder for Account entities.
type AccountGroupBy struct {
selector

View File

@@ -247,6 +247,40 @@ func (_u *AccountUpdate) ClearLastUsedAt() *AccountUpdate {
return _u
}
// SetExpiresAt sets the "expires_at" field.
func (_u *AccountUpdate) SetExpiresAt(v time.Time) *AccountUpdate {
_u.mutation.SetExpiresAt(v)
return _u
}
// SetNillableExpiresAt sets the "expires_at" field if the given value is not nil.
func (_u *AccountUpdate) SetNillableExpiresAt(v *time.Time) *AccountUpdate {
if v != nil {
_u.SetExpiresAt(*v)
}
return _u
}
// ClearExpiresAt clears the value of the "expires_at" field.
func (_u *AccountUpdate) ClearExpiresAt() *AccountUpdate {
_u.mutation.ClearExpiresAt()
return _u
}
// SetAutoPauseOnExpired sets the "auto_pause_on_expired" field.
func (_u *AccountUpdate) SetAutoPauseOnExpired(v bool) *AccountUpdate {
_u.mutation.SetAutoPauseOnExpired(v)
return _u
}
// SetNillableAutoPauseOnExpired sets the "auto_pause_on_expired" field if the given value is not nil.
func (_u *AccountUpdate) SetNillableAutoPauseOnExpired(v *bool) *AccountUpdate {
if v != nil {
_u.SetAutoPauseOnExpired(*v)
}
return _u
}
// SetSchedulable sets the "schedulable" field.
func (_u *AccountUpdate) SetSchedulable(v bool) *AccountUpdate {
_u.mutation.SetSchedulable(v)
@@ -610,6 +644,15 @@ func (_u *AccountUpdate) sqlSave(ctx context.Context) (_node int, err error) {
if _u.mutation.LastUsedAtCleared() {
_spec.ClearField(account.FieldLastUsedAt, field.TypeTime)
}
if value, ok := _u.mutation.ExpiresAt(); ok {
_spec.SetField(account.FieldExpiresAt, field.TypeTime, value)
}
if _u.mutation.ExpiresAtCleared() {
_spec.ClearField(account.FieldExpiresAt, field.TypeTime)
}
if value, ok := _u.mutation.AutoPauseOnExpired(); ok {
_spec.SetField(account.FieldAutoPauseOnExpired, field.TypeBool, value)
}
if value, ok := _u.mutation.Schedulable(); ok {
_spec.SetField(account.FieldSchedulable, field.TypeBool, value)
}
@@ -1016,6 +1059,40 @@ func (_u *AccountUpdateOne) ClearLastUsedAt() *AccountUpdateOne {
return _u
}
// SetExpiresAt sets the "expires_at" field.
func (_u *AccountUpdateOne) SetExpiresAt(v time.Time) *AccountUpdateOne {
_u.mutation.SetExpiresAt(v)
return _u
}
// SetNillableExpiresAt sets the "expires_at" field if the given value is not nil.
func (_u *AccountUpdateOne) SetNillableExpiresAt(v *time.Time) *AccountUpdateOne {
if v != nil {
_u.SetExpiresAt(*v)
}
return _u
}
// ClearExpiresAt clears the value of the "expires_at" field.
func (_u *AccountUpdateOne) ClearExpiresAt() *AccountUpdateOne {
_u.mutation.ClearExpiresAt()
return _u
}
// SetAutoPauseOnExpired sets the "auto_pause_on_expired" field.
func (_u *AccountUpdateOne) SetAutoPauseOnExpired(v bool) *AccountUpdateOne {
_u.mutation.SetAutoPauseOnExpired(v)
return _u
}
// SetNillableAutoPauseOnExpired sets the "auto_pause_on_expired" field if the given value is not nil.
func (_u *AccountUpdateOne) SetNillableAutoPauseOnExpired(v *bool) *AccountUpdateOne {
if v != nil {
_u.SetAutoPauseOnExpired(*v)
}
return _u
}
// SetSchedulable sets the "schedulable" field.
func (_u *AccountUpdateOne) SetSchedulable(v bool) *AccountUpdateOne {
_u.mutation.SetSchedulable(v)
@@ -1409,6 +1486,15 @@ func (_u *AccountUpdateOne) sqlSave(ctx context.Context) (_node *Account, err er
if _u.mutation.LastUsedAtCleared() {
_spec.ClearField(account.FieldLastUsedAt, field.TypeTime)
}
if value, ok := _u.mutation.ExpiresAt(); ok {
_spec.SetField(account.FieldExpiresAt, field.TypeTime, value)
}
if _u.mutation.ExpiresAtCleared() {
_spec.ClearField(account.FieldExpiresAt, field.TypeTime)
}
if value, ok := _u.mutation.AutoPauseOnExpired(); ok {
_spec.SetField(account.FieldAutoPauseOnExpired, field.TypeBool, value)
}
if value, ok := _u.mutation.Schedulable(); ok {
_spec.SetField(account.FieldSchedulable, field.TypeBool, value)
}

View File

@@ -8,6 +8,7 @@ import (
"math"
"entgo.io/ent"
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"github.com/Wei-Shaw/sub2api/ent/account"
@@ -25,6 +26,7 @@ type AccountGroupQuery struct {
predicates []predicate.AccountGroup
withAccount *AccountQuery
withGroup *GroupQuery
modifiers []func(*sql.Selector)
// intermediate query (i.e. traversal path).
sql *sql.Selector
path func(context.Context) (*sql.Selector, error)
@@ -347,6 +349,9 @@ func (_q *AccountGroupQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]
node.Edges.loadedTypes = loadedTypes
return node.assignValues(columns, values)
}
if len(_q.modifiers) > 0 {
_spec.Modifiers = _q.modifiers
}
for i := range hooks {
hooks[i](ctx, _spec)
}
@@ -432,6 +437,9 @@ func (_q *AccountGroupQuery) loadGroup(ctx context.Context, query *GroupQuery, n
func (_q *AccountGroupQuery) sqlCount(ctx context.Context) (int, error) {
_spec := _q.querySpec()
if len(_q.modifiers) > 0 {
_spec.Modifiers = _q.modifiers
}
_spec.Unique = false
_spec.Node.Columns = nil
return sqlgraph.CountNodes(ctx, _q.driver, _spec)
@@ -495,6 +503,9 @@ func (_q *AccountGroupQuery) sqlQuery(ctx context.Context) *sql.Selector {
if _q.ctx.Unique != nil && *_q.ctx.Unique {
selector.Distinct()
}
for _, m := range _q.modifiers {
m(selector)
}
for _, p := range _q.predicates {
p(selector)
}
@@ -512,6 +523,32 @@ func (_q *AccountGroupQuery) sqlQuery(ctx context.Context) *sql.Selector {
return selector
}
// ForUpdate locks the selected rows against concurrent updates, and prevent them from being
// updated, deleted or "selected ... for update" by other sessions, until the transaction is
// either committed or rolled-back.
func (_q *AccountGroupQuery) ForUpdate(opts ...sql.LockOption) *AccountGroupQuery {
if _q.driver.Dialect() == dialect.Postgres {
_q.Unique(false)
}
_q.modifiers = append(_q.modifiers, func(s *sql.Selector) {
s.ForUpdate(opts...)
})
return _q
}
// ForShare behaves similarly to ForUpdate, except that it acquires a shared mode lock
// on any rows that are read. Other sessions can read the rows, but cannot modify them
// until your transaction commits.
func (_q *AccountGroupQuery) ForShare(opts ...sql.LockOption) *AccountGroupQuery {
if _q.driver.Dialect() == dialect.Postgres {
_q.Unique(false)
}
_q.modifiers = append(_q.modifiers, func(s *sql.Selector) {
s.ForShare(opts...)
})
return _q
}
// AccountGroupGroupBy is the group-by builder for AccountGroup entities.
type AccountGroupGroupBy struct {
selector

View File

@@ -3,6 +3,7 @@
package ent
import (
"encoding/json"
"fmt"
"strings"
"time"
@@ -35,6 +36,10 @@ type APIKey struct {
GroupID *int64 `json:"group_id,omitempty"`
// Status holds the value of the "status" field.
Status string `json:"status,omitempty"`
// Allowed IPs/CIDRs, e.g. ["192.168.1.100", "10.0.0.0/8"]
IPWhitelist []string `json:"ip_whitelist,omitempty"`
// Blocked IPs/CIDRs
IPBlacklist []string `json:"ip_blacklist,omitempty"`
// Edges holds the relations/edges for other nodes in the graph.
// The values are being populated by the APIKeyQuery when eager-loading is set.
Edges APIKeyEdges `json:"edges"`
@@ -90,6 +95,8 @@ func (*APIKey) scanValues(columns []string) ([]any, error) {
values := make([]any, len(columns))
for i := range columns {
switch columns[i] {
case apikey.FieldIPWhitelist, apikey.FieldIPBlacklist:
values[i] = new([]byte)
case apikey.FieldID, apikey.FieldUserID, apikey.FieldGroupID:
values[i] = new(sql.NullInt64)
case apikey.FieldKey, apikey.FieldName, apikey.FieldStatus:
@@ -167,6 +174,22 @@ func (_m *APIKey) assignValues(columns []string, values []any) error {
} else if value.Valid {
_m.Status = value.String
}
case apikey.FieldIPWhitelist:
if value, ok := values[i].(*[]byte); !ok {
return fmt.Errorf("unexpected type %T for field ip_whitelist", values[i])
} else if value != nil && len(*value) > 0 {
if err := json.Unmarshal(*value, &_m.IPWhitelist); err != nil {
return fmt.Errorf("unmarshal field ip_whitelist: %w", err)
}
}
case apikey.FieldIPBlacklist:
if value, ok := values[i].(*[]byte); !ok {
return fmt.Errorf("unexpected type %T for field ip_blacklist", values[i])
} else if value != nil && len(*value) > 0 {
if err := json.Unmarshal(*value, &_m.IPBlacklist); err != nil {
return fmt.Errorf("unmarshal field ip_blacklist: %w", err)
}
}
default:
_m.selectValues.Set(columns[i], values[i])
}
@@ -245,6 +268,12 @@ func (_m *APIKey) String() string {
builder.WriteString(", ")
builder.WriteString("status=")
builder.WriteString(_m.Status)
builder.WriteString(", ")
builder.WriteString("ip_whitelist=")
builder.WriteString(fmt.Sprintf("%v", _m.IPWhitelist))
builder.WriteString(", ")
builder.WriteString("ip_blacklist=")
builder.WriteString(fmt.Sprintf("%v", _m.IPBlacklist))
builder.WriteByte(')')
return builder.String()
}

View File

@@ -31,6 +31,10 @@ const (
FieldGroupID = "group_id"
// FieldStatus holds the string denoting the status field in the database.
FieldStatus = "status"
// FieldIPWhitelist holds the string denoting the ip_whitelist field in the database.
FieldIPWhitelist = "ip_whitelist"
// FieldIPBlacklist holds the string denoting the ip_blacklist field in the database.
FieldIPBlacklist = "ip_blacklist"
// EdgeUser holds the string denoting the user edge name in mutations.
EdgeUser = "user"
// EdgeGroup holds the string denoting the group edge name in mutations.
@@ -73,6 +77,8 @@ var Columns = []string{
FieldName,
FieldGroupID,
FieldStatus,
FieldIPWhitelist,
FieldIPBlacklist,
}
// ValidColumn reports if the column name is valid (part of the table columns).

View File

@@ -470,6 +470,26 @@ func StatusContainsFold(v string) predicate.APIKey {
return predicate.APIKey(sql.FieldContainsFold(FieldStatus, v))
}
// IPWhitelistIsNil applies the IsNil predicate on the "ip_whitelist" field.
func IPWhitelistIsNil() predicate.APIKey {
return predicate.APIKey(sql.FieldIsNull(FieldIPWhitelist))
}
// IPWhitelistNotNil applies the NotNil predicate on the "ip_whitelist" field.
func IPWhitelistNotNil() predicate.APIKey {
return predicate.APIKey(sql.FieldNotNull(FieldIPWhitelist))
}
// IPBlacklistIsNil applies the IsNil predicate on the "ip_blacklist" field.
func IPBlacklistIsNil() predicate.APIKey {
return predicate.APIKey(sql.FieldIsNull(FieldIPBlacklist))
}
// IPBlacklistNotNil applies the NotNil predicate on the "ip_blacklist" field.
func IPBlacklistNotNil() predicate.APIKey {
return predicate.APIKey(sql.FieldNotNull(FieldIPBlacklist))
}
// HasUser applies the HasEdge predicate on the "user" edge.
func HasUser() predicate.APIKey {
return predicate.APIKey(func(s *sql.Selector) {

View File

@@ -113,6 +113,18 @@ func (_c *APIKeyCreate) SetNillableStatus(v *string) *APIKeyCreate {
return _c
}
// SetIPWhitelist sets the "ip_whitelist" field.
func (_c *APIKeyCreate) SetIPWhitelist(v []string) *APIKeyCreate {
_c.mutation.SetIPWhitelist(v)
return _c
}
// SetIPBlacklist sets the "ip_blacklist" field.
func (_c *APIKeyCreate) SetIPBlacklist(v []string) *APIKeyCreate {
_c.mutation.SetIPBlacklist(v)
return _c
}
// SetUser sets the "user" edge to the User entity.
func (_c *APIKeyCreate) SetUser(v *User) *APIKeyCreate {
return _c.SetUserID(v.ID)
@@ -285,6 +297,14 @@ func (_c *APIKeyCreate) createSpec() (*APIKey, *sqlgraph.CreateSpec) {
_spec.SetField(apikey.FieldStatus, field.TypeString, value)
_node.Status = value
}
if value, ok := _c.mutation.IPWhitelist(); ok {
_spec.SetField(apikey.FieldIPWhitelist, field.TypeJSON, value)
_node.IPWhitelist = value
}
if value, ok := _c.mutation.IPBlacklist(); ok {
_spec.SetField(apikey.FieldIPBlacklist, field.TypeJSON, value)
_node.IPBlacklist = value
}
if nodes := _c.mutation.UserIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O,
@@ -483,6 +503,42 @@ func (u *APIKeyUpsert) UpdateStatus() *APIKeyUpsert {
return u
}
// SetIPWhitelist sets the "ip_whitelist" field.
func (u *APIKeyUpsert) SetIPWhitelist(v []string) *APIKeyUpsert {
u.Set(apikey.FieldIPWhitelist, v)
return u
}
// UpdateIPWhitelist sets the "ip_whitelist" field to the value that was provided on create.
func (u *APIKeyUpsert) UpdateIPWhitelist() *APIKeyUpsert {
u.SetExcluded(apikey.FieldIPWhitelist)
return u
}
// ClearIPWhitelist clears the value of the "ip_whitelist" field.
func (u *APIKeyUpsert) ClearIPWhitelist() *APIKeyUpsert {
u.SetNull(apikey.FieldIPWhitelist)
return u
}
// SetIPBlacklist sets the "ip_blacklist" field.
func (u *APIKeyUpsert) SetIPBlacklist(v []string) *APIKeyUpsert {
u.Set(apikey.FieldIPBlacklist, v)
return u
}
// UpdateIPBlacklist sets the "ip_blacklist" field to the value that was provided on create.
func (u *APIKeyUpsert) UpdateIPBlacklist() *APIKeyUpsert {
u.SetExcluded(apikey.FieldIPBlacklist)
return u
}
// ClearIPBlacklist clears the value of the "ip_blacklist" field.
func (u *APIKeyUpsert) ClearIPBlacklist() *APIKeyUpsert {
u.SetNull(apikey.FieldIPBlacklist)
return u
}
// UpdateNewValues updates the mutable fields using the new values that were set on create.
// Using this option is equivalent to using:
//
@@ -640,6 +696,48 @@ func (u *APIKeyUpsertOne) UpdateStatus() *APIKeyUpsertOne {
})
}
// SetIPWhitelist sets the "ip_whitelist" field.
func (u *APIKeyUpsertOne) SetIPWhitelist(v []string) *APIKeyUpsertOne {
return u.Update(func(s *APIKeyUpsert) {
s.SetIPWhitelist(v)
})
}
// UpdateIPWhitelist sets the "ip_whitelist" field to the value that was provided on create.
func (u *APIKeyUpsertOne) UpdateIPWhitelist() *APIKeyUpsertOne {
return u.Update(func(s *APIKeyUpsert) {
s.UpdateIPWhitelist()
})
}
// ClearIPWhitelist clears the value of the "ip_whitelist" field.
func (u *APIKeyUpsertOne) ClearIPWhitelist() *APIKeyUpsertOne {
return u.Update(func(s *APIKeyUpsert) {
s.ClearIPWhitelist()
})
}
// SetIPBlacklist sets the "ip_blacklist" field.
func (u *APIKeyUpsertOne) SetIPBlacklist(v []string) *APIKeyUpsertOne {
return u.Update(func(s *APIKeyUpsert) {
s.SetIPBlacklist(v)
})
}
// UpdateIPBlacklist sets the "ip_blacklist" field to the value that was provided on create.
func (u *APIKeyUpsertOne) UpdateIPBlacklist() *APIKeyUpsertOne {
return u.Update(func(s *APIKeyUpsert) {
s.UpdateIPBlacklist()
})
}
// ClearIPBlacklist clears the value of the "ip_blacklist" field.
func (u *APIKeyUpsertOne) ClearIPBlacklist() *APIKeyUpsertOne {
return u.Update(func(s *APIKeyUpsert) {
s.ClearIPBlacklist()
})
}
// Exec executes the query.
func (u *APIKeyUpsertOne) Exec(ctx context.Context) error {
if len(u.create.conflict) == 0 {
@@ -963,6 +1061,48 @@ func (u *APIKeyUpsertBulk) UpdateStatus() *APIKeyUpsertBulk {
})
}
// SetIPWhitelist sets the "ip_whitelist" field.
func (u *APIKeyUpsertBulk) SetIPWhitelist(v []string) *APIKeyUpsertBulk {
return u.Update(func(s *APIKeyUpsert) {
s.SetIPWhitelist(v)
})
}
// UpdateIPWhitelist sets the "ip_whitelist" field to the value that was provided on create.
func (u *APIKeyUpsertBulk) UpdateIPWhitelist() *APIKeyUpsertBulk {
return u.Update(func(s *APIKeyUpsert) {
s.UpdateIPWhitelist()
})
}
// ClearIPWhitelist clears the value of the "ip_whitelist" field.
func (u *APIKeyUpsertBulk) ClearIPWhitelist() *APIKeyUpsertBulk {
return u.Update(func(s *APIKeyUpsert) {
s.ClearIPWhitelist()
})
}
// SetIPBlacklist sets the "ip_blacklist" field.
func (u *APIKeyUpsertBulk) SetIPBlacklist(v []string) *APIKeyUpsertBulk {
return u.Update(func(s *APIKeyUpsert) {
s.SetIPBlacklist(v)
})
}
// UpdateIPBlacklist sets the "ip_blacklist" field to the value that was provided on create.
func (u *APIKeyUpsertBulk) UpdateIPBlacklist() *APIKeyUpsertBulk {
return u.Update(func(s *APIKeyUpsert) {
s.UpdateIPBlacklist()
})
}
// ClearIPBlacklist clears the value of the "ip_blacklist" field.
func (u *APIKeyUpsertBulk) ClearIPBlacklist() *APIKeyUpsertBulk {
return u.Update(func(s *APIKeyUpsert) {
s.ClearIPBlacklist()
})
}
// Exec executes the query.
func (u *APIKeyUpsertBulk) Exec(ctx context.Context) error {
if u.create.err != nil {

View File

@@ -9,6 +9,7 @@ import (
"math"
"entgo.io/ent"
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field"
@@ -29,6 +30,7 @@ type APIKeyQuery struct {
withUser *UserQuery
withGroup *GroupQuery
withUsageLogs *UsageLogQuery
modifiers []func(*sql.Selector)
// intermediate query (i.e. traversal path).
sql *sql.Selector
path func(context.Context) (*sql.Selector, error)
@@ -458,6 +460,9 @@ func (_q *APIKeyQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*APIKe
node.Edges.loadedTypes = loadedTypes
return node.assignValues(columns, values)
}
if len(_q.modifiers) > 0 {
_spec.Modifiers = _q.modifiers
}
for i := range hooks {
hooks[i](ctx, _spec)
}
@@ -583,6 +588,9 @@ func (_q *APIKeyQuery) loadUsageLogs(ctx context.Context, query *UsageLogQuery,
func (_q *APIKeyQuery) sqlCount(ctx context.Context) (int, error) {
_spec := _q.querySpec()
if len(_q.modifiers) > 0 {
_spec.Modifiers = _q.modifiers
}
_spec.Node.Columns = _q.ctx.Fields
if len(_q.ctx.Fields) > 0 {
_spec.Unique = _q.ctx.Unique != nil && *_q.ctx.Unique
@@ -651,6 +659,9 @@ func (_q *APIKeyQuery) sqlQuery(ctx context.Context) *sql.Selector {
if _q.ctx.Unique != nil && *_q.ctx.Unique {
selector.Distinct()
}
for _, m := range _q.modifiers {
m(selector)
}
for _, p := range _q.predicates {
p(selector)
}
@@ -668,6 +679,32 @@ func (_q *APIKeyQuery) sqlQuery(ctx context.Context) *sql.Selector {
return selector
}
// ForUpdate locks the selected rows against concurrent updates, and prevent them from being
// updated, deleted or "selected ... for update" by other sessions, until the transaction is
// either committed or rolled-back.
func (_q *APIKeyQuery) ForUpdate(opts ...sql.LockOption) *APIKeyQuery {
if _q.driver.Dialect() == dialect.Postgres {
_q.Unique(false)
}
_q.modifiers = append(_q.modifiers, func(s *sql.Selector) {
s.ForUpdate(opts...)
})
return _q
}
// ForShare behaves similarly to ForUpdate, except that it acquires a shared mode lock
// on any rows that are read. Other sessions can read the rows, but cannot modify them
// until your transaction commits.
func (_q *APIKeyQuery) ForShare(opts ...sql.LockOption) *APIKeyQuery {
if _q.driver.Dialect() == dialect.Postgres {
_q.Unique(false)
}
_q.modifiers = append(_q.modifiers, func(s *sql.Selector) {
s.ForShare(opts...)
})
return _q
}
// APIKeyGroupBy is the group-by builder for APIKey entities.
type APIKeyGroupBy struct {
selector

View File

@@ -10,6 +10,7 @@ import (
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/dialect/sql/sqljson"
"entgo.io/ent/schema/field"
"github.com/Wei-Shaw/sub2api/ent/apikey"
"github.com/Wei-Shaw/sub2api/ent/group"
@@ -133,6 +134,42 @@ func (_u *APIKeyUpdate) SetNillableStatus(v *string) *APIKeyUpdate {
return _u
}
// SetIPWhitelist sets the "ip_whitelist" field.
func (_u *APIKeyUpdate) SetIPWhitelist(v []string) *APIKeyUpdate {
_u.mutation.SetIPWhitelist(v)
return _u
}
// AppendIPWhitelist appends value to the "ip_whitelist" field.
func (_u *APIKeyUpdate) AppendIPWhitelist(v []string) *APIKeyUpdate {
_u.mutation.AppendIPWhitelist(v)
return _u
}
// ClearIPWhitelist clears the value of the "ip_whitelist" field.
func (_u *APIKeyUpdate) ClearIPWhitelist() *APIKeyUpdate {
_u.mutation.ClearIPWhitelist()
return _u
}
// SetIPBlacklist sets the "ip_blacklist" field.
func (_u *APIKeyUpdate) SetIPBlacklist(v []string) *APIKeyUpdate {
_u.mutation.SetIPBlacklist(v)
return _u
}
// AppendIPBlacklist appends value to the "ip_blacklist" field.
func (_u *APIKeyUpdate) AppendIPBlacklist(v []string) *APIKeyUpdate {
_u.mutation.AppendIPBlacklist(v)
return _u
}
// ClearIPBlacklist clears the value of the "ip_blacklist" field.
func (_u *APIKeyUpdate) ClearIPBlacklist() *APIKeyUpdate {
_u.mutation.ClearIPBlacklist()
return _u
}
// SetUser sets the "user" edge to the User entity.
func (_u *APIKeyUpdate) SetUser(v *User) *APIKeyUpdate {
return _u.SetUserID(v.ID)
@@ -291,6 +328,28 @@ func (_u *APIKeyUpdate) sqlSave(ctx context.Context) (_node int, err error) {
if value, ok := _u.mutation.Status(); ok {
_spec.SetField(apikey.FieldStatus, field.TypeString, value)
}
if value, ok := _u.mutation.IPWhitelist(); ok {
_spec.SetField(apikey.FieldIPWhitelist, field.TypeJSON, value)
}
if value, ok := _u.mutation.AppendedIPWhitelist(); ok {
_spec.AddModifier(func(u *sql.UpdateBuilder) {
sqljson.Append(u, apikey.FieldIPWhitelist, value)
})
}
if _u.mutation.IPWhitelistCleared() {
_spec.ClearField(apikey.FieldIPWhitelist, field.TypeJSON)
}
if value, ok := _u.mutation.IPBlacklist(); ok {
_spec.SetField(apikey.FieldIPBlacklist, field.TypeJSON, value)
}
if value, ok := _u.mutation.AppendedIPBlacklist(); ok {
_spec.AddModifier(func(u *sql.UpdateBuilder) {
sqljson.Append(u, apikey.FieldIPBlacklist, value)
})
}
if _u.mutation.IPBlacklistCleared() {
_spec.ClearField(apikey.FieldIPBlacklist, field.TypeJSON)
}
if _u.mutation.UserCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O,
@@ -516,6 +575,42 @@ func (_u *APIKeyUpdateOne) SetNillableStatus(v *string) *APIKeyUpdateOne {
return _u
}
// SetIPWhitelist sets the "ip_whitelist" field.
func (_u *APIKeyUpdateOne) SetIPWhitelist(v []string) *APIKeyUpdateOne {
_u.mutation.SetIPWhitelist(v)
return _u
}
// AppendIPWhitelist appends value to the "ip_whitelist" field.
func (_u *APIKeyUpdateOne) AppendIPWhitelist(v []string) *APIKeyUpdateOne {
_u.mutation.AppendIPWhitelist(v)
return _u
}
// ClearIPWhitelist clears the value of the "ip_whitelist" field.
func (_u *APIKeyUpdateOne) ClearIPWhitelist() *APIKeyUpdateOne {
_u.mutation.ClearIPWhitelist()
return _u
}
// SetIPBlacklist sets the "ip_blacklist" field.
func (_u *APIKeyUpdateOne) SetIPBlacklist(v []string) *APIKeyUpdateOne {
_u.mutation.SetIPBlacklist(v)
return _u
}
// AppendIPBlacklist appends value to the "ip_blacklist" field.
func (_u *APIKeyUpdateOne) AppendIPBlacklist(v []string) *APIKeyUpdateOne {
_u.mutation.AppendIPBlacklist(v)
return _u
}
// ClearIPBlacklist clears the value of the "ip_blacklist" field.
func (_u *APIKeyUpdateOne) ClearIPBlacklist() *APIKeyUpdateOne {
_u.mutation.ClearIPBlacklist()
return _u
}
// SetUser sets the "user" edge to the User entity.
func (_u *APIKeyUpdateOne) SetUser(v *User) *APIKeyUpdateOne {
return _u.SetUserID(v.ID)
@@ -704,6 +799,28 @@ func (_u *APIKeyUpdateOne) sqlSave(ctx context.Context) (_node *APIKey, err erro
if value, ok := _u.mutation.Status(); ok {
_spec.SetField(apikey.FieldStatus, field.TypeString, value)
}
if value, ok := _u.mutation.IPWhitelist(); ok {
_spec.SetField(apikey.FieldIPWhitelist, field.TypeJSON, value)
}
if value, ok := _u.mutation.AppendedIPWhitelist(); ok {
_spec.AddModifier(func(u *sql.UpdateBuilder) {
sqljson.Append(u, apikey.FieldIPWhitelist, value)
})
}
if _u.mutation.IPWhitelistCleared() {
_spec.ClearField(apikey.FieldIPWhitelist, field.TypeJSON)
}
if value, ok := _u.mutation.IPBlacklist(); ok {
_spec.SetField(apikey.FieldIPBlacklist, field.TypeJSON, value)
}
if value, ok := _u.mutation.AppendedIPBlacklist(); ok {
_spec.AddModifier(func(u *sql.UpdateBuilder) {
sqljson.Append(u, apikey.FieldIPBlacklist, value)
})
}
if _u.mutation.IPBlacklistCleared() {
_spec.ClearField(apikey.FieldIPBlacklist, field.TypeJSON)
}
if _u.mutation.UserCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O,

View File

@@ -19,6 +19,8 @@ import (
"github.com/Wei-Shaw/sub2api/ent/accountgroup"
"github.com/Wei-Shaw/sub2api/ent/apikey"
"github.com/Wei-Shaw/sub2api/ent/group"
"github.com/Wei-Shaw/sub2api/ent/promocode"
"github.com/Wei-Shaw/sub2api/ent/promocodeusage"
"github.com/Wei-Shaw/sub2api/ent/proxy"
"github.com/Wei-Shaw/sub2api/ent/redeemcode"
"github.com/Wei-Shaw/sub2api/ent/setting"
@@ -45,6 +47,10 @@ type Client struct {
AccountGroup *AccountGroupClient
// Group is the client for interacting with the Group builders.
Group *GroupClient
// PromoCode is the client for interacting with the PromoCode builders.
PromoCode *PromoCodeClient
// PromoCodeUsage is the client for interacting with the PromoCodeUsage builders.
PromoCodeUsage *PromoCodeUsageClient
// Proxy is the client for interacting with the Proxy builders.
Proxy *ProxyClient
// RedeemCode is the client for interacting with the RedeemCode builders.
@@ -78,6 +84,8 @@ func (c *Client) init() {
c.Account = NewAccountClient(c.config)
c.AccountGroup = NewAccountGroupClient(c.config)
c.Group = NewGroupClient(c.config)
c.PromoCode = NewPromoCodeClient(c.config)
c.PromoCodeUsage = NewPromoCodeUsageClient(c.config)
c.Proxy = NewProxyClient(c.config)
c.RedeemCode = NewRedeemCodeClient(c.config)
c.Setting = NewSettingClient(c.config)
@@ -183,6 +191,8 @@ func (c *Client) Tx(ctx context.Context) (*Tx, error) {
Account: NewAccountClient(cfg),
AccountGroup: NewAccountGroupClient(cfg),
Group: NewGroupClient(cfg),
PromoCode: NewPromoCodeClient(cfg),
PromoCodeUsage: NewPromoCodeUsageClient(cfg),
Proxy: NewProxyClient(cfg),
RedeemCode: NewRedeemCodeClient(cfg),
Setting: NewSettingClient(cfg),
@@ -215,6 +225,8 @@ func (c *Client) BeginTx(ctx context.Context, opts *sql.TxOptions) (*Tx, error)
Account: NewAccountClient(cfg),
AccountGroup: NewAccountGroupClient(cfg),
Group: NewGroupClient(cfg),
PromoCode: NewPromoCodeClient(cfg),
PromoCodeUsage: NewPromoCodeUsageClient(cfg),
Proxy: NewProxyClient(cfg),
RedeemCode: NewRedeemCodeClient(cfg),
Setting: NewSettingClient(cfg),
@@ -253,9 +265,9 @@ func (c *Client) Close() error {
// In order to add hooks to a specific client, call: `client.Node.Use(...)`.
func (c *Client) Use(hooks ...Hook) {
for _, n := range []interface{ Use(...Hook) }{
c.APIKey, c.Account, c.AccountGroup, c.Group, c.Proxy, c.RedeemCode, c.Setting,
c.UsageLog, c.User, c.UserAllowedGroup, c.UserAttributeDefinition,
c.UserAttributeValue, c.UserSubscription,
c.APIKey, c.Account, c.AccountGroup, c.Group, c.PromoCode, c.PromoCodeUsage,
c.Proxy, c.RedeemCode, c.Setting, c.UsageLog, c.User, c.UserAllowedGroup,
c.UserAttributeDefinition, c.UserAttributeValue, c.UserSubscription,
} {
n.Use(hooks...)
}
@@ -265,9 +277,9 @@ func (c *Client) Use(hooks ...Hook) {
// In order to add interceptors to a specific client, call: `client.Node.Intercept(...)`.
func (c *Client) Intercept(interceptors ...Interceptor) {
for _, n := range []interface{ Intercept(...Interceptor) }{
c.APIKey, c.Account, c.AccountGroup, c.Group, c.Proxy, c.RedeemCode, c.Setting,
c.UsageLog, c.User, c.UserAllowedGroup, c.UserAttributeDefinition,
c.UserAttributeValue, c.UserSubscription,
c.APIKey, c.Account, c.AccountGroup, c.Group, c.PromoCode, c.PromoCodeUsage,
c.Proxy, c.RedeemCode, c.Setting, c.UsageLog, c.User, c.UserAllowedGroup,
c.UserAttributeDefinition, c.UserAttributeValue, c.UserSubscription,
} {
n.Intercept(interceptors...)
}
@@ -284,6 +296,10 @@ func (c *Client) Mutate(ctx context.Context, m Mutation) (Value, error) {
return c.AccountGroup.mutate(ctx, m)
case *GroupMutation:
return c.Group.mutate(ctx, m)
case *PromoCodeMutation:
return c.PromoCode.mutate(ctx, m)
case *PromoCodeUsageMutation:
return c.PromoCodeUsage.mutate(ctx, m)
case *ProxyMutation:
return c.Proxy.mutate(ctx, m)
case *RedeemCodeMutation:
@@ -1068,6 +1084,320 @@ func (c *GroupClient) mutate(ctx context.Context, m *GroupMutation) (Value, erro
}
}
// PromoCodeClient is a client for the PromoCode schema.
type PromoCodeClient struct {
config
}
// NewPromoCodeClient returns a client for the PromoCode from the given config.
func NewPromoCodeClient(c config) *PromoCodeClient {
return &PromoCodeClient{config: c}
}
// Use adds a list of mutation hooks to the hooks stack.
// A call to `Use(f, g, h)` equals to `promocode.Hooks(f(g(h())))`.
func (c *PromoCodeClient) Use(hooks ...Hook) {
c.hooks.PromoCode = append(c.hooks.PromoCode, hooks...)
}
// Intercept adds a list of query interceptors to the interceptors stack.
// A call to `Intercept(f, g, h)` equals to `promocode.Intercept(f(g(h())))`.
func (c *PromoCodeClient) Intercept(interceptors ...Interceptor) {
c.inters.PromoCode = append(c.inters.PromoCode, interceptors...)
}
// Create returns a builder for creating a PromoCode entity.
func (c *PromoCodeClient) Create() *PromoCodeCreate {
mutation := newPromoCodeMutation(c.config, OpCreate)
return &PromoCodeCreate{config: c.config, hooks: c.Hooks(), mutation: mutation}
}
// CreateBulk returns a builder for creating a bulk of PromoCode entities.
func (c *PromoCodeClient) CreateBulk(builders ...*PromoCodeCreate) *PromoCodeCreateBulk {
return &PromoCodeCreateBulk{config: c.config, builders: builders}
}
// MapCreateBulk creates a bulk creation builder from the given slice. For each item in the slice, the function creates
// a builder and applies setFunc on it.
func (c *PromoCodeClient) MapCreateBulk(slice any, setFunc func(*PromoCodeCreate, int)) *PromoCodeCreateBulk {
rv := reflect.ValueOf(slice)
if rv.Kind() != reflect.Slice {
return &PromoCodeCreateBulk{err: fmt.Errorf("calling to PromoCodeClient.MapCreateBulk with wrong type %T, need slice", slice)}
}
builders := make([]*PromoCodeCreate, rv.Len())
for i := 0; i < rv.Len(); i++ {
builders[i] = c.Create()
setFunc(builders[i], i)
}
return &PromoCodeCreateBulk{config: c.config, builders: builders}
}
// Update returns an update builder for PromoCode.
func (c *PromoCodeClient) Update() *PromoCodeUpdate {
mutation := newPromoCodeMutation(c.config, OpUpdate)
return &PromoCodeUpdate{config: c.config, hooks: c.Hooks(), mutation: mutation}
}
// UpdateOne returns an update builder for the given entity.
func (c *PromoCodeClient) UpdateOne(_m *PromoCode) *PromoCodeUpdateOne {
mutation := newPromoCodeMutation(c.config, OpUpdateOne, withPromoCode(_m))
return &PromoCodeUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation}
}
// UpdateOneID returns an update builder for the given id.
func (c *PromoCodeClient) UpdateOneID(id int64) *PromoCodeUpdateOne {
mutation := newPromoCodeMutation(c.config, OpUpdateOne, withPromoCodeID(id))
return &PromoCodeUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation}
}
// Delete returns a delete builder for PromoCode.
func (c *PromoCodeClient) Delete() *PromoCodeDelete {
mutation := newPromoCodeMutation(c.config, OpDelete)
return &PromoCodeDelete{config: c.config, hooks: c.Hooks(), mutation: mutation}
}
// DeleteOne returns a builder for deleting the given entity.
func (c *PromoCodeClient) DeleteOne(_m *PromoCode) *PromoCodeDeleteOne {
return c.DeleteOneID(_m.ID)
}
// DeleteOneID returns a builder for deleting the given entity by its id.
func (c *PromoCodeClient) DeleteOneID(id int64) *PromoCodeDeleteOne {
builder := c.Delete().Where(promocode.ID(id))
builder.mutation.id = &id
builder.mutation.op = OpDeleteOne
return &PromoCodeDeleteOne{builder}
}
// Query returns a query builder for PromoCode.
func (c *PromoCodeClient) Query() *PromoCodeQuery {
return &PromoCodeQuery{
config: c.config,
ctx: &QueryContext{Type: TypePromoCode},
inters: c.Interceptors(),
}
}
// Get returns a PromoCode entity by its id.
func (c *PromoCodeClient) Get(ctx context.Context, id int64) (*PromoCode, error) {
return c.Query().Where(promocode.ID(id)).Only(ctx)
}
// GetX is like Get, but panics if an error occurs.
func (c *PromoCodeClient) GetX(ctx context.Context, id int64) *PromoCode {
obj, err := c.Get(ctx, id)
if err != nil {
panic(err)
}
return obj
}
// QueryUsageRecords queries the usage_records edge of a PromoCode.
func (c *PromoCodeClient) QueryUsageRecords(_m *PromoCode) *PromoCodeUsageQuery {
query := (&PromoCodeUsageClient{config: c.config}).Query()
query.path = func(context.Context) (fromV *sql.Selector, _ error) {
id := _m.ID
step := sqlgraph.NewStep(
sqlgraph.From(promocode.Table, promocode.FieldID, id),
sqlgraph.To(promocodeusage.Table, promocodeusage.FieldID),
sqlgraph.Edge(sqlgraph.O2M, false, promocode.UsageRecordsTable, promocode.UsageRecordsColumn),
)
fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step)
return fromV, nil
}
return query
}
// Hooks returns the client hooks.
func (c *PromoCodeClient) Hooks() []Hook {
return c.hooks.PromoCode
}
// Interceptors returns the client interceptors.
func (c *PromoCodeClient) Interceptors() []Interceptor {
return c.inters.PromoCode
}
func (c *PromoCodeClient) mutate(ctx context.Context, m *PromoCodeMutation) (Value, error) {
switch m.Op() {
case OpCreate:
return (&PromoCodeCreate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx)
case OpUpdate:
return (&PromoCodeUpdate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx)
case OpUpdateOne:
return (&PromoCodeUpdateOne{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx)
case OpDelete, OpDeleteOne:
return (&PromoCodeDelete{config: c.config, hooks: c.Hooks(), mutation: m}).Exec(ctx)
default:
return nil, fmt.Errorf("ent: unknown PromoCode mutation op: %q", m.Op())
}
}
// PromoCodeUsageClient is a client for the PromoCodeUsage schema.
type PromoCodeUsageClient struct {
config
}
// NewPromoCodeUsageClient returns a client for the PromoCodeUsage from the given config.
func NewPromoCodeUsageClient(c config) *PromoCodeUsageClient {
return &PromoCodeUsageClient{config: c}
}
// Use adds a list of mutation hooks to the hooks stack.
// A call to `Use(f, g, h)` equals to `promocodeusage.Hooks(f(g(h())))`.
func (c *PromoCodeUsageClient) Use(hooks ...Hook) {
c.hooks.PromoCodeUsage = append(c.hooks.PromoCodeUsage, hooks...)
}
// Intercept adds a list of query interceptors to the interceptors stack.
// A call to `Intercept(f, g, h)` equals to `promocodeusage.Intercept(f(g(h())))`.
func (c *PromoCodeUsageClient) Intercept(interceptors ...Interceptor) {
c.inters.PromoCodeUsage = append(c.inters.PromoCodeUsage, interceptors...)
}
// Create returns a builder for creating a PromoCodeUsage entity.
func (c *PromoCodeUsageClient) Create() *PromoCodeUsageCreate {
mutation := newPromoCodeUsageMutation(c.config, OpCreate)
return &PromoCodeUsageCreate{config: c.config, hooks: c.Hooks(), mutation: mutation}
}
// CreateBulk returns a builder for creating a bulk of PromoCodeUsage entities.
func (c *PromoCodeUsageClient) CreateBulk(builders ...*PromoCodeUsageCreate) *PromoCodeUsageCreateBulk {
return &PromoCodeUsageCreateBulk{config: c.config, builders: builders}
}
// MapCreateBulk creates a bulk creation builder from the given slice. For each item in the slice, the function creates
// a builder and applies setFunc on it.
func (c *PromoCodeUsageClient) MapCreateBulk(slice any, setFunc func(*PromoCodeUsageCreate, int)) *PromoCodeUsageCreateBulk {
rv := reflect.ValueOf(slice)
if rv.Kind() != reflect.Slice {
return &PromoCodeUsageCreateBulk{err: fmt.Errorf("calling to PromoCodeUsageClient.MapCreateBulk with wrong type %T, need slice", slice)}
}
builders := make([]*PromoCodeUsageCreate, rv.Len())
for i := 0; i < rv.Len(); i++ {
builders[i] = c.Create()
setFunc(builders[i], i)
}
return &PromoCodeUsageCreateBulk{config: c.config, builders: builders}
}
// Update returns an update builder for PromoCodeUsage.
func (c *PromoCodeUsageClient) Update() *PromoCodeUsageUpdate {
mutation := newPromoCodeUsageMutation(c.config, OpUpdate)
return &PromoCodeUsageUpdate{config: c.config, hooks: c.Hooks(), mutation: mutation}
}
// UpdateOne returns an update builder for the given entity.
func (c *PromoCodeUsageClient) UpdateOne(_m *PromoCodeUsage) *PromoCodeUsageUpdateOne {
mutation := newPromoCodeUsageMutation(c.config, OpUpdateOne, withPromoCodeUsage(_m))
return &PromoCodeUsageUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation}
}
// UpdateOneID returns an update builder for the given id.
func (c *PromoCodeUsageClient) UpdateOneID(id int64) *PromoCodeUsageUpdateOne {
mutation := newPromoCodeUsageMutation(c.config, OpUpdateOne, withPromoCodeUsageID(id))
return &PromoCodeUsageUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation}
}
// Delete returns a delete builder for PromoCodeUsage.
func (c *PromoCodeUsageClient) Delete() *PromoCodeUsageDelete {
mutation := newPromoCodeUsageMutation(c.config, OpDelete)
return &PromoCodeUsageDelete{config: c.config, hooks: c.Hooks(), mutation: mutation}
}
// DeleteOne returns a builder for deleting the given entity.
func (c *PromoCodeUsageClient) DeleteOne(_m *PromoCodeUsage) *PromoCodeUsageDeleteOne {
return c.DeleteOneID(_m.ID)
}
// DeleteOneID returns a builder for deleting the given entity by its id.
func (c *PromoCodeUsageClient) DeleteOneID(id int64) *PromoCodeUsageDeleteOne {
builder := c.Delete().Where(promocodeusage.ID(id))
builder.mutation.id = &id
builder.mutation.op = OpDeleteOne
return &PromoCodeUsageDeleteOne{builder}
}
// Query returns a query builder for PromoCodeUsage.
func (c *PromoCodeUsageClient) Query() *PromoCodeUsageQuery {
return &PromoCodeUsageQuery{
config: c.config,
ctx: &QueryContext{Type: TypePromoCodeUsage},
inters: c.Interceptors(),
}
}
// Get returns a PromoCodeUsage entity by its id.
func (c *PromoCodeUsageClient) Get(ctx context.Context, id int64) (*PromoCodeUsage, error) {
return c.Query().Where(promocodeusage.ID(id)).Only(ctx)
}
// GetX is like Get, but panics if an error occurs.
func (c *PromoCodeUsageClient) GetX(ctx context.Context, id int64) *PromoCodeUsage {
obj, err := c.Get(ctx, id)
if err != nil {
panic(err)
}
return obj
}
// QueryPromoCode queries the promo_code edge of a PromoCodeUsage.
func (c *PromoCodeUsageClient) QueryPromoCode(_m *PromoCodeUsage) *PromoCodeQuery {
query := (&PromoCodeClient{config: c.config}).Query()
query.path = func(context.Context) (fromV *sql.Selector, _ error) {
id := _m.ID
step := sqlgraph.NewStep(
sqlgraph.From(promocodeusage.Table, promocodeusage.FieldID, id),
sqlgraph.To(promocode.Table, promocode.FieldID),
sqlgraph.Edge(sqlgraph.M2O, true, promocodeusage.PromoCodeTable, promocodeusage.PromoCodeColumn),
)
fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step)
return fromV, nil
}
return query
}
// QueryUser queries the user edge of a PromoCodeUsage.
func (c *PromoCodeUsageClient) QueryUser(_m *PromoCodeUsage) *UserQuery {
query := (&UserClient{config: c.config}).Query()
query.path = func(context.Context) (fromV *sql.Selector, _ error) {
id := _m.ID
step := sqlgraph.NewStep(
sqlgraph.From(promocodeusage.Table, promocodeusage.FieldID, id),
sqlgraph.To(user.Table, user.FieldID),
sqlgraph.Edge(sqlgraph.M2O, true, promocodeusage.UserTable, promocodeusage.UserColumn),
)
fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step)
return fromV, nil
}
return query
}
// Hooks returns the client hooks.
func (c *PromoCodeUsageClient) Hooks() []Hook {
return c.hooks.PromoCodeUsage
}
// Interceptors returns the client interceptors.
func (c *PromoCodeUsageClient) Interceptors() []Interceptor {
return c.inters.PromoCodeUsage
}
func (c *PromoCodeUsageClient) mutate(ctx context.Context, m *PromoCodeUsageMutation) (Value, error) {
switch m.Op() {
case OpCreate:
return (&PromoCodeUsageCreate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx)
case OpUpdate:
return (&PromoCodeUsageUpdate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx)
case OpUpdateOne:
return (&PromoCodeUsageUpdateOne{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx)
case OpDelete, OpDeleteOne:
return (&PromoCodeUsageDelete{config: c.config, hooks: c.Hooks(), mutation: m}).Exec(ctx)
default:
return nil, fmt.Errorf("ent: unknown PromoCodeUsage mutation op: %q", m.Op())
}
}
// ProxyClient is a client for the Proxy schema.
type ProxyClient struct {
config
@@ -1950,6 +2280,22 @@ func (c *UserClient) QueryAttributeValues(_m *User) *UserAttributeValueQuery {
return query
}
// QueryPromoCodeUsages queries the promo_code_usages edge of a User.
func (c *UserClient) QueryPromoCodeUsages(_m *User) *PromoCodeUsageQuery {
query := (&PromoCodeUsageClient{config: c.config}).Query()
query.path = func(context.Context) (fromV *sql.Selector, _ error) {
id := _m.ID
step := sqlgraph.NewStep(
sqlgraph.From(user.Table, user.FieldID, id),
sqlgraph.To(promocodeusage.Table, promocodeusage.FieldID),
sqlgraph.Edge(sqlgraph.O2M, false, user.PromoCodeUsagesTable, user.PromoCodeUsagesColumn),
)
fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step)
return fromV, nil
}
return query
}
// QueryUserAllowedGroups queries the user_allowed_groups edge of a User.
func (c *UserClient) QueryUserAllowedGroups(_m *User) *UserAllowedGroupQuery {
query := (&UserAllowedGroupClient{config: c.config}).Query()
@@ -2627,14 +2973,14 @@ func (c *UserSubscriptionClient) mutate(ctx context.Context, m *UserSubscription
// hooks and interceptors per client, for fast access.
type (
hooks struct {
APIKey, Account, AccountGroup, Group, Proxy, RedeemCode, Setting, UsageLog,
User, UserAllowedGroup, UserAttributeDefinition, UserAttributeValue,
UserSubscription []ent.Hook
APIKey, Account, AccountGroup, Group, PromoCode, PromoCodeUsage, Proxy,
RedeemCode, Setting, UsageLog, User, UserAllowedGroup, UserAttributeDefinition,
UserAttributeValue, UserSubscription []ent.Hook
}
inters struct {
APIKey, Account, AccountGroup, Group, Proxy, RedeemCode, Setting, UsageLog,
User, UserAllowedGroup, UserAttributeDefinition, UserAttributeValue,
UserSubscription []ent.Interceptor
APIKey, Account, AccountGroup, Group, PromoCode, PromoCodeUsage, Proxy,
RedeemCode, Setting, UsageLog, User, UserAllowedGroup, UserAttributeDefinition,
UserAttributeValue, UserSubscription []ent.Interceptor
}
)

View File

@@ -16,6 +16,8 @@ import (
"github.com/Wei-Shaw/sub2api/ent/accountgroup"
"github.com/Wei-Shaw/sub2api/ent/apikey"
"github.com/Wei-Shaw/sub2api/ent/group"
"github.com/Wei-Shaw/sub2api/ent/promocode"
"github.com/Wei-Shaw/sub2api/ent/promocodeusage"
"github.com/Wei-Shaw/sub2api/ent/proxy"
"github.com/Wei-Shaw/sub2api/ent/redeemcode"
"github.com/Wei-Shaw/sub2api/ent/setting"
@@ -89,6 +91,8 @@ func checkColumn(t, c string) error {
account.Table: account.ValidColumn,
accountgroup.Table: accountgroup.ValidColumn,
group.Table: group.ValidColumn,
promocode.Table: promocode.ValidColumn,
promocodeusage.Table: promocodeusage.ValidColumn,
proxy.Table: proxy.ValidColumn,
redeemcode.Table: redeemcode.ValidColumn,
setting.Table: setting.ValidColumn,

View File

@@ -2,4 +2,5 @@
package ent
// 启用 sql/execquery 以生成 ExecContext/QueryContext 的透传接口,便于事务内执行原生 SQL。
//go:generate go run -mod=mod entgo.io/ent/cmd/ent generate --feature sql/upsert,intercept,sql/execquery --idtype int64 ./schema
// 启用 sql/lock 以支持 FOR UPDATE 行锁。
//go:generate go run -mod=mod entgo.io/ent/cmd/ent generate --feature sql/upsert,intercept,sql/execquery,sql/lock --idtype int64 ./schema

View File

@@ -45,6 +45,16 @@ type Group struct {
MonthlyLimitUsd *float64 `json:"monthly_limit_usd,omitempty"`
// DefaultValidityDays holds the value of the "default_validity_days" field.
DefaultValidityDays int `json:"default_validity_days,omitempty"`
// ImagePrice1k holds the value of the "image_price_1k" field.
ImagePrice1k *float64 `json:"image_price_1k,omitempty"`
// ImagePrice2k holds the value of the "image_price_2k" field.
ImagePrice2k *float64 `json:"image_price_2k,omitempty"`
// ImagePrice4k holds the value of the "image_price_4k" field.
ImagePrice4k *float64 `json:"image_price_4k,omitempty"`
// 是否仅允许 Claude Code 客户端
ClaudeCodeOnly bool `json:"claude_code_only,omitempty"`
// 非 Claude Code 请求降级使用的分组 ID
FallbackGroupID *int64 `json:"fallback_group_id,omitempty"`
// Edges holds the relations/edges for other nodes in the graph.
// The values are being populated by the GroupQuery when eager-loading is set.
Edges GroupEdges `json:"edges"`
@@ -151,11 +161,11 @@ func (*Group) scanValues(columns []string) ([]any, error) {
values := make([]any, len(columns))
for i := range columns {
switch columns[i] {
case group.FieldIsExclusive:
case group.FieldIsExclusive, group.FieldClaudeCodeOnly:
values[i] = new(sql.NullBool)
case group.FieldRateMultiplier, group.FieldDailyLimitUsd, group.FieldWeeklyLimitUsd, group.FieldMonthlyLimitUsd:
case group.FieldRateMultiplier, group.FieldDailyLimitUsd, group.FieldWeeklyLimitUsd, group.FieldMonthlyLimitUsd, group.FieldImagePrice1k, group.FieldImagePrice2k, group.FieldImagePrice4k:
values[i] = new(sql.NullFloat64)
case group.FieldID, group.FieldDefaultValidityDays:
case group.FieldID, group.FieldDefaultValidityDays, group.FieldFallbackGroupID:
values[i] = new(sql.NullInt64)
case group.FieldName, group.FieldDescription, group.FieldStatus, group.FieldPlatform, group.FieldSubscriptionType:
values[i] = new(sql.NullString)
@@ -271,6 +281,40 @@ func (_m *Group) assignValues(columns []string, values []any) error {
} else if value.Valid {
_m.DefaultValidityDays = int(value.Int64)
}
case group.FieldImagePrice1k:
if value, ok := values[i].(*sql.NullFloat64); !ok {
return fmt.Errorf("unexpected type %T for field image_price_1k", values[i])
} else if value.Valid {
_m.ImagePrice1k = new(float64)
*_m.ImagePrice1k = value.Float64
}
case group.FieldImagePrice2k:
if value, ok := values[i].(*sql.NullFloat64); !ok {
return fmt.Errorf("unexpected type %T for field image_price_2k", values[i])
} else if value.Valid {
_m.ImagePrice2k = new(float64)
*_m.ImagePrice2k = value.Float64
}
case group.FieldImagePrice4k:
if value, ok := values[i].(*sql.NullFloat64); !ok {
return fmt.Errorf("unexpected type %T for field image_price_4k", values[i])
} else if value.Valid {
_m.ImagePrice4k = new(float64)
*_m.ImagePrice4k = value.Float64
}
case group.FieldClaudeCodeOnly:
if value, ok := values[i].(*sql.NullBool); !ok {
return fmt.Errorf("unexpected type %T for field claude_code_only", values[i])
} else if value.Valid {
_m.ClaudeCodeOnly = value.Bool
}
case group.FieldFallbackGroupID:
if value, ok := values[i].(*sql.NullInt64); !ok {
return fmt.Errorf("unexpected type %T for field fallback_group_id", values[i])
} else if value.Valid {
_m.FallbackGroupID = new(int64)
*_m.FallbackGroupID = value.Int64
}
default:
_m.selectValues.Set(columns[i], values[i])
}
@@ -398,6 +442,29 @@ func (_m *Group) String() string {
builder.WriteString(", ")
builder.WriteString("default_validity_days=")
builder.WriteString(fmt.Sprintf("%v", _m.DefaultValidityDays))
builder.WriteString(", ")
if v := _m.ImagePrice1k; v != nil {
builder.WriteString("image_price_1k=")
builder.WriteString(fmt.Sprintf("%v", *v))
}
builder.WriteString(", ")
if v := _m.ImagePrice2k; v != nil {
builder.WriteString("image_price_2k=")
builder.WriteString(fmt.Sprintf("%v", *v))
}
builder.WriteString(", ")
if v := _m.ImagePrice4k; v != nil {
builder.WriteString("image_price_4k=")
builder.WriteString(fmt.Sprintf("%v", *v))
}
builder.WriteString(", ")
builder.WriteString("claude_code_only=")
builder.WriteString(fmt.Sprintf("%v", _m.ClaudeCodeOnly))
builder.WriteString(", ")
if v := _m.FallbackGroupID; v != nil {
builder.WriteString("fallback_group_id=")
builder.WriteString(fmt.Sprintf("%v", *v))
}
builder.WriteByte(')')
return builder.String()
}

View File

@@ -43,6 +43,16 @@ const (
FieldMonthlyLimitUsd = "monthly_limit_usd"
// FieldDefaultValidityDays holds the string denoting the default_validity_days field in the database.
FieldDefaultValidityDays = "default_validity_days"
// FieldImagePrice1k holds the string denoting the image_price_1k field in the database.
FieldImagePrice1k = "image_price_1k"
// FieldImagePrice2k holds the string denoting the image_price_2k field in the database.
FieldImagePrice2k = "image_price_2k"
// FieldImagePrice4k holds the string denoting the image_price_4k field in the database.
FieldImagePrice4k = "image_price_4k"
// FieldClaudeCodeOnly holds the string denoting the claude_code_only field in the database.
FieldClaudeCodeOnly = "claude_code_only"
// FieldFallbackGroupID holds the string denoting the fallback_group_id field in the database.
FieldFallbackGroupID = "fallback_group_id"
// EdgeAPIKeys holds the string denoting the api_keys edge name in mutations.
EdgeAPIKeys = "api_keys"
// EdgeRedeemCodes holds the string denoting the redeem_codes edge name in mutations.
@@ -132,6 +142,11 @@ var Columns = []string{
FieldWeeklyLimitUsd,
FieldMonthlyLimitUsd,
FieldDefaultValidityDays,
FieldImagePrice1k,
FieldImagePrice2k,
FieldImagePrice4k,
FieldClaudeCodeOnly,
FieldFallbackGroupID,
}
var (
@@ -187,6 +202,8 @@ var (
SubscriptionTypeValidator func(string) error
// DefaultDefaultValidityDays holds the default value on creation for the "default_validity_days" field.
DefaultDefaultValidityDays int
// DefaultClaudeCodeOnly holds the default value on creation for the "claude_code_only" field.
DefaultClaudeCodeOnly bool
)
// OrderOption defines the ordering options for the Group queries.
@@ -267,6 +284,31 @@ func ByDefaultValidityDays(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldDefaultValidityDays, opts...).ToFunc()
}
// ByImagePrice1k orders the results by the image_price_1k field.
func ByImagePrice1k(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldImagePrice1k, opts...).ToFunc()
}
// ByImagePrice2k orders the results by the image_price_2k field.
func ByImagePrice2k(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldImagePrice2k, opts...).ToFunc()
}
// ByImagePrice4k orders the results by the image_price_4k field.
func ByImagePrice4k(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldImagePrice4k, opts...).ToFunc()
}
// ByClaudeCodeOnly orders the results by the claude_code_only field.
func ByClaudeCodeOnly(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldClaudeCodeOnly, opts...).ToFunc()
}
// ByFallbackGroupID orders the results by the fallback_group_id field.
func ByFallbackGroupID(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldFallbackGroupID, opts...).ToFunc()
}
// ByAPIKeysCount orders the results by api_keys count.
func ByAPIKeysCount(opts ...sql.OrderTermOption) OrderOption {
return func(s *sql.Selector) {

View File

@@ -125,6 +125,31 @@ func DefaultValidityDays(v int) predicate.Group {
return predicate.Group(sql.FieldEQ(FieldDefaultValidityDays, v))
}
// ImagePrice1k applies equality check predicate on the "image_price_1k" field. It's identical to ImagePrice1kEQ.
func ImagePrice1k(v float64) predicate.Group {
return predicate.Group(sql.FieldEQ(FieldImagePrice1k, v))
}
// ImagePrice2k applies equality check predicate on the "image_price_2k" field. It's identical to ImagePrice2kEQ.
func ImagePrice2k(v float64) predicate.Group {
return predicate.Group(sql.FieldEQ(FieldImagePrice2k, v))
}
// ImagePrice4k applies equality check predicate on the "image_price_4k" field. It's identical to ImagePrice4kEQ.
func ImagePrice4k(v float64) predicate.Group {
return predicate.Group(sql.FieldEQ(FieldImagePrice4k, v))
}
// ClaudeCodeOnly applies equality check predicate on the "claude_code_only" field. It's identical to ClaudeCodeOnlyEQ.
func ClaudeCodeOnly(v bool) predicate.Group {
return predicate.Group(sql.FieldEQ(FieldClaudeCodeOnly, v))
}
// FallbackGroupID applies equality check predicate on the "fallback_group_id" field. It's identical to FallbackGroupIDEQ.
func FallbackGroupID(v int64) predicate.Group {
return predicate.Group(sql.FieldEQ(FieldFallbackGroupID, v))
}
// CreatedAtEQ applies the EQ predicate on the "created_at" field.
func CreatedAtEQ(v time.Time) predicate.Group {
return predicate.Group(sql.FieldEQ(FieldCreatedAt, v))
@@ -830,6 +855,216 @@ func DefaultValidityDaysLTE(v int) predicate.Group {
return predicate.Group(sql.FieldLTE(FieldDefaultValidityDays, v))
}
// ImagePrice1kEQ applies the EQ predicate on the "image_price_1k" field.
func ImagePrice1kEQ(v float64) predicate.Group {
return predicate.Group(sql.FieldEQ(FieldImagePrice1k, v))
}
// ImagePrice1kNEQ applies the NEQ predicate on the "image_price_1k" field.
func ImagePrice1kNEQ(v float64) predicate.Group {
return predicate.Group(sql.FieldNEQ(FieldImagePrice1k, v))
}
// ImagePrice1kIn applies the In predicate on the "image_price_1k" field.
func ImagePrice1kIn(vs ...float64) predicate.Group {
return predicate.Group(sql.FieldIn(FieldImagePrice1k, vs...))
}
// ImagePrice1kNotIn applies the NotIn predicate on the "image_price_1k" field.
func ImagePrice1kNotIn(vs ...float64) predicate.Group {
return predicate.Group(sql.FieldNotIn(FieldImagePrice1k, vs...))
}
// ImagePrice1kGT applies the GT predicate on the "image_price_1k" field.
func ImagePrice1kGT(v float64) predicate.Group {
return predicate.Group(sql.FieldGT(FieldImagePrice1k, v))
}
// ImagePrice1kGTE applies the GTE predicate on the "image_price_1k" field.
func ImagePrice1kGTE(v float64) predicate.Group {
return predicate.Group(sql.FieldGTE(FieldImagePrice1k, v))
}
// ImagePrice1kLT applies the LT predicate on the "image_price_1k" field.
func ImagePrice1kLT(v float64) predicate.Group {
return predicate.Group(sql.FieldLT(FieldImagePrice1k, v))
}
// ImagePrice1kLTE applies the LTE predicate on the "image_price_1k" field.
func ImagePrice1kLTE(v float64) predicate.Group {
return predicate.Group(sql.FieldLTE(FieldImagePrice1k, v))
}
// ImagePrice1kIsNil applies the IsNil predicate on the "image_price_1k" field.
func ImagePrice1kIsNil() predicate.Group {
return predicate.Group(sql.FieldIsNull(FieldImagePrice1k))
}
// ImagePrice1kNotNil applies the NotNil predicate on the "image_price_1k" field.
func ImagePrice1kNotNil() predicate.Group {
return predicate.Group(sql.FieldNotNull(FieldImagePrice1k))
}
// ImagePrice2kEQ applies the EQ predicate on the "image_price_2k" field.
func ImagePrice2kEQ(v float64) predicate.Group {
return predicate.Group(sql.FieldEQ(FieldImagePrice2k, v))
}
// ImagePrice2kNEQ applies the NEQ predicate on the "image_price_2k" field.
func ImagePrice2kNEQ(v float64) predicate.Group {
return predicate.Group(sql.FieldNEQ(FieldImagePrice2k, v))
}
// ImagePrice2kIn applies the In predicate on the "image_price_2k" field.
func ImagePrice2kIn(vs ...float64) predicate.Group {
return predicate.Group(sql.FieldIn(FieldImagePrice2k, vs...))
}
// ImagePrice2kNotIn applies the NotIn predicate on the "image_price_2k" field.
func ImagePrice2kNotIn(vs ...float64) predicate.Group {
return predicate.Group(sql.FieldNotIn(FieldImagePrice2k, vs...))
}
// ImagePrice2kGT applies the GT predicate on the "image_price_2k" field.
func ImagePrice2kGT(v float64) predicate.Group {
return predicate.Group(sql.FieldGT(FieldImagePrice2k, v))
}
// ImagePrice2kGTE applies the GTE predicate on the "image_price_2k" field.
func ImagePrice2kGTE(v float64) predicate.Group {
return predicate.Group(sql.FieldGTE(FieldImagePrice2k, v))
}
// ImagePrice2kLT applies the LT predicate on the "image_price_2k" field.
func ImagePrice2kLT(v float64) predicate.Group {
return predicate.Group(sql.FieldLT(FieldImagePrice2k, v))
}
// ImagePrice2kLTE applies the LTE predicate on the "image_price_2k" field.
func ImagePrice2kLTE(v float64) predicate.Group {
return predicate.Group(sql.FieldLTE(FieldImagePrice2k, v))
}
// ImagePrice2kIsNil applies the IsNil predicate on the "image_price_2k" field.
func ImagePrice2kIsNil() predicate.Group {
return predicate.Group(sql.FieldIsNull(FieldImagePrice2k))
}
// ImagePrice2kNotNil applies the NotNil predicate on the "image_price_2k" field.
func ImagePrice2kNotNil() predicate.Group {
return predicate.Group(sql.FieldNotNull(FieldImagePrice2k))
}
// ImagePrice4kEQ applies the EQ predicate on the "image_price_4k" field.
func ImagePrice4kEQ(v float64) predicate.Group {
return predicate.Group(sql.FieldEQ(FieldImagePrice4k, v))
}
// ImagePrice4kNEQ applies the NEQ predicate on the "image_price_4k" field.
func ImagePrice4kNEQ(v float64) predicate.Group {
return predicate.Group(sql.FieldNEQ(FieldImagePrice4k, v))
}
// ImagePrice4kIn applies the In predicate on the "image_price_4k" field.
func ImagePrice4kIn(vs ...float64) predicate.Group {
return predicate.Group(sql.FieldIn(FieldImagePrice4k, vs...))
}
// ImagePrice4kNotIn applies the NotIn predicate on the "image_price_4k" field.
func ImagePrice4kNotIn(vs ...float64) predicate.Group {
return predicate.Group(sql.FieldNotIn(FieldImagePrice4k, vs...))
}
// ImagePrice4kGT applies the GT predicate on the "image_price_4k" field.
func ImagePrice4kGT(v float64) predicate.Group {
return predicate.Group(sql.FieldGT(FieldImagePrice4k, v))
}
// ImagePrice4kGTE applies the GTE predicate on the "image_price_4k" field.
func ImagePrice4kGTE(v float64) predicate.Group {
return predicate.Group(sql.FieldGTE(FieldImagePrice4k, v))
}
// ImagePrice4kLT applies the LT predicate on the "image_price_4k" field.
func ImagePrice4kLT(v float64) predicate.Group {
return predicate.Group(sql.FieldLT(FieldImagePrice4k, v))
}
// ImagePrice4kLTE applies the LTE predicate on the "image_price_4k" field.
func ImagePrice4kLTE(v float64) predicate.Group {
return predicate.Group(sql.FieldLTE(FieldImagePrice4k, v))
}
// ImagePrice4kIsNil applies the IsNil predicate on the "image_price_4k" field.
func ImagePrice4kIsNil() predicate.Group {
return predicate.Group(sql.FieldIsNull(FieldImagePrice4k))
}
// ImagePrice4kNotNil applies the NotNil predicate on the "image_price_4k" field.
func ImagePrice4kNotNil() predicate.Group {
return predicate.Group(sql.FieldNotNull(FieldImagePrice4k))
}
// ClaudeCodeOnlyEQ applies the EQ predicate on the "claude_code_only" field.
func ClaudeCodeOnlyEQ(v bool) predicate.Group {
return predicate.Group(sql.FieldEQ(FieldClaudeCodeOnly, v))
}
// ClaudeCodeOnlyNEQ applies the NEQ predicate on the "claude_code_only" field.
func ClaudeCodeOnlyNEQ(v bool) predicate.Group {
return predicate.Group(sql.FieldNEQ(FieldClaudeCodeOnly, v))
}
// FallbackGroupIDEQ applies the EQ predicate on the "fallback_group_id" field.
func FallbackGroupIDEQ(v int64) predicate.Group {
return predicate.Group(sql.FieldEQ(FieldFallbackGroupID, v))
}
// FallbackGroupIDNEQ applies the NEQ predicate on the "fallback_group_id" field.
func FallbackGroupIDNEQ(v int64) predicate.Group {
return predicate.Group(sql.FieldNEQ(FieldFallbackGroupID, v))
}
// FallbackGroupIDIn applies the In predicate on the "fallback_group_id" field.
func FallbackGroupIDIn(vs ...int64) predicate.Group {
return predicate.Group(sql.FieldIn(FieldFallbackGroupID, vs...))
}
// FallbackGroupIDNotIn applies the NotIn predicate on the "fallback_group_id" field.
func FallbackGroupIDNotIn(vs ...int64) predicate.Group {
return predicate.Group(sql.FieldNotIn(FieldFallbackGroupID, vs...))
}
// FallbackGroupIDGT applies the GT predicate on the "fallback_group_id" field.
func FallbackGroupIDGT(v int64) predicate.Group {
return predicate.Group(sql.FieldGT(FieldFallbackGroupID, v))
}
// FallbackGroupIDGTE applies the GTE predicate on the "fallback_group_id" field.
func FallbackGroupIDGTE(v int64) predicate.Group {
return predicate.Group(sql.FieldGTE(FieldFallbackGroupID, v))
}
// FallbackGroupIDLT applies the LT predicate on the "fallback_group_id" field.
func FallbackGroupIDLT(v int64) predicate.Group {
return predicate.Group(sql.FieldLT(FieldFallbackGroupID, v))
}
// FallbackGroupIDLTE applies the LTE predicate on the "fallback_group_id" field.
func FallbackGroupIDLTE(v int64) predicate.Group {
return predicate.Group(sql.FieldLTE(FieldFallbackGroupID, v))
}
// FallbackGroupIDIsNil applies the IsNil predicate on the "fallback_group_id" field.
func FallbackGroupIDIsNil() predicate.Group {
return predicate.Group(sql.FieldIsNull(FieldFallbackGroupID))
}
// FallbackGroupIDNotNil applies the NotNil predicate on the "fallback_group_id" field.
func FallbackGroupIDNotNil() predicate.Group {
return predicate.Group(sql.FieldNotNull(FieldFallbackGroupID))
}
// HasAPIKeys applies the HasEdge predicate on the "api_keys" edge.
func HasAPIKeys() predicate.Group {
return predicate.Group(func(s *sql.Selector) {

View File

@@ -216,6 +216,76 @@ func (_c *GroupCreate) SetNillableDefaultValidityDays(v *int) *GroupCreate {
return _c
}
// SetImagePrice1k sets the "image_price_1k" field.
func (_c *GroupCreate) SetImagePrice1k(v float64) *GroupCreate {
_c.mutation.SetImagePrice1k(v)
return _c
}
// SetNillableImagePrice1k sets the "image_price_1k" field if the given value is not nil.
func (_c *GroupCreate) SetNillableImagePrice1k(v *float64) *GroupCreate {
if v != nil {
_c.SetImagePrice1k(*v)
}
return _c
}
// SetImagePrice2k sets the "image_price_2k" field.
func (_c *GroupCreate) SetImagePrice2k(v float64) *GroupCreate {
_c.mutation.SetImagePrice2k(v)
return _c
}
// SetNillableImagePrice2k sets the "image_price_2k" field if the given value is not nil.
func (_c *GroupCreate) SetNillableImagePrice2k(v *float64) *GroupCreate {
if v != nil {
_c.SetImagePrice2k(*v)
}
return _c
}
// SetImagePrice4k sets the "image_price_4k" field.
func (_c *GroupCreate) SetImagePrice4k(v float64) *GroupCreate {
_c.mutation.SetImagePrice4k(v)
return _c
}
// SetNillableImagePrice4k sets the "image_price_4k" field if the given value is not nil.
func (_c *GroupCreate) SetNillableImagePrice4k(v *float64) *GroupCreate {
if v != nil {
_c.SetImagePrice4k(*v)
}
return _c
}
// SetClaudeCodeOnly sets the "claude_code_only" field.
func (_c *GroupCreate) SetClaudeCodeOnly(v bool) *GroupCreate {
_c.mutation.SetClaudeCodeOnly(v)
return _c
}
// SetNillableClaudeCodeOnly sets the "claude_code_only" field if the given value is not nil.
func (_c *GroupCreate) SetNillableClaudeCodeOnly(v *bool) *GroupCreate {
if v != nil {
_c.SetClaudeCodeOnly(*v)
}
return _c
}
// SetFallbackGroupID sets the "fallback_group_id" field.
func (_c *GroupCreate) SetFallbackGroupID(v int64) *GroupCreate {
_c.mutation.SetFallbackGroupID(v)
return _c
}
// SetNillableFallbackGroupID sets the "fallback_group_id" field if the given value is not nil.
func (_c *GroupCreate) SetNillableFallbackGroupID(v *int64) *GroupCreate {
if v != nil {
_c.SetFallbackGroupID(*v)
}
return _c
}
// AddAPIKeyIDs adds the "api_keys" edge to the APIKey entity by IDs.
func (_c *GroupCreate) AddAPIKeyIDs(ids ...int64) *GroupCreate {
_c.mutation.AddAPIKeyIDs(ids...)
@@ -381,6 +451,10 @@ func (_c *GroupCreate) defaults() error {
v := group.DefaultDefaultValidityDays
_c.mutation.SetDefaultValidityDays(v)
}
if _, ok := _c.mutation.ClaudeCodeOnly(); !ok {
v := group.DefaultClaudeCodeOnly
_c.mutation.SetClaudeCodeOnly(v)
}
return nil
}
@@ -433,6 +507,9 @@ func (_c *GroupCreate) check() error {
if _, ok := _c.mutation.DefaultValidityDays(); !ok {
return &ValidationError{Name: "default_validity_days", err: errors.New(`ent: missing required field "Group.default_validity_days"`)}
}
if _, ok := _c.mutation.ClaudeCodeOnly(); !ok {
return &ValidationError{Name: "claude_code_only", err: errors.New(`ent: missing required field "Group.claude_code_only"`)}
}
return nil
}
@@ -516,6 +593,26 @@ func (_c *GroupCreate) createSpec() (*Group, *sqlgraph.CreateSpec) {
_spec.SetField(group.FieldDefaultValidityDays, field.TypeInt, value)
_node.DefaultValidityDays = value
}
if value, ok := _c.mutation.ImagePrice1k(); ok {
_spec.SetField(group.FieldImagePrice1k, field.TypeFloat64, value)
_node.ImagePrice1k = &value
}
if value, ok := _c.mutation.ImagePrice2k(); ok {
_spec.SetField(group.FieldImagePrice2k, field.TypeFloat64, value)
_node.ImagePrice2k = &value
}
if value, ok := _c.mutation.ImagePrice4k(); ok {
_spec.SetField(group.FieldImagePrice4k, field.TypeFloat64, value)
_node.ImagePrice4k = &value
}
if value, ok := _c.mutation.ClaudeCodeOnly(); ok {
_spec.SetField(group.FieldClaudeCodeOnly, field.TypeBool, value)
_node.ClaudeCodeOnly = value
}
if value, ok := _c.mutation.FallbackGroupID(); ok {
_spec.SetField(group.FieldFallbackGroupID, field.TypeInt64, value)
_node.FallbackGroupID = &value
}
if nodes := _c.mutation.APIKeysIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,
@@ -888,6 +985,114 @@ func (u *GroupUpsert) AddDefaultValidityDays(v int) *GroupUpsert {
return u
}
// SetImagePrice1k sets the "image_price_1k" field.
func (u *GroupUpsert) SetImagePrice1k(v float64) *GroupUpsert {
u.Set(group.FieldImagePrice1k, v)
return u
}
// UpdateImagePrice1k sets the "image_price_1k" field to the value that was provided on create.
func (u *GroupUpsert) UpdateImagePrice1k() *GroupUpsert {
u.SetExcluded(group.FieldImagePrice1k)
return u
}
// AddImagePrice1k adds v to the "image_price_1k" field.
func (u *GroupUpsert) AddImagePrice1k(v float64) *GroupUpsert {
u.Add(group.FieldImagePrice1k, v)
return u
}
// ClearImagePrice1k clears the value of the "image_price_1k" field.
func (u *GroupUpsert) ClearImagePrice1k() *GroupUpsert {
u.SetNull(group.FieldImagePrice1k)
return u
}
// SetImagePrice2k sets the "image_price_2k" field.
func (u *GroupUpsert) SetImagePrice2k(v float64) *GroupUpsert {
u.Set(group.FieldImagePrice2k, v)
return u
}
// UpdateImagePrice2k sets the "image_price_2k" field to the value that was provided on create.
func (u *GroupUpsert) UpdateImagePrice2k() *GroupUpsert {
u.SetExcluded(group.FieldImagePrice2k)
return u
}
// AddImagePrice2k adds v to the "image_price_2k" field.
func (u *GroupUpsert) AddImagePrice2k(v float64) *GroupUpsert {
u.Add(group.FieldImagePrice2k, v)
return u
}
// ClearImagePrice2k clears the value of the "image_price_2k" field.
func (u *GroupUpsert) ClearImagePrice2k() *GroupUpsert {
u.SetNull(group.FieldImagePrice2k)
return u
}
// SetImagePrice4k sets the "image_price_4k" field.
func (u *GroupUpsert) SetImagePrice4k(v float64) *GroupUpsert {
u.Set(group.FieldImagePrice4k, v)
return u
}
// UpdateImagePrice4k sets the "image_price_4k" field to the value that was provided on create.
func (u *GroupUpsert) UpdateImagePrice4k() *GroupUpsert {
u.SetExcluded(group.FieldImagePrice4k)
return u
}
// AddImagePrice4k adds v to the "image_price_4k" field.
func (u *GroupUpsert) AddImagePrice4k(v float64) *GroupUpsert {
u.Add(group.FieldImagePrice4k, v)
return u
}
// ClearImagePrice4k clears the value of the "image_price_4k" field.
func (u *GroupUpsert) ClearImagePrice4k() *GroupUpsert {
u.SetNull(group.FieldImagePrice4k)
return u
}
// SetClaudeCodeOnly sets the "claude_code_only" field.
func (u *GroupUpsert) SetClaudeCodeOnly(v bool) *GroupUpsert {
u.Set(group.FieldClaudeCodeOnly, v)
return u
}
// UpdateClaudeCodeOnly sets the "claude_code_only" field to the value that was provided on create.
func (u *GroupUpsert) UpdateClaudeCodeOnly() *GroupUpsert {
u.SetExcluded(group.FieldClaudeCodeOnly)
return u
}
// SetFallbackGroupID sets the "fallback_group_id" field.
func (u *GroupUpsert) SetFallbackGroupID(v int64) *GroupUpsert {
u.Set(group.FieldFallbackGroupID, v)
return u
}
// UpdateFallbackGroupID sets the "fallback_group_id" field to the value that was provided on create.
func (u *GroupUpsert) UpdateFallbackGroupID() *GroupUpsert {
u.SetExcluded(group.FieldFallbackGroupID)
return u
}
// AddFallbackGroupID adds v to the "fallback_group_id" field.
func (u *GroupUpsert) AddFallbackGroupID(v int64) *GroupUpsert {
u.Add(group.FieldFallbackGroupID, v)
return u
}
// ClearFallbackGroupID clears the value of the "fallback_group_id" field.
func (u *GroupUpsert) ClearFallbackGroupID() *GroupUpsert {
u.SetNull(group.FieldFallbackGroupID)
return u
}
// UpdateNewValues updates the mutable fields using the new values that were set on create.
// Using this option is equivalent to using:
//
@@ -1185,6 +1390,132 @@ func (u *GroupUpsertOne) UpdateDefaultValidityDays() *GroupUpsertOne {
})
}
// SetImagePrice1k sets the "image_price_1k" field.
func (u *GroupUpsertOne) SetImagePrice1k(v float64) *GroupUpsertOne {
return u.Update(func(s *GroupUpsert) {
s.SetImagePrice1k(v)
})
}
// AddImagePrice1k adds v to the "image_price_1k" field.
func (u *GroupUpsertOne) AddImagePrice1k(v float64) *GroupUpsertOne {
return u.Update(func(s *GroupUpsert) {
s.AddImagePrice1k(v)
})
}
// UpdateImagePrice1k sets the "image_price_1k" field to the value that was provided on create.
func (u *GroupUpsertOne) UpdateImagePrice1k() *GroupUpsertOne {
return u.Update(func(s *GroupUpsert) {
s.UpdateImagePrice1k()
})
}
// ClearImagePrice1k clears the value of the "image_price_1k" field.
func (u *GroupUpsertOne) ClearImagePrice1k() *GroupUpsertOne {
return u.Update(func(s *GroupUpsert) {
s.ClearImagePrice1k()
})
}
// SetImagePrice2k sets the "image_price_2k" field.
func (u *GroupUpsertOne) SetImagePrice2k(v float64) *GroupUpsertOne {
return u.Update(func(s *GroupUpsert) {
s.SetImagePrice2k(v)
})
}
// AddImagePrice2k adds v to the "image_price_2k" field.
func (u *GroupUpsertOne) AddImagePrice2k(v float64) *GroupUpsertOne {
return u.Update(func(s *GroupUpsert) {
s.AddImagePrice2k(v)
})
}
// UpdateImagePrice2k sets the "image_price_2k" field to the value that was provided on create.
func (u *GroupUpsertOne) UpdateImagePrice2k() *GroupUpsertOne {
return u.Update(func(s *GroupUpsert) {
s.UpdateImagePrice2k()
})
}
// ClearImagePrice2k clears the value of the "image_price_2k" field.
func (u *GroupUpsertOne) ClearImagePrice2k() *GroupUpsertOne {
return u.Update(func(s *GroupUpsert) {
s.ClearImagePrice2k()
})
}
// SetImagePrice4k sets the "image_price_4k" field.
func (u *GroupUpsertOne) SetImagePrice4k(v float64) *GroupUpsertOne {
return u.Update(func(s *GroupUpsert) {
s.SetImagePrice4k(v)
})
}
// AddImagePrice4k adds v to the "image_price_4k" field.
func (u *GroupUpsertOne) AddImagePrice4k(v float64) *GroupUpsertOne {
return u.Update(func(s *GroupUpsert) {
s.AddImagePrice4k(v)
})
}
// UpdateImagePrice4k sets the "image_price_4k" field to the value that was provided on create.
func (u *GroupUpsertOne) UpdateImagePrice4k() *GroupUpsertOne {
return u.Update(func(s *GroupUpsert) {
s.UpdateImagePrice4k()
})
}
// ClearImagePrice4k clears the value of the "image_price_4k" field.
func (u *GroupUpsertOne) ClearImagePrice4k() *GroupUpsertOne {
return u.Update(func(s *GroupUpsert) {
s.ClearImagePrice4k()
})
}
// SetClaudeCodeOnly sets the "claude_code_only" field.
func (u *GroupUpsertOne) SetClaudeCodeOnly(v bool) *GroupUpsertOne {
return u.Update(func(s *GroupUpsert) {
s.SetClaudeCodeOnly(v)
})
}
// UpdateClaudeCodeOnly sets the "claude_code_only" field to the value that was provided on create.
func (u *GroupUpsertOne) UpdateClaudeCodeOnly() *GroupUpsertOne {
return u.Update(func(s *GroupUpsert) {
s.UpdateClaudeCodeOnly()
})
}
// SetFallbackGroupID sets the "fallback_group_id" field.
func (u *GroupUpsertOne) SetFallbackGroupID(v int64) *GroupUpsertOne {
return u.Update(func(s *GroupUpsert) {
s.SetFallbackGroupID(v)
})
}
// AddFallbackGroupID adds v to the "fallback_group_id" field.
func (u *GroupUpsertOne) AddFallbackGroupID(v int64) *GroupUpsertOne {
return u.Update(func(s *GroupUpsert) {
s.AddFallbackGroupID(v)
})
}
// UpdateFallbackGroupID sets the "fallback_group_id" field to the value that was provided on create.
func (u *GroupUpsertOne) UpdateFallbackGroupID() *GroupUpsertOne {
return u.Update(func(s *GroupUpsert) {
s.UpdateFallbackGroupID()
})
}
// ClearFallbackGroupID clears the value of the "fallback_group_id" field.
func (u *GroupUpsertOne) ClearFallbackGroupID() *GroupUpsertOne {
return u.Update(func(s *GroupUpsert) {
s.ClearFallbackGroupID()
})
}
// Exec executes the query.
func (u *GroupUpsertOne) Exec(ctx context.Context) error {
if len(u.create.conflict) == 0 {
@@ -1648,6 +1979,132 @@ func (u *GroupUpsertBulk) UpdateDefaultValidityDays() *GroupUpsertBulk {
})
}
// SetImagePrice1k sets the "image_price_1k" field.
func (u *GroupUpsertBulk) SetImagePrice1k(v float64) *GroupUpsertBulk {
return u.Update(func(s *GroupUpsert) {
s.SetImagePrice1k(v)
})
}
// AddImagePrice1k adds v to the "image_price_1k" field.
func (u *GroupUpsertBulk) AddImagePrice1k(v float64) *GroupUpsertBulk {
return u.Update(func(s *GroupUpsert) {
s.AddImagePrice1k(v)
})
}
// UpdateImagePrice1k sets the "image_price_1k" field to the value that was provided on create.
func (u *GroupUpsertBulk) UpdateImagePrice1k() *GroupUpsertBulk {
return u.Update(func(s *GroupUpsert) {
s.UpdateImagePrice1k()
})
}
// ClearImagePrice1k clears the value of the "image_price_1k" field.
func (u *GroupUpsertBulk) ClearImagePrice1k() *GroupUpsertBulk {
return u.Update(func(s *GroupUpsert) {
s.ClearImagePrice1k()
})
}
// SetImagePrice2k sets the "image_price_2k" field.
func (u *GroupUpsertBulk) SetImagePrice2k(v float64) *GroupUpsertBulk {
return u.Update(func(s *GroupUpsert) {
s.SetImagePrice2k(v)
})
}
// AddImagePrice2k adds v to the "image_price_2k" field.
func (u *GroupUpsertBulk) AddImagePrice2k(v float64) *GroupUpsertBulk {
return u.Update(func(s *GroupUpsert) {
s.AddImagePrice2k(v)
})
}
// UpdateImagePrice2k sets the "image_price_2k" field to the value that was provided on create.
func (u *GroupUpsertBulk) UpdateImagePrice2k() *GroupUpsertBulk {
return u.Update(func(s *GroupUpsert) {
s.UpdateImagePrice2k()
})
}
// ClearImagePrice2k clears the value of the "image_price_2k" field.
func (u *GroupUpsertBulk) ClearImagePrice2k() *GroupUpsertBulk {
return u.Update(func(s *GroupUpsert) {
s.ClearImagePrice2k()
})
}
// SetImagePrice4k sets the "image_price_4k" field.
func (u *GroupUpsertBulk) SetImagePrice4k(v float64) *GroupUpsertBulk {
return u.Update(func(s *GroupUpsert) {
s.SetImagePrice4k(v)
})
}
// AddImagePrice4k adds v to the "image_price_4k" field.
func (u *GroupUpsertBulk) AddImagePrice4k(v float64) *GroupUpsertBulk {
return u.Update(func(s *GroupUpsert) {
s.AddImagePrice4k(v)
})
}
// UpdateImagePrice4k sets the "image_price_4k" field to the value that was provided on create.
func (u *GroupUpsertBulk) UpdateImagePrice4k() *GroupUpsertBulk {
return u.Update(func(s *GroupUpsert) {
s.UpdateImagePrice4k()
})
}
// ClearImagePrice4k clears the value of the "image_price_4k" field.
func (u *GroupUpsertBulk) ClearImagePrice4k() *GroupUpsertBulk {
return u.Update(func(s *GroupUpsert) {
s.ClearImagePrice4k()
})
}
// SetClaudeCodeOnly sets the "claude_code_only" field.
func (u *GroupUpsertBulk) SetClaudeCodeOnly(v bool) *GroupUpsertBulk {
return u.Update(func(s *GroupUpsert) {
s.SetClaudeCodeOnly(v)
})
}
// UpdateClaudeCodeOnly sets the "claude_code_only" field to the value that was provided on create.
func (u *GroupUpsertBulk) UpdateClaudeCodeOnly() *GroupUpsertBulk {
return u.Update(func(s *GroupUpsert) {
s.UpdateClaudeCodeOnly()
})
}
// SetFallbackGroupID sets the "fallback_group_id" field.
func (u *GroupUpsertBulk) SetFallbackGroupID(v int64) *GroupUpsertBulk {
return u.Update(func(s *GroupUpsert) {
s.SetFallbackGroupID(v)
})
}
// AddFallbackGroupID adds v to the "fallback_group_id" field.
func (u *GroupUpsertBulk) AddFallbackGroupID(v int64) *GroupUpsertBulk {
return u.Update(func(s *GroupUpsert) {
s.AddFallbackGroupID(v)
})
}
// UpdateFallbackGroupID sets the "fallback_group_id" field to the value that was provided on create.
func (u *GroupUpsertBulk) UpdateFallbackGroupID() *GroupUpsertBulk {
return u.Update(func(s *GroupUpsert) {
s.UpdateFallbackGroupID()
})
}
// ClearFallbackGroupID clears the value of the "fallback_group_id" field.
func (u *GroupUpsertBulk) ClearFallbackGroupID() *GroupUpsertBulk {
return u.Update(func(s *GroupUpsert) {
s.ClearFallbackGroupID()
})
}
// Exec executes the query.
func (u *GroupUpsertBulk) Exec(ctx context.Context) error {
if u.create.err != nil {

View File

@@ -9,6 +9,7 @@ import (
"math"
"entgo.io/ent"
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field"
@@ -39,6 +40,7 @@ type GroupQuery struct {
withAllowedUsers *UserQuery
withAccountGroups *AccountGroupQuery
withUserAllowedGroups *UserAllowedGroupQuery
modifiers []func(*sql.Selector)
// intermediate query (i.e. traversal path).
sql *sql.Selector
path func(context.Context) (*sql.Selector, error)
@@ -643,6 +645,9 @@ func (_q *GroupQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*Group,
node.Edges.loadedTypes = loadedTypes
return node.assignValues(columns, values)
}
if len(_q.modifiers) > 0 {
_spec.Modifiers = _q.modifiers
}
for i := range hooks {
hooks[i](ctx, _spec)
}
@@ -1025,6 +1030,9 @@ func (_q *GroupQuery) loadUserAllowedGroups(ctx context.Context, query *UserAllo
func (_q *GroupQuery) sqlCount(ctx context.Context) (int, error) {
_spec := _q.querySpec()
if len(_q.modifiers) > 0 {
_spec.Modifiers = _q.modifiers
}
_spec.Node.Columns = _q.ctx.Fields
if len(_q.ctx.Fields) > 0 {
_spec.Unique = _q.ctx.Unique != nil && *_q.ctx.Unique
@@ -1087,6 +1095,9 @@ func (_q *GroupQuery) sqlQuery(ctx context.Context) *sql.Selector {
if _q.ctx.Unique != nil && *_q.ctx.Unique {
selector.Distinct()
}
for _, m := range _q.modifiers {
m(selector)
}
for _, p := range _q.predicates {
p(selector)
}
@@ -1104,6 +1115,32 @@ func (_q *GroupQuery) sqlQuery(ctx context.Context) *sql.Selector {
return selector
}
// ForUpdate locks the selected rows against concurrent updates, and prevent them from being
// updated, deleted or "selected ... for update" by other sessions, until the transaction is
// either committed or rolled-back.
func (_q *GroupQuery) ForUpdate(opts ...sql.LockOption) *GroupQuery {
if _q.driver.Dialect() == dialect.Postgres {
_q.Unique(false)
}
_q.modifiers = append(_q.modifiers, func(s *sql.Selector) {
s.ForUpdate(opts...)
})
return _q
}
// ForShare behaves similarly to ForUpdate, except that it acquires a shared mode lock
// on any rows that are read. Other sessions can read the rows, but cannot modify them
// until your transaction commits.
func (_q *GroupQuery) ForShare(opts ...sql.LockOption) *GroupQuery {
if _q.driver.Dialect() == dialect.Postgres {
_q.Unique(false)
}
_q.modifiers = append(_q.modifiers, func(s *sql.Selector) {
s.ForShare(opts...)
})
return _q
}
// GroupGroupBy is the group-by builder for Group entities.
type GroupGroupBy struct {
selector

View File

@@ -273,6 +273,128 @@ func (_u *GroupUpdate) AddDefaultValidityDays(v int) *GroupUpdate {
return _u
}
// SetImagePrice1k sets the "image_price_1k" field.
func (_u *GroupUpdate) SetImagePrice1k(v float64) *GroupUpdate {
_u.mutation.ResetImagePrice1k()
_u.mutation.SetImagePrice1k(v)
return _u
}
// SetNillableImagePrice1k sets the "image_price_1k" field if the given value is not nil.
func (_u *GroupUpdate) SetNillableImagePrice1k(v *float64) *GroupUpdate {
if v != nil {
_u.SetImagePrice1k(*v)
}
return _u
}
// AddImagePrice1k adds value to the "image_price_1k" field.
func (_u *GroupUpdate) AddImagePrice1k(v float64) *GroupUpdate {
_u.mutation.AddImagePrice1k(v)
return _u
}
// ClearImagePrice1k clears the value of the "image_price_1k" field.
func (_u *GroupUpdate) ClearImagePrice1k() *GroupUpdate {
_u.mutation.ClearImagePrice1k()
return _u
}
// SetImagePrice2k sets the "image_price_2k" field.
func (_u *GroupUpdate) SetImagePrice2k(v float64) *GroupUpdate {
_u.mutation.ResetImagePrice2k()
_u.mutation.SetImagePrice2k(v)
return _u
}
// SetNillableImagePrice2k sets the "image_price_2k" field if the given value is not nil.
func (_u *GroupUpdate) SetNillableImagePrice2k(v *float64) *GroupUpdate {
if v != nil {
_u.SetImagePrice2k(*v)
}
return _u
}
// AddImagePrice2k adds value to the "image_price_2k" field.
func (_u *GroupUpdate) AddImagePrice2k(v float64) *GroupUpdate {
_u.mutation.AddImagePrice2k(v)
return _u
}
// ClearImagePrice2k clears the value of the "image_price_2k" field.
func (_u *GroupUpdate) ClearImagePrice2k() *GroupUpdate {
_u.mutation.ClearImagePrice2k()
return _u
}
// SetImagePrice4k sets the "image_price_4k" field.
func (_u *GroupUpdate) SetImagePrice4k(v float64) *GroupUpdate {
_u.mutation.ResetImagePrice4k()
_u.mutation.SetImagePrice4k(v)
return _u
}
// SetNillableImagePrice4k sets the "image_price_4k" field if the given value is not nil.
func (_u *GroupUpdate) SetNillableImagePrice4k(v *float64) *GroupUpdate {
if v != nil {
_u.SetImagePrice4k(*v)
}
return _u
}
// AddImagePrice4k adds value to the "image_price_4k" field.
func (_u *GroupUpdate) AddImagePrice4k(v float64) *GroupUpdate {
_u.mutation.AddImagePrice4k(v)
return _u
}
// ClearImagePrice4k clears the value of the "image_price_4k" field.
func (_u *GroupUpdate) ClearImagePrice4k() *GroupUpdate {
_u.mutation.ClearImagePrice4k()
return _u
}
// SetClaudeCodeOnly sets the "claude_code_only" field.
func (_u *GroupUpdate) SetClaudeCodeOnly(v bool) *GroupUpdate {
_u.mutation.SetClaudeCodeOnly(v)
return _u
}
// SetNillableClaudeCodeOnly sets the "claude_code_only" field if the given value is not nil.
func (_u *GroupUpdate) SetNillableClaudeCodeOnly(v *bool) *GroupUpdate {
if v != nil {
_u.SetClaudeCodeOnly(*v)
}
return _u
}
// SetFallbackGroupID sets the "fallback_group_id" field.
func (_u *GroupUpdate) SetFallbackGroupID(v int64) *GroupUpdate {
_u.mutation.ResetFallbackGroupID()
_u.mutation.SetFallbackGroupID(v)
return _u
}
// SetNillableFallbackGroupID sets the "fallback_group_id" field if the given value is not nil.
func (_u *GroupUpdate) SetNillableFallbackGroupID(v *int64) *GroupUpdate {
if v != nil {
_u.SetFallbackGroupID(*v)
}
return _u
}
// AddFallbackGroupID adds value to the "fallback_group_id" field.
func (_u *GroupUpdate) AddFallbackGroupID(v int64) *GroupUpdate {
_u.mutation.AddFallbackGroupID(v)
return _u
}
// ClearFallbackGroupID clears the value of the "fallback_group_id" field.
func (_u *GroupUpdate) ClearFallbackGroupID() *GroupUpdate {
_u.mutation.ClearFallbackGroupID()
return _u
}
// AddAPIKeyIDs adds the "api_keys" edge to the APIKey entity by IDs.
func (_u *GroupUpdate) AddAPIKeyIDs(ids ...int64) *GroupUpdate {
_u.mutation.AddAPIKeyIDs(ids...)
@@ -642,6 +764,45 @@ func (_u *GroupUpdate) sqlSave(ctx context.Context) (_node int, err error) {
if value, ok := _u.mutation.AddedDefaultValidityDays(); ok {
_spec.AddField(group.FieldDefaultValidityDays, field.TypeInt, value)
}
if value, ok := _u.mutation.ImagePrice1k(); ok {
_spec.SetField(group.FieldImagePrice1k, field.TypeFloat64, value)
}
if value, ok := _u.mutation.AddedImagePrice1k(); ok {
_spec.AddField(group.FieldImagePrice1k, field.TypeFloat64, value)
}
if _u.mutation.ImagePrice1kCleared() {
_spec.ClearField(group.FieldImagePrice1k, field.TypeFloat64)
}
if value, ok := _u.mutation.ImagePrice2k(); ok {
_spec.SetField(group.FieldImagePrice2k, field.TypeFloat64, value)
}
if value, ok := _u.mutation.AddedImagePrice2k(); ok {
_spec.AddField(group.FieldImagePrice2k, field.TypeFloat64, value)
}
if _u.mutation.ImagePrice2kCleared() {
_spec.ClearField(group.FieldImagePrice2k, field.TypeFloat64)
}
if value, ok := _u.mutation.ImagePrice4k(); ok {
_spec.SetField(group.FieldImagePrice4k, field.TypeFloat64, value)
}
if value, ok := _u.mutation.AddedImagePrice4k(); ok {
_spec.AddField(group.FieldImagePrice4k, field.TypeFloat64, value)
}
if _u.mutation.ImagePrice4kCleared() {
_spec.ClearField(group.FieldImagePrice4k, field.TypeFloat64)
}
if value, ok := _u.mutation.ClaudeCodeOnly(); ok {
_spec.SetField(group.FieldClaudeCodeOnly, field.TypeBool, value)
}
if value, ok := _u.mutation.FallbackGroupID(); ok {
_spec.SetField(group.FieldFallbackGroupID, field.TypeInt64, value)
}
if value, ok := _u.mutation.AddedFallbackGroupID(); ok {
_spec.AddField(group.FieldFallbackGroupID, field.TypeInt64, value)
}
if _u.mutation.FallbackGroupIDCleared() {
_spec.ClearField(group.FieldFallbackGroupID, field.TypeInt64)
}
if _u.mutation.APIKeysCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,
@@ -1195,6 +1356,128 @@ func (_u *GroupUpdateOne) AddDefaultValidityDays(v int) *GroupUpdateOne {
return _u
}
// SetImagePrice1k sets the "image_price_1k" field.
func (_u *GroupUpdateOne) SetImagePrice1k(v float64) *GroupUpdateOne {
_u.mutation.ResetImagePrice1k()
_u.mutation.SetImagePrice1k(v)
return _u
}
// SetNillableImagePrice1k sets the "image_price_1k" field if the given value is not nil.
func (_u *GroupUpdateOne) SetNillableImagePrice1k(v *float64) *GroupUpdateOne {
if v != nil {
_u.SetImagePrice1k(*v)
}
return _u
}
// AddImagePrice1k adds value to the "image_price_1k" field.
func (_u *GroupUpdateOne) AddImagePrice1k(v float64) *GroupUpdateOne {
_u.mutation.AddImagePrice1k(v)
return _u
}
// ClearImagePrice1k clears the value of the "image_price_1k" field.
func (_u *GroupUpdateOne) ClearImagePrice1k() *GroupUpdateOne {
_u.mutation.ClearImagePrice1k()
return _u
}
// SetImagePrice2k sets the "image_price_2k" field.
func (_u *GroupUpdateOne) SetImagePrice2k(v float64) *GroupUpdateOne {
_u.mutation.ResetImagePrice2k()
_u.mutation.SetImagePrice2k(v)
return _u
}
// SetNillableImagePrice2k sets the "image_price_2k" field if the given value is not nil.
func (_u *GroupUpdateOne) SetNillableImagePrice2k(v *float64) *GroupUpdateOne {
if v != nil {
_u.SetImagePrice2k(*v)
}
return _u
}
// AddImagePrice2k adds value to the "image_price_2k" field.
func (_u *GroupUpdateOne) AddImagePrice2k(v float64) *GroupUpdateOne {
_u.mutation.AddImagePrice2k(v)
return _u
}
// ClearImagePrice2k clears the value of the "image_price_2k" field.
func (_u *GroupUpdateOne) ClearImagePrice2k() *GroupUpdateOne {
_u.mutation.ClearImagePrice2k()
return _u
}
// SetImagePrice4k sets the "image_price_4k" field.
func (_u *GroupUpdateOne) SetImagePrice4k(v float64) *GroupUpdateOne {
_u.mutation.ResetImagePrice4k()
_u.mutation.SetImagePrice4k(v)
return _u
}
// SetNillableImagePrice4k sets the "image_price_4k" field if the given value is not nil.
func (_u *GroupUpdateOne) SetNillableImagePrice4k(v *float64) *GroupUpdateOne {
if v != nil {
_u.SetImagePrice4k(*v)
}
return _u
}
// AddImagePrice4k adds value to the "image_price_4k" field.
func (_u *GroupUpdateOne) AddImagePrice4k(v float64) *GroupUpdateOne {
_u.mutation.AddImagePrice4k(v)
return _u
}
// ClearImagePrice4k clears the value of the "image_price_4k" field.
func (_u *GroupUpdateOne) ClearImagePrice4k() *GroupUpdateOne {
_u.mutation.ClearImagePrice4k()
return _u
}
// SetClaudeCodeOnly sets the "claude_code_only" field.
func (_u *GroupUpdateOne) SetClaudeCodeOnly(v bool) *GroupUpdateOne {
_u.mutation.SetClaudeCodeOnly(v)
return _u
}
// SetNillableClaudeCodeOnly sets the "claude_code_only" field if the given value is not nil.
func (_u *GroupUpdateOne) SetNillableClaudeCodeOnly(v *bool) *GroupUpdateOne {
if v != nil {
_u.SetClaudeCodeOnly(*v)
}
return _u
}
// SetFallbackGroupID sets the "fallback_group_id" field.
func (_u *GroupUpdateOne) SetFallbackGroupID(v int64) *GroupUpdateOne {
_u.mutation.ResetFallbackGroupID()
_u.mutation.SetFallbackGroupID(v)
return _u
}
// SetNillableFallbackGroupID sets the "fallback_group_id" field if the given value is not nil.
func (_u *GroupUpdateOne) SetNillableFallbackGroupID(v *int64) *GroupUpdateOne {
if v != nil {
_u.SetFallbackGroupID(*v)
}
return _u
}
// AddFallbackGroupID adds value to the "fallback_group_id" field.
func (_u *GroupUpdateOne) AddFallbackGroupID(v int64) *GroupUpdateOne {
_u.mutation.AddFallbackGroupID(v)
return _u
}
// ClearFallbackGroupID clears the value of the "fallback_group_id" field.
func (_u *GroupUpdateOne) ClearFallbackGroupID() *GroupUpdateOne {
_u.mutation.ClearFallbackGroupID()
return _u
}
// AddAPIKeyIDs adds the "api_keys" edge to the APIKey entity by IDs.
func (_u *GroupUpdateOne) AddAPIKeyIDs(ids ...int64) *GroupUpdateOne {
_u.mutation.AddAPIKeyIDs(ids...)
@@ -1594,6 +1877,45 @@ func (_u *GroupUpdateOne) sqlSave(ctx context.Context) (_node *Group, err error)
if value, ok := _u.mutation.AddedDefaultValidityDays(); ok {
_spec.AddField(group.FieldDefaultValidityDays, field.TypeInt, value)
}
if value, ok := _u.mutation.ImagePrice1k(); ok {
_spec.SetField(group.FieldImagePrice1k, field.TypeFloat64, value)
}
if value, ok := _u.mutation.AddedImagePrice1k(); ok {
_spec.AddField(group.FieldImagePrice1k, field.TypeFloat64, value)
}
if _u.mutation.ImagePrice1kCleared() {
_spec.ClearField(group.FieldImagePrice1k, field.TypeFloat64)
}
if value, ok := _u.mutation.ImagePrice2k(); ok {
_spec.SetField(group.FieldImagePrice2k, field.TypeFloat64, value)
}
if value, ok := _u.mutation.AddedImagePrice2k(); ok {
_spec.AddField(group.FieldImagePrice2k, field.TypeFloat64, value)
}
if _u.mutation.ImagePrice2kCleared() {
_spec.ClearField(group.FieldImagePrice2k, field.TypeFloat64)
}
if value, ok := _u.mutation.ImagePrice4k(); ok {
_spec.SetField(group.FieldImagePrice4k, field.TypeFloat64, value)
}
if value, ok := _u.mutation.AddedImagePrice4k(); ok {
_spec.AddField(group.FieldImagePrice4k, field.TypeFloat64, value)
}
if _u.mutation.ImagePrice4kCleared() {
_spec.ClearField(group.FieldImagePrice4k, field.TypeFloat64)
}
if value, ok := _u.mutation.ClaudeCodeOnly(); ok {
_spec.SetField(group.FieldClaudeCodeOnly, field.TypeBool, value)
}
if value, ok := _u.mutation.FallbackGroupID(); ok {
_spec.SetField(group.FieldFallbackGroupID, field.TypeInt64, value)
}
if value, ok := _u.mutation.AddedFallbackGroupID(); ok {
_spec.AddField(group.FieldFallbackGroupID, field.TypeInt64, value)
}
if _u.mutation.FallbackGroupIDCleared() {
_spec.ClearField(group.FieldFallbackGroupID, field.TypeInt64)
}
if _u.mutation.APIKeysCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,

View File

@@ -57,6 +57,30 @@ func (f GroupFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Value, error
return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.GroupMutation", m)
}
// The PromoCodeFunc type is an adapter to allow the use of ordinary
// function as PromoCode mutator.
type PromoCodeFunc func(context.Context, *ent.PromoCodeMutation) (ent.Value, error)
// Mutate calls f(ctx, m).
func (f PromoCodeFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Value, error) {
if mv, ok := m.(*ent.PromoCodeMutation); ok {
return f(ctx, mv)
}
return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.PromoCodeMutation", m)
}
// The PromoCodeUsageFunc type is an adapter to allow the use of ordinary
// function as PromoCodeUsage mutator.
type PromoCodeUsageFunc func(context.Context, *ent.PromoCodeUsageMutation) (ent.Value, error)
// Mutate calls f(ctx, m).
func (f PromoCodeUsageFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Value, error) {
if mv, ok := m.(*ent.PromoCodeUsageMutation); ok {
return f(ctx, mv)
}
return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.PromoCodeUsageMutation", m)
}
// The ProxyFunc type is an adapter to allow the use of ordinary
// function as Proxy mutator.
type ProxyFunc func(context.Context, *ent.ProxyMutation) (ent.Value, error)

View File

@@ -13,6 +13,8 @@ import (
"github.com/Wei-Shaw/sub2api/ent/apikey"
"github.com/Wei-Shaw/sub2api/ent/group"
"github.com/Wei-Shaw/sub2api/ent/predicate"
"github.com/Wei-Shaw/sub2api/ent/promocode"
"github.com/Wei-Shaw/sub2api/ent/promocodeusage"
"github.com/Wei-Shaw/sub2api/ent/proxy"
"github.com/Wei-Shaw/sub2api/ent/redeemcode"
"github.com/Wei-Shaw/sub2api/ent/setting"
@@ -188,6 +190,60 @@ func (f TraverseGroup) Traverse(ctx context.Context, q ent.Query) error {
return fmt.Errorf("unexpected query type %T. expect *ent.GroupQuery", q)
}
// The PromoCodeFunc type is an adapter to allow the use of ordinary function as a Querier.
type PromoCodeFunc func(context.Context, *ent.PromoCodeQuery) (ent.Value, error)
// Query calls f(ctx, q).
func (f PromoCodeFunc) Query(ctx context.Context, q ent.Query) (ent.Value, error) {
if q, ok := q.(*ent.PromoCodeQuery); ok {
return f(ctx, q)
}
return nil, fmt.Errorf("unexpected query type %T. expect *ent.PromoCodeQuery", q)
}
// The TraversePromoCode type is an adapter to allow the use of ordinary function as Traverser.
type TraversePromoCode func(context.Context, *ent.PromoCodeQuery) error
// Intercept is a dummy implementation of Intercept that returns the next Querier in the pipeline.
func (f TraversePromoCode) Intercept(next ent.Querier) ent.Querier {
return next
}
// Traverse calls f(ctx, q).
func (f TraversePromoCode) Traverse(ctx context.Context, q ent.Query) error {
if q, ok := q.(*ent.PromoCodeQuery); ok {
return f(ctx, q)
}
return fmt.Errorf("unexpected query type %T. expect *ent.PromoCodeQuery", q)
}
// The PromoCodeUsageFunc type is an adapter to allow the use of ordinary function as a Querier.
type PromoCodeUsageFunc func(context.Context, *ent.PromoCodeUsageQuery) (ent.Value, error)
// Query calls f(ctx, q).
func (f PromoCodeUsageFunc) Query(ctx context.Context, q ent.Query) (ent.Value, error) {
if q, ok := q.(*ent.PromoCodeUsageQuery); ok {
return f(ctx, q)
}
return nil, fmt.Errorf("unexpected query type %T. expect *ent.PromoCodeUsageQuery", q)
}
// The TraversePromoCodeUsage type is an adapter to allow the use of ordinary function as Traverser.
type TraversePromoCodeUsage func(context.Context, *ent.PromoCodeUsageQuery) error
// Intercept is a dummy implementation of Intercept that returns the next Querier in the pipeline.
func (f TraversePromoCodeUsage) Intercept(next ent.Querier) ent.Querier {
return next
}
// Traverse calls f(ctx, q).
func (f TraversePromoCodeUsage) Traverse(ctx context.Context, q ent.Query) error {
if q, ok := q.(*ent.PromoCodeUsageQuery); ok {
return f(ctx, q)
}
return fmt.Errorf("unexpected query type %T. expect *ent.PromoCodeUsageQuery", q)
}
// The ProxyFunc type is an adapter to allow the use of ordinary function as a Querier.
type ProxyFunc func(context.Context, *ent.ProxyQuery) (ent.Value, error)
@@ -442,6 +498,10 @@ func NewQuery(q ent.Query) (Query, error) {
return &query[*ent.AccountGroupQuery, predicate.AccountGroup, accountgroup.OrderOption]{typ: ent.TypeAccountGroup, tq: q}, nil
case *ent.GroupQuery:
return &query[*ent.GroupQuery, predicate.Group, group.OrderOption]{typ: ent.TypeGroup, tq: q}, nil
case *ent.PromoCodeQuery:
return &query[*ent.PromoCodeQuery, predicate.PromoCode, promocode.OrderOption]{typ: ent.TypePromoCode, tq: q}, nil
case *ent.PromoCodeUsageQuery:
return &query[*ent.PromoCodeUsageQuery, predicate.PromoCodeUsage, promocodeusage.OrderOption]{typ: ent.TypePromoCodeUsage, tq: q}, nil
case *ent.ProxyQuery:
return &query[*ent.ProxyQuery, predicate.Proxy, proxy.OrderOption]{typ: ent.TypeProxy, tq: q}, nil
case *ent.RedeemCodeQuery:

View File

@@ -18,6 +18,8 @@ var (
{Name: "key", Type: field.TypeString, Unique: true, Size: 128},
{Name: "name", Type: field.TypeString, Size: 100},
{Name: "status", Type: field.TypeString, Size: 20, Default: "active"},
{Name: "ip_whitelist", Type: field.TypeJSON, Nullable: true},
{Name: "ip_blacklist", Type: field.TypeJSON, Nullable: true},
{Name: "group_id", Type: field.TypeInt64, Nullable: true},
{Name: "user_id", Type: field.TypeInt64},
}
@@ -29,13 +31,13 @@ var (
ForeignKeys: []*schema.ForeignKey{
{
Symbol: "api_keys_groups_api_keys",
Columns: []*schema.Column{APIKeysColumns[7]},
Columns: []*schema.Column{APIKeysColumns[9]},
RefColumns: []*schema.Column{GroupsColumns[0]},
OnDelete: schema.SetNull,
},
{
Symbol: "api_keys_users_api_keys",
Columns: []*schema.Column{APIKeysColumns[8]},
Columns: []*schema.Column{APIKeysColumns[10]},
RefColumns: []*schema.Column{UsersColumns[0]},
OnDelete: schema.NoAction,
},
@@ -44,12 +46,12 @@ var (
{
Name: "apikey_user_id",
Unique: false,
Columns: []*schema.Column{APIKeysColumns[8]},
Columns: []*schema.Column{APIKeysColumns[10]},
},
{
Name: "apikey_group_id",
Unique: false,
Columns: []*schema.Column{APIKeysColumns[7]},
Columns: []*schema.Column{APIKeysColumns[9]},
},
{
Name: "apikey_status",
@@ -80,6 +82,8 @@ var (
{Name: "status", Type: field.TypeString, Size: 20, Default: "active"},
{Name: "error_message", Type: field.TypeString, Nullable: true, SchemaType: map[string]string{"postgres": "text"}},
{Name: "last_used_at", Type: field.TypeTime, Nullable: true, SchemaType: map[string]string{"postgres": "timestamptz"}},
{Name: "expires_at", Type: field.TypeTime, Nullable: true, SchemaType: map[string]string{"postgres": "timestamptz"}},
{Name: "auto_pause_on_expired", Type: field.TypeBool, Default: true},
{Name: "schedulable", Type: field.TypeBool, Default: true},
{Name: "rate_limited_at", Type: field.TypeTime, Nullable: true, SchemaType: map[string]string{"postgres": "timestamptz"}},
{Name: "rate_limit_reset_at", Type: field.TypeTime, Nullable: true, SchemaType: map[string]string{"postgres": "timestamptz"}},
@@ -97,7 +101,7 @@ var (
ForeignKeys: []*schema.ForeignKey{
{
Symbol: "accounts_proxies_proxy",
Columns: []*schema.Column{AccountsColumns[22]},
Columns: []*schema.Column{AccountsColumns[24]},
RefColumns: []*schema.Column{ProxiesColumns[0]},
OnDelete: schema.SetNull,
},
@@ -121,7 +125,7 @@ var (
{
Name: "account_proxy_id",
Unique: false,
Columns: []*schema.Column{AccountsColumns[22]},
Columns: []*schema.Column{AccountsColumns[24]},
},
{
Name: "account_priority",
@@ -136,22 +140,22 @@ var (
{
Name: "account_schedulable",
Unique: false,
Columns: []*schema.Column{AccountsColumns[15]},
Columns: []*schema.Column{AccountsColumns[17]},
},
{
Name: "account_rate_limited_at",
Unique: false,
Columns: []*schema.Column{AccountsColumns[16]},
Columns: []*schema.Column{AccountsColumns[18]},
},
{
Name: "account_rate_limit_reset_at",
Unique: false,
Columns: []*schema.Column{AccountsColumns[17]},
Columns: []*schema.Column{AccountsColumns[19]},
},
{
Name: "account_overload_until",
Unique: false,
Columns: []*schema.Column{AccountsColumns[18]},
Columns: []*schema.Column{AccountsColumns[20]},
},
{
Name: "account_deleted_at",
@@ -216,6 +220,11 @@ var (
{Name: "weekly_limit_usd", Type: field.TypeFloat64, Nullable: true, SchemaType: map[string]string{"postgres": "decimal(20,8)"}},
{Name: "monthly_limit_usd", Type: field.TypeFloat64, Nullable: true, SchemaType: map[string]string{"postgres": "decimal(20,8)"}},
{Name: "default_validity_days", Type: field.TypeInt, Default: 30},
{Name: "image_price_1k", Type: field.TypeFloat64, Nullable: true, SchemaType: map[string]string{"postgres": "decimal(20,8)"}},
{Name: "image_price_2k", Type: field.TypeFloat64, Nullable: true, SchemaType: map[string]string{"postgres": "decimal(20,8)"}},
{Name: "image_price_4k", Type: field.TypeFloat64, Nullable: true, SchemaType: map[string]string{"postgres": "decimal(20,8)"}},
{Name: "claude_code_only", Type: field.TypeBool, Default: false},
{Name: "fallback_group_id", Type: field.TypeInt64, Nullable: true},
}
// GroupsTable holds the schema information for the "groups" table.
GroupsTable = &schema.Table{
@@ -250,6 +259,82 @@ var (
},
},
}
// PromoCodesColumns holds the columns for the "promo_codes" table.
PromoCodesColumns = []*schema.Column{
{Name: "id", Type: field.TypeInt64, Increment: true},
{Name: "code", Type: field.TypeString, Unique: true, Size: 32},
{Name: "bonus_amount", Type: field.TypeFloat64, Default: 0, SchemaType: map[string]string{"postgres": "decimal(20,8)"}},
{Name: "max_uses", Type: field.TypeInt, Default: 0},
{Name: "used_count", Type: field.TypeInt, Default: 0},
{Name: "status", Type: field.TypeString, Size: 20, Default: "active"},
{Name: "expires_at", Type: field.TypeTime, Nullable: true, SchemaType: map[string]string{"postgres": "timestamptz"}},
{Name: "notes", Type: field.TypeString, Nullable: true, SchemaType: map[string]string{"postgres": "text"}},
{Name: "created_at", Type: field.TypeTime, SchemaType: map[string]string{"postgres": "timestamptz"}},
{Name: "updated_at", Type: field.TypeTime, SchemaType: map[string]string{"postgres": "timestamptz"}},
}
// PromoCodesTable holds the schema information for the "promo_codes" table.
PromoCodesTable = &schema.Table{
Name: "promo_codes",
Columns: PromoCodesColumns,
PrimaryKey: []*schema.Column{PromoCodesColumns[0]},
Indexes: []*schema.Index{
{
Name: "promocode_status",
Unique: false,
Columns: []*schema.Column{PromoCodesColumns[5]},
},
{
Name: "promocode_expires_at",
Unique: false,
Columns: []*schema.Column{PromoCodesColumns[6]},
},
},
}
// PromoCodeUsagesColumns holds the columns for the "promo_code_usages" table.
PromoCodeUsagesColumns = []*schema.Column{
{Name: "id", Type: field.TypeInt64, Increment: true},
{Name: "bonus_amount", Type: field.TypeFloat64, SchemaType: map[string]string{"postgres": "decimal(20,8)"}},
{Name: "used_at", Type: field.TypeTime, SchemaType: map[string]string{"postgres": "timestamptz"}},
{Name: "promo_code_id", Type: field.TypeInt64},
{Name: "user_id", Type: field.TypeInt64},
}
// PromoCodeUsagesTable holds the schema information for the "promo_code_usages" table.
PromoCodeUsagesTable = &schema.Table{
Name: "promo_code_usages",
Columns: PromoCodeUsagesColumns,
PrimaryKey: []*schema.Column{PromoCodeUsagesColumns[0]},
ForeignKeys: []*schema.ForeignKey{
{
Symbol: "promo_code_usages_promo_codes_usage_records",
Columns: []*schema.Column{PromoCodeUsagesColumns[3]},
RefColumns: []*schema.Column{PromoCodesColumns[0]},
OnDelete: schema.NoAction,
},
{
Symbol: "promo_code_usages_users_promo_code_usages",
Columns: []*schema.Column{PromoCodeUsagesColumns[4]},
RefColumns: []*schema.Column{UsersColumns[0]},
OnDelete: schema.NoAction,
},
},
Indexes: []*schema.Index{
{
Name: "promocodeusage_promo_code_id",
Unique: false,
Columns: []*schema.Column{PromoCodeUsagesColumns[3]},
},
{
Name: "promocodeusage_user_id",
Unique: false,
Columns: []*schema.Column{PromoCodeUsagesColumns[4]},
},
{
Name: "promocodeusage_promo_code_id_user_id",
Unique: true,
Columns: []*schema.Column{PromoCodeUsagesColumns[3], PromoCodeUsagesColumns[4]},
},
},
}
// ProxiesColumns holds the columns for the "proxies" table.
ProxiesColumns = []*schema.Column{
{Name: "id", Type: field.TypeInt64, Increment: true},
@@ -368,6 +453,10 @@ var (
{Name: "stream", Type: field.TypeBool, Default: false},
{Name: "duration_ms", Type: field.TypeInt, Nullable: true},
{Name: "first_token_ms", Type: field.TypeInt, Nullable: true},
{Name: "user_agent", Type: field.TypeString, Nullable: true, Size: 512},
{Name: "ip_address", Type: field.TypeString, Nullable: true, Size: 45},
{Name: "image_count", Type: field.TypeInt, Default: 0},
{Name: "image_size", Type: field.TypeString, Nullable: true, Size: 10},
{Name: "created_at", Type: field.TypeTime, SchemaType: map[string]string{"postgres": "timestamptz"}},
{Name: "api_key_id", Type: field.TypeInt64},
{Name: "account_id", Type: field.TypeInt64},
@@ -383,31 +472,31 @@ var (
ForeignKeys: []*schema.ForeignKey{
{
Symbol: "usage_logs_api_keys_usage_logs",
Columns: []*schema.Column{UsageLogsColumns[21]},
Columns: []*schema.Column{UsageLogsColumns[25]},
RefColumns: []*schema.Column{APIKeysColumns[0]},
OnDelete: schema.NoAction,
},
{
Symbol: "usage_logs_accounts_usage_logs",
Columns: []*schema.Column{UsageLogsColumns[22]},
Columns: []*schema.Column{UsageLogsColumns[26]},
RefColumns: []*schema.Column{AccountsColumns[0]},
OnDelete: schema.NoAction,
},
{
Symbol: "usage_logs_groups_usage_logs",
Columns: []*schema.Column{UsageLogsColumns[23]},
Columns: []*schema.Column{UsageLogsColumns[27]},
RefColumns: []*schema.Column{GroupsColumns[0]},
OnDelete: schema.SetNull,
},
{
Symbol: "usage_logs_users_usage_logs",
Columns: []*schema.Column{UsageLogsColumns[24]},
Columns: []*schema.Column{UsageLogsColumns[28]},
RefColumns: []*schema.Column{UsersColumns[0]},
OnDelete: schema.NoAction,
},
{
Symbol: "usage_logs_user_subscriptions_usage_logs",
Columns: []*schema.Column{UsageLogsColumns[25]},
Columns: []*schema.Column{UsageLogsColumns[29]},
RefColumns: []*schema.Column{UserSubscriptionsColumns[0]},
OnDelete: schema.SetNull,
},
@@ -416,32 +505,32 @@ var (
{
Name: "usagelog_user_id",
Unique: false,
Columns: []*schema.Column{UsageLogsColumns[24]},
Columns: []*schema.Column{UsageLogsColumns[28]},
},
{
Name: "usagelog_api_key_id",
Unique: false,
Columns: []*schema.Column{UsageLogsColumns[21]},
Columns: []*schema.Column{UsageLogsColumns[25]},
},
{
Name: "usagelog_account_id",
Unique: false,
Columns: []*schema.Column{UsageLogsColumns[22]},
Columns: []*schema.Column{UsageLogsColumns[26]},
},
{
Name: "usagelog_group_id",
Unique: false,
Columns: []*schema.Column{UsageLogsColumns[23]},
Columns: []*schema.Column{UsageLogsColumns[27]},
},
{
Name: "usagelog_subscription_id",
Unique: false,
Columns: []*schema.Column{UsageLogsColumns[25]},
Columns: []*schema.Column{UsageLogsColumns[29]},
},
{
Name: "usagelog_created_at",
Unique: false,
Columns: []*schema.Column{UsageLogsColumns[20]},
Columns: []*schema.Column{UsageLogsColumns[24]},
},
{
Name: "usagelog_model",
@@ -456,12 +545,12 @@ var (
{
Name: "usagelog_user_id_created_at",
Unique: false,
Columns: []*schema.Column{UsageLogsColumns[24], UsageLogsColumns[20]},
Columns: []*schema.Column{UsageLogsColumns[28], UsageLogsColumns[24]},
},
{
Name: "usagelog_api_key_id_created_at",
Unique: false,
Columns: []*schema.Column{UsageLogsColumns[21], UsageLogsColumns[20]},
Columns: []*schema.Column{UsageLogsColumns[25], UsageLogsColumns[24]},
},
},
}
@@ -707,6 +796,8 @@ var (
AccountsTable,
AccountGroupsTable,
GroupsTable,
PromoCodesTable,
PromoCodeUsagesTable,
ProxiesTable,
RedeemCodesTable,
SettingsTable,
@@ -737,6 +828,14 @@ func init() {
GroupsTable.Annotation = &entsql.Annotation{
Table: "groups",
}
PromoCodesTable.Annotation = &entsql.Annotation{
Table: "promo_codes",
}
PromoCodeUsagesTable.ForeignKeys[0].RefTable = PromoCodesTable
PromoCodeUsagesTable.ForeignKeys[1].RefTable = UsersTable
PromoCodeUsagesTable.Annotation = &entsql.Annotation{
Table: "promo_code_usages",
}
ProxiesTable.Annotation = &entsql.Annotation{
Table: "proxies",
}

File diff suppressed because it is too large Load Diff

View File

@@ -18,6 +18,12 @@ type AccountGroup func(*sql.Selector)
// Group is the predicate function for group builders.
type Group func(*sql.Selector)
// PromoCode is the predicate function for promocode builders.
type PromoCode func(*sql.Selector)
// PromoCodeUsage is the predicate function for promocodeusage builders.
type PromoCodeUsage func(*sql.Selector)
// Proxy is the predicate function for proxy builders.
type Proxy func(*sql.Selector)

228
backend/ent/promocode.go Normal file
View File

@@ -0,0 +1,228 @@
// Code generated by ent, DO NOT EDIT.
package ent
import (
"fmt"
"strings"
"time"
"entgo.io/ent"
"entgo.io/ent/dialect/sql"
"github.com/Wei-Shaw/sub2api/ent/promocode"
)
// PromoCode is the model entity for the PromoCode schema.
type PromoCode struct {
config `json:"-"`
// ID of the ent.
ID int64 `json:"id,omitempty"`
// 优惠码
Code string `json:"code,omitempty"`
// 赠送余额金额
BonusAmount float64 `json:"bonus_amount,omitempty"`
// 最大使用次数0表示无限制
MaxUses int `json:"max_uses,omitempty"`
// 已使用次数
UsedCount int `json:"used_count,omitempty"`
// 状态: active, disabled
Status string `json:"status,omitempty"`
// 过期时间null表示永不过期
ExpiresAt *time.Time `json:"expires_at,omitempty"`
// 备注
Notes *string `json:"notes,omitempty"`
// CreatedAt holds the value of the "created_at" field.
CreatedAt time.Time `json:"created_at,omitempty"`
// UpdatedAt holds the value of the "updated_at" field.
UpdatedAt time.Time `json:"updated_at,omitempty"`
// Edges holds the relations/edges for other nodes in the graph.
// The values are being populated by the PromoCodeQuery when eager-loading is set.
Edges PromoCodeEdges `json:"edges"`
selectValues sql.SelectValues
}
// PromoCodeEdges holds the relations/edges for other nodes in the graph.
type PromoCodeEdges struct {
// UsageRecords holds the value of the usage_records edge.
UsageRecords []*PromoCodeUsage `json:"usage_records,omitempty"`
// loadedTypes holds the information for reporting if a
// type was loaded (or requested) in eager-loading or not.
loadedTypes [1]bool
}
// UsageRecordsOrErr returns the UsageRecords value or an error if the edge
// was not loaded in eager-loading.
func (e PromoCodeEdges) UsageRecordsOrErr() ([]*PromoCodeUsage, error) {
if e.loadedTypes[0] {
return e.UsageRecords, nil
}
return nil, &NotLoadedError{edge: "usage_records"}
}
// scanValues returns the types for scanning values from sql.Rows.
func (*PromoCode) scanValues(columns []string) ([]any, error) {
values := make([]any, len(columns))
for i := range columns {
switch columns[i] {
case promocode.FieldBonusAmount:
values[i] = new(sql.NullFloat64)
case promocode.FieldID, promocode.FieldMaxUses, promocode.FieldUsedCount:
values[i] = new(sql.NullInt64)
case promocode.FieldCode, promocode.FieldStatus, promocode.FieldNotes:
values[i] = new(sql.NullString)
case promocode.FieldExpiresAt, promocode.FieldCreatedAt, promocode.FieldUpdatedAt:
values[i] = new(sql.NullTime)
default:
values[i] = new(sql.UnknownType)
}
}
return values, nil
}
// assignValues assigns the values that were returned from sql.Rows (after scanning)
// to the PromoCode fields.
func (_m *PromoCode) assignValues(columns []string, values []any) error {
if m, n := len(values), len(columns); m < n {
return fmt.Errorf("mismatch number of scan values: %d != %d", m, n)
}
for i := range columns {
switch columns[i] {
case promocode.FieldID:
value, ok := values[i].(*sql.NullInt64)
if !ok {
return fmt.Errorf("unexpected type %T for field id", value)
}
_m.ID = int64(value.Int64)
case promocode.FieldCode:
if value, ok := values[i].(*sql.NullString); !ok {
return fmt.Errorf("unexpected type %T for field code", values[i])
} else if value.Valid {
_m.Code = value.String
}
case promocode.FieldBonusAmount:
if value, ok := values[i].(*sql.NullFloat64); !ok {
return fmt.Errorf("unexpected type %T for field bonus_amount", values[i])
} else if value.Valid {
_m.BonusAmount = value.Float64
}
case promocode.FieldMaxUses:
if value, ok := values[i].(*sql.NullInt64); !ok {
return fmt.Errorf("unexpected type %T for field max_uses", values[i])
} else if value.Valid {
_m.MaxUses = int(value.Int64)
}
case promocode.FieldUsedCount:
if value, ok := values[i].(*sql.NullInt64); !ok {
return fmt.Errorf("unexpected type %T for field used_count", values[i])
} else if value.Valid {
_m.UsedCount = int(value.Int64)
}
case promocode.FieldStatus:
if value, ok := values[i].(*sql.NullString); !ok {
return fmt.Errorf("unexpected type %T for field status", values[i])
} else if value.Valid {
_m.Status = value.String
}
case promocode.FieldExpiresAt:
if value, ok := values[i].(*sql.NullTime); !ok {
return fmt.Errorf("unexpected type %T for field expires_at", values[i])
} else if value.Valid {
_m.ExpiresAt = new(time.Time)
*_m.ExpiresAt = value.Time
}
case promocode.FieldNotes:
if value, ok := values[i].(*sql.NullString); !ok {
return fmt.Errorf("unexpected type %T for field notes", values[i])
} else if value.Valid {
_m.Notes = new(string)
*_m.Notes = value.String
}
case promocode.FieldCreatedAt:
if value, ok := values[i].(*sql.NullTime); !ok {
return fmt.Errorf("unexpected type %T for field created_at", values[i])
} else if value.Valid {
_m.CreatedAt = value.Time
}
case promocode.FieldUpdatedAt:
if value, ok := values[i].(*sql.NullTime); !ok {
return fmt.Errorf("unexpected type %T for field updated_at", values[i])
} else if value.Valid {
_m.UpdatedAt = value.Time
}
default:
_m.selectValues.Set(columns[i], values[i])
}
}
return nil
}
// Value returns the ent.Value that was dynamically selected and assigned to the PromoCode.
// This includes values selected through modifiers, order, etc.
func (_m *PromoCode) Value(name string) (ent.Value, error) {
return _m.selectValues.Get(name)
}
// QueryUsageRecords queries the "usage_records" edge of the PromoCode entity.
func (_m *PromoCode) QueryUsageRecords() *PromoCodeUsageQuery {
return NewPromoCodeClient(_m.config).QueryUsageRecords(_m)
}
// Update returns a builder for updating this PromoCode.
// Note that you need to call PromoCode.Unwrap() before calling this method if this PromoCode
// was returned from a transaction, and the transaction was committed or rolled back.
func (_m *PromoCode) Update() *PromoCodeUpdateOne {
return NewPromoCodeClient(_m.config).UpdateOne(_m)
}
// Unwrap unwraps the PromoCode entity that was returned from a transaction after it was closed,
// so that all future queries will be executed through the driver which created the transaction.
func (_m *PromoCode) Unwrap() *PromoCode {
_tx, ok := _m.config.driver.(*txDriver)
if !ok {
panic("ent: PromoCode is not a transactional entity")
}
_m.config.driver = _tx.drv
return _m
}
// String implements the fmt.Stringer.
func (_m *PromoCode) String() string {
var builder strings.Builder
builder.WriteString("PromoCode(")
builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID))
builder.WriteString("code=")
builder.WriteString(_m.Code)
builder.WriteString(", ")
builder.WriteString("bonus_amount=")
builder.WriteString(fmt.Sprintf("%v", _m.BonusAmount))
builder.WriteString(", ")
builder.WriteString("max_uses=")
builder.WriteString(fmt.Sprintf("%v", _m.MaxUses))
builder.WriteString(", ")
builder.WriteString("used_count=")
builder.WriteString(fmt.Sprintf("%v", _m.UsedCount))
builder.WriteString(", ")
builder.WriteString("status=")
builder.WriteString(_m.Status)
builder.WriteString(", ")
if v := _m.ExpiresAt; v != nil {
builder.WriteString("expires_at=")
builder.WriteString(v.Format(time.ANSIC))
}
builder.WriteString(", ")
if v := _m.Notes; v != nil {
builder.WriteString("notes=")
builder.WriteString(*v)
}
builder.WriteString(", ")
builder.WriteString("created_at=")
builder.WriteString(_m.CreatedAt.Format(time.ANSIC))
builder.WriteString(", ")
builder.WriteString("updated_at=")
builder.WriteString(_m.UpdatedAt.Format(time.ANSIC))
builder.WriteByte(')')
return builder.String()
}
// PromoCodes is a parsable slice of PromoCode.
type PromoCodes []*PromoCode

View File

@@ -0,0 +1,165 @@
// Code generated by ent, DO NOT EDIT.
package promocode
import (
"time"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
)
const (
// Label holds the string label denoting the promocode type in the database.
Label = "promo_code"
// FieldID holds the string denoting the id field in the database.
FieldID = "id"
// FieldCode holds the string denoting the code field in the database.
FieldCode = "code"
// FieldBonusAmount holds the string denoting the bonus_amount field in the database.
FieldBonusAmount = "bonus_amount"
// FieldMaxUses holds the string denoting the max_uses field in the database.
FieldMaxUses = "max_uses"
// FieldUsedCount holds the string denoting the used_count field in the database.
FieldUsedCount = "used_count"
// FieldStatus holds the string denoting the status field in the database.
FieldStatus = "status"
// FieldExpiresAt holds the string denoting the expires_at field in the database.
FieldExpiresAt = "expires_at"
// FieldNotes holds the string denoting the notes field in the database.
FieldNotes = "notes"
// FieldCreatedAt holds the string denoting the created_at field in the database.
FieldCreatedAt = "created_at"
// FieldUpdatedAt holds the string denoting the updated_at field in the database.
FieldUpdatedAt = "updated_at"
// EdgeUsageRecords holds the string denoting the usage_records edge name in mutations.
EdgeUsageRecords = "usage_records"
// Table holds the table name of the promocode in the database.
Table = "promo_codes"
// UsageRecordsTable is the table that holds the usage_records relation/edge.
UsageRecordsTable = "promo_code_usages"
// UsageRecordsInverseTable is the table name for the PromoCodeUsage entity.
// It exists in this package in order to avoid circular dependency with the "promocodeusage" package.
UsageRecordsInverseTable = "promo_code_usages"
// UsageRecordsColumn is the table column denoting the usage_records relation/edge.
UsageRecordsColumn = "promo_code_id"
)
// Columns holds all SQL columns for promocode fields.
var Columns = []string{
FieldID,
FieldCode,
FieldBonusAmount,
FieldMaxUses,
FieldUsedCount,
FieldStatus,
FieldExpiresAt,
FieldNotes,
FieldCreatedAt,
FieldUpdatedAt,
}
// ValidColumn reports if the column name is valid (part of the table columns).
func ValidColumn(column string) bool {
for i := range Columns {
if column == Columns[i] {
return true
}
}
return false
}
var (
// CodeValidator is a validator for the "code" field. It is called by the builders before save.
CodeValidator func(string) error
// DefaultBonusAmount holds the default value on creation for the "bonus_amount" field.
DefaultBonusAmount float64
// DefaultMaxUses holds the default value on creation for the "max_uses" field.
DefaultMaxUses int
// DefaultUsedCount holds the default value on creation for the "used_count" field.
DefaultUsedCount int
// DefaultStatus holds the default value on creation for the "status" field.
DefaultStatus string
// StatusValidator is a validator for the "status" field. It is called by the builders before save.
StatusValidator func(string) error
// DefaultCreatedAt holds the default value on creation for the "created_at" field.
DefaultCreatedAt func() time.Time
// DefaultUpdatedAt holds the default value on creation for the "updated_at" field.
DefaultUpdatedAt func() time.Time
// UpdateDefaultUpdatedAt holds the default value on update for the "updated_at" field.
UpdateDefaultUpdatedAt func() time.Time
)
// OrderOption defines the ordering options for the PromoCode queries.
type OrderOption func(*sql.Selector)
// ByID orders the results by the id field.
func ByID(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldID, opts...).ToFunc()
}
// ByCode orders the results by the code field.
func ByCode(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldCode, opts...).ToFunc()
}
// ByBonusAmount orders the results by the bonus_amount field.
func ByBonusAmount(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldBonusAmount, opts...).ToFunc()
}
// ByMaxUses orders the results by the max_uses field.
func ByMaxUses(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldMaxUses, opts...).ToFunc()
}
// ByUsedCount orders the results by the used_count field.
func ByUsedCount(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldUsedCount, opts...).ToFunc()
}
// ByStatus orders the results by the status field.
func ByStatus(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldStatus, opts...).ToFunc()
}
// ByExpiresAt orders the results by the expires_at field.
func ByExpiresAt(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldExpiresAt, opts...).ToFunc()
}
// ByNotes orders the results by the notes field.
func ByNotes(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldNotes, opts...).ToFunc()
}
// ByCreatedAt orders the results by the created_at field.
func ByCreatedAt(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldCreatedAt, opts...).ToFunc()
}
// ByUpdatedAt orders the results by the updated_at field.
func ByUpdatedAt(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldUpdatedAt, opts...).ToFunc()
}
// ByUsageRecordsCount orders the results by usage_records count.
func ByUsageRecordsCount(opts ...sql.OrderTermOption) OrderOption {
return func(s *sql.Selector) {
sqlgraph.OrderByNeighborsCount(s, newUsageRecordsStep(), opts...)
}
}
// ByUsageRecords orders the results by usage_records terms.
func ByUsageRecords(term sql.OrderTerm, terms ...sql.OrderTerm) OrderOption {
return func(s *sql.Selector) {
sqlgraph.OrderByNeighborTerms(s, newUsageRecordsStep(), append([]sql.OrderTerm{term}, terms...)...)
}
}
func newUsageRecordsStep() *sqlgraph.Step {
return sqlgraph.NewStep(
sqlgraph.From(Table, FieldID),
sqlgraph.To(UsageRecordsInverseTable, FieldID),
sqlgraph.Edge(sqlgraph.O2M, false, UsageRecordsTable, UsageRecordsColumn),
)
}

View File

@@ -0,0 +1,594 @@
// Code generated by ent, DO NOT EDIT.
package promocode
import (
"time"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"github.com/Wei-Shaw/sub2api/ent/predicate"
)
// ID filters vertices based on their ID field.
func ID(id int64) predicate.PromoCode {
return predicate.PromoCode(sql.FieldEQ(FieldID, id))
}
// IDEQ applies the EQ predicate on the ID field.
func IDEQ(id int64) predicate.PromoCode {
return predicate.PromoCode(sql.FieldEQ(FieldID, id))
}
// IDNEQ applies the NEQ predicate on the ID field.
func IDNEQ(id int64) predicate.PromoCode {
return predicate.PromoCode(sql.FieldNEQ(FieldID, id))
}
// IDIn applies the In predicate on the ID field.
func IDIn(ids ...int64) predicate.PromoCode {
return predicate.PromoCode(sql.FieldIn(FieldID, ids...))
}
// IDNotIn applies the NotIn predicate on the ID field.
func IDNotIn(ids ...int64) predicate.PromoCode {
return predicate.PromoCode(sql.FieldNotIn(FieldID, ids...))
}
// IDGT applies the GT predicate on the ID field.
func IDGT(id int64) predicate.PromoCode {
return predicate.PromoCode(sql.FieldGT(FieldID, id))
}
// IDGTE applies the GTE predicate on the ID field.
func IDGTE(id int64) predicate.PromoCode {
return predicate.PromoCode(sql.FieldGTE(FieldID, id))
}
// IDLT applies the LT predicate on the ID field.
func IDLT(id int64) predicate.PromoCode {
return predicate.PromoCode(sql.FieldLT(FieldID, id))
}
// IDLTE applies the LTE predicate on the ID field.
func IDLTE(id int64) predicate.PromoCode {
return predicate.PromoCode(sql.FieldLTE(FieldID, id))
}
// Code applies equality check predicate on the "code" field. It's identical to CodeEQ.
func Code(v string) predicate.PromoCode {
return predicate.PromoCode(sql.FieldEQ(FieldCode, v))
}
// BonusAmount applies equality check predicate on the "bonus_amount" field. It's identical to BonusAmountEQ.
func BonusAmount(v float64) predicate.PromoCode {
return predicate.PromoCode(sql.FieldEQ(FieldBonusAmount, v))
}
// MaxUses applies equality check predicate on the "max_uses" field. It's identical to MaxUsesEQ.
func MaxUses(v int) predicate.PromoCode {
return predicate.PromoCode(sql.FieldEQ(FieldMaxUses, v))
}
// UsedCount applies equality check predicate on the "used_count" field. It's identical to UsedCountEQ.
func UsedCount(v int) predicate.PromoCode {
return predicate.PromoCode(sql.FieldEQ(FieldUsedCount, v))
}
// Status applies equality check predicate on the "status" field. It's identical to StatusEQ.
func Status(v string) predicate.PromoCode {
return predicate.PromoCode(sql.FieldEQ(FieldStatus, v))
}
// ExpiresAt applies equality check predicate on the "expires_at" field. It's identical to ExpiresAtEQ.
func ExpiresAt(v time.Time) predicate.PromoCode {
return predicate.PromoCode(sql.FieldEQ(FieldExpiresAt, v))
}
// Notes applies equality check predicate on the "notes" field. It's identical to NotesEQ.
func Notes(v string) predicate.PromoCode {
return predicate.PromoCode(sql.FieldEQ(FieldNotes, v))
}
// CreatedAt applies equality check predicate on the "created_at" field. It's identical to CreatedAtEQ.
func CreatedAt(v time.Time) predicate.PromoCode {
return predicate.PromoCode(sql.FieldEQ(FieldCreatedAt, v))
}
// UpdatedAt applies equality check predicate on the "updated_at" field. It's identical to UpdatedAtEQ.
func UpdatedAt(v time.Time) predicate.PromoCode {
return predicate.PromoCode(sql.FieldEQ(FieldUpdatedAt, v))
}
// CodeEQ applies the EQ predicate on the "code" field.
func CodeEQ(v string) predicate.PromoCode {
return predicate.PromoCode(sql.FieldEQ(FieldCode, v))
}
// CodeNEQ applies the NEQ predicate on the "code" field.
func CodeNEQ(v string) predicate.PromoCode {
return predicate.PromoCode(sql.FieldNEQ(FieldCode, v))
}
// CodeIn applies the In predicate on the "code" field.
func CodeIn(vs ...string) predicate.PromoCode {
return predicate.PromoCode(sql.FieldIn(FieldCode, vs...))
}
// CodeNotIn applies the NotIn predicate on the "code" field.
func CodeNotIn(vs ...string) predicate.PromoCode {
return predicate.PromoCode(sql.FieldNotIn(FieldCode, vs...))
}
// CodeGT applies the GT predicate on the "code" field.
func CodeGT(v string) predicate.PromoCode {
return predicate.PromoCode(sql.FieldGT(FieldCode, v))
}
// CodeGTE applies the GTE predicate on the "code" field.
func CodeGTE(v string) predicate.PromoCode {
return predicate.PromoCode(sql.FieldGTE(FieldCode, v))
}
// CodeLT applies the LT predicate on the "code" field.
func CodeLT(v string) predicate.PromoCode {
return predicate.PromoCode(sql.FieldLT(FieldCode, v))
}
// CodeLTE applies the LTE predicate on the "code" field.
func CodeLTE(v string) predicate.PromoCode {
return predicate.PromoCode(sql.FieldLTE(FieldCode, v))
}
// CodeContains applies the Contains predicate on the "code" field.
func CodeContains(v string) predicate.PromoCode {
return predicate.PromoCode(sql.FieldContains(FieldCode, v))
}
// CodeHasPrefix applies the HasPrefix predicate on the "code" field.
func CodeHasPrefix(v string) predicate.PromoCode {
return predicate.PromoCode(sql.FieldHasPrefix(FieldCode, v))
}
// CodeHasSuffix applies the HasSuffix predicate on the "code" field.
func CodeHasSuffix(v string) predicate.PromoCode {
return predicate.PromoCode(sql.FieldHasSuffix(FieldCode, v))
}
// CodeEqualFold applies the EqualFold predicate on the "code" field.
func CodeEqualFold(v string) predicate.PromoCode {
return predicate.PromoCode(sql.FieldEqualFold(FieldCode, v))
}
// CodeContainsFold applies the ContainsFold predicate on the "code" field.
func CodeContainsFold(v string) predicate.PromoCode {
return predicate.PromoCode(sql.FieldContainsFold(FieldCode, v))
}
// BonusAmountEQ applies the EQ predicate on the "bonus_amount" field.
func BonusAmountEQ(v float64) predicate.PromoCode {
return predicate.PromoCode(sql.FieldEQ(FieldBonusAmount, v))
}
// BonusAmountNEQ applies the NEQ predicate on the "bonus_amount" field.
func BonusAmountNEQ(v float64) predicate.PromoCode {
return predicate.PromoCode(sql.FieldNEQ(FieldBonusAmount, v))
}
// BonusAmountIn applies the In predicate on the "bonus_amount" field.
func BonusAmountIn(vs ...float64) predicate.PromoCode {
return predicate.PromoCode(sql.FieldIn(FieldBonusAmount, vs...))
}
// BonusAmountNotIn applies the NotIn predicate on the "bonus_amount" field.
func BonusAmountNotIn(vs ...float64) predicate.PromoCode {
return predicate.PromoCode(sql.FieldNotIn(FieldBonusAmount, vs...))
}
// BonusAmountGT applies the GT predicate on the "bonus_amount" field.
func BonusAmountGT(v float64) predicate.PromoCode {
return predicate.PromoCode(sql.FieldGT(FieldBonusAmount, v))
}
// BonusAmountGTE applies the GTE predicate on the "bonus_amount" field.
func BonusAmountGTE(v float64) predicate.PromoCode {
return predicate.PromoCode(sql.FieldGTE(FieldBonusAmount, v))
}
// BonusAmountLT applies the LT predicate on the "bonus_amount" field.
func BonusAmountLT(v float64) predicate.PromoCode {
return predicate.PromoCode(sql.FieldLT(FieldBonusAmount, v))
}
// BonusAmountLTE applies the LTE predicate on the "bonus_amount" field.
func BonusAmountLTE(v float64) predicate.PromoCode {
return predicate.PromoCode(sql.FieldLTE(FieldBonusAmount, v))
}
// MaxUsesEQ applies the EQ predicate on the "max_uses" field.
func MaxUsesEQ(v int) predicate.PromoCode {
return predicate.PromoCode(sql.FieldEQ(FieldMaxUses, v))
}
// MaxUsesNEQ applies the NEQ predicate on the "max_uses" field.
func MaxUsesNEQ(v int) predicate.PromoCode {
return predicate.PromoCode(sql.FieldNEQ(FieldMaxUses, v))
}
// MaxUsesIn applies the In predicate on the "max_uses" field.
func MaxUsesIn(vs ...int) predicate.PromoCode {
return predicate.PromoCode(sql.FieldIn(FieldMaxUses, vs...))
}
// MaxUsesNotIn applies the NotIn predicate on the "max_uses" field.
func MaxUsesNotIn(vs ...int) predicate.PromoCode {
return predicate.PromoCode(sql.FieldNotIn(FieldMaxUses, vs...))
}
// MaxUsesGT applies the GT predicate on the "max_uses" field.
func MaxUsesGT(v int) predicate.PromoCode {
return predicate.PromoCode(sql.FieldGT(FieldMaxUses, v))
}
// MaxUsesGTE applies the GTE predicate on the "max_uses" field.
func MaxUsesGTE(v int) predicate.PromoCode {
return predicate.PromoCode(sql.FieldGTE(FieldMaxUses, v))
}
// MaxUsesLT applies the LT predicate on the "max_uses" field.
func MaxUsesLT(v int) predicate.PromoCode {
return predicate.PromoCode(sql.FieldLT(FieldMaxUses, v))
}
// MaxUsesLTE applies the LTE predicate on the "max_uses" field.
func MaxUsesLTE(v int) predicate.PromoCode {
return predicate.PromoCode(sql.FieldLTE(FieldMaxUses, v))
}
// UsedCountEQ applies the EQ predicate on the "used_count" field.
func UsedCountEQ(v int) predicate.PromoCode {
return predicate.PromoCode(sql.FieldEQ(FieldUsedCount, v))
}
// UsedCountNEQ applies the NEQ predicate on the "used_count" field.
func UsedCountNEQ(v int) predicate.PromoCode {
return predicate.PromoCode(sql.FieldNEQ(FieldUsedCount, v))
}
// UsedCountIn applies the In predicate on the "used_count" field.
func UsedCountIn(vs ...int) predicate.PromoCode {
return predicate.PromoCode(sql.FieldIn(FieldUsedCount, vs...))
}
// UsedCountNotIn applies the NotIn predicate on the "used_count" field.
func UsedCountNotIn(vs ...int) predicate.PromoCode {
return predicate.PromoCode(sql.FieldNotIn(FieldUsedCount, vs...))
}
// UsedCountGT applies the GT predicate on the "used_count" field.
func UsedCountGT(v int) predicate.PromoCode {
return predicate.PromoCode(sql.FieldGT(FieldUsedCount, v))
}
// UsedCountGTE applies the GTE predicate on the "used_count" field.
func UsedCountGTE(v int) predicate.PromoCode {
return predicate.PromoCode(sql.FieldGTE(FieldUsedCount, v))
}
// UsedCountLT applies the LT predicate on the "used_count" field.
func UsedCountLT(v int) predicate.PromoCode {
return predicate.PromoCode(sql.FieldLT(FieldUsedCount, v))
}
// UsedCountLTE applies the LTE predicate on the "used_count" field.
func UsedCountLTE(v int) predicate.PromoCode {
return predicate.PromoCode(sql.FieldLTE(FieldUsedCount, v))
}
// StatusEQ applies the EQ predicate on the "status" field.
func StatusEQ(v string) predicate.PromoCode {
return predicate.PromoCode(sql.FieldEQ(FieldStatus, v))
}
// StatusNEQ applies the NEQ predicate on the "status" field.
func StatusNEQ(v string) predicate.PromoCode {
return predicate.PromoCode(sql.FieldNEQ(FieldStatus, v))
}
// StatusIn applies the In predicate on the "status" field.
func StatusIn(vs ...string) predicate.PromoCode {
return predicate.PromoCode(sql.FieldIn(FieldStatus, vs...))
}
// StatusNotIn applies the NotIn predicate on the "status" field.
func StatusNotIn(vs ...string) predicate.PromoCode {
return predicate.PromoCode(sql.FieldNotIn(FieldStatus, vs...))
}
// StatusGT applies the GT predicate on the "status" field.
func StatusGT(v string) predicate.PromoCode {
return predicate.PromoCode(sql.FieldGT(FieldStatus, v))
}
// StatusGTE applies the GTE predicate on the "status" field.
func StatusGTE(v string) predicate.PromoCode {
return predicate.PromoCode(sql.FieldGTE(FieldStatus, v))
}
// StatusLT applies the LT predicate on the "status" field.
func StatusLT(v string) predicate.PromoCode {
return predicate.PromoCode(sql.FieldLT(FieldStatus, v))
}
// StatusLTE applies the LTE predicate on the "status" field.
func StatusLTE(v string) predicate.PromoCode {
return predicate.PromoCode(sql.FieldLTE(FieldStatus, v))
}
// StatusContains applies the Contains predicate on the "status" field.
func StatusContains(v string) predicate.PromoCode {
return predicate.PromoCode(sql.FieldContains(FieldStatus, v))
}
// StatusHasPrefix applies the HasPrefix predicate on the "status" field.
func StatusHasPrefix(v string) predicate.PromoCode {
return predicate.PromoCode(sql.FieldHasPrefix(FieldStatus, v))
}
// StatusHasSuffix applies the HasSuffix predicate on the "status" field.
func StatusHasSuffix(v string) predicate.PromoCode {
return predicate.PromoCode(sql.FieldHasSuffix(FieldStatus, v))
}
// StatusEqualFold applies the EqualFold predicate on the "status" field.
func StatusEqualFold(v string) predicate.PromoCode {
return predicate.PromoCode(sql.FieldEqualFold(FieldStatus, v))
}
// StatusContainsFold applies the ContainsFold predicate on the "status" field.
func StatusContainsFold(v string) predicate.PromoCode {
return predicate.PromoCode(sql.FieldContainsFold(FieldStatus, v))
}
// ExpiresAtEQ applies the EQ predicate on the "expires_at" field.
func ExpiresAtEQ(v time.Time) predicate.PromoCode {
return predicate.PromoCode(sql.FieldEQ(FieldExpiresAt, v))
}
// ExpiresAtNEQ applies the NEQ predicate on the "expires_at" field.
func ExpiresAtNEQ(v time.Time) predicate.PromoCode {
return predicate.PromoCode(sql.FieldNEQ(FieldExpiresAt, v))
}
// ExpiresAtIn applies the In predicate on the "expires_at" field.
func ExpiresAtIn(vs ...time.Time) predicate.PromoCode {
return predicate.PromoCode(sql.FieldIn(FieldExpiresAt, vs...))
}
// ExpiresAtNotIn applies the NotIn predicate on the "expires_at" field.
func ExpiresAtNotIn(vs ...time.Time) predicate.PromoCode {
return predicate.PromoCode(sql.FieldNotIn(FieldExpiresAt, vs...))
}
// ExpiresAtGT applies the GT predicate on the "expires_at" field.
func ExpiresAtGT(v time.Time) predicate.PromoCode {
return predicate.PromoCode(sql.FieldGT(FieldExpiresAt, v))
}
// ExpiresAtGTE applies the GTE predicate on the "expires_at" field.
func ExpiresAtGTE(v time.Time) predicate.PromoCode {
return predicate.PromoCode(sql.FieldGTE(FieldExpiresAt, v))
}
// ExpiresAtLT applies the LT predicate on the "expires_at" field.
func ExpiresAtLT(v time.Time) predicate.PromoCode {
return predicate.PromoCode(sql.FieldLT(FieldExpiresAt, v))
}
// ExpiresAtLTE applies the LTE predicate on the "expires_at" field.
func ExpiresAtLTE(v time.Time) predicate.PromoCode {
return predicate.PromoCode(sql.FieldLTE(FieldExpiresAt, v))
}
// ExpiresAtIsNil applies the IsNil predicate on the "expires_at" field.
func ExpiresAtIsNil() predicate.PromoCode {
return predicate.PromoCode(sql.FieldIsNull(FieldExpiresAt))
}
// ExpiresAtNotNil applies the NotNil predicate on the "expires_at" field.
func ExpiresAtNotNil() predicate.PromoCode {
return predicate.PromoCode(sql.FieldNotNull(FieldExpiresAt))
}
// NotesEQ applies the EQ predicate on the "notes" field.
func NotesEQ(v string) predicate.PromoCode {
return predicate.PromoCode(sql.FieldEQ(FieldNotes, v))
}
// NotesNEQ applies the NEQ predicate on the "notes" field.
func NotesNEQ(v string) predicate.PromoCode {
return predicate.PromoCode(sql.FieldNEQ(FieldNotes, v))
}
// NotesIn applies the In predicate on the "notes" field.
func NotesIn(vs ...string) predicate.PromoCode {
return predicate.PromoCode(sql.FieldIn(FieldNotes, vs...))
}
// NotesNotIn applies the NotIn predicate on the "notes" field.
func NotesNotIn(vs ...string) predicate.PromoCode {
return predicate.PromoCode(sql.FieldNotIn(FieldNotes, vs...))
}
// NotesGT applies the GT predicate on the "notes" field.
func NotesGT(v string) predicate.PromoCode {
return predicate.PromoCode(sql.FieldGT(FieldNotes, v))
}
// NotesGTE applies the GTE predicate on the "notes" field.
func NotesGTE(v string) predicate.PromoCode {
return predicate.PromoCode(sql.FieldGTE(FieldNotes, v))
}
// NotesLT applies the LT predicate on the "notes" field.
func NotesLT(v string) predicate.PromoCode {
return predicate.PromoCode(sql.FieldLT(FieldNotes, v))
}
// NotesLTE applies the LTE predicate on the "notes" field.
func NotesLTE(v string) predicate.PromoCode {
return predicate.PromoCode(sql.FieldLTE(FieldNotes, v))
}
// NotesContains applies the Contains predicate on the "notes" field.
func NotesContains(v string) predicate.PromoCode {
return predicate.PromoCode(sql.FieldContains(FieldNotes, v))
}
// NotesHasPrefix applies the HasPrefix predicate on the "notes" field.
func NotesHasPrefix(v string) predicate.PromoCode {
return predicate.PromoCode(sql.FieldHasPrefix(FieldNotes, v))
}
// NotesHasSuffix applies the HasSuffix predicate on the "notes" field.
func NotesHasSuffix(v string) predicate.PromoCode {
return predicate.PromoCode(sql.FieldHasSuffix(FieldNotes, v))
}
// NotesIsNil applies the IsNil predicate on the "notes" field.
func NotesIsNil() predicate.PromoCode {
return predicate.PromoCode(sql.FieldIsNull(FieldNotes))
}
// NotesNotNil applies the NotNil predicate on the "notes" field.
func NotesNotNil() predicate.PromoCode {
return predicate.PromoCode(sql.FieldNotNull(FieldNotes))
}
// NotesEqualFold applies the EqualFold predicate on the "notes" field.
func NotesEqualFold(v string) predicate.PromoCode {
return predicate.PromoCode(sql.FieldEqualFold(FieldNotes, v))
}
// NotesContainsFold applies the ContainsFold predicate on the "notes" field.
func NotesContainsFold(v string) predicate.PromoCode {
return predicate.PromoCode(sql.FieldContainsFold(FieldNotes, v))
}
// CreatedAtEQ applies the EQ predicate on the "created_at" field.
func CreatedAtEQ(v time.Time) predicate.PromoCode {
return predicate.PromoCode(sql.FieldEQ(FieldCreatedAt, v))
}
// CreatedAtNEQ applies the NEQ predicate on the "created_at" field.
func CreatedAtNEQ(v time.Time) predicate.PromoCode {
return predicate.PromoCode(sql.FieldNEQ(FieldCreatedAt, v))
}
// CreatedAtIn applies the In predicate on the "created_at" field.
func CreatedAtIn(vs ...time.Time) predicate.PromoCode {
return predicate.PromoCode(sql.FieldIn(FieldCreatedAt, vs...))
}
// CreatedAtNotIn applies the NotIn predicate on the "created_at" field.
func CreatedAtNotIn(vs ...time.Time) predicate.PromoCode {
return predicate.PromoCode(sql.FieldNotIn(FieldCreatedAt, vs...))
}
// CreatedAtGT applies the GT predicate on the "created_at" field.
func CreatedAtGT(v time.Time) predicate.PromoCode {
return predicate.PromoCode(sql.FieldGT(FieldCreatedAt, v))
}
// CreatedAtGTE applies the GTE predicate on the "created_at" field.
func CreatedAtGTE(v time.Time) predicate.PromoCode {
return predicate.PromoCode(sql.FieldGTE(FieldCreatedAt, v))
}
// CreatedAtLT applies the LT predicate on the "created_at" field.
func CreatedAtLT(v time.Time) predicate.PromoCode {
return predicate.PromoCode(sql.FieldLT(FieldCreatedAt, v))
}
// CreatedAtLTE applies the LTE predicate on the "created_at" field.
func CreatedAtLTE(v time.Time) predicate.PromoCode {
return predicate.PromoCode(sql.FieldLTE(FieldCreatedAt, v))
}
// UpdatedAtEQ applies the EQ predicate on the "updated_at" field.
func UpdatedAtEQ(v time.Time) predicate.PromoCode {
return predicate.PromoCode(sql.FieldEQ(FieldUpdatedAt, v))
}
// UpdatedAtNEQ applies the NEQ predicate on the "updated_at" field.
func UpdatedAtNEQ(v time.Time) predicate.PromoCode {
return predicate.PromoCode(sql.FieldNEQ(FieldUpdatedAt, v))
}
// UpdatedAtIn applies the In predicate on the "updated_at" field.
func UpdatedAtIn(vs ...time.Time) predicate.PromoCode {
return predicate.PromoCode(sql.FieldIn(FieldUpdatedAt, vs...))
}
// UpdatedAtNotIn applies the NotIn predicate on the "updated_at" field.
func UpdatedAtNotIn(vs ...time.Time) predicate.PromoCode {
return predicate.PromoCode(sql.FieldNotIn(FieldUpdatedAt, vs...))
}
// UpdatedAtGT applies the GT predicate on the "updated_at" field.
func UpdatedAtGT(v time.Time) predicate.PromoCode {
return predicate.PromoCode(sql.FieldGT(FieldUpdatedAt, v))
}
// UpdatedAtGTE applies the GTE predicate on the "updated_at" field.
func UpdatedAtGTE(v time.Time) predicate.PromoCode {
return predicate.PromoCode(sql.FieldGTE(FieldUpdatedAt, v))
}
// UpdatedAtLT applies the LT predicate on the "updated_at" field.
func UpdatedAtLT(v time.Time) predicate.PromoCode {
return predicate.PromoCode(sql.FieldLT(FieldUpdatedAt, v))
}
// UpdatedAtLTE applies the LTE predicate on the "updated_at" field.
func UpdatedAtLTE(v time.Time) predicate.PromoCode {
return predicate.PromoCode(sql.FieldLTE(FieldUpdatedAt, v))
}
// HasUsageRecords applies the HasEdge predicate on the "usage_records" edge.
func HasUsageRecords() predicate.PromoCode {
return predicate.PromoCode(func(s *sql.Selector) {
step := sqlgraph.NewStep(
sqlgraph.From(Table, FieldID),
sqlgraph.Edge(sqlgraph.O2M, false, UsageRecordsTable, UsageRecordsColumn),
)
sqlgraph.HasNeighbors(s, step)
})
}
// HasUsageRecordsWith applies the HasEdge predicate on the "usage_records" edge with a given conditions (other predicates).
func HasUsageRecordsWith(preds ...predicate.PromoCodeUsage) predicate.PromoCode {
return predicate.PromoCode(func(s *sql.Selector) {
step := newUsageRecordsStep()
sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) {
for _, p := range preds {
p(s)
}
})
})
}
// And groups predicates with the AND operator between them.
func And(predicates ...predicate.PromoCode) predicate.PromoCode {
return predicate.PromoCode(sql.AndPredicates(predicates...))
}
// Or groups predicates with the OR operator between them.
func Or(predicates ...predicate.PromoCode) predicate.PromoCode {
return predicate.PromoCode(sql.OrPredicates(predicates...))
}
// Not applies the not operator on the given predicate.
func Not(p predicate.PromoCode) predicate.PromoCode {
return predicate.PromoCode(sql.NotPredicates(p))
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,88 @@
// Code generated by ent, DO NOT EDIT.
package ent
import (
"context"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field"
"github.com/Wei-Shaw/sub2api/ent/predicate"
"github.com/Wei-Shaw/sub2api/ent/promocode"
)
// PromoCodeDelete is the builder for deleting a PromoCode entity.
type PromoCodeDelete struct {
config
hooks []Hook
mutation *PromoCodeMutation
}
// Where appends a list predicates to the PromoCodeDelete builder.
func (_d *PromoCodeDelete) Where(ps ...predicate.PromoCode) *PromoCodeDelete {
_d.mutation.Where(ps...)
return _d
}
// Exec executes the deletion query and returns how many vertices were deleted.
func (_d *PromoCodeDelete) Exec(ctx context.Context) (int, error) {
return withHooks(ctx, _d.sqlExec, _d.mutation, _d.hooks)
}
// ExecX is like Exec, but panics if an error occurs.
func (_d *PromoCodeDelete) ExecX(ctx context.Context) int {
n, err := _d.Exec(ctx)
if err != nil {
panic(err)
}
return n
}
func (_d *PromoCodeDelete) sqlExec(ctx context.Context) (int, error) {
_spec := sqlgraph.NewDeleteSpec(promocode.Table, sqlgraph.NewFieldSpec(promocode.FieldID, field.TypeInt64))
if ps := _d.mutation.predicates; len(ps) > 0 {
_spec.Predicate = func(selector *sql.Selector) {
for i := range ps {
ps[i](selector)
}
}
}
affected, err := sqlgraph.DeleteNodes(ctx, _d.driver, _spec)
if err != nil && sqlgraph.IsConstraintError(err) {
err = &ConstraintError{msg: err.Error(), wrap: err}
}
_d.mutation.done = true
return affected, err
}
// PromoCodeDeleteOne is the builder for deleting a single PromoCode entity.
type PromoCodeDeleteOne struct {
_d *PromoCodeDelete
}
// Where appends a list predicates to the PromoCodeDelete builder.
func (_d *PromoCodeDeleteOne) Where(ps ...predicate.PromoCode) *PromoCodeDeleteOne {
_d._d.mutation.Where(ps...)
return _d
}
// Exec executes the deletion query.
func (_d *PromoCodeDeleteOne) Exec(ctx context.Context) error {
n, err := _d._d.Exec(ctx)
switch {
case err != nil:
return err
case n == 0:
return &NotFoundError{promocode.Label}
default:
return nil
}
}
// ExecX is like Exec, but panics if an error occurs.
func (_d *PromoCodeDeleteOne) ExecX(ctx context.Context) {
if err := _d.Exec(ctx); err != nil {
panic(err)
}
}

View File

@@ -0,0 +1,643 @@
// Code generated by ent, DO NOT EDIT.
package ent
import (
"context"
"database/sql/driver"
"fmt"
"math"
"entgo.io/ent"
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field"
"github.com/Wei-Shaw/sub2api/ent/predicate"
"github.com/Wei-Shaw/sub2api/ent/promocode"
"github.com/Wei-Shaw/sub2api/ent/promocodeusage"
)
// PromoCodeQuery is the builder for querying PromoCode entities.
type PromoCodeQuery struct {
config
ctx *QueryContext
order []promocode.OrderOption
inters []Interceptor
predicates []predicate.PromoCode
withUsageRecords *PromoCodeUsageQuery
modifiers []func(*sql.Selector)
// intermediate query (i.e. traversal path).
sql *sql.Selector
path func(context.Context) (*sql.Selector, error)
}
// Where adds a new predicate for the PromoCodeQuery builder.
func (_q *PromoCodeQuery) Where(ps ...predicate.PromoCode) *PromoCodeQuery {
_q.predicates = append(_q.predicates, ps...)
return _q
}
// Limit the number of records to be returned by this query.
func (_q *PromoCodeQuery) Limit(limit int) *PromoCodeQuery {
_q.ctx.Limit = &limit
return _q
}
// Offset to start from.
func (_q *PromoCodeQuery) Offset(offset int) *PromoCodeQuery {
_q.ctx.Offset = &offset
return _q
}
// Unique configures the query builder to filter duplicate records on query.
// By default, unique is set to true, and can be disabled using this method.
func (_q *PromoCodeQuery) Unique(unique bool) *PromoCodeQuery {
_q.ctx.Unique = &unique
return _q
}
// Order specifies how the records should be ordered.
func (_q *PromoCodeQuery) Order(o ...promocode.OrderOption) *PromoCodeQuery {
_q.order = append(_q.order, o...)
return _q
}
// QueryUsageRecords chains the current query on the "usage_records" edge.
func (_q *PromoCodeQuery) QueryUsageRecords() *PromoCodeUsageQuery {
query := (&PromoCodeUsageClient{config: _q.config}).Query()
query.path = func(ctx context.Context) (fromU *sql.Selector, err error) {
if err := _q.prepareQuery(ctx); err != nil {
return nil, err
}
selector := _q.sqlQuery(ctx)
if err := selector.Err(); err != nil {
return nil, err
}
step := sqlgraph.NewStep(
sqlgraph.From(promocode.Table, promocode.FieldID, selector),
sqlgraph.To(promocodeusage.Table, promocodeusage.FieldID),
sqlgraph.Edge(sqlgraph.O2M, false, promocode.UsageRecordsTable, promocode.UsageRecordsColumn),
)
fromU = sqlgraph.SetNeighbors(_q.driver.Dialect(), step)
return fromU, nil
}
return query
}
// First returns the first PromoCode entity from the query.
// Returns a *NotFoundError when no PromoCode was found.
func (_q *PromoCodeQuery) First(ctx context.Context) (*PromoCode, error) {
nodes, err := _q.Limit(1).All(setContextOp(ctx, _q.ctx, ent.OpQueryFirst))
if err != nil {
return nil, err
}
if len(nodes) == 0 {
return nil, &NotFoundError{promocode.Label}
}
return nodes[0], nil
}
// FirstX is like First, but panics if an error occurs.
func (_q *PromoCodeQuery) FirstX(ctx context.Context) *PromoCode {
node, err := _q.First(ctx)
if err != nil && !IsNotFound(err) {
panic(err)
}
return node
}
// FirstID returns the first PromoCode ID from the query.
// Returns a *NotFoundError when no PromoCode ID was found.
func (_q *PromoCodeQuery) FirstID(ctx context.Context) (id int64, err error) {
var ids []int64
if ids, err = _q.Limit(1).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryFirstID)); err != nil {
return
}
if len(ids) == 0 {
err = &NotFoundError{promocode.Label}
return
}
return ids[0], nil
}
// FirstIDX is like FirstID, but panics if an error occurs.
func (_q *PromoCodeQuery) FirstIDX(ctx context.Context) int64 {
id, err := _q.FirstID(ctx)
if err != nil && !IsNotFound(err) {
panic(err)
}
return id
}
// Only returns a single PromoCode entity found by the query, ensuring it only returns one.
// Returns a *NotSingularError when more than one PromoCode entity is found.
// Returns a *NotFoundError when no PromoCode entities are found.
func (_q *PromoCodeQuery) Only(ctx context.Context) (*PromoCode, error) {
nodes, err := _q.Limit(2).All(setContextOp(ctx, _q.ctx, ent.OpQueryOnly))
if err != nil {
return nil, err
}
switch len(nodes) {
case 1:
return nodes[0], nil
case 0:
return nil, &NotFoundError{promocode.Label}
default:
return nil, &NotSingularError{promocode.Label}
}
}
// OnlyX is like Only, but panics if an error occurs.
func (_q *PromoCodeQuery) OnlyX(ctx context.Context) *PromoCode {
node, err := _q.Only(ctx)
if err != nil {
panic(err)
}
return node
}
// OnlyID is like Only, but returns the only PromoCode ID in the query.
// Returns a *NotSingularError when more than one PromoCode ID is found.
// Returns a *NotFoundError when no entities are found.
func (_q *PromoCodeQuery) OnlyID(ctx context.Context) (id int64, err error) {
var ids []int64
if ids, err = _q.Limit(2).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryOnlyID)); err != nil {
return
}
switch len(ids) {
case 1:
id = ids[0]
case 0:
err = &NotFoundError{promocode.Label}
default:
err = &NotSingularError{promocode.Label}
}
return
}
// OnlyIDX is like OnlyID, but panics if an error occurs.
func (_q *PromoCodeQuery) OnlyIDX(ctx context.Context) int64 {
id, err := _q.OnlyID(ctx)
if err != nil {
panic(err)
}
return id
}
// All executes the query and returns a list of PromoCodes.
func (_q *PromoCodeQuery) All(ctx context.Context) ([]*PromoCode, error) {
ctx = setContextOp(ctx, _q.ctx, ent.OpQueryAll)
if err := _q.prepareQuery(ctx); err != nil {
return nil, err
}
qr := querierAll[[]*PromoCode, *PromoCodeQuery]()
return withInterceptors[[]*PromoCode](ctx, _q, qr, _q.inters)
}
// AllX is like All, but panics if an error occurs.
func (_q *PromoCodeQuery) AllX(ctx context.Context) []*PromoCode {
nodes, err := _q.All(ctx)
if err != nil {
panic(err)
}
return nodes
}
// IDs executes the query and returns a list of PromoCode IDs.
func (_q *PromoCodeQuery) IDs(ctx context.Context) (ids []int64, err error) {
if _q.ctx.Unique == nil && _q.path != nil {
_q.Unique(true)
}
ctx = setContextOp(ctx, _q.ctx, ent.OpQueryIDs)
if err = _q.Select(promocode.FieldID).Scan(ctx, &ids); err != nil {
return nil, err
}
return ids, nil
}
// IDsX is like IDs, but panics if an error occurs.
func (_q *PromoCodeQuery) IDsX(ctx context.Context) []int64 {
ids, err := _q.IDs(ctx)
if err != nil {
panic(err)
}
return ids
}
// Count returns the count of the given query.
func (_q *PromoCodeQuery) Count(ctx context.Context) (int, error) {
ctx = setContextOp(ctx, _q.ctx, ent.OpQueryCount)
if err := _q.prepareQuery(ctx); err != nil {
return 0, err
}
return withInterceptors[int](ctx, _q, querierCount[*PromoCodeQuery](), _q.inters)
}
// CountX is like Count, but panics if an error occurs.
func (_q *PromoCodeQuery) CountX(ctx context.Context) int {
count, err := _q.Count(ctx)
if err != nil {
panic(err)
}
return count
}
// Exist returns true if the query has elements in the graph.
func (_q *PromoCodeQuery) Exist(ctx context.Context) (bool, error) {
ctx = setContextOp(ctx, _q.ctx, ent.OpQueryExist)
switch _, err := _q.FirstID(ctx); {
case IsNotFound(err):
return false, nil
case err != nil:
return false, fmt.Errorf("ent: check existence: %w", err)
default:
return true, nil
}
}
// ExistX is like Exist, but panics if an error occurs.
func (_q *PromoCodeQuery) ExistX(ctx context.Context) bool {
exist, err := _q.Exist(ctx)
if err != nil {
panic(err)
}
return exist
}
// Clone returns a duplicate of the PromoCodeQuery builder, including all associated steps. It can be
// used to prepare common query builders and use them differently after the clone is made.
func (_q *PromoCodeQuery) Clone() *PromoCodeQuery {
if _q == nil {
return nil
}
return &PromoCodeQuery{
config: _q.config,
ctx: _q.ctx.Clone(),
order: append([]promocode.OrderOption{}, _q.order...),
inters: append([]Interceptor{}, _q.inters...),
predicates: append([]predicate.PromoCode{}, _q.predicates...),
withUsageRecords: _q.withUsageRecords.Clone(),
// clone intermediate query.
sql: _q.sql.Clone(),
path: _q.path,
}
}
// WithUsageRecords tells the query-builder to eager-load the nodes that are connected to
// the "usage_records" edge. The optional arguments are used to configure the query builder of the edge.
func (_q *PromoCodeQuery) WithUsageRecords(opts ...func(*PromoCodeUsageQuery)) *PromoCodeQuery {
query := (&PromoCodeUsageClient{config: _q.config}).Query()
for _, opt := range opts {
opt(query)
}
_q.withUsageRecords = query
return _q
}
// GroupBy is used to group vertices by one or more fields/columns.
// It is often used with aggregate functions, like: count, max, mean, min, sum.
//
// Example:
//
// var v []struct {
// Code string `json:"code,omitempty"`
// Count int `json:"count,omitempty"`
// }
//
// client.PromoCode.Query().
// GroupBy(promocode.FieldCode).
// Aggregate(ent.Count()).
// Scan(ctx, &v)
func (_q *PromoCodeQuery) GroupBy(field string, fields ...string) *PromoCodeGroupBy {
_q.ctx.Fields = append([]string{field}, fields...)
grbuild := &PromoCodeGroupBy{build: _q}
grbuild.flds = &_q.ctx.Fields
grbuild.label = promocode.Label
grbuild.scan = grbuild.Scan
return grbuild
}
// Select allows the selection one or more fields/columns for the given query,
// instead of selecting all fields in the entity.
//
// Example:
//
// var v []struct {
// Code string `json:"code,omitempty"`
// }
//
// client.PromoCode.Query().
// Select(promocode.FieldCode).
// Scan(ctx, &v)
func (_q *PromoCodeQuery) Select(fields ...string) *PromoCodeSelect {
_q.ctx.Fields = append(_q.ctx.Fields, fields...)
sbuild := &PromoCodeSelect{PromoCodeQuery: _q}
sbuild.label = promocode.Label
sbuild.flds, sbuild.scan = &_q.ctx.Fields, sbuild.Scan
return sbuild
}
// Aggregate returns a PromoCodeSelect configured with the given aggregations.
func (_q *PromoCodeQuery) Aggregate(fns ...AggregateFunc) *PromoCodeSelect {
return _q.Select().Aggregate(fns...)
}
func (_q *PromoCodeQuery) prepareQuery(ctx context.Context) error {
for _, inter := range _q.inters {
if inter == nil {
return fmt.Errorf("ent: uninitialized interceptor (forgotten import ent/runtime?)")
}
if trv, ok := inter.(Traverser); ok {
if err := trv.Traverse(ctx, _q); err != nil {
return err
}
}
}
for _, f := range _q.ctx.Fields {
if !promocode.ValidColumn(f) {
return &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)}
}
}
if _q.path != nil {
prev, err := _q.path(ctx)
if err != nil {
return err
}
_q.sql = prev
}
return nil
}
func (_q *PromoCodeQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*PromoCode, error) {
var (
nodes = []*PromoCode{}
_spec = _q.querySpec()
loadedTypes = [1]bool{
_q.withUsageRecords != nil,
}
)
_spec.ScanValues = func(columns []string) ([]any, error) {
return (*PromoCode).scanValues(nil, columns)
}
_spec.Assign = func(columns []string, values []any) error {
node := &PromoCode{config: _q.config}
nodes = append(nodes, node)
node.Edges.loadedTypes = loadedTypes
return node.assignValues(columns, values)
}
if len(_q.modifiers) > 0 {
_spec.Modifiers = _q.modifiers
}
for i := range hooks {
hooks[i](ctx, _spec)
}
if err := sqlgraph.QueryNodes(ctx, _q.driver, _spec); err != nil {
return nil, err
}
if len(nodes) == 0 {
return nodes, nil
}
if query := _q.withUsageRecords; query != nil {
if err := _q.loadUsageRecords(ctx, query, nodes,
func(n *PromoCode) { n.Edges.UsageRecords = []*PromoCodeUsage{} },
func(n *PromoCode, e *PromoCodeUsage) { n.Edges.UsageRecords = append(n.Edges.UsageRecords, e) }); err != nil {
return nil, err
}
}
return nodes, nil
}
func (_q *PromoCodeQuery) loadUsageRecords(ctx context.Context, query *PromoCodeUsageQuery, nodes []*PromoCode, init func(*PromoCode), assign func(*PromoCode, *PromoCodeUsage)) error {
fks := make([]driver.Value, 0, len(nodes))
nodeids := make(map[int64]*PromoCode)
for i := range nodes {
fks = append(fks, nodes[i].ID)
nodeids[nodes[i].ID] = nodes[i]
if init != nil {
init(nodes[i])
}
}
if len(query.ctx.Fields) > 0 {
query.ctx.AppendFieldOnce(promocodeusage.FieldPromoCodeID)
}
query.Where(predicate.PromoCodeUsage(func(s *sql.Selector) {
s.Where(sql.InValues(s.C(promocode.UsageRecordsColumn), fks...))
}))
neighbors, err := query.All(ctx)
if err != nil {
return err
}
for _, n := range neighbors {
fk := n.PromoCodeID
node, ok := nodeids[fk]
if !ok {
return fmt.Errorf(`unexpected referenced foreign-key "promo_code_id" returned %v for node %v`, fk, n.ID)
}
assign(node, n)
}
return nil
}
func (_q *PromoCodeQuery) sqlCount(ctx context.Context) (int, error) {
_spec := _q.querySpec()
if len(_q.modifiers) > 0 {
_spec.Modifiers = _q.modifiers
}
_spec.Node.Columns = _q.ctx.Fields
if len(_q.ctx.Fields) > 0 {
_spec.Unique = _q.ctx.Unique != nil && *_q.ctx.Unique
}
return sqlgraph.CountNodes(ctx, _q.driver, _spec)
}
func (_q *PromoCodeQuery) querySpec() *sqlgraph.QuerySpec {
_spec := sqlgraph.NewQuerySpec(promocode.Table, promocode.Columns, sqlgraph.NewFieldSpec(promocode.FieldID, field.TypeInt64))
_spec.From = _q.sql
if unique := _q.ctx.Unique; unique != nil {
_spec.Unique = *unique
} else if _q.path != nil {
_spec.Unique = true
}
if fields := _q.ctx.Fields; len(fields) > 0 {
_spec.Node.Columns = make([]string, 0, len(fields))
_spec.Node.Columns = append(_spec.Node.Columns, promocode.FieldID)
for i := range fields {
if fields[i] != promocode.FieldID {
_spec.Node.Columns = append(_spec.Node.Columns, fields[i])
}
}
}
if ps := _q.predicates; len(ps) > 0 {
_spec.Predicate = func(selector *sql.Selector) {
for i := range ps {
ps[i](selector)
}
}
}
if limit := _q.ctx.Limit; limit != nil {
_spec.Limit = *limit
}
if offset := _q.ctx.Offset; offset != nil {
_spec.Offset = *offset
}
if ps := _q.order; len(ps) > 0 {
_spec.Order = func(selector *sql.Selector) {
for i := range ps {
ps[i](selector)
}
}
}
return _spec
}
func (_q *PromoCodeQuery) sqlQuery(ctx context.Context) *sql.Selector {
builder := sql.Dialect(_q.driver.Dialect())
t1 := builder.Table(promocode.Table)
columns := _q.ctx.Fields
if len(columns) == 0 {
columns = promocode.Columns
}
selector := builder.Select(t1.Columns(columns...)...).From(t1)
if _q.sql != nil {
selector = _q.sql
selector.Select(selector.Columns(columns...)...)
}
if _q.ctx.Unique != nil && *_q.ctx.Unique {
selector.Distinct()
}
for _, m := range _q.modifiers {
m(selector)
}
for _, p := range _q.predicates {
p(selector)
}
for _, p := range _q.order {
p(selector)
}
if offset := _q.ctx.Offset; offset != nil {
// limit is mandatory for offset clause. We start
// with default value, and override it below if needed.
selector.Offset(*offset).Limit(math.MaxInt32)
}
if limit := _q.ctx.Limit; limit != nil {
selector.Limit(*limit)
}
return selector
}
// ForUpdate locks the selected rows against concurrent updates, and prevent them from being
// updated, deleted or "selected ... for update" by other sessions, until the transaction is
// either committed or rolled-back.
func (_q *PromoCodeQuery) ForUpdate(opts ...sql.LockOption) *PromoCodeQuery {
if _q.driver.Dialect() == dialect.Postgres {
_q.Unique(false)
}
_q.modifiers = append(_q.modifiers, func(s *sql.Selector) {
s.ForUpdate(opts...)
})
return _q
}
// ForShare behaves similarly to ForUpdate, except that it acquires a shared mode lock
// on any rows that are read. Other sessions can read the rows, but cannot modify them
// until your transaction commits.
func (_q *PromoCodeQuery) ForShare(opts ...sql.LockOption) *PromoCodeQuery {
if _q.driver.Dialect() == dialect.Postgres {
_q.Unique(false)
}
_q.modifiers = append(_q.modifiers, func(s *sql.Selector) {
s.ForShare(opts...)
})
return _q
}
// PromoCodeGroupBy is the group-by builder for PromoCode entities.
type PromoCodeGroupBy struct {
selector
build *PromoCodeQuery
}
// Aggregate adds the given aggregation functions to the group-by query.
func (_g *PromoCodeGroupBy) Aggregate(fns ...AggregateFunc) *PromoCodeGroupBy {
_g.fns = append(_g.fns, fns...)
return _g
}
// Scan applies the selector query and scans the result into the given value.
func (_g *PromoCodeGroupBy) Scan(ctx context.Context, v any) error {
ctx = setContextOp(ctx, _g.build.ctx, ent.OpQueryGroupBy)
if err := _g.build.prepareQuery(ctx); err != nil {
return err
}
return scanWithInterceptors[*PromoCodeQuery, *PromoCodeGroupBy](ctx, _g.build, _g, _g.build.inters, v)
}
func (_g *PromoCodeGroupBy) sqlScan(ctx context.Context, root *PromoCodeQuery, v any) error {
selector := root.sqlQuery(ctx).Select()
aggregation := make([]string, 0, len(_g.fns))
for _, fn := range _g.fns {
aggregation = append(aggregation, fn(selector))
}
if len(selector.SelectedColumns()) == 0 {
columns := make([]string, 0, len(*_g.flds)+len(_g.fns))
for _, f := range *_g.flds {
columns = append(columns, selector.C(f))
}
columns = append(columns, aggregation...)
selector.Select(columns...)
}
selector.GroupBy(selector.Columns(*_g.flds...)...)
if err := selector.Err(); err != nil {
return err
}
rows := &sql.Rows{}
query, args := selector.Query()
if err := _g.build.driver.Query(ctx, query, args, rows); err != nil {
return err
}
defer rows.Close()
return sql.ScanSlice(rows, v)
}
// PromoCodeSelect is the builder for selecting fields of PromoCode entities.
type PromoCodeSelect struct {
*PromoCodeQuery
selector
}
// Aggregate adds the given aggregation functions to the selector query.
func (_s *PromoCodeSelect) Aggregate(fns ...AggregateFunc) *PromoCodeSelect {
_s.fns = append(_s.fns, fns...)
return _s
}
// Scan applies the selector query and scans the result into the given value.
func (_s *PromoCodeSelect) Scan(ctx context.Context, v any) error {
ctx = setContextOp(ctx, _s.ctx, ent.OpQuerySelect)
if err := _s.prepareQuery(ctx); err != nil {
return err
}
return scanWithInterceptors[*PromoCodeQuery, *PromoCodeSelect](ctx, _s.PromoCodeQuery, _s, _s.inters, v)
}
func (_s *PromoCodeSelect) sqlScan(ctx context.Context, root *PromoCodeQuery, v any) error {
selector := root.sqlQuery(ctx)
aggregation := make([]string, 0, len(_s.fns))
for _, fn := range _s.fns {
aggregation = append(aggregation, fn(selector))
}
switch n := len(*_s.selector.flds); {
case n == 0 && len(aggregation) > 0:
selector.Select(aggregation...)
case n != 0 && len(aggregation) > 0:
selector.AppendSelect(aggregation...)
}
rows := &sql.Rows{}
query, args := selector.Query()
if err := _s.driver.Query(ctx, query, args, rows); err != nil {
return err
}
defer rows.Close()
return sql.ScanSlice(rows, v)
}

View File

@@ -0,0 +1,745 @@
// Code generated by ent, DO NOT EDIT.
package ent
import (
"context"
"errors"
"fmt"
"time"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field"
"github.com/Wei-Shaw/sub2api/ent/predicate"
"github.com/Wei-Shaw/sub2api/ent/promocode"
"github.com/Wei-Shaw/sub2api/ent/promocodeusage"
)
// PromoCodeUpdate is the builder for updating PromoCode entities.
type PromoCodeUpdate struct {
config
hooks []Hook
mutation *PromoCodeMutation
}
// Where appends a list predicates to the PromoCodeUpdate builder.
func (_u *PromoCodeUpdate) Where(ps ...predicate.PromoCode) *PromoCodeUpdate {
_u.mutation.Where(ps...)
return _u
}
// SetCode sets the "code" field.
func (_u *PromoCodeUpdate) SetCode(v string) *PromoCodeUpdate {
_u.mutation.SetCode(v)
return _u
}
// SetNillableCode sets the "code" field if the given value is not nil.
func (_u *PromoCodeUpdate) SetNillableCode(v *string) *PromoCodeUpdate {
if v != nil {
_u.SetCode(*v)
}
return _u
}
// SetBonusAmount sets the "bonus_amount" field.
func (_u *PromoCodeUpdate) SetBonusAmount(v float64) *PromoCodeUpdate {
_u.mutation.ResetBonusAmount()
_u.mutation.SetBonusAmount(v)
return _u
}
// SetNillableBonusAmount sets the "bonus_amount" field if the given value is not nil.
func (_u *PromoCodeUpdate) SetNillableBonusAmount(v *float64) *PromoCodeUpdate {
if v != nil {
_u.SetBonusAmount(*v)
}
return _u
}
// AddBonusAmount adds value to the "bonus_amount" field.
func (_u *PromoCodeUpdate) AddBonusAmount(v float64) *PromoCodeUpdate {
_u.mutation.AddBonusAmount(v)
return _u
}
// SetMaxUses sets the "max_uses" field.
func (_u *PromoCodeUpdate) SetMaxUses(v int) *PromoCodeUpdate {
_u.mutation.ResetMaxUses()
_u.mutation.SetMaxUses(v)
return _u
}
// SetNillableMaxUses sets the "max_uses" field if the given value is not nil.
func (_u *PromoCodeUpdate) SetNillableMaxUses(v *int) *PromoCodeUpdate {
if v != nil {
_u.SetMaxUses(*v)
}
return _u
}
// AddMaxUses adds value to the "max_uses" field.
func (_u *PromoCodeUpdate) AddMaxUses(v int) *PromoCodeUpdate {
_u.mutation.AddMaxUses(v)
return _u
}
// SetUsedCount sets the "used_count" field.
func (_u *PromoCodeUpdate) SetUsedCount(v int) *PromoCodeUpdate {
_u.mutation.ResetUsedCount()
_u.mutation.SetUsedCount(v)
return _u
}
// SetNillableUsedCount sets the "used_count" field if the given value is not nil.
func (_u *PromoCodeUpdate) SetNillableUsedCount(v *int) *PromoCodeUpdate {
if v != nil {
_u.SetUsedCount(*v)
}
return _u
}
// AddUsedCount adds value to the "used_count" field.
func (_u *PromoCodeUpdate) AddUsedCount(v int) *PromoCodeUpdate {
_u.mutation.AddUsedCount(v)
return _u
}
// SetStatus sets the "status" field.
func (_u *PromoCodeUpdate) SetStatus(v string) *PromoCodeUpdate {
_u.mutation.SetStatus(v)
return _u
}
// SetNillableStatus sets the "status" field if the given value is not nil.
func (_u *PromoCodeUpdate) SetNillableStatus(v *string) *PromoCodeUpdate {
if v != nil {
_u.SetStatus(*v)
}
return _u
}
// SetExpiresAt sets the "expires_at" field.
func (_u *PromoCodeUpdate) SetExpiresAt(v time.Time) *PromoCodeUpdate {
_u.mutation.SetExpiresAt(v)
return _u
}
// SetNillableExpiresAt sets the "expires_at" field if the given value is not nil.
func (_u *PromoCodeUpdate) SetNillableExpiresAt(v *time.Time) *PromoCodeUpdate {
if v != nil {
_u.SetExpiresAt(*v)
}
return _u
}
// ClearExpiresAt clears the value of the "expires_at" field.
func (_u *PromoCodeUpdate) ClearExpiresAt() *PromoCodeUpdate {
_u.mutation.ClearExpiresAt()
return _u
}
// SetNotes sets the "notes" field.
func (_u *PromoCodeUpdate) SetNotes(v string) *PromoCodeUpdate {
_u.mutation.SetNotes(v)
return _u
}
// SetNillableNotes sets the "notes" field if the given value is not nil.
func (_u *PromoCodeUpdate) SetNillableNotes(v *string) *PromoCodeUpdate {
if v != nil {
_u.SetNotes(*v)
}
return _u
}
// ClearNotes clears the value of the "notes" field.
func (_u *PromoCodeUpdate) ClearNotes() *PromoCodeUpdate {
_u.mutation.ClearNotes()
return _u
}
// SetUpdatedAt sets the "updated_at" field.
func (_u *PromoCodeUpdate) SetUpdatedAt(v time.Time) *PromoCodeUpdate {
_u.mutation.SetUpdatedAt(v)
return _u
}
// AddUsageRecordIDs adds the "usage_records" edge to the PromoCodeUsage entity by IDs.
func (_u *PromoCodeUpdate) AddUsageRecordIDs(ids ...int64) *PromoCodeUpdate {
_u.mutation.AddUsageRecordIDs(ids...)
return _u
}
// AddUsageRecords adds the "usage_records" edges to the PromoCodeUsage entity.
func (_u *PromoCodeUpdate) AddUsageRecords(v ...*PromoCodeUsage) *PromoCodeUpdate {
ids := make([]int64, len(v))
for i := range v {
ids[i] = v[i].ID
}
return _u.AddUsageRecordIDs(ids...)
}
// Mutation returns the PromoCodeMutation object of the builder.
func (_u *PromoCodeUpdate) Mutation() *PromoCodeMutation {
return _u.mutation
}
// ClearUsageRecords clears all "usage_records" edges to the PromoCodeUsage entity.
func (_u *PromoCodeUpdate) ClearUsageRecords() *PromoCodeUpdate {
_u.mutation.ClearUsageRecords()
return _u
}
// RemoveUsageRecordIDs removes the "usage_records" edge to PromoCodeUsage entities by IDs.
func (_u *PromoCodeUpdate) RemoveUsageRecordIDs(ids ...int64) *PromoCodeUpdate {
_u.mutation.RemoveUsageRecordIDs(ids...)
return _u
}
// RemoveUsageRecords removes "usage_records" edges to PromoCodeUsage entities.
func (_u *PromoCodeUpdate) RemoveUsageRecords(v ...*PromoCodeUsage) *PromoCodeUpdate {
ids := make([]int64, len(v))
for i := range v {
ids[i] = v[i].ID
}
return _u.RemoveUsageRecordIDs(ids...)
}
// Save executes the query and returns the number of nodes affected by the update operation.
func (_u *PromoCodeUpdate) Save(ctx context.Context) (int, error) {
_u.defaults()
return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks)
}
// SaveX is like Save, but panics if an error occurs.
func (_u *PromoCodeUpdate) SaveX(ctx context.Context) int {
affected, err := _u.Save(ctx)
if err != nil {
panic(err)
}
return affected
}
// Exec executes the query.
func (_u *PromoCodeUpdate) Exec(ctx context.Context) error {
_, err := _u.Save(ctx)
return err
}
// ExecX is like Exec, but panics if an error occurs.
func (_u *PromoCodeUpdate) ExecX(ctx context.Context) {
if err := _u.Exec(ctx); err != nil {
panic(err)
}
}
// defaults sets the default values of the builder before save.
func (_u *PromoCodeUpdate) defaults() {
if _, ok := _u.mutation.UpdatedAt(); !ok {
v := promocode.UpdateDefaultUpdatedAt()
_u.mutation.SetUpdatedAt(v)
}
}
// check runs all checks and user-defined validators on the builder.
func (_u *PromoCodeUpdate) check() error {
if v, ok := _u.mutation.Code(); ok {
if err := promocode.CodeValidator(v); err != nil {
return &ValidationError{Name: "code", err: fmt.Errorf(`ent: validator failed for field "PromoCode.code": %w`, err)}
}
}
if v, ok := _u.mutation.Status(); ok {
if err := promocode.StatusValidator(v); err != nil {
return &ValidationError{Name: "status", err: fmt.Errorf(`ent: validator failed for field "PromoCode.status": %w`, err)}
}
}
return nil
}
func (_u *PromoCodeUpdate) sqlSave(ctx context.Context) (_node int, err error) {
if err := _u.check(); err != nil {
return _node, err
}
_spec := sqlgraph.NewUpdateSpec(promocode.Table, promocode.Columns, sqlgraph.NewFieldSpec(promocode.FieldID, field.TypeInt64))
if ps := _u.mutation.predicates; len(ps) > 0 {
_spec.Predicate = func(selector *sql.Selector) {
for i := range ps {
ps[i](selector)
}
}
}
if value, ok := _u.mutation.Code(); ok {
_spec.SetField(promocode.FieldCode, field.TypeString, value)
}
if value, ok := _u.mutation.BonusAmount(); ok {
_spec.SetField(promocode.FieldBonusAmount, field.TypeFloat64, value)
}
if value, ok := _u.mutation.AddedBonusAmount(); ok {
_spec.AddField(promocode.FieldBonusAmount, field.TypeFloat64, value)
}
if value, ok := _u.mutation.MaxUses(); ok {
_spec.SetField(promocode.FieldMaxUses, field.TypeInt, value)
}
if value, ok := _u.mutation.AddedMaxUses(); ok {
_spec.AddField(promocode.FieldMaxUses, field.TypeInt, value)
}
if value, ok := _u.mutation.UsedCount(); ok {
_spec.SetField(promocode.FieldUsedCount, field.TypeInt, value)
}
if value, ok := _u.mutation.AddedUsedCount(); ok {
_spec.AddField(promocode.FieldUsedCount, field.TypeInt, value)
}
if value, ok := _u.mutation.Status(); ok {
_spec.SetField(promocode.FieldStatus, field.TypeString, value)
}
if value, ok := _u.mutation.ExpiresAt(); ok {
_spec.SetField(promocode.FieldExpiresAt, field.TypeTime, value)
}
if _u.mutation.ExpiresAtCleared() {
_spec.ClearField(promocode.FieldExpiresAt, field.TypeTime)
}
if value, ok := _u.mutation.Notes(); ok {
_spec.SetField(promocode.FieldNotes, field.TypeString, value)
}
if _u.mutation.NotesCleared() {
_spec.ClearField(promocode.FieldNotes, field.TypeString)
}
if value, ok := _u.mutation.UpdatedAt(); ok {
_spec.SetField(promocode.FieldUpdatedAt, field.TypeTime, value)
}
if _u.mutation.UsageRecordsCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,
Inverse: false,
Table: promocode.UsageRecordsTable,
Columns: []string{promocode.UsageRecordsColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(promocodeusage.FieldID, field.TypeInt64),
},
}
_spec.Edges.Clear = append(_spec.Edges.Clear, edge)
}
if nodes := _u.mutation.RemovedUsageRecordsIDs(); len(nodes) > 0 && !_u.mutation.UsageRecordsCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,
Inverse: false,
Table: promocode.UsageRecordsTable,
Columns: []string{promocode.UsageRecordsColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(promocodeusage.FieldID, field.TypeInt64),
},
}
for _, k := range nodes {
edge.Target.Nodes = append(edge.Target.Nodes, k)
}
_spec.Edges.Clear = append(_spec.Edges.Clear, edge)
}
if nodes := _u.mutation.UsageRecordsIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,
Inverse: false,
Table: promocode.UsageRecordsTable,
Columns: []string{promocode.UsageRecordsColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(promocodeusage.FieldID, field.TypeInt64),
},
}
for _, k := range nodes {
edge.Target.Nodes = append(edge.Target.Nodes, k)
}
_spec.Edges.Add = append(_spec.Edges.Add, edge)
}
if _node, err = sqlgraph.UpdateNodes(ctx, _u.driver, _spec); err != nil {
if _, ok := err.(*sqlgraph.NotFoundError); ok {
err = &NotFoundError{promocode.Label}
} else if sqlgraph.IsConstraintError(err) {
err = &ConstraintError{msg: err.Error(), wrap: err}
}
return 0, err
}
_u.mutation.done = true
return _node, nil
}
// PromoCodeUpdateOne is the builder for updating a single PromoCode entity.
type PromoCodeUpdateOne struct {
config
fields []string
hooks []Hook
mutation *PromoCodeMutation
}
// SetCode sets the "code" field.
func (_u *PromoCodeUpdateOne) SetCode(v string) *PromoCodeUpdateOne {
_u.mutation.SetCode(v)
return _u
}
// SetNillableCode sets the "code" field if the given value is not nil.
func (_u *PromoCodeUpdateOne) SetNillableCode(v *string) *PromoCodeUpdateOne {
if v != nil {
_u.SetCode(*v)
}
return _u
}
// SetBonusAmount sets the "bonus_amount" field.
func (_u *PromoCodeUpdateOne) SetBonusAmount(v float64) *PromoCodeUpdateOne {
_u.mutation.ResetBonusAmount()
_u.mutation.SetBonusAmount(v)
return _u
}
// SetNillableBonusAmount sets the "bonus_amount" field if the given value is not nil.
func (_u *PromoCodeUpdateOne) SetNillableBonusAmount(v *float64) *PromoCodeUpdateOne {
if v != nil {
_u.SetBonusAmount(*v)
}
return _u
}
// AddBonusAmount adds value to the "bonus_amount" field.
func (_u *PromoCodeUpdateOne) AddBonusAmount(v float64) *PromoCodeUpdateOne {
_u.mutation.AddBonusAmount(v)
return _u
}
// SetMaxUses sets the "max_uses" field.
func (_u *PromoCodeUpdateOne) SetMaxUses(v int) *PromoCodeUpdateOne {
_u.mutation.ResetMaxUses()
_u.mutation.SetMaxUses(v)
return _u
}
// SetNillableMaxUses sets the "max_uses" field if the given value is not nil.
func (_u *PromoCodeUpdateOne) SetNillableMaxUses(v *int) *PromoCodeUpdateOne {
if v != nil {
_u.SetMaxUses(*v)
}
return _u
}
// AddMaxUses adds value to the "max_uses" field.
func (_u *PromoCodeUpdateOne) AddMaxUses(v int) *PromoCodeUpdateOne {
_u.mutation.AddMaxUses(v)
return _u
}
// SetUsedCount sets the "used_count" field.
func (_u *PromoCodeUpdateOne) SetUsedCount(v int) *PromoCodeUpdateOne {
_u.mutation.ResetUsedCount()
_u.mutation.SetUsedCount(v)
return _u
}
// SetNillableUsedCount sets the "used_count" field if the given value is not nil.
func (_u *PromoCodeUpdateOne) SetNillableUsedCount(v *int) *PromoCodeUpdateOne {
if v != nil {
_u.SetUsedCount(*v)
}
return _u
}
// AddUsedCount adds value to the "used_count" field.
func (_u *PromoCodeUpdateOne) AddUsedCount(v int) *PromoCodeUpdateOne {
_u.mutation.AddUsedCount(v)
return _u
}
// SetStatus sets the "status" field.
func (_u *PromoCodeUpdateOne) SetStatus(v string) *PromoCodeUpdateOne {
_u.mutation.SetStatus(v)
return _u
}
// SetNillableStatus sets the "status" field if the given value is not nil.
func (_u *PromoCodeUpdateOne) SetNillableStatus(v *string) *PromoCodeUpdateOne {
if v != nil {
_u.SetStatus(*v)
}
return _u
}
// SetExpiresAt sets the "expires_at" field.
func (_u *PromoCodeUpdateOne) SetExpiresAt(v time.Time) *PromoCodeUpdateOne {
_u.mutation.SetExpiresAt(v)
return _u
}
// SetNillableExpiresAt sets the "expires_at" field if the given value is not nil.
func (_u *PromoCodeUpdateOne) SetNillableExpiresAt(v *time.Time) *PromoCodeUpdateOne {
if v != nil {
_u.SetExpiresAt(*v)
}
return _u
}
// ClearExpiresAt clears the value of the "expires_at" field.
func (_u *PromoCodeUpdateOne) ClearExpiresAt() *PromoCodeUpdateOne {
_u.mutation.ClearExpiresAt()
return _u
}
// SetNotes sets the "notes" field.
func (_u *PromoCodeUpdateOne) SetNotes(v string) *PromoCodeUpdateOne {
_u.mutation.SetNotes(v)
return _u
}
// SetNillableNotes sets the "notes" field if the given value is not nil.
func (_u *PromoCodeUpdateOne) SetNillableNotes(v *string) *PromoCodeUpdateOne {
if v != nil {
_u.SetNotes(*v)
}
return _u
}
// ClearNotes clears the value of the "notes" field.
func (_u *PromoCodeUpdateOne) ClearNotes() *PromoCodeUpdateOne {
_u.mutation.ClearNotes()
return _u
}
// SetUpdatedAt sets the "updated_at" field.
func (_u *PromoCodeUpdateOne) SetUpdatedAt(v time.Time) *PromoCodeUpdateOne {
_u.mutation.SetUpdatedAt(v)
return _u
}
// AddUsageRecordIDs adds the "usage_records" edge to the PromoCodeUsage entity by IDs.
func (_u *PromoCodeUpdateOne) AddUsageRecordIDs(ids ...int64) *PromoCodeUpdateOne {
_u.mutation.AddUsageRecordIDs(ids...)
return _u
}
// AddUsageRecords adds the "usage_records" edges to the PromoCodeUsage entity.
func (_u *PromoCodeUpdateOne) AddUsageRecords(v ...*PromoCodeUsage) *PromoCodeUpdateOne {
ids := make([]int64, len(v))
for i := range v {
ids[i] = v[i].ID
}
return _u.AddUsageRecordIDs(ids...)
}
// Mutation returns the PromoCodeMutation object of the builder.
func (_u *PromoCodeUpdateOne) Mutation() *PromoCodeMutation {
return _u.mutation
}
// ClearUsageRecords clears all "usage_records" edges to the PromoCodeUsage entity.
func (_u *PromoCodeUpdateOne) ClearUsageRecords() *PromoCodeUpdateOne {
_u.mutation.ClearUsageRecords()
return _u
}
// RemoveUsageRecordIDs removes the "usage_records" edge to PromoCodeUsage entities by IDs.
func (_u *PromoCodeUpdateOne) RemoveUsageRecordIDs(ids ...int64) *PromoCodeUpdateOne {
_u.mutation.RemoveUsageRecordIDs(ids...)
return _u
}
// RemoveUsageRecords removes "usage_records" edges to PromoCodeUsage entities.
func (_u *PromoCodeUpdateOne) RemoveUsageRecords(v ...*PromoCodeUsage) *PromoCodeUpdateOne {
ids := make([]int64, len(v))
for i := range v {
ids[i] = v[i].ID
}
return _u.RemoveUsageRecordIDs(ids...)
}
// Where appends a list predicates to the PromoCodeUpdate builder.
func (_u *PromoCodeUpdateOne) Where(ps ...predicate.PromoCode) *PromoCodeUpdateOne {
_u.mutation.Where(ps...)
return _u
}
// Select allows selecting one or more fields (columns) of the returned entity.
// The default is selecting all fields defined in the entity schema.
func (_u *PromoCodeUpdateOne) Select(field string, fields ...string) *PromoCodeUpdateOne {
_u.fields = append([]string{field}, fields...)
return _u
}
// Save executes the query and returns the updated PromoCode entity.
func (_u *PromoCodeUpdateOne) Save(ctx context.Context) (*PromoCode, error) {
_u.defaults()
return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks)
}
// SaveX is like Save, but panics if an error occurs.
func (_u *PromoCodeUpdateOne) SaveX(ctx context.Context) *PromoCode {
node, err := _u.Save(ctx)
if err != nil {
panic(err)
}
return node
}
// Exec executes the query on the entity.
func (_u *PromoCodeUpdateOne) Exec(ctx context.Context) error {
_, err := _u.Save(ctx)
return err
}
// ExecX is like Exec, but panics if an error occurs.
func (_u *PromoCodeUpdateOne) ExecX(ctx context.Context) {
if err := _u.Exec(ctx); err != nil {
panic(err)
}
}
// defaults sets the default values of the builder before save.
func (_u *PromoCodeUpdateOne) defaults() {
if _, ok := _u.mutation.UpdatedAt(); !ok {
v := promocode.UpdateDefaultUpdatedAt()
_u.mutation.SetUpdatedAt(v)
}
}
// check runs all checks and user-defined validators on the builder.
func (_u *PromoCodeUpdateOne) check() error {
if v, ok := _u.mutation.Code(); ok {
if err := promocode.CodeValidator(v); err != nil {
return &ValidationError{Name: "code", err: fmt.Errorf(`ent: validator failed for field "PromoCode.code": %w`, err)}
}
}
if v, ok := _u.mutation.Status(); ok {
if err := promocode.StatusValidator(v); err != nil {
return &ValidationError{Name: "status", err: fmt.Errorf(`ent: validator failed for field "PromoCode.status": %w`, err)}
}
}
return nil
}
func (_u *PromoCodeUpdateOne) sqlSave(ctx context.Context) (_node *PromoCode, err error) {
if err := _u.check(); err != nil {
return _node, err
}
_spec := sqlgraph.NewUpdateSpec(promocode.Table, promocode.Columns, sqlgraph.NewFieldSpec(promocode.FieldID, field.TypeInt64))
id, ok := _u.mutation.ID()
if !ok {
return nil, &ValidationError{Name: "id", err: errors.New(`ent: missing "PromoCode.id" for update`)}
}
_spec.Node.ID.Value = id
if fields := _u.fields; len(fields) > 0 {
_spec.Node.Columns = make([]string, 0, len(fields))
_spec.Node.Columns = append(_spec.Node.Columns, promocode.FieldID)
for _, f := range fields {
if !promocode.ValidColumn(f) {
return nil, &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)}
}
if f != promocode.FieldID {
_spec.Node.Columns = append(_spec.Node.Columns, f)
}
}
}
if ps := _u.mutation.predicates; len(ps) > 0 {
_spec.Predicate = func(selector *sql.Selector) {
for i := range ps {
ps[i](selector)
}
}
}
if value, ok := _u.mutation.Code(); ok {
_spec.SetField(promocode.FieldCode, field.TypeString, value)
}
if value, ok := _u.mutation.BonusAmount(); ok {
_spec.SetField(promocode.FieldBonusAmount, field.TypeFloat64, value)
}
if value, ok := _u.mutation.AddedBonusAmount(); ok {
_spec.AddField(promocode.FieldBonusAmount, field.TypeFloat64, value)
}
if value, ok := _u.mutation.MaxUses(); ok {
_spec.SetField(promocode.FieldMaxUses, field.TypeInt, value)
}
if value, ok := _u.mutation.AddedMaxUses(); ok {
_spec.AddField(promocode.FieldMaxUses, field.TypeInt, value)
}
if value, ok := _u.mutation.UsedCount(); ok {
_spec.SetField(promocode.FieldUsedCount, field.TypeInt, value)
}
if value, ok := _u.mutation.AddedUsedCount(); ok {
_spec.AddField(promocode.FieldUsedCount, field.TypeInt, value)
}
if value, ok := _u.mutation.Status(); ok {
_spec.SetField(promocode.FieldStatus, field.TypeString, value)
}
if value, ok := _u.mutation.ExpiresAt(); ok {
_spec.SetField(promocode.FieldExpiresAt, field.TypeTime, value)
}
if _u.mutation.ExpiresAtCleared() {
_spec.ClearField(promocode.FieldExpiresAt, field.TypeTime)
}
if value, ok := _u.mutation.Notes(); ok {
_spec.SetField(promocode.FieldNotes, field.TypeString, value)
}
if _u.mutation.NotesCleared() {
_spec.ClearField(promocode.FieldNotes, field.TypeString)
}
if value, ok := _u.mutation.UpdatedAt(); ok {
_spec.SetField(promocode.FieldUpdatedAt, field.TypeTime, value)
}
if _u.mutation.UsageRecordsCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,
Inverse: false,
Table: promocode.UsageRecordsTable,
Columns: []string{promocode.UsageRecordsColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(promocodeusage.FieldID, field.TypeInt64),
},
}
_spec.Edges.Clear = append(_spec.Edges.Clear, edge)
}
if nodes := _u.mutation.RemovedUsageRecordsIDs(); len(nodes) > 0 && !_u.mutation.UsageRecordsCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,
Inverse: false,
Table: promocode.UsageRecordsTable,
Columns: []string{promocode.UsageRecordsColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(promocodeusage.FieldID, field.TypeInt64),
},
}
for _, k := range nodes {
edge.Target.Nodes = append(edge.Target.Nodes, k)
}
_spec.Edges.Clear = append(_spec.Edges.Clear, edge)
}
if nodes := _u.mutation.UsageRecordsIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,
Inverse: false,
Table: promocode.UsageRecordsTable,
Columns: []string{promocode.UsageRecordsColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(promocodeusage.FieldID, field.TypeInt64),
},
}
for _, k := range nodes {
edge.Target.Nodes = append(edge.Target.Nodes, k)
}
_spec.Edges.Add = append(_spec.Edges.Add, edge)
}
_node = &PromoCode{config: _u.config}
_spec.Assign = _node.assignValues
_spec.ScanValues = _node.scanValues
if err = sqlgraph.UpdateNode(ctx, _u.driver, _spec); err != nil {
if _, ok := err.(*sqlgraph.NotFoundError); ok {
err = &NotFoundError{promocode.Label}
} else if sqlgraph.IsConstraintError(err) {
err = &ConstraintError{msg: err.Error(), wrap: err}
}
return nil, err
}
_u.mutation.done = true
return _node, nil
}

View File

@@ -0,0 +1,187 @@
// Code generated by ent, DO NOT EDIT.
package ent
import (
"fmt"
"strings"
"time"
"entgo.io/ent"
"entgo.io/ent/dialect/sql"
"github.com/Wei-Shaw/sub2api/ent/promocode"
"github.com/Wei-Shaw/sub2api/ent/promocodeusage"
"github.com/Wei-Shaw/sub2api/ent/user"
)
// PromoCodeUsage is the model entity for the PromoCodeUsage schema.
type PromoCodeUsage struct {
config `json:"-"`
// ID of the ent.
ID int64 `json:"id,omitempty"`
// 优惠码ID
PromoCodeID int64 `json:"promo_code_id,omitempty"`
// 使用用户ID
UserID int64 `json:"user_id,omitempty"`
// 实际赠送金额
BonusAmount float64 `json:"bonus_amount,omitempty"`
// 使用时间
UsedAt time.Time `json:"used_at,omitempty"`
// Edges holds the relations/edges for other nodes in the graph.
// The values are being populated by the PromoCodeUsageQuery when eager-loading is set.
Edges PromoCodeUsageEdges `json:"edges"`
selectValues sql.SelectValues
}
// PromoCodeUsageEdges holds the relations/edges for other nodes in the graph.
type PromoCodeUsageEdges struct {
// PromoCode holds the value of the promo_code edge.
PromoCode *PromoCode `json:"promo_code,omitempty"`
// User holds the value of the user edge.
User *User `json:"user,omitempty"`
// loadedTypes holds the information for reporting if a
// type was loaded (or requested) in eager-loading or not.
loadedTypes [2]bool
}
// PromoCodeOrErr returns the PromoCode value or an error if the edge
// was not loaded in eager-loading, or loaded but was not found.
func (e PromoCodeUsageEdges) PromoCodeOrErr() (*PromoCode, error) {
if e.PromoCode != nil {
return e.PromoCode, nil
} else if e.loadedTypes[0] {
return nil, &NotFoundError{label: promocode.Label}
}
return nil, &NotLoadedError{edge: "promo_code"}
}
// UserOrErr returns the User value or an error if the edge
// was not loaded in eager-loading, or loaded but was not found.
func (e PromoCodeUsageEdges) UserOrErr() (*User, error) {
if e.User != nil {
return e.User, nil
} else if e.loadedTypes[1] {
return nil, &NotFoundError{label: user.Label}
}
return nil, &NotLoadedError{edge: "user"}
}
// scanValues returns the types for scanning values from sql.Rows.
func (*PromoCodeUsage) scanValues(columns []string) ([]any, error) {
values := make([]any, len(columns))
for i := range columns {
switch columns[i] {
case promocodeusage.FieldBonusAmount:
values[i] = new(sql.NullFloat64)
case promocodeusage.FieldID, promocodeusage.FieldPromoCodeID, promocodeusage.FieldUserID:
values[i] = new(sql.NullInt64)
case promocodeusage.FieldUsedAt:
values[i] = new(sql.NullTime)
default:
values[i] = new(sql.UnknownType)
}
}
return values, nil
}
// assignValues assigns the values that were returned from sql.Rows (after scanning)
// to the PromoCodeUsage fields.
func (_m *PromoCodeUsage) assignValues(columns []string, values []any) error {
if m, n := len(values), len(columns); m < n {
return fmt.Errorf("mismatch number of scan values: %d != %d", m, n)
}
for i := range columns {
switch columns[i] {
case promocodeusage.FieldID:
value, ok := values[i].(*sql.NullInt64)
if !ok {
return fmt.Errorf("unexpected type %T for field id", value)
}
_m.ID = int64(value.Int64)
case promocodeusage.FieldPromoCodeID:
if value, ok := values[i].(*sql.NullInt64); !ok {
return fmt.Errorf("unexpected type %T for field promo_code_id", values[i])
} else if value.Valid {
_m.PromoCodeID = value.Int64
}
case promocodeusage.FieldUserID:
if value, ok := values[i].(*sql.NullInt64); !ok {
return fmt.Errorf("unexpected type %T for field user_id", values[i])
} else if value.Valid {
_m.UserID = value.Int64
}
case promocodeusage.FieldBonusAmount:
if value, ok := values[i].(*sql.NullFloat64); !ok {
return fmt.Errorf("unexpected type %T for field bonus_amount", values[i])
} else if value.Valid {
_m.BonusAmount = value.Float64
}
case promocodeusage.FieldUsedAt:
if value, ok := values[i].(*sql.NullTime); !ok {
return fmt.Errorf("unexpected type %T for field used_at", values[i])
} else if value.Valid {
_m.UsedAt = value.Time
}
default:
_m.selectValues.Set(columns[i], values[i])
}
}
return nil
}
// Value returns the ent.Value that was dynamically selected and assigned to the PromoCodeUsage.
// This includes values selected through modifiers, order, etc.
func (_m *PromoCodeUsage) Value(name string) (ent.Value, error) {
return _m.selectValues.Get(name)
}
// QueryPromoCode queries the "promo_code" edge of the PromoCodeUsage entity.
func (_m *PromoCodeUsage) QueryPromoCode() *PromoCodeQuery {
return NewPromoCodeUsageClient(_m.config).QueryPromoCode(_m)
}
// QueryUser queries the "user" edge of the PromoCodeUsage entity.
func (_m *PromoCodeUsage) QueryUser() *UserQuery {
return NewPromoCodeUsageClient(_m.config).QueryUser(_m)
}
// Update returns a builder for updating this PromoCodeUsage.
// Note that you need to call PromoCodeUsage.Unwrap() before calling this method if this PromoCodeUsage
// was returned from a transaction, and the transaction was committed or rolled back.
func (_m *PromoCodeUsage) Update() *PromoCodeUsageUpdateOne {
return NewPromoCodeUsageClient(_m.config).UpdateOne(_m)
}
// Unwrap unwraps the PromoCodeUsage entity that was returned from a transaction after it was closed,
// so that all future queries will be executed through the driver which created the transaction.
func (_m *PromoCodeUsage) Unwrap() *PromoCodeUsage {
_tx, ok := _m.config.driver.(*txDriver)
if !ok {
panic("ent: PromoCodeUsage is not a transactional entity")
}
_m.config.driver = _tx.drv
return _m
}
// String implements the fmt.Stringer.
func (_m *PromoCodeUsage) String() string {
var builder strings.Builder
builder.WriteString("PromoCodeUsage(")
builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID))
builder.WriteString("promo_code_id=")
builder.WriteString(fmt.Sprintf("%v", _m.PromoCodeID))
builder.WriteString(", ")
builder.WriteString("user_id=")
builder.WriteString(fmt.Sprintf("%v", _m.UserID))
builder.WriteString(", ")
builder.WriteString("bonus_amount=")
builder.WriteString(fmt.Sprintf("%v", _m.BonusAmount))
builder.WriteString(", ")
builder.WriteString("used_at=")
builder.WriteString(_m.UsedAt.Format(time.ANSIC))
builder.WriteByte(')')
return builder.String()
}
// PromoCodeUsages is a parsable slice of PromoCodeUsage.
type PromoCodeUsages []*PromoCodeUsage

View File

@@ -0,0 +1,125 @@
// Code generated by ent, DO NOT EDIT.
package promocodeusage
import (
"time"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
)
const (
// Label holds the string label denoting the promocodeusage type in the database.
Label = "promo_code_usage"
// FieldID holds the string denoting the id field in the database.
FieldID = "id"
// FieldPromoCodeID holds the string denoting the promo_code_id field in the database.
FieldPromoCodeID = "promo_code_id"
// FieldUserID holds the string denoting the user_id field in the database.
FieldUserID = "user_id"
// FieldBonusAmount holds the string denoting the bonus_amount field in the database.
FieldBonusAmount = "bonus_amount"
// FieldUsedAt holds the string denoting the used_at field in the database.
FieldUsedAt = "used_at"
// EdgePromoCode holds the string denoting the promo_code edge name in mutations.
EdgePromoCode = "promo_code"
// EdgeUser holds the string denoting the user edge name in mutations.
EdgeUser = "user"
// Table holds the table name of the promocodeusage in the database.
Table = "promo_code_usages"
// PromoCodeTable is the table that holds the promo_code relation/edge.
PromoCodeTable = "promo_code_usages"
// PromoCodeInverseTable is the table name for the PromoCode entity.
// It exists in this package in order to avoid circular dependency with the "promocode" package.
PromoCodeInverseTable = "promo_codes"
// PromoCodeColumn is the table column denoting the promo_code relation/edge.
PromoCodeColumn = "promo_code_id"
// UserTable is the table that holds the user relation/edge.
UserTable = "promo_code_usages"
// UserInverseTable is the table name for the User entity.
// It exists in this package in order to avoid circular dependency with the "user" package.
UserInverseTable = "users"
// UserColumn is the table column denoting the user relation/edge.
UserColumn = "user_id"
)
// Columns holds all SQL columns for promocodeusage fields.
var Columns = []string{
FieldID,
FieldPromoCodeID,
FieldUserID,
FieldBonusAmount,
FieldUsedAt,
}
// ValidColumn reports if the column name is valid (part of the table columns).
func ValidColumn(column string) bool {
for i := range Columns {
if column == Columns[i] {
return true
}
}
return false
}
var (
// DefaultUsedAt holds the default value on creation for the "used_at" field.
DefaultUsedAt func() time.Time
)
// OrderOption defines the ordering options for the PromoCodeUsage queries.
type OrderOption func(*sql.Selector)
// ByID orders the results by the id field.
func ByID(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldID, opts...).ToFunc()
}
// ByPromoCodeID orders the results by the promo_code_id field.
func ByPromoCodeID(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldPromoCodeID, opts...).ToFunc()
}
// ByUserID orders the results by the user_id field.
func ByUserID(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldUserID, opts...).ToFunc()
}
// ByBonusAmount orders the results by the bonus_amount field.
func ByBonusAmount(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldBonusAmount, opts...).ToFunc()
}
// ByUsedAt orders the results by the used_at field.
func ByUsedAt(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldUsedAt, opts...).ToFunc()
}
// ByPromoCodeField orders the results by promo_code field.
func ByPromoCodeField(field string, opts ...sql.OrderTermOption) OrderOption {
return func(s *sql.Selector) {
sqlgraph.OrderByNeighborTerms(s, newPromoCodeStep(), sql.OrderByField(field, opts...))
}
}
// ByUserField orders the results by user field.
func ByUserField(field string, opts ...sql.OrderTermOption) OrderOption {
return func(s *sql.Selector) {
sqlgraph.OrderByNeighborTerms(s, newUserStep(), sql.OrderByField(field, opts...))
}
}
func newPromoCodeStep() *sqlgraph.Step {
return sqlgraph.NewStep(
sqlgraph.From(Table, FieldID),
sqlgraph.To(PromoCodeInverseTable, FieldID),
sqlgraph.Edge(sqlgraph.M2O, true, PromoCodeTable, PromoCodeColumn),
)
}
func newUserStep() *sqlgraph.Step {
return sqlgraph.NewStep(
sqlgraph.From(Table, FieldID),
sqlgraph.To(UserInverseTable, FieldID),
sqlgraph.Edge(sqlgraph.M2O, true, UserTable, UserColumn),
)
}

View File

@@ -0,0 +1,257 @@
// Code generated by ent, DO NOT EDIT.
package promocodeusage
import (
"time"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"github.com/Wei-Shaw/sub2api/ent/predicate"
)
// ID filters vertices based on their ID field.
func ID(id int64) predicate.PromoCodeUsage {
return predicate.PromoCodeUsage(sql.FieldEQ(FieldID, id))
}
// IDEQ applies the EQ predicate on the ID field.
func IDEQ(id int64) predicate.PromoCodeUsage {
return predicate.PromoCodeUsage(sql.FieldEQ(FieldID, id))
}
// IDNEQ applies the NEQ predicate on the ID field.
func IDNEQ(id int64) predicate.PromoCodeUsage {
return predicate.PromoCodeUsage(sql.FieldNEQ(FieldID, id))
}
// IDIn applies the In predicate on the ID field.
func IDIn(ids ...int64) predicate.PromoCodeUsage {
return predicate.PromoCodeUsage(sql.FieldIn(FieldID, ids...))
}
// IDNotIn applies the NotIn predicate on the ID field.
func IDNotIn(ids ...int64) predicate.PromoCodeUsage {
return predicate.PromoCodeUsage(sql.FieldNotIn(FieldID, ids...))
}
// IDGT applies the GT predicate on the ID field.
func IDGT(id int64) predicate.PromoCodeUsage {
return predicate.PromoCodeUsage(sql.FieldGT(FieldID, id))
}
// IDGTE applies the GTE predicate on the ID field.
func IDGTE(id int64) predicate.PromoCodeUsage {
return predicate.PromoCodeUsage(sql.FieldGTE(FieldID, id))
}
// IDLT applies the LT predicate on the ID field.
func IDLT(id int64) predicate.PromoCodeUsage {
return predicate.PromoCodeUsage(sql.FieldLT(FieldID, id))
}
// IDLTE applies the LTE predicate on the ID field.
func IDLTE(id int64) predicate.PromoCodeUsage {
return predicate.PromoCodeUsage(sql.FieldLTE(FieldID, id))
}
// PromoCodeID applies equality check predicate on the "promo_code_id" field. It's identical to PromoCodeIDEQ.
func PromoCodeID(v int64) predicate.PromoCodeUsage {
return predicate.PromoCodeUsage(sql.FieldEQ(FieldPromoCodeID, v))
}
// UserID applies equality check predicate on the "user_id" field. It's identical to UserIDEQ.
func UserID(v int64) predicate.PromoCodeUsage {
return predicate.PromoCodeUsage(sql.FieldEQ(FieldUserID, v))
}
// BonusAmount applies equality check predicate on the "bonus_amount" field. It's identical to BonusAmountEQ.
func BonusAmount(v float64) predicate.PromoCodeUsage {
return predicate.PromoCodeUsage(sql.FieldEQ(FieldBonusAmount, v))
}
// UsedAt applies equality check predicate on the "used_at" field. It's identical to UsedAtEQ.
func UsedAt(v time.Time) predicate.PromoCodeUsage {
return predicate.PromoCodeUsage(sql.FieldEQ(FieldUsedAt, v))
}
// PromoCodeIDEQ applies the EQ predicate on the "promo_code_id" field.
func PromoCodeIDEQ(v int64) predicate.PromoCodeUsage {
return predicate.PromoCodeUsage(sql.FieldEQ(FieldPromoCodeID, v))
}
// PromoCodeIDNEQ applies the NEQ predicate on the "promo_code_id" field.
func PromoCodeIDNEQ(v int64) predicate.PromoCodeUsage {
return predicate.PromoCodeUsage(sql.FieldNEQ(FieldPromoCodeID, v))
}
// PromoCodeIDIn applies the In predicate on the "promo_code_id" field.
func PromoCodeIDIn(vs ...int64) predicate.PromoCodeUsage {
return predicate.PromoCodeUsage(sql.FieldIn(FieldPromoCodeID, vs...))
}
// PromoCodeIDNotIn applies the NotIn predicate on the "promo_code_id" field.
func PromoCodeIDNotIn(vs ...int64) predicate.PromoCodeUsage {
return predicate.PromoCodeUsage(sql.FieldNotIn(FieldPromoCodeID, vs...))
}
// UserIDEQ applies the EQ predicate on the "user_id" field.
func UserIDEQ(v int64) predicate.PromoCodeUsage {
return predicate.PromoCodeUsage(sql.FieldEQ(FieldUserID, v))
}
// UserIDNEQ applies the NEQ predicate on the "user_id" field.
func UserIDNEQ(v int64) predicate.PromoCodeUsage {
return predicate.PromoCodeUsage(sql.FieldNEQ(FieldUserID, v))
}
// UserIDIn applies the In predicate on the "user_id" field.
func UserIDIn(vs ...int64) predicate.PromoCodeUsage {
return predicate.PromoCodeUsage(sql.FieldIn(FieldUserID, vs...))
}
// UserIDNotIn applies the NotIn predicate on the "user_id" field.
func UserIDNotIn(vs ...int64) predicate.PromoCodeUsage {
return predicate.PromoCodeUsage(sql.FieldNotIn(FieldUserID, vs...))
}
// BonusAmountEQ applies the EQ predicate on the "bonus_amount" field.
func BonusAmountEQ(v float64) predicate.PromoCodeUsage {
return predicate.PromoCodeUsage(sql.FieldEQ(FieldBonusAmount, v))
}
// BonusAmountNEQ applies the NEQ predicate on the "bonus_amount" field.
func BonusAmountNEQ(v float64) predicate.PromoCodeUsage {
return predicate.PromoCodeUsage(sql.FieldNEQ(FieldBonusAmount, v))
}
// BonusAmountIn applies the In predicate on the "bonus_amount" field.
func BonusAmountIn(vs ...float64) predicate.PromoCodeUsage {
return predicate.PromoCodeUsage(sql.FieldIn(FieldBonusAmount, vs...))
}
// BonusAmountNotIn applies the NotIn predicate on the "bonus_amount" field.
func BonusAmountNotIn(vs ...float64) predicate.PromoCodeUsage {
return predicate.PromoCodeUsage(sql.FieldNotIn(FieldBonusAmount, vs...))
}
// BonusAmountGT applies the GT predicate on the "bonus_amount" field.
func BonusAmountGT(v float64) predicate.PromoCodeUsage {
return predicate.PromoCodeUsage(sql.FieldGT(FieldBonusAmount, v))
}
// BonusAmountGTE applies the GTE predicate on the "bonus_amount" field.
func BonusAmountGTE(v float64) predicate.PromoCodeUsage {
return predicate.PromoCodeUsage(sql.FieldGTE(FieldBonusAmount, v))
}
// BonusAmountLT applies the LT predicate on the "bonus_amount" field.
func BonusAmountLT(v float64) predicate.PromoCodeUsage {
return predicate.PromoCodeUsage(sql.FieldLT(FieldBonusAmount, v))
}
// BonusAmountLTE applies the LTE predicate on the "bonus_amount" field.
func BonusAmountLTE(v float64) predicate.PromoCodeUsage {
return predicate.PromoCodeUsage(sql.FieldLTE(FieldBonusAmount, v))
}
// UsedAtEQ applies the EQ predicate on the "used_at" field.
func UsedAtEQ(v time.Time) predicate.PromoCodeUsage {
return predicate.PromoCodeUsage(sql.FieldEQ(FieldUsedAt, v))
}
// UsedAtNEQ applies the NEQ predicate on the "used_at" field.
func UsedAtNEQ(v time.Time) predicate.PromoCodeUsage {
return predicate.PromoCodeUsage(sql.FieldNEQ(FieldUsedAt, v))
}
// UsedAtIn applies the In predicate on the "used_at" field.
func UsedAtIn(vs ...time.Time) predicate.PromoCodeUsage {
return predicate.PromoCodeUsage(sql.FieldIn(FieldUsedAt, vs...))
}
// UsedAtNotIn applies the NotIn predicate on the "used_at" field.
func UsedAtNotIn(vs ...time.Time) predicate.PromoCodeUsage {
return predicate.PromoCodeUsage(sql.FieldNotIn(FieldUsedAt, vs...))
}
// UsedAtGT applies the GT predicate on the "used_at" field.
func UsedAtGT(v time.Time) predicate.PromoCodeUsage {
return predicate.PromoCodeUsage(sql.FieldGT(FieldUsedAt, v))
}
// UsedAtGTE applies the GTE predicate on the "used_at" field.
func UsedAtGTE(v time.Time) predicate.PromoCodeUsage {
return predicate.PromoCodeUsage(sql.FieldGTE(FieldUsedAt, v))
}
// UsedAtLT applies the LT predicate on the "used_at" field.
func UsedAtLT(v time.Time) predicate.PromoCodeUsage {
return predicate.PromoCodeUsage(sql.FieldLT(FieldUsedAt, v))
}
// UsedAtLTE applies the LTE predicate on the "used_at" field.
func UsedAtLTE(v time.Time) predicate.PromoCodeUsage {
return predicate.PromoCodeUsage(sql.FieldLTE(FieldUsedAt, v))
}
// HasPromoCode applies the HasEdge predicate on the "promo_code" edge.
func HasPromoCode() predicate.PromoCodeUsage {
return predicate.PromoCodeUsage(func(s *sql.Selector) {
step := sqlgraph.NewStep(
sqlgraph.From(Table, FieldID),
sqlgraph.Edge(sqlgraph.M2O, true, PromoCodeTable, PromoCodeColumn),
)
sqlgraph.HasNeighbors(s, step)
})
}
// HasPromoCodeWith applies the HasEdge predicate on the "promo_code" edge with a given conditions (other predicates).
func HasPromoCodeWith(preds ...predicate.PromoCode) predicate.PromoCodeUsage {
return predicate.PromoCodeUsage(func(s *sql.Selector) {
step := newPromoCodeStep()
sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) {
for _, p := range preds {
p(s)
}
})
})
}
// HasUser applies the HasEdge predicate on the "user" edge.
func HasUser() predicate.PromoCodeUsage {
return predicate.PromoCodeUsage(func(s *sql.Selector) {
step := sqlgraph.NewStep(
sqlgraph.From(Table, FieldID),
sqlgraph.Edge(sqlgraph.M2O, true, UserTable, UserColumn),
)
sqlgraph.HasNeighbors(s, step)
})
}
// HasUserWith applies the HasEdge predicate on the "user" edge with a given conditions (other predicates).
func HasUserWith(preds ...predicate.User) predicate.PromoCodeUsage {
return predicate.PromoCodeUsage(func(s *sql.Selector) {
step := newUserStep()
sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) {
for _, p := range preds {
p(s)
}
})
})
}
// And groups predicates with the AND operator between them.
func And(predicates ...predicate.PromoCodeUsage) predicate.PromoCodeUsage {
return predicate.PromoCodeUsage(sql.AndPredicates(predicates...))
}
// Or groups predicates with the OR operator between them.
func Or(predicates ...predicate.PromoCodeUsage) predicate.PromoCodeUsage {
return predicate.PromoCodeUsage(sql.OrPredicates(predicates...))
}
// Not applies the not operator on the given predicate.
func Not(p predicate.PromoCodeUsage) predicate.PromoCodeUsage {
return predicate.PromoCodeUsage(sql.NotPredicates(p))
}

View File

@@ -0,0 +1,696 @@
// Code generated by ent, DO NOT EDIT.
package ent
import (
"context"
"errors"
"fmt"
"time"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field"
"github.com/Wei-Shaw/sub2api/ent/promocode"
"github.com/Wei-Shaw/sub2api/ent/promocodeusage"
"github.com/Wei-Shaw/sub2api/ent/user"
)
// PromoCodeUsageCreate is the builder for creating a PromoCodeUsage entity.
type PromoCodeUsageCreate struct {
config
mutation *PromoCodeUsageMutation
hooks []Hook
conflict []sql.ConflictOption
}
// SetPromoCodeID sets the "promo_code_id" field.
func (_c *PromoCodeUsageCreate) SetPromoCodeID(v int64) *PromoCodeUsageCreate {
_c.mutation.SetPromoCodeID(v)
return _c
}
// SetUserID sets the "user_id" field.
func (_c *PromoCodeUsageCreate) SetUserID(v int64) *PromoCodeUsageCreate {
_c.mutation.SetUserID(v)
return _c
}
// SetBonusAmount sets the "bonus_amount" field.
func (_c *PromoCodeUsageCreate) SetBonusAmount(v float64) *PromoCodeUsageCreate {
_c.mutation.SetBonusAmount(v)
return _c
}
// SetUsedAt sets the "used_at" field.
func (_c *PromoCodeUsageCreate) SetUsedAt(v time.Time) *PromoCodeUsageCreate {
_c.mutation.SetUsedAt(v)
return _c
}
// SetNillableUsedAt sets the "used_at" field if the given value is not nil.
func (_c *PromoCodeUsageCreate) SetNillableUsedAt(v *time.Time) *PromoCodeUsageCreate {
if v != nil {
_c.SetUsedAt(*v)
}
return _c
}
// SetPromoCode sets the "promo_code" edge to the PromoCode entity.
func (_c *PromoCodeUsageCreate) SetPromoCode(v *PromoCode) *PromoCodeUsageCreate {
return _c.SetPromoCodeID(v.ID)
}
// SetUser sets the "user" edge to the User entity.
func (_c *PromoCodeUsageCreate) SetUser(v *User) *PromoCodeUsageCreate {
return _c.SetUserID(v.ID)
}
// Mutation returns the PromoCodeUsageMutation object of the builder.
func (_c *PromoCodeUsageCreate) Mutation() *PromoCodeUsageMutation {
return _c.mutation
}
// Save creates the PromoCodeUsage in the database.
func (_c *PromoCodeUsageCreate) Save(ctx context.Context) (*PromoCodeUsage, error) {
_c.defaults()
return withHooks(ctx, _c.sqlSave, _c.mutation, _c.hooks)
}
// SaveX calls Save and panics if Save returns an error.
func (_c *PromoCodeUsageCreate) SaveX(ctx context.Context) *PromoCodeUsage {
v, err := _c.Save(ctx)
if err != nil {
panic(err)
}
return v
}
// Exec executes the query.
func (_c *PromoCodeUsageCreate) Exec(ctx context.Context) error {
_, err := _c.Save(ctx)
return err
}
// ExecX is like Exec, but panics if an error occurs.
func (_c *PromoCodeUsageCreate) ExecX(ctx context.Context) {
if err := _c.Exec(ctx); err != nil {
panic(err)
}
}
// defaults sets the default values of the builder before save.
func (_c *PromoCodeUsageCreate) defaults() {
if _, ok := _c.mutation.UsedAt(); !ok {
v := promocodeusage.DefaultUsedAt()
_c.mutation.SetUsedAt(v)
}
}
// check runs all checks and user-defined validators on the builder.
func (_c *PromoCodeUsageCreate) check() error {
if _, ok := _c.mutation.PromoCodeID(); !ok {
return &ValidationError{Name: "promo_code_id", err: errors.New(`ent: missing required field "PromoCodeUsage.promo_code_id"`)}
}
if _, ok := _c.mutation.UserID(); !ok {
return &ValidationError{Name: "user_id", err: errors.New(`ent: missing required field "PromoCodeUsage.user_id"`)}
}
if _, ok := _c.mutation.BonusAmount(); !ok {
return &ValidationError{Name: "bonus_amount", err: errors.New(`ent: missing required field "PromoCodeUsage.bonus_amount"`)}
}
if _, ok := _c.mutation.UsedAt(); !ok {
return &ValidationError{Name: "used_at", err: errors.New(`ent: missing required field "PromoCodeUsage.used_at"`)}
}
if len(_c.mutation.PromoCodeIDs()) == 0 {
return &ValidationError{Name: "promo_code", err: errors.New(`ent: missing required edge "PromoCodeUsage.promo_code"`)}
}
if len(_c.mutation.UserIDs()) == 0 {
return &ValidationError{Name: "user", err: errors.New(`ent: missing required edge "PromoCodeUsage.user"`)}
}
return nil
}
func (_c *PromoCodeUsageCreate) sqlSave(ctx context.Context) (*PromoCodeUsage, error) {
if err := _c.check(); err != nil {
return nil, err
}
_node, _spec := _c.createSpec()
if err := sqlgraph.CreateNode(ctx, _c.driver, _spec); err != nil {
if sqlgraph.IsConstraintError(err) {
err = &ConstraintError{msg: err.Error(), wrap: err}
}
return nil, err
}
id := _spec.ID.Value.(int64)
_node.ID = int64(id)
_c.mutation.id = &_node.ID
_c.mutation.done = true
return _node, nil
}
func (_c *PromoCodeUsageCreate) createSpec() (*PromoCodeUsage, *sqlgraph.CreateSpec) {
var (
_node = &PromoCodeUsage{config: _c.config}
_spec = sqlgraph.NewCreateSpec(promocodeusage.Table, sqlgraph.NewFieldSpec(promocodeusage.FieldID, field.TypeInt64))
)
_spec.OnConflict = _c.conflict
if value, ok := _c.mutation.BonusAmount(); ok {
_spec.SetField(promocodeusage.FieldBonusAmount, field.TypeFloat64, value)
_node.BonusAmount = value
}
if value, ok := _c.mutation.UsedAt(); ok {
_spec.SetField(promocodeusage.FieldUsedAt, field.TypeTime, value)
_node.UsedAt = value
}
if nodes := _c.mutation.PromoCodeIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O,
Inverse: true,
Table: promocodeusage.PromoCodeTable,
Columns: []string{promocodeusage.PromoCodeColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(promocode.FieldID, field.TypeInt64),
},
}
for _, k := range nodes {
edge.Target.Nodes = append(edge.Target.Nodes, k)
}
_node.PromoCodeID = nodes[0]
_spec.Edges = append(_spec.Edges, edge)
}
if nodes := _c.mutation.UserIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O,
Inverse: true,
Table: promocodeusage.UserTable,
Columns: []string{promocodeusage.UserColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeInt64),
},
}
for _, k := range nodes {
edge.Target.Nodes = append(edge.Target.Nodes, k)
}
_node.UserID = nodes[0]
_spec.Edges = append(_spec.Edges, edge)
}
return _node, _spec
}
// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause
// of the `INSERT` statement. For example:
//
// client.PromoCodeUsage.Create().
// SetPromoCodeID(v).
// OnConflict(
// // Update the row with the new values
// // the was proposed for insertion.
// sql.ResolveWithNewValues(),
// ).
// // Override some of the fields with custom
// // update values.
// Update(func(u *ent.PromoCodeUsageUpsert) {
// SetPromoCodeID(v+v).
// }).
// Exec(ctx)
func (_c *PromoCodeUsageCreate) OnConflict(opts ...sql.ConflictOption) *PromoCodeUsageUpsertOne {
_c.conflict = opts
return &PromoCodeUsageUpsertOne{
create: _c,
}
}
// OnConflictColumns calls `OnConflict` and configures the columns
// as conflict target. Using this option is equivalent to using:
//
// client.PromoCodeUsage.Create().
// OnConflict(sql.ConflictColumns(columns...)).
// Exec(ctx)
func (_c *PromoCodeUsageCreate) OnConflictColumns(columns ...string) *PromoCodeUsageUpsertOne {
_c.conflict = append(_c.conflict, sql.ConflictColumns(columns...))
return &PromoCodeUsageUpsertOne{
create: _c,
}
}
type (
// PromoCodeUsageUpsertOne is the builder for "upsert"-ing
// one PromoCodeUsage node.
PromoCodeUsageUpsertOne struct {
create *PromoCodeUsageCreate
}
// PromoCodeUsageUpsert is the "OnConflict" setter.
PromoCodeUsageUpsert struct {
*sql.UpdateSet
}
)
// SetPromoCodeID sets the "promo_code_id" field.
func (u *PromoCodeUsageUpsert) SetPromoCodeID(v int64) *PromoCodeUsageUpsert {
u.Set(promocodeusage.FieldPromoCodeID, v)
return u
}
// UpdatePromoCodeID sets the "promo_code_id" field to the value that was provided on create.
func (u *PromoCodeUsageUpsert) UpdatePromoCodeID() *PromoCodeUsageUpsert {
u.SetExcluded(promocodeusage.FieldPromoCodeID)
return u
}
// SetUserID sets the "user_id" field.
func (u *PromoCodeUsageUpsert) SetUserID(v int64) *PromoCodeUsageUpsert {
u.Set(promocodeusage.FieldUserID, v)
return u
}
// UpdateUserID sets the "user_id" field to the value that was provided on create.
func (u *PromoCodeUsageUpsert) UpdateUserID() *PromoCodeUsageUpsert {
u.SetExcluded(promocodeusage.FieldUserID)
return u
}
// SetBonusAmount sets the "bonus_amount" field.
func (u *PromoCodeUsageUpsert) SetBonusAmount(v float64) *PromoCodeUsageUpsert {
u.Set(promocodeusage.FieldBonusAmount, v)
return u
}
// UpdateBonusAmount sets the "bonus_amount" field to the value that was provided on create.
func (u *PromoCodeUsageUpsert) UpdateBonusAmount() *PromoCodeUsageUpsert {
u.SetExcluded(promocodeusage.FieldBonusAmount)
return u
}
// AddBonusAmount adds v to the "bonus_amount" field.
func (u *PromoCodeUsageUpsert) AddBonusAmount(v float64) *PromoCodeUsageUpsert {
u.Add(promocodeusage.FieldBonusAmount, v)
return u
}
// SetUsedAt sets the "used_at" field.
func (u *PromoCodeUsageUpsert) SetUsedAt(v time.Time) *PromoCodeUsageUpsert {
u.Set(promocodeusage.FieldUsedAt, v)
return u
}
// UpdateUsedAt sets the "used_at" field to the value that was provided on create.
func (u *PromoCodeUsageUpsert) UpdateUsedAt() *PromoCodeUsageUpsert {
u.SetExcluded(promocodeusage.FieldUsedAt)
return u
}
// UpdateNewValues updates the mutable fields using the new values that were set on create.
// Using this option is equivalent to using:
//
// client.PromoCodeUsage.Create().
// OnConflict(
// sql.ResolveWithNewValues(),
// ).
// Exec(ctx)
func (u *PromoCodeUsageUpsertOne) UpdateNewValues() *PromoCodeUsageUpsertOne {
u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues())
return u
}
// Ignore sets each column to itself in case of conflict.
// Using this option is equivalent to using:
//
// client.PromoCodeUsage.Create().
// OnConflict(sql.ResolveWithIgnore()).
// Exec(ctx)
func (u *PromoCodeUsageUpsertOne) Ignore() *PromoCodeUsageUpsertOne {
u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore())
return u
}
// DoNothing configures the conflict_action to `DO NOTHING`.
// Supported only by SQLite and PostgreSQL.
func (u *PromoCodeUsageUpsertOne) DoNothing() *PromoCodeUsageUpsertOne {
u.create.conflict = append(u.create.conflict, sql.DoNothing())
return u
}
// Update allows overriding fields `UPDATE` values. See the PromoCodeUsageCreate.OnConflict
// documentation for more info.
func (u *PromoCodeUsageUpsertOne) Update(set func(*PromoCodeUsageUpsert)) *PromoCodeUsageUpsertOne {
u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) {
set(&PromoCodeUsageUpsert{UpdateSet: update})
}))
return u
}
// SetPromoCodeID sets the "promo_code_id" field.
func (u *PromoCodeUsageUpsertOne) SetPromoCodeID(v int64) *PromoCodeUsageUpsertOne {
return u.Update(func(s *PromoCodeUsageUpsert) {
s.SetPromoCodeID(v)
})
}
// UpdatePromoCodeID sets the "promo_code_id" field to the value that was provided on create.
func (u *PromoCodeUsageUpsertOne) UpdatePromoCodeID() *PromoCodeUsageUpsertOne {
return u.Update(func(s *PromoCodeUsageUpsert) {
s.UpdatePromoCodeID()
})
}
// SetUserID sets the "user_id" field.
func (u *PromoCodeUsageUpsertOne) SetUserID(v int64) *PromoCodeUsageUpsertOne {
return u.Update(func(s *PromoCodeUsageUpsert) {
s.SetUserID(v)
})
}
// UpdateUserID sets the "user_id" field to the value that was provided on create.
func (u *PromoCodeUsageUpsertOne) UpdateUserID() *PromoCodeUsageUpsertOne {
return u.Update(func(s *PromoCodeUsageUpsert) {
s.UpdateUserID()
})
}
// SetBonusAmount sets the "bonus_amount" field.
func (u *PromoCodeUsageUpsertOne) SetBonusAmount(v float64) *PromoCodeUsageUpsertOne {
return u.Update(func(s *PromoCodeUsageUpsert) {
s.SetBonusAmount(v)
})
}
// AddBonusAmount adds v to the "bonus_amount" field.
func (u *PromoCodeUsageUpsertOne) AddBonusAmount(v float64) *PromoCodeUsageUpsertOne {
return u.Update(func(s *PromoCodeUsageUpsert) {
s.AddBonusAmount(v)
})
}
// UpdateBonusAmount sets the "bonus_amount" field to the value that was provided on create.
func (u *PromoCodeUsageUpsertOne) UpdateBonusAmount() *PromoCodeUsageUpsertOne {
return u.Update(func(s *PromoCodeUsageUpsert) {
s.UpdateBonusAmount()
})
}
// SetUsedAt sets the "used_at" field.
func (u *PromoCodeUsageUpsertOne) SetUsedAt(v time.Time) *PromoCodeUsageUpsertOne {
return u.Update(func(s *PromoCodeUsageUpsert) {
s.SetUsedAt(v)
})
}
// UpdateUsedAt sets the "used_at" field to the value that was provided on create.
func (u *PromoCodeUsageUpsertOne) UpdateUsedAt() *PromoCodeUsageUpsertOne {
return u.Update(func(s *PromoCodeUsageUpsert) {
s.UpdateUsedAt()
})
}
// Exec executes the query.
func (u *PromoCodeUsageUpsertOne) Exec(ctx context.Context) error {
if len(u.create.conflict) == 0 {
return errors.New("ent: missing options for PromoCodeUsageCreate.OnConflict")
}
return u.create.Exec(ctx)
}
// ExecX is like Exec, but panics if an error occurs.
func (u *PromoCodeUsageUpsertOne) ExecX(ctx context.Context) {
if err := u.create.Exec(ctx); err != nil {
panic(err)
}
}
// Exec executes the UPSERT query and returns the inserted/updated ID.
func (u *PromoCodeUsageUpsertOne) ID(ctx context.Context) (id int64, err error) {
node, err := u.create.Save(ctx)
if err != nil {
return id, err
}
return node.ID, nil
}
// IDX is like ID, but panics if an error occurs.
func (u *PromoCodeUsageUpsertOne) IDX(ctx context.Context) int64 {
id, err := u.ID(ctx)
if err != nil {
panic(err)
}
return id
}
// PromoCodeUsageCreateBulk is the builder for creating many PromoCodeUsage entities in bulk.
type PromoCodeUsageCreateBulk struct {
config
err error
builders []*PromoCodeUsageCreate
conflict []sql.ConflictOption
}
// Save creates the PromoCodeUsage entities in the database.
func (_c *PromoCodeUsageCreateBulk) Save(ctx context.Context) ([]*PromoCodeUsage, error) {
if _c.err != nil {
return nil, _c.err
}
specs := make([]*sqlgraph.CreateSpec, len(_c.builders))
nodes := make([]*PromoCodeUsage, len(_c.builders))
mutators := make([]Mutator, len(_c.builders))
for i := range _c.builders {
func(i int, root context.Context) {
builder := _c.builders[i]
builder.defaults()
var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) {
mutation, ok := m.(*PromoCodeUsageMutation)
if !ok {
return nil, fmt.Errorf("unexpected mutation type %T", m)
}
if err := builder.check(); err != nil {
return nil, err
}
builder.mutation = mutation
var err error
nodes[i], specs[i] = builder.createSpec()
if i < len(mutators)-1 {
_, err = mutators[i+1].Mutate(root, _c.builders[i+1].mutation)
} else {
spec := &sqlgraph.BatchCreateSpec{Nodes: specs}
spec.OnConflict = _c.conflict
// Invoke the actual operation on the latest mutation in the chain.
if err = sqlgraph.BatchCreate(ctx, _c.driver, spec); err != nil {
if sqlgraph.IsConstraintError(err) {
err = &ConstraintError{msg: err.Error(), wrap: err}
}
}
}
if err != nil {
return nil, err
}
mutation.id = &nodes[i].ID
if specs[i].ID.Value != nil {
id := specs[i].ID.Value.(int64)
nodes[i].ID = int64(id)
}
mutation.done = true
return nodes[i], nil
})
for i := len(builder.hooks) - 1; i >= 0; i-- {
mut = builder.hooks[i](mut)
}
mutators[i] = mut
}(i, ctx)
}
if len(mutators) > 0 {
if _, err := mutators[0].Mutate(ctx, _c.builders[0].mutation); err != nil {
return nil, err
}
}
return nodes, nil
}
// SaveX is like Save, but panics if an error occurs.
func (_c *PromoCodeUsageCreateBulk) SaveX(ctx context.Context) []*PromoCodeUsage {
v, err := _c.Save(ctx)
if err != nil {
panic(err)
}
return v
}
// Exec executes the query.
func (_c *PromoCodeUsageCreateBulk) Exec(ctx context.Context) error {
_, err := _c.Save(ctx)
return err
}
// ExecX is like Exec, but panics if an error occurs.
func (_c *PromoCodeUsageCreateBulk) ExecX(ctx context.Context) {
if err := _c.Exec(ctx); err != nil {
panic(err)
}
}
// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause
// of the `INSERT` statement. For example:
//
// client.PromoCodeUsage.CreateBulk(builders...).
// OnConflict(
// // Update the row with the new values
// // the was proposed for insertion.
// sql.ResolveWithNewValues(),
// ).
// // Override some of the fields with custom
// // update values.
// Update(func(u *ent.PromoCodeUsageUpsert) {
// SetPromoCodeID(v+v).
// }).
// Exec(ctx)
func (_c *PromoCodeUsageCreateBulk) OnConflict(opts ...sql.ConflictOption) *PromoCodeUsageUpsertBulk {
_c.conflict = opts
return &PromoCodeUsageUpsertBulk{
create: _c,
}
}
// OnConflictColumns calls `OnConflict` and configures the columns
// as conflict target. Using this option is equivalent to using:
//
// client.PromoCodeUsage.Create().
// OnConflict(sql.ConflictColumns(columns...)).
// Exec(ctx)
func (_c *PromoCodeUsageCreateBulk) OnConflictColumns(columns ...string) *PromoCodeUsageUpsertBulk {
_c.conflict = append(_c.conflict, sql.ConflictColumns(columns...))
return &PromoCodeUsageUpsertBulk{
create: _c,
}
}
// PromoCodeUsageUpsertBulk is the builder for "upsert"-ing
// a bulk of PromoCodeUsage nodes.
type PromoCodeUsageUpsertBulk struct {
create *PromoCodeUsageCreateBulk
}
// UpdateNewValues updates the mutable fields using the new values that
// were set on create. Using this option is equivalent to using:
//
// client.PromoCodeUsage.Create().
// OnConflict(
// sql.ResolveWithNewValues(),
// ).
// Exec(ctx)
func (u *PromoCodeUsageUpsertBulk) UpdateNewValues() *PromoCodeUsageUpsertBulk {
u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues())
return u
}
// Ignore sets each column to itself in case of conflict.
// Using this option is equivalent to using:
//
// client.PromoCodeUsage.Create().
// OnConflict(sql.ResolveWithIgnore()).
// Exec(ctx)
func (u *PromoCodeUsageUpsertBulk) Ignore() *PromoCodeUsageUpsertBulk {
u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore())
return u
}
// DoNothing configures the conflict_action to `DO NOTHING`.
// Supported only by SQLite and PostgreSQL.
func (u *PromoCodeUsageUpsertBulk) DoNothing() *PromoCodeUsageUpsertBulk {
u.create.conflict = append(u.create.conflict, sql.DoNothing())
return u
}
// Update allows overriding fields `UPDATE` values. See the PromoCodeUsageCreateBulk.OnConflict
// documentation for more info.
func (u *PromoCodeUsageUpsertBulk) Update(set func(*PromoCodeUsageUpsert)) *PromoCodeUsageUpsertBulk {
u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) {
set(&PromoCodeUsageUpsert{UpdateSet: update})
}))
return u
}
// SetPromoCodeID sets the "promo_code_id" field.
func (u *PromoCodeUsageUpsertBulk) SetPromoCodeID(v int64) *PromoCodeUsageUpsertBulk {
return u.Update(func(s *PromoCodeUsageUpsert) {
s.SetPromoCodeID(v)
})
}
// UpdatePromoCodeID sets the "promo_code_id" field to the value that was provided on create.
func (u *PromoCodeUsageUpsertBulk) UpdatePromoCodeID() *PromoCodeUsageUpsertBulk {
return u.Update(func(s *PromoCodeUsageUpsert) {
s.UpdatePromoCodeID()
})
}
// SetUserID sets the "user_id" field.
func (u *PromoCodeUsageUpsertBulk) SetUserID(v int64) *PromoCodeUsageUpsertBulk {
return u.Update(func(s *PromoCodeUsageUpsert) {
s.SetUserID(v)
})
}
// UpdateUserID sets the "user_id" field to the value that was provided on create.
func (u *PromoCodeUsageUpsertBulk) UpdateUserID() *PromoCodeUsageUpsertBulk {
return u.Update(func(s *PromoCodeUsageUpsert) {
s.UpdateUserID()
})
}
// SetBonusAmount sets the "bonus_amount" field.
func (u *PromoCodeUsageUpsertBulk) SetBonusAmount(v float64) *PromoCodeUsageUpsertBulk {
return u.Update(func(s *PromoCodeUsageUpsert) {
s.SetBonusAmount(v)
})
}
// AddBonusAmount adds v to the "bonus_amount" field.
func (u *PromoCodeUsageUpsertBulk) AddBonusAmount(v float64) *PromoCodeUsageUpsertBulk {
return u.Update(func(s *PromoCodeUsageUpsert) {
s.AddBonusAmount(v)
})
}
// UpdateBonusAmount sets the "bonus_amount" field to the value that was provided on create.
func (u *PromoCodeUsageUpsertBulk) UpdateBonusAmount() *PromoCodeUsageUpsertBulk {
return u.Update(func(s *PromoCodeUsageUpsert) {
s.UpdateBonusAmount()
})
}
// SetUsedAt sets the "used_at" field.
func (u *PromoCodeUsageUpsertBulk) SetUsedAt(v time.Time) *PromoCodeUsageUpsertBulk {
return u.Update(func(s *PromoCodeUsageUpsert) {
s.SetUsedAt(v)
})
}
// UpdateUsedAt sets the "used_at" field to the value that was provided on create.
func (u *PromoCodeUsageUpsertBulk) UpdateUsedAt() *PromoCodeUsageUpsertBulk {
return u.Update(func(s *PromoCodeUsageUpsert) {
s.UpdateUsedAt()
})
}
// Exec executes the query.
func (u *PromoCodeUsageUpsertBulk) Exec(ctx context.Context) error {
if u.create.err != nil {
return u.create.err
}
for i, b := range u.create.builders {
if len(b.conflict) != 0 {
return fmt.Errorf("ent: OnConflict was set for builder %d. Set it on the PromoCodeUsageCreateBulk instead", i)
}
}
if len(u.create.conflict) == 0 {
return errors.New("ent: missing options for PromoCodeUsageCreateBulk.OnConflict")
}
return u.create.Exec(ctx)
}
// ExecX is like Exec, but panics if an error occurs.
func (u *PromoCodeUsageUpsertBulk) ExecX(ctx context.Context) {
if err := u.create.Exec(ctx); err != nil {
panic(err)
}
}

View File

@@ -0,0 +1,88 @@
// Code generated by ent, DO NOT EDIT.
package ent
import (
"context"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field"
"github.com/Wei-Shaw/sub2api/ent/predicate"
"github.com/Wei-Shaw/sub2api/ent/promocodeusage"
)
// PromoCodeUsageDelete is the builder for deleting a PromoCodeUsage entity.
type PromoCodeUsageDelete struct {
config
hooks []Hook
mutation *PromoCodeUsageMutation
}
// Where appends a list predicates to the PromoCodeUsageDelete builder.
func (_d *PromoCodeUsageDelete) Where(ps ...predicate.PromoCodeUsage) *PromoCodeUsageDelete {
_d.mutation.Where(ps...)
return _d
}
// Exec executes the deletion query and returns how many vertices were deleted.
func (_d *PromoCodeUsageDelete) Exec(ctx context.Context) (int, error) {
return withHooks(ctx, _d.sqlExec, _d.mutation, _d.hooks)
}
// ExecX is like Exec, but panics if an error occurs.
func (_d *PromoCodeUsageDelete) ExecX(ctx context.Context) int {
n, err := _d.Exec(ctx)
if err != nil {
panic(err)
}
return n
}
func (_d *PromoCodeUsageDelete) sqlExec(ctx context.Context) (int, error) {
_spec := sqlgraph.NewDeleteSpec(promocodeusage.Table, sqlgraph.NewFieldSpec(promocodeusage.FieldID, field.TypeInt64))
if ps := _d.mutation.predicates; len(ps) > 0 {
_spec.Predicate = func(selector *sql.Selector) {
for i := range ps {
ps[i](selector)
}
}
}
affected, err := sqlgraph.DeleteNodes(ctx, _d.driver, _spec)
if err != nil && sqlgraph.IsConstraintError(err) {
err = &ConstraintError{msg: err.Error(), wrap: err}
}
_d.mutation.done = true
return affected, err
}
// PromoCodeUsageDeleteOne is the builder for deleting a single PromoCodeUsage entity.
type PromoCodeUsageDeleteOne struct {
_d *PromoCodeUsageDelete
}
// Where appends a list predicates to the PromoCodeUsageDelete builder.
func (_d *PromoCodeUsageDeleteOne) Where(ps ...predicate.PromoCodeUsage) *PromoCodeUsageDeleteOne {
_d._d.mutation.Where(ps...)
return _d
}
// Exec executes the deletion query.
func (_d *PromoCodeUsageDeleteOne) Exec(ctx context.Context) error {
n, err := _d._d.Exec(ctx)
switch {
case err != nil:
return err
case n == 0:
return &NotFoundError{promocodeusage.Label}
default:
return nil
}
}
// ExecX is like Exec, but panics if an error occurs.
func (_d *PromoCodeUsageDeleteOne) ExecX(ctx context.Context) {
if err := _d.Exec(ctx); err != nil {
panic(err)
}
}

View File

@@ -0,0 +1,718 @@
// Code generated by ent, DO NOT EDIT.
package ent
import (
"context"
"fmt"
"math"
"entgo.io/ent"
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field"
"github.com/Wei-Shaw/sub2api/ent/predicate"
"github.com/Wei-Shaw/sub2api/ent/promocode"
"github.com/Wei-Shaw/sub2api/ent/promocodeusage"
"github.com/Wei-Shaw/sub2api/ent/user"
)
// PromoCodeUsageQuery is the builder for querying PromoCodeUsage entities.
type PromoCodeUsageQuery struct {
config
ctx *QueryContext
order []promocodeusage.OrderOption
inters []Interceptor
predicates []predicate.PromoCodeUsage
withPromoCode *PromoCodeQuery
withUser *UserQuery
modifiers []func(*sql.Selector)
// intermediate query (i.e. traversal path).
sql *sql.Selector
path func(context.Context) (*sql.Selector, error)
}
// Where adds a new predicate for the PromoCodeUsageQuery builder.
func (_q *PromoCodeUsageQuery) Where(ps ...predicate.PromoCodeUsage) *PromoCodeUsageQuery {
_q.predicates = append(_q.predicates, ps...)
return _q
}
// Limit the number of records to be returned by this query.
func (_q *PromoCodeUsageQuery) Limit(limit int) *PromoCodeUsageQuery {
_q.ctx.Limit = &limit
return _q
}
// Offset to start from.
func (_q *PromoCodeUsageQuery) Offset(offset int) *PromoCodeUsageQuery {
_q.ctx.Offset = &offset
return _q
}
// Unique configures the query builder to filter duplicate records on query.
// By default, unique is set to true, and can be disabled using this method.
func (_q *PromoCodeUsageQuery) Unique(unique bool) *PromoCodeUsageQuery {
_q.ctx.Unique = &unique
return _q
}
// Order specifies how the records should be ordered.
func (_q *PromoCodeUsageQuery) Order(o ...promocodeusage.OrderOption) *PromoCodeUsageQuery {
_q.order = append(_q.order, o...)
return _q
}
// QueryPromoCode chains the current query on the "promo_code" edge.
func (_q *PromoCodeUsageQuery) QueryPromoCode() *PromoCodeQuery {
query := (&PromoCodeClient{config: _q.config}).Query()
query.path = func(ctx context.Context) (fromU *sql.Selector, err error) {
if err := _q.prepareQuery(ctx); err != nil {
return nil, err
}
selector := _q.sqlQuery(ctx)
if err := selector.Err(); err != nil {
return nil, err
}
step := sqlgraph.NewStep(
sqlgraph.From(promocodeusage.Table, promocodeusage.FieldID, selector),
sqlgraph.To(promocode.Table, promocode.FieldID),
sqlgraph.Edge(sqlgraph.M2O, true, promocodeusage.PromoCodeTable, promocodeusage.PromoCodeColumn),
)
fromU = sqlgraph.SetNeighbors(_q.driver.Dialect(), step)
return fromU, nil
}
return query
}
// QueryUser chains the current query on the "user" edge.
func (_q *PromoCodeUsageQuery) QueryUser() *UserQuery {
query := (&UserClient{config: _q.config}).Query()
query.path = func(ctx context.Context) (fromU *sql.Selector, err error) {
if err := _q.prepareQuery(ctx); err != nil {
return nil, err
}
selector := _q.sqlQuery(ctx)
if err := selector.Err(); err != nil {
return nil, err
}
step := sqlgraph.NewStep(
sqlgraph.From(promocodeusage.Table, promocodeusage.FieldID, selector),
sqlgraph.To(user.Table, user.FieldID),
sqlgraph.Edge(sqlgraph.M2O, true, promocodeusage.UserTable, promocodeusage.UserColumn),
)
fromU = sqlgraph.SetNeighbors(_q.driver.Dialect(), step)
return fromU, nil
}
return query
}
// First returns the first PromoCodeUsage entity from the query.
// Returns a *NotFoundError when no PromoCodeUsage was found.
func (_q *PromoCodeUsageQuery) First(ctx context.Context) (*PromoCodeUsage, error) {
nodes, err := _q.Limit(1).All(setContextOp(ctx, _q.ctx, ent.OpQueryFirst))
if err != nil {
return nil, err
}
if len(nodes) == 0 {
return nil, &NotFoundError{promocodeusage.Label}
}
return nodes[0], nil
}
// FirstX is like First, but panics if an error occurs.
func (_q *PromoCodeUsageQuery) FirstX(ctx context.Context) *PromoCodeUsage {
node, err := _q.First(ctx)
if err != nil && !IsNotFound(err) {
panic(err)
}
return node
}
// FirstID returns the first PromoCodeUsage ID from the query.
// Returns a *NotFoundError when no PromoCodeUsage ID was found.
func (_q *PromoCodeUsageQuery) FirstID(ctx context.Context) (id int64, err error) {
var ids []int64
if ids, err = _q.Limit(1).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryFirstID)); err != nil {
return
}
if len(ids) == 0 {
err = &NotFoundError{promocodeusage.Label}
return
}
return ids[0], nil
}
// FirstIDX is like FirstID, but panics if an error occurs.
func (_q *PromoCodeUsageQuery) FirstIDX(ctx context.Context) int64 {
id, err := _q.FirstID(ctx)
if err != nil && !IsNotFound(err) {
panic(err)
}
return id
}
// Only returns a single PromoCodeUsage entity found by the query, ensuring it only returns one.
// Returns a *NotSingularError when more than one PromoCodeUsage entity is found.
// Returns a *NotFoundError when no PromoCodeUsage entities are found.
func (_q *PromoCodeUsageQuery) Only(ctx context.Context) (*PromoCodeUsage, error) {
nodes, err := _q.Limit(2).All(setContextOp(ctx, _q.ctx, ent.OpQueryOnly))
if err != nil {
return nil, err
}
switch len(nodes) {
case 1:
return nodes[0], nil
case 0:
return nil, &NotFoundError{promocodeusage.Label}
default:
return nil, &NotSingularError{promocodeusage.Label}
}
}
// OnlyX is like Only, but panics if an error occurs.
func (_q *PromoCodeUsageQuery) OnlyX(ctx context.Context) *PromoCodeUsage {
node, err := _q.Only(ctx)
if err != nil {
panic(err)
}
return node
}
// OnlyID is like Only, but returns the only PromoCodeUsage ID in the query.
// Returns a *NotSingularError when more than one PromoCodeUsage ID is found.
// Returns a *NotFoundError when no entities are found.
func (_q *PromoCodeUsageQuery) OnlyID(ctx context.Context) (id int64, err error) {
var ids []int64
if ids, err = _q.Limit(2).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryOnlyID)); err != nil {
return
}
switch len(ids) {
case 1:
id = ids[0]
case 0:
err = &NotFoundError{promocodeusage.Label}
default:
err = &NotSingularError{promocodeusage.Label}
}
return
}
// OnlyIDX is like OnlyID, but panics if an error occurs.
func (_q *PromoCodeUsageQuery) OnlyIDX(ctx context.Context) int64 {
id, err := _q.OnlyID(ctx)
if err != nil {
panic(err)
}
return id
}
// All executes the query and returns a list of PromoCodeUsages.
func (_q *PromoCodeUsageQuery) All(ctx context.Context) ([]*PromoCodeUsage, error) {
ctx = setContextOp(ctx, _q.ctx, ent.OpQueryAll)
if err := _q.prepareQuery(ctx); err != nil {
return nil, err
}
qr := querierAll[[]*PromoCodeUsage, *PromoCodeUsageQuery]()
return withInterceptors[[]*PromoCodeUsage](ctx, _q, qr, _q.inters)
}
// AllX is like All, but panics if an error occurs.
func (_q *PromoCodeUsageQuery) AllX(ctx context.Context) []*PromoCodeUsage {
nodes, err := _q.All(ctx)
if err != nil {
panic(err)
}
return nodes
}
// IDs executes the query and returns a list of PromoCodeUsage IDs.
func (_q *PromoCodeUsageQuery) IDs(ctx context.Context) (ids []int64, err error) {
if _q.ctx.Unique == nil && _q.path != nil {
_q.Unique(true)
}
ctx = setContextOp(ctx, _q.ctx, ent.OpQueryIDs)
if err = _q.Select(promocodeusage.FieldID).Scan(ctx, &ids); err != nil {
return nil, err
}
return ids, nil
}
// IDsX is like IDs, but panics if an error occurs.
func (_q *PromoCodeUsageQuery) IDsX(ctx context.Context) []int64 {
ids, err := _q.IDs(ctx)
if err != nil {
panic(err)
}
return ids
}
// Count returns the count of the given query.
func (_q *PromoCodeUsageQuery) Count(ctx context.Context) (int, error) {
ctx = setContextOp(ctx, _q.ctx, ent.OpQueryCount)
if err := _q.prepareQuery(ctx); err != nil {
return 0, err
}
return withInterceptors[int](ctx, _q, querierCount[*PromoCodeUsageQuery](), _q.inters)
}
// CountX is like Count, but panics if an error occurs.
func (_q *PromoCodeUsageQuery) CountX(ctx context.Context) int {
count, err := _q.Count(ctx)
if err != nil {
panic(err)
}
return count
}
// Exist returns true if the query has elements in the graph.
func (_q *PromoCodeUsageQuery) Exist(ctx context.Context) (bool, error) {
ctx = setContextOp(ctx, _q.ctx, ent.OpQueryExist)
switch _, err := _q.FirstID(ctx); {
case IsNotFound(err):
return false, nil
case err != nil:
return false, fmt.Errorf("ent: check existence: %w", err)
default:
return true, nil
}
}
// ExistX is like Exist, but panics if an error occurs.
func (_q *PromoCodeUsageQuery) ExistX(ctx context.Context) bool {
exist, err := _q.Exist(ctx)
if err != nil {
panic(err)
}
return exist
}
// Clone returns a duplicate of the PromoCodeUsageQuery builder, including all associated steps. It can be
// used to prepare common query builders and use them differently after the clone is made.
func (_q *PromoCodeUsageQuery) Clone() *PromoCodeUsageQuery {
if _q == nil {
return nil
}
return &PromoCodeUsageQuery{
config: _q.config,
ctx: _q.ctx.Clone(),
order: append([]promocodeusage.OrderOption{}, _q.order...),
inters: append([]Interceptor{}, _q.inters...),
predicates: append([]predicate.PromoCodeUsage{}, _q.predicates...),
withPromoCode: _q.withPromoCode.Clone(),
withUser: _q.withUser.Clone(),
// clone intermediate query.
sql: _q.sql.Clone(),
path: _q.path,
}
}
// WithPromoCode tells the query-builder to eager-load the nodes that are connected to
// the "promo_code" edge. The optional arguments are used to configure the query builder of the edge.
func (_q *PromoCodeUsageQuery) WithPromoCode(opts ...func(*PromoCodeQuery)) *PromoCodeUsageQuery {
query := (&PromoCodeClient{config: _q.config}).Query()
for _, opt := range opts {
opt(query)
}
_q.withPromoCode = query
return _q
}
// WithUser tells the query-builder to eager-load the nodes that are connected to
// the "user" edge. The optional arguments are used to configure the query builder of the edge.
func (_q *PromoCodeUsageQuery) WithUser(opts ...func(*UserQuery)) *PromoCodeUsageQuery {
query := (&UserClient{config: _q.config}).Query()
for _, opt := range opts {
opt(query)
}
_q.withUser = query
return _q
}
// GroupBy is used to group vertices by one or more fields/columns.
// It is often used with aggregate functions, like: count, max, mean, min, sum.
//
// Example:
//
// var v []struct {
// PromoCodeID int64 `json:"promo_code_id,omitempty"`
// Count int `json:"count,omitempty"`
// }
//
// client.PromoCodeUsage.Query().
// GroupBy(promocodeusage.FieldPromoCodeID).
// Aggregate(ent.Count()).
// Scan(ctx, &v)
func (_q *PromoCodeUsageQuery) GroupBy(field string, fields ...string) *PromoCodeUsageGroupBy {
_q.ctx.Fields = append([]string{field}, fields...)
grbuild := &PromoCodeUsageGroupBy{build: _q}
grbuild.flds = &_q.ctx.Fields
grbuild.label = promocodeusage.Label
grbuild.scan = grbuild.Scan
return grbuild
}
// Select allows the selection one or more fields/columns for the given query,
// instead of selecting all fields in the entity.
//
// Example:
//
// var v []struct {
// PromoCodeID int64 `json:"promo_code_id,omitempty"`
// }
//
// client.PromoCodeUsage.Query().
// Select(promocodeusage.FieldPromoCodeID).
// Scan(ctx, &v)
func (_q *PromoCodeUsageQuery) Select(fields ...string) *PromoCodeUsageSelect {
_q.ctx.Fields = append(_q.ctx.Fields, fields...)
sbuild := &PromoCodeUsageSelect{PromoCodeUsageQuery: _q}
sbuild.label = promocodeusage.Label
sbuild.flds, sbuild.scan = &_q.ctx.Fields, sbuild.Scan
return sbuild
}
// Aggregate returns a PromoCodeUsageSelect configured with the given aggregations.
func (_q *PromoCodeUsageQuery) Aggregate(fns ...AggregateFunc) *PromoCodeUsageSelect {
return _q.Select().Aggregate(fns...)
}
func (_q *PromoCodeUsageQuery) prepareQuery(ctx context.Context) error {
for _, inter := range _q.inters {
if inter == nil {
return fmt.Errorf("ent: uninitialized interceptor (forgotten import ent/runtime?)")
}
if trv, ok := inter.(Traverser); ok {
if err := trv.Traverse(ctx, _q); err != nil {
return err
}
}
}
for _, f := range _q.ctx.Fields {
if !promocodeusage.ValidColumn(f) {
return &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)}
}
}
if _q.path != nil {
prev, err := _q.path(ctx)
if err != nil {
return err
}
_q.sql = prev
}
return nil
}
func (_q *PromoCodeUsageQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*PromoCodeUsage, error) {
var (
nodes = []*PromoCodeUsage{}
_spec = _q.querySpec()
loadedTypes = [2]bool{
_q.withPromoCode != nil,
_q.withUser != nil,
}
)
_spec.ScanValues = func(columns []string) ([]any, error) {
return (*PromoCodeUsage).scanValues(nil, columns)
}
_spec.Assign = func(columns []string, values []any) error {
node := &PromoCodeUsage{config: _q.config}
nodes = append(nodes, node)
node.Edges.loadedTypes = loadedTypes
return node.assignValues(columns, values)
}
if len(_q.modifiers) > 0 {
_spec.Modifiers = _q.modifiers
}
for i := range hooks {
hooks[i](ctx, _spec)
}
if err := sqlgraph.QueryNodes(ctx, _q.driver, _spec); err != nil {
return nil, err
}
if len(nodes) == 0 {
return nodes, nil
}
if query := _q.withPromoCode; query != nil {
if err := _q.loadPromoCode(ctx, query, nodes, nil,
func(n *PromoCodeUsage, e *PromoCode) { n.Edges.PromoCode = e }); err != nil {
return nil, err
}
}
if query := _q.withUser; query != nil {
if err := _q.loadUser(ctx, query, nodes, nil,
func(n *PromoCodeUsage, e *User) { n.Edges.User = e }); err != nil {
return nil, err
}
}
return nodes, nil
}
func (_q *PromoCodeUsageQuery) loadPromoCode(ctx context.Context, query *PromoCodeQuery, nodes []*PromoCodeUsage, init func(*PromoCodeUsage), assign func(*PromoCodeUsage, *PromoCode)) error {
ids := make([]int64, 0, len(nodes))
nodeids := make(map[int64][]*PromoCodeUsage)
for i := range nodes {
fk := nodes[i].PromoCodeID
if _, ok := nodeids[fk]; !ok {
ids = append(ids, fk)
}
nodeids[fk] = append(nodeids[fk], nodes[i])
}
if len(ids) == 0 {
return nil
}
query.Where(promocode.IDIn(ids...))
neighbors, err := query.All(ctx)
if err != nil {
return err
}
for _, n := range neighbors {
nodes, ok := nodeids[n.ID]
if !ok {
return fmt.Errorf(`unexpected foreign-key "promo_code_id" returned %v`, n.ID)
}
for i := range nodes {
assign(nodes[i], n)
}
}
return nil
}
func (_q *PromoCodeUsageQuery) loadUser(ctx context.Context, query *UserQuery, nodes []*PromoCodeUsage, init func(*PromoCodeUsage), assign func(*PromoCodeUsage, *User)) error {
ids := make([]int64, 0, len(nodes))
nodeids := make(map[int64][]*PromoCodeUsage)
for i := range nodes {
fk := nodes[i].UserID
if _, ok := nodeids[fk]; !ok {
ids = append(ids, fk)
}
nodeids[fk] = append(nodeids[fk], nodes[i])
}
if len(ids) == 0 {
return nil
}
query.Where(user.IDIn(ids...))
neighbors, err := query.All(ctx)
if err != nil {
return err
}
for _, n := range neighbors {
nodes, ok := nodeids[n.ID]
if !ok {
return fmt.Errorf(`unexpected foreign-key "user_id" returned %v`, n.ID)
}
for i := range nodes {
assign(nodes[i], n)
}
}
return nil
}
func (_q *PromoCodeUsageQuery) sqlCount(ctx context.Context) (int, error) {
_spec := _q.querySpec()
if len(_q.modifiers) > 0 {
_spec.Modifiers = _q.modifiers
}
_spec.Node.Columns = _q.ctx.Fields
if len(_q.ctx.Fields) > 0 {
_spec.Unique = _q.ctx.Unique != nil && *_q.ctx.Unique
}
return sqlgraph.CountNodes(ctx, _q.driver, _spec)
}
func (_q *PromoCodeUsageQuery) querySpec() *sqlgraph.QuerySpec {
_spec := sqlgraph.NewQuerySpec(promocodeusage.Table, promocodeusage.Columns, sqlgraph.NewFieldSpec(promocodeusage.FieldID, field.TypeInt64))
_spec.From = _q.sql
if unique := _q.ctx.Unique; unique != nil {
_spec.Unique = *unique
} else if _q.path != nil {
_spec.Unique = true
}
if fields := _q.ctx.Fields; len(fields) > 0 {
_spec.Node.Columns = make([]string, 0, len(fields))
_spec.Node.Columns = append(_spec.Node.Columns, promocodeusage.FieldID)
for i := range fields {
if fields[i] != promocodeusage.FieldID {
_spec.Node.Columns = append(_spec.Node.Columns, fields[i])
}
}
if _q.withPromoCode != nil {
_spec.Node.AddColumnOnce(promocodeusage.FieldPromoCodeID)
}
if _q.withUser != nil {
_spec.Node.AddColumnOnce(promocodeusage.FieldUserID)
}
}
if ps := _q.predicates; len(ps) > 0 {
_spec.Predicate = func(selector *sql.Selector) {
for i := range ps {
ps[i](selector)
}
}
}
if limit := _q.ctx.Limit; limit != nil {
_spec.Limit = *limit
}
if offset := _q.ctx.Offset; offset != nil {
_spec.Offset = *offset
}
if ps := _q.order; len(ps) > 0 {
_spec.Order = func(selector *sql.Selector) {
for i := range ps {
ps[i](selector)
}
}
}
return _spec
}
func (_q *PromoCodeUsageQuery) sqlQuery(ctx context.Context) *sql.Selector {
builder := sql.Dialect(_q.driver.Dialect())
t1 := builder.Table(promocodeusage.Table)
columns := _q.ctx.Fields
if len(columns) == 0 {
columns = promocodeusage.Columns
}
selector := builder.Select(t1.Columns(columns...)...).From(t1)
if _q.sql != nil {
selector = _q.sql
selector.Select(selector.Columns(columns...)...)
}
if _q.ctx.Unique != nil && *_q.ctx.Unique {
selector.Distinct()
}
for _, m := range _q.modifiers {
m(selector)
}
for _, p := range _q.predicates {
p(selector)
}
for _, p := range _q.order {
p(selector)
}
if offset := _q.ctx.Offset; offset != nil {
// limit is mandatory for offset clause. We start
// with default value, and override it below if needed.
selector.Offset(*offset).Limit(math.MaxInt32)
}
if limit := _q.ctx.Limit; limit != nil {
selector.Limit(*limit)
}
return selector
}
// ForUpdate locks the selected rows against concurrent updates, and prevent them from being
// updated, deleted or "selected ... for update" by other sessions, until the transaction is
// either committed or rolled-back.
func (_q *PromoCodeUsageQuery) ForUpdate(opts ...sql.LockOption) *PromoCodeUsageQuery {
if _q.driver.Dialect() == dialect.Postgres {
_q.Unique(false)
}
_q.modifiers = append(_q.modifiers, func(s *sql.Selector) {
s.ForUpdate(opts...)
})
return _q
}
// ForShare behaves similarly to ForUpdate, except that it acquires a shared mode lock
// on any rows that are read. Other sessions can read the rows, but cannot modify them
// until your transaction commits.
func (_q *PromoCodeUsageQuery) ForShare(opts ...sql.LockOption) *PromoCodeUsageQuery {
if _q.driver.Dialect() == dialect.Postgres {
_q.Unique(false)
}
_q.modifiers = append(_q.modifiers, func(s *sql.Selector) {
s.ForShare(opts...)
})
return _q
}
// PromoCodeUsageGroupBy is the group-by builder for PromoCodeUsage entities.
type PromoCodeUsageGroupBy struct {
selector
build *PromoCodeUsageQuery
}
// Aggregate adds the given aggregation functions to the group-by query.
func (_g *PromoCodeUsageGroupBy) Aggregate(fns ...AggregateFunc) *PromoCodeUsageGroupBy {
_g.fns = append(_g.fns, fns...)
return _g
}
// Scan applies the selector query and scans the result into the given value.
func (_g *PromoCodeUsageGroupBy) Scan(ctx context.Context, v any) error {
ctx = setContextOp(ctx, _g.build.ctx, ent.OpQueryGroupBy)
if err := _g.build.prepareQuery(ctx); err != nil {
return err
}
return scanWithInterceptors[*PromoCodeUsageQuery, *PromoCodeUsageGroupBy](ctx, _g.build, _g, _g.build.inters, v)
}
func (_g *PromoCodeUsageGroupBy) sqlScan(ctx context.Context, root *PromoCodeUsageQuery, v any) error {
selector := root.sqlQuery(ctx).Select()
aggregation := make([]string, 0, len(_g.fns))
for _, fn := range _g.fns {
aggregation = append(aggregation, fn(selector))
}
if len(selector.SelectedColumns()) == 0 {
columns := make([]string, 0, len(*_g.flds)+len(_g.fns))
for _, f := range *_g.flds {
columns = append(columns, selector.C(f))
}
columns = append(columns, aggregation...)
selector.Select(columns...)
}
selector.GroupBy(selector.Columns(*_g.flds...)...)
if err := selector.Err(); err != nil {
return err
}
rows := &sql.Rows{}
query, args := selector.Query()
if err := _g.build.driver.Query(ctx, query, args, rows); err != nil {
return err
}
defer rows.Close()
return sql.ScanSlice(rows, v)
}
// PromoCodeUsageSelect is the builder for selecting fields of PromoCodeUsage entities.
type PromoCodeUsageSelect struct {
*PromoCodeUsageQuery
selector
}
// Aggregate adds the given aggregation functions to the selector query.
func (_s *PromoCodeUsageSelect) Aggregate(fns ...AggregateFunc) *PromoCodeUsageSelect {
_s.fns = append(_s.fns, fns...)
return _s
}
// Scan applies the selector query and scans the result into the given value.
func (_s *PromoCodeUsageSelect) Scan(ctx context.Context, v any) error {
ctx = setContextOp(ctx, _s.ctx, ent.OpQuerySelect)
if err := _s.prepareQuery(ctx); err != nil {
return err
}
return scanWithInterceptors[*PromoCodeUsageQuery, *PromoCodeUsageSelect](ctx, _s.PromoCodeUsageQuery, _s, _s.inters, v)
}
func (_s *PromoCodeUsageSelect) sqlScan(ctx context.Context, root *PromoCodeUsageQuery, v any) error {
selector := root.sqlQuery(ctx)
aggregation := make([]string, 0, len(_s.fns))
for _, fn := range _s.fns {
aggregation = append(aggregation, fn(selector))
}
switch n := len(*_s.selector.flds); {
case n == 0 && len(aggregation) > 0:
selector.Select(aggregation...)
case n != 0 && len(aggregation) > 0:
selector.AppendSelect(aggregation...)
}
rows := &sql.Rows{}
query, args := selector.Query()
if err := _s.driver.Query(ctx, query, args, rows); err != nil {
return err
}
defer rows.Close()
return sql.ScanSlice(rows, v)
}

View File

@@ -0,0 +1,510 @@
// Code generated by ent, DO NOT EDIT.
package ent
import (
"context"
"errors"
"fmt"
"time"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field"
"github.com/Wei-Shaw/sub2api/ent/predicate"
"github.com/Wei-Shaw/sub2api/ent/promocode"
"github.com/Wei-Shaw/sub2api/ent/promocodeusage"
"github.com/Wei-Shaw/sub2api/ent/user"
)
// PromoCodeUsageUpdate is the builder for updating PromoCodeUsage entities.
type PromoCodeUsageUpdate struct {
config
hooks []Hook
mutation *PromoCodeUsageMutation
}
// Where appends a list predicates to the PromoCodeUsageUpdate builder.
func (_u *PromoCodeUsageUpdate) Where(ps ...predicate.PromoCodeUsage) *PromoCodeUsageUpdate {
_u.mutation.Where(ps...)
return _u
}
// SetPromoCodeID sets the "promo_code_id" field.
func (_u *PromoCodeUsageUpdate) SetPromoCodeID(v int64) *PromoCodeUsageUpdate {
_u.mutation.SetPromoCodeID(v)
return _u
}
// SetNillablePromoCodeID sets the "promo_code_id" field if the given value is not nil.
func (_u *PromoCodeUsageUpdate) SetNillablePromoCodeID(v *int64) *PromoCodeUsageUpdate {
if v != nil {
_u.SetPromoCodeID(*v)
}
return _u
}
// SetUserID sets the "user_id" field.
func (_u *PromoCodeUsageUpdate) SetUserID(v int64) *PromoCodeUsageUpdate {
_u.mutation.SetUserID(v)
return _u
}
// SetNillableUserID sets the "user_id" field if the given value is not nil.
func (_u *PromoCodeUsageUpdate) SetNillableUserID(v *int64) *PromoCodeUsageUpdate {
if v != nil {
_u.SetUserID(*v)
}
return _u
}
// SetBonusAmount sets the "bonus_amount" field.
func (_u *PromoCodeUsageUpdate) SetBonusAmount(v float64) *PromoCodeUsageUpdate {
_u.mutation.ResetBonusAmount()
_u.mutation.SetBonusAmount(v)
return _u
}
// SetNillableBonusAmount sets the "bonus_amount" field if the given value is not nil.
func (_u *PromoCodeUsageUpdate) SetNillableBonusAmount(v *float64) *PromoCodeUsageUpdate {
if v != nil {
_u.SetBonusAmount(*v)
}
return _u
}
// AddBonusAmount adds value to the "bonus_amount" field.
func (_u *PromoCodeUsageUpdate) AddBonusAmount(v float64) *PromoCodeUsageUpdate {
_u.mutation.AddBonusAmount(v)
return _u
}
// SetUsedAt sets the "used_at" field.
func (_u *PromoCodeUsageUpdate) SetUsedAt(v time.Time) *PromoCodeUsageUpdate {
_u.mutation.SetUsedAt(v)
return _u
}
// SetNillableUsedAt sets the "used_at" field if the given value is not nil.
func (_u *PromoCodeUsageUpdate) SetNillableUsedAt(v *time.Time) *PromoCodeUsageUpdate {
if v != nil {
_u.SetUsedAt(*v)
}
return _u
}
// SetPromoCode sets the "promo_code" edge to the PromoCode entity.
func (_u *PromoCodeUsageUpdate) SetPromoCode(v *PromoCode) *PromoCodeUsageUpdate {
return _u.SetPromoCodeID(v.ID)
}
// SetUser sets the "user" edge to the User entity.
func (_u *PromoCodeUsageUpdate) SetUser(v *User) *PromoCodeUsageUpdate {
return _u.SetUserID(v.ID)
}
// Mutation returns the PromoCodeUsageMutation object of the builder.
func (_u *PromoCodeUsageUpdate) Mutation() *PromoCodeUsageMutation {
return _u.mutation
}
// ClearPromoCode clears the "promo_code" edge to the PromoCode entity.
func (_u *PromoCodeUsageUpdate) ClearPromoCode() *PromoCodeUsageUpdate {
_u.mutation.ClearPromoCode()
return _u
}
// ClearUser clears the "user" edge to the User entity.
func (_u *PromoCodeUsageUpdate) ClearUser() *PromoCodeUsageUpdate {
_u.mutation.ClearUser()
return _u
}
// Save executes the query and returns the number of nodes affected by the update operation.
func (_u *PromoCodeUsageUpdate) Save(ctx context.Context) (int, error) {
return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks)
}
// SaveX is like Save, but panics if an error occurs.
func (_u *PromoCodeUsageUpdate) SaveX(ctx context.Context) int {
affected, err := _u.Save(ctx)
if err != nil {
panic(err)
}
return affected
}
// Exec executes the query.
func (_u *PromoCodeUsageUpdate) Exec(ctx context.Context) error {
_, err := _u.Save(ctx)
return err
}
// ExecX is like Exec, but panics if an error occurs.
func (_u *PromoCodeUsageUpdate) ExecX(ctx context.Context) {
if err := _u.Exec(ctx); err != nil {
panic(err)
}
}
// check runs all checks and user-defined validators on the builder.
func (_u *PromoCodeUsageUpdate) check() error {
if _u.mutation.PromoCodeCleared() && len(_u.mutation.PromoCodeIDs()) > 0 {
return errors.New(`ent: clearing a required unique edge "PromoCodeUsage.promo_code"`)
}
if _u.mutation.UserCleared() && len(_u.mutation.UserIDs()) > 0 {
return errors.New(`ent: clearing a required unique edge "PromoCodeUsage.user"`)
}
return nil
}
func (_u *PromoCodeUsageUpdate) sqlSave(ctx context.Context) (_node int, err error) {
if err := _u.check(); err != nil {
return _node, err
}
_spec := sqlgraph.NewUpdateSpec(promocodeusage.Table, promocodeusage.Columns, sqlgraph.NewFieldSpec(promocodeusage.FieldID, field.TypeInt64))
if ps := _u.mutation.predicates; len(ps) > 0 {
_spec.Predicate = func(selector *sql.Selector) {
for i := range ps {
ps[i](selector)
}
}
}
if value, ok := _u.mutation.BonusAmount(); ok {
_spec.SetField(promocodeusage.FieldBonusAmount, field.TypeFloat64, value)
}
if value, ok := _u.mutation.AddedBonusAmount(); ok {
_spec.AddField(promocodeusage.FieldBonusAmount, field.TypeFloat64, value)
}
if value, ok := _u.mutation.UsedAt(); ok {
_spec.SetField(promocodeusage.FieldUsedAt, field.TypeTime, value)
}
if _u.mutation.PromoCodeCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O,
Inverse: true,
Table: promocodeusage.PromoCodeTable,
Columns: []string{promocodeusage.PromoCodeColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(promocode.FieldID, field.TypeInt64),
},
}
_spec.Edges.Clear = append(_spec.Edges.Clear, edge)
}
if nodes := _u.mutation.PromoCodeIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O,
Inverse: true,
Table: promocodeusage.PromoCodeTable,
Columns: []string{promocodeusage.PromoCodeColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(promocode.FieldID, field.TypeInt64),
},
}
for _, k := range nodes {
edge.Target.Nodes = append(edge.Target.Nodes, k)
}
_spec.Edges.Add = append(_spec.Edges.Add, edge)
}
if _u.mutation.UserCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O,
Inverse: true,
Table: promocodeusage.UserTable,
Columns: []string{promocodeusage.UserColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeInt64),
},
}
_spec.Edges.Clear = append(_spec.Edges.Clear, edge)
}
if nodes := _u.mutation.UserIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O,
Inverse: true,
Table: promocodeusage.UserTable,
Columns: []string{promocodeusage.UserColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeInt64),
},
}
for _, k := range nodes {
edge.Target.Nodes = append(edge.Target.Nodes, k)
}
_spec.Edges.Add = append(_spec.Edges.Add, edge)
}
if _node, err = sqlgraph.UpdateNodes(ctx, _u.driver, _spec); err != nil {
if _, ok := err.(*sqlgraph.NotFoundError); ok {
err = &NotFoundError{promocodeusage.Label}
} else if sqlgraph.IsConstraintError(err) {
err = &ConstraintError{msg: err.Error(), wrap: err}
}
return 0, err
}
_u.mutation.done = true
return _node, nil
}
// PromoCodeUsageUpdateOne is the builder for updating a single PromoCodeUsage entity.
type PromoCodeUsageUpdateOne struct {
config
fields []string
hooks []Hook
mutation *PromoCodeUsageMutation
}
// SetPromoCodeID sets the "promo_code_id" field.
func (_u *PromoCodeUsageUpdateOne) SetPromoCodeID(v int64) *PromoCodeUsageUpdateOne {
_u.mutation.SetPromoCodeID(v)
return _u
}
// SetNillablePromoCodeID sets the "promo_code_id" field if the given value is not nil.
func (_u *PromoCodeUsageUpdateOne) SetNillablePromoCodeID(v *int64) *PromoCodeUsageUpdateOne {
if v != nil {
_u.SetPromoCodeID(*v)
}
return _u
}
// SetUserID sets the "user_id" field.
func (_u *PromoCodeUsageUpdateOne) SetUserID(v int64) *PromoCodeUsageUpdateOne {
_u.mutation.SetUserID(v)
return _u
}
// SetNillableUserID sets the "user_id" field if the given value is not nil.
func (_u *PromoCodeUsageUpdateOne) SetNillableUserID(v *int64) *PromoCodeUsageUpdateOne {
if v != nil {
_u.SetUserID(*v)
}
return _u
}
// SetBonusAmount sets the "bonus_amount" field.
func (_u *PromoCodeUsageUpdateOne) SetBonusAmount(v float64) *PromoCodeUsageUpdateOne {
_u.mutation.ResetBonusAmount()
_u.mutation.SetBonusAmount(v)
return _u
}
// SetNillableBonusAmount sets the "bonus_amount" field if the given value is not nil.
func (_u *PromoCodeUsageUpdateOne) SetNillableBonusAmount(v *float64) *PromoCodeUsageUpdateOne {
if v != nil {
_u.SetBonusAmount(*v)
}
return _u
}
// AddBonusAmount adds value to the "bonus_amount" field.
func (_u *PromoCodeUsageUpdateOne) AddBonusAmount(v float64) *PromoCodeUsageUpdateOne {
_u.mutation.AddBonusAmount(v)
return _u
}
// SetUsedAt sets the "used_at" field.
func (_u *PromoCodeUsageUpdateOne) SetUsedAt(v time.Time) *PromoCodeUsageUpdateOne {
_u.mutation.SetUsedAt(v)
return _u
}
// SetNillableUsedAt sets the "used_at" field if the given value is not nil.
func (_u *PromoCodeUsageUpdateOne) SetNillableUsedAt(v *time.Time) *PromoCodeUsageUpdateOne {
if v != nil {
_u.SetUsedAt(*v)
}
return _u
}
// SetPromoCode sets the "promo_code" edge to the PromoCode entity.
func (_u *PromoCodeUsageUpdateOne) SetPromoCode(v *PromoCode) *PromoCodeUsageUpdateOne {
return _u.SetPromoCodeID(v.ID)
}
// SetUser sets the "user" edge to the User entity.
func (_u *PromoCodeUsageUpdateOne) SetUser(v *User) *PromoCodeUsageUpdateOne {
return _u.SetUserID(v.ID)
}
// Mutation returns the PromoCodeUsageMutation object of the builder.
func (_u *PromoCodeUsageUpdateOne) Mutation() *PromoCodeUsageMutation {
return _u.mutation
}
// ClearPromoCode clears the "promo_code" edge to the PromoCode entity.
func (_u *PromoCodeUsageUpdateOne) ClearPromoCode() *PromoCodeUsageUpdateOne {
_u.mutation.ClearPromoCode()
return _u
}
// ClearUser clears the "user" edge to the User entity.
func (_u *PromoCodeUsageUpdateOne) ClearUser() *PromoCodeUsageUpdateOne {
_u.mutation.ClearUser()
return _u
}
// Where appends a list predicates to the PromoCodeUsageUpdate builder.
func (_u *PromoCodeUsageUpdateOne) Where(ps ...predicate.PromoCodeUsage) *PromoCodeUsageUpdateOne {
_u.mutation.Where(ps...)
return _u
}
// Select allows selecting one or more fields (columns) of the returned entity.
// The default is selecting all fields defined in the entity schema.
func (_u *PromoCodeUsageUpdateOne) Select(field string, fields ...string) *PromoCodeUsageUpdateOne {
_u.fields = append([]string{field}, fields...)
return _u
}
// Save executes the query and returns the updated PromoCodeUsage entity.
func (_u *PromoCodeUsageUpdateOne) Save(ctx context.Context) (*PromoCodeUsage, error) {
return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks)
}
// SaveX is like Save, but panics if an error occurs.
func (_u *PromoCodeUsageUpdateOne) SaveX(ctx context.Context) *PromoCodeUsage {
node, err := _u.Save(ctx)
if err != nil {
panic(err)
}
return node
}
// Exec executes the query on the entity.
func (_u *PromoCodeUsageUpdateOne) Exec(ctx context.Context) error {
_, err := _u.Save(ctx)
return err
}
// ExecX is like Exec, but panics if an error occurs.
func (_u *PromoCodeUsageUpdateOne) ExecX(ctx context.Context) {
if err := _u.Exec(ctx); err != nil {
panic(err)
}
}
// check runs all checks and user-defined validators on the builder.
func (_u *PromoCodeUsageUpdateOne) check() error {
if _u.mutation.PromoCodeCleared() && len(_u.mutation.PromoCodeIDs()) > 0 {
return errors.New(`ent: clearing a required unique edge "PromoCodeUsage.promo_code"`)
}
if _u.mutation.UserCleared() && len(_u.mutation.UserIDs()) > 0 {
return errors.New(`ent: clearing a required unique edge "PromoCodeUsage.user"`)
}
return nil
}
func (_u *PromoCodeUsageUpdateOne) sqlSave(ctx context.Context) (_node *PromoCodeUsage, err error) {
if err := _u.check(); err != nil {
return _node, err
}
_spec := sqlgraph.NewUpdateSpec(promocodeusage.Table, promocodeusage.Columns, sqlgraph.NewFieldSpec(promocodeusage.FieldID, field.TypeInt64))
id, ok := _u.mutation.ID()
if !ok {
return nil, &ValidationError{Name: "id", err: errors.New(`ent: missing "PromoCodeUsage.id" for update`)}
}
_spec.Node.ID.Value = id
if fields := _u.fields; len(fields) > 0 {
_spec.Node.Columns = make([]string, 0, len(fields))
_spec.Node.Columns = append(_spec.Node.Columns, promocodeusage.FieldID)
for _, f := range fields {
if !promocodeusage.ValidColumn(f) {
return nil, &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)}
}
if f != promocodeusage.FieldID {
_spec.Node.Columns = append(_spec.Node.Columns, f)
}
}
}
if ps := _u.mutation.predicates; len(ps) > 0 {
_spec.Predicate = func(selector *sql.Selector) {
for i := range ps {
ps[i](selector)
}
}
}
if value, ok := _u.mutation.BonusAmount(); ok {
_spec.SetField(promocodeusage.FieldBonusAmount, field.TypeFloat64, value)
}
if value, ok := _u.mutation.AddedBonusAmount(); ok {
_spec.AddField(promocodeusage.FieldBonusAmount, field.TypeFloat64, value)
}
if value, ok := _u.mutation.UsedAt(); ok {
_spec.SetField(promocodeusage.FieldUsedAt, field.TypeTime, value)
}
if _u.mutation.PromoCodeCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O,
Inverse: true,
Table: promocodeusage.PromoCodeTable,
Columns: []string{promocodeusage.PromoCodeColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(promocode.FieldID, field.TypeInt64),
},
}
_spec.Edges.Clear = append(_spec.Edges.Clear, edge)
}
if nodes := _u.mutation.PromoCodeIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O,
Inverse: true,
Table: promocodeusage.PromoCodeTable,
Columns: []string{promocodeusage.PromoCodeColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(promocode.FieldID, field.TypeInt64),
},
}
for _, k := range nodes {
edge.Target.Nodes = append(edge.Target.Nodes, k)
}
_spec.Edges.Add = append(_spec.Edges.Add, edge)
}
if _u.mutation.UserCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O,
Inverse: true,
Table: promocodeusage.UserTable,
Columns: []string{promocodeusage.UserColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeInt64),
},
}
_spec.Edges.Clear = append(_spec.Edges.Clear, edge)
}
if nodes := _u.mutation.UserIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O,
Inverse: true,
Table: promocodeusage.UserTable,
Columns: []string{promocodeusage.UserColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeInt64),
},
}
for _, k := range nodes {
edge.Target.Nodes = append(edge.Target.Nodes, k)
}
_spec.Edges.Add = append(_spec.Edges.Add, edge)
}
_node = &PromoCodeUsage{config: _u.config}
_spec.Assign = _node.assignValues
_spec.ScanValues = _node.scanValues
if err = sqlgraph.UpdateNode(ctx, _u.driver, _spec); err != nil {
if _, ok := err.(*sqlgraph.NotFoundError); ok {
err = &NotFoundError{promocodeusage.Label}
} else if sqlgraph.IsConstraintError(err) {
err = &ConstraintError{msg: err.Error(), wrap: err}
}
return nil, err
}
_u.mutation.done = true
return _node, nil
}

View File

@@ -9,6 +9,7 @@ import (
"math"
"entgo.io/ent"
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field"
@@ -25,6 +26,7 @@ type ProxyQuery struct {
inters []Interceptor
predicates []predicate.Proxy
withAccounts *AccountQuery
modifiers []func(*sql.Selector)
// intermediate query (i.e. traversal path).
sql *sql.Selector
path func(context.Context) (*sql.Selector, error)
@@ -384,6 +386,9 @@ func (_q *ProxyQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*Proxy,
node.Edges.loadedTypes = loadedTypes
return node.assignValues(columns, values)
}
if len(_q.modifiers) > 0 {
_spec.Modifiers = _q.modifiers
}
for i := range hooks {
hooks[i](ctx, _spec)
}
@@ -439,6 +444,9 @@ func (_q *ProxyQuery) loadAccounts(ctx context.Context, query *AccountQuery, nod
func (_q *ProxyQuery) sqlCount(ctx context.Context) (int, error) {
_spec := _q.querySpec()
if len(_q.modifiers) > 0 {
_spec.Modifiers = _q.modifiers
}
_spec.Node.Columns = _q.ctx.Fields
if len(_q.ctx.Fields) > 0 {
_spec.Unique = _q.ctx.Unique != nil && *_q.ctx.Unique
@@ -501,6 +509,9 @@ func (_q *ProxyQuery) sqlQuery(ctx context.Context) *sql.Selector {
if _q.ctx.Unique != nil && *_q.ctx.Unique {
selector.Distinct()
}
for _, m := range _q.modifiers {
m(selector)
}
for _, p := range _q.predicates {
p(selector)
}
@@ -518,6 +529,32 @@ func (_q *ProxyQuery) sqlQuery(ctx context.Context) *sql.Selector {
return selector
}
// ForUpdate locks the selected rows against concurrent updates, and prevent them from being
// updated, deleted or "selected ... for update" by other sessions, until the transaction is
// either committed or rolled-back.
func (_q *ProxyQuery) ForUpdate(opts ...sql.LockOption) *ProxyQuery {
if _q.driver.Dialect() == dialect.Postgres {
_q.Unique(false)
}
_q.modifiers = append(_q.modifiers, func(s *sql.Selector) {
s.ForUpdate(opts...)
})
return _q
}
// ForShare behaves similarly to ForUpdate, except that it acquires a shared mode lock
// on any rows that are read. Other sessions can read the rows, but cannot modify them
// until your transaction commits.
func (_q *ProxyQuery) ForShare(opts ...sql.LockOption) *ProxyQuery {
if _q.driver.Dialect() == dialect.Postgres {
_q.Unique(false)
}
_q.modifiers = append(_q.modifiers, func(s *sql.Selector) {
s.ForShare(opts...)
})
return _q
}
// ProxyGroupBy is the group-by builder for Proxy entities.
type ProxyGroupBy struct {
selector

View File

@@ -8,6 +8,7 @@ import (
"math"
"entgo.io/ent"
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field"
@@ -26,6 +27,7 @@ type RedeemCodeQuery struct {
predicates []predicate.RedeemCode
withUser *UserQuery
withGroup *GroupQuery
modifiers []func(*sql.Selector)
// intermediate query (i.e. traversal path).
sql *sql.Selector
path func(context.Context) (*sql.Selector, error)
@@ -420,6 +422,9 @@ func (_q *RedeemCodeQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*R
node.Edges.loadedTypes = loadedTypes
return node.assignValues(columns, values)
}
if len(_q.modifiers) > 0 {
_spec.Modifiers = _q.modifiers
}
for i := range hooks {
hooks[i](ctx, _spec)
}
@@ -511,6 +516,9 @@ func (_q *RedeemCodeQuery) loadGroup(ctx context.Context, query *GroupQuery, nod
func (_q *RedeemCodeQuery) sqlCount(ctx context.Context) (int, error) {
_spec := _q.querySpec()
if len(_q.modifiers) > 0 {
_spec.Modifiers = _q.modifiers
}
_spec.Node.Columns = _q.ctx.Fields
if len(_q.ctx.Fields) > 0 {
_spec.Unique = _q.ctx.Unique != nil && *_q.ctx.Unique
@@ -579,6 +587,9 @@ func (_q *RedeemCodeQuery) sqlQuery(ctx context.Context) *sql.Selector {
if _q.ctx.Unique != nil && *_q.ctx.Unique {
selector.Distinct()
}
for _, m := range _q.modifiers {
m(selector)
}
for _, p := range _q.predicates {
p(selector)
}
@@ -596,6 +607,32 @@ func (_q *RedeemCodeQuery) sqlQuery(ctx context.Context) *sql.Selector {
return selector
}
// ForUpdate locks the selected rows against concurrent updates, and prevent them from being
// updated, deleted or "selected ... for update" by other sessions, until the transaction is
// either committed or rolled-back.
func (_q *RedeemCodeQuery) ForUpdate(opts ...sql.LockOption) *RedeemCodeQuery {
if _q.driver.Dialect() == dialect.Postgres {
_q.Unique(false)
}
_q.modifiers = append(_q.modifiers, func(s *sql.Selector) {
s.ForUpdate(opts...)
})
return _q
}
// ForShare behaves similarly to ForUpdate, except that it acquires a shared mode lock
// on any rows that are read. Other sessions can read the rows, but cannot modify them
// until your transaction commits.
func (_q *RedeemCodeQuery) ForShare(opts ...sql.LockOption) *RedeemCodeQuery {
if _q.driver.Dialect() == dialect.Postgres {
_q.Unique(false)
}
_q.modifiers = append(_q.modifiers, func(s *sql.Selector) {
s.ForShare(opts...)
})
return _q
}
// RedeemCodeGroupBy is the group-by builder for RedeemCode entities.
type RedeemCodeGroupBy struct {
selector

View File

@@ -9,6 +9,8 @@ import (
"github.com/Wei-Shaw/sub2api/ent/accountgroup"
"github.com/Wei-Shaw/sub2api/ent/apikey"
"github.com/Wei-Shaw/sub2api/ent/group"
"github.com/Wei-Shaw/sub2api/ent/promocode"
"github.com/Wei-Shaw/sub2api/ent/promocodeusage"
"github.com/Wei-Shaw/sub2api/ent/proxy"
"github.com/Wei-Shaw/sub2api/ent/redeemcode"
"github.com/Wei-Shaw/sub2api/ent/schema"
@@ -181,12 +183,16 @@ func init() {
account.DefaultStatus = accountDescStatus.Default.(string)
// account.StatusValidator is a validator for the "status" field. It is called by the builders before save.
account.StatusValidator = accountDescStatus.Validators[0].(func(string) error)
// accountDescAutoPauseOnExpired is the schema descriptor for auto_pause_on_expired field.
accountDescAutoPauseOnExpired := accountFields[13].Descriptor()
// account.DefaultAutoPauseOnExpired holds the default value on creation for the auto_pause_on_expired field.
account.DefaultAutoPauseOnExpired = accountDescAutoPauseOnExpired.Default.(bool)
// accountDescSchedulable is the schema descriptor for schedulable field.
accountDescSchedulable := accountFields[12].Descriptor()
accountDescSchedulable := accountFields[14].Descriptor()
// account.DefaultSchedulable holds the default value on creation for the schedulable field.
account.DefaultSchedulable = accountDescSchedulable.Default.(bool)
// accountDescSessionWindowStatus is the schema descriptor for session_window_status field.
accountDescSessionWindowStatus := accountFields[18].Descriptor()
accountDescSessionWindowStatus := accountFields[20].Descriptor()
// account.SessionWindowStatusValidator is a validator for the "session_window_status" field. It is called by the builders before save.
account.SessionWindowStatusValidator = accountDescSessionWindowStatus.Validators[0].(func(string) error)
accountgroupFields := schema.AccountGroup{}.Fields()
@@ -266,6 +272,64 @@ func init() {
groupDescDefaultValidityDays := groupFields[10].Descriptor()
// group.DefaultDefaultValidityDays holds the default value on creation for the default_validity_days field.
group.DefaultDefaultValidityDays = groupDescDefaultValidityDays.Default.(int)
// groupDescClaudeCodeOnly is the schema descriptor for claude_code_only field.
groupDescClaudeCodeOnly := groupFields[14].Descriptor()
// group.DefaultClaudeCodeOnly holds the default value on creation for the claude_code_only field.
group.DefaultClaudeCodeOnly = groupDescClaudeCodeOnly.Default.(bool)
promocodeFields := schema.PromoCode{}.Fields()
_ = promocodeFields
// promocodeDescCode is the schema descriptor for code field.
promocodeDescCode := promocodeFields[0].Descriptor()
// promocode.CodeValidator is a validator for the "code" field. It is called by the builders before save.
promocode.CodeValidator = func() func(string) error {
validators := promocodeDescCode.Validators
fns := [...]func(string) error{
validators[0].(func(string) error),
validators[1].(func(string) error),
}
return func(code string) error {
for _, fn := range fns {
if err := fn(code); err != nil {
return err
}
}
return nil
}
}()
// promocodeDescBonusAmount is the schema descriptor for bonus_amount field.
promocodeDescBonusAmount := promocodeFields[1].Descriptor()
// promocode.DefaultBonusAmount holds the default value on creation for the bonus_amount field.
promocode.DefaultBonusAmount = promocodeDescBonusAmount.Default.(float64)
// promocodeDescMaxUses is the schema descriptor for max_uses field.
promocodeDescMaxUses := promocodeFields[2].Descriptor()
// promocode.DefaultMaxUses holds the default value on creation for the max_uses field.
promocode.DefaultMaxUses = promocodeDescMaxUses.Default.(int)
// promocodeDescUsedCount is the schema descriptor for used_count field.
promocodeDescUsedCount := promocodeFields[3].Descriptor()
// promocode.DefaultUsedCount holds the default value on creation for the used_count field.
promocode.DefaultUsedCount = promocodeDescUsedCount.Default.(int)
// promocodeDescStatus is the schema descriptor for status field.
promocodeDescStatus := promocodeFields[4].Descriptor()
// promocode.DefaultStatus holds the default value on creation for the status field.
promocode.DefaultStatus = promocodeDescStatus.Default.(string)
// promocode.StatusValidator is a validator for the "status" field. It is called by the builders before save.
promocode.StatusValidator = promocodeDescStatus.Validators[0].(func(string) error)
// promocodeDescCreatedAt is the schema descriptor for created_at field.
promocodeDescCreatedAt := promocodeFields[7].Descriptor()
// promocode.DefaultCreatedAt holds the default value on creation for the created_at field.
promocode.DefaultCreatedAt = promocodeDescCreatedAt.Default.(func() time.Time)
// promocodeDescUpdatedAt is the schema descriptor for updated_at field.
promocodeDescUpdatedAt := promocodeFields[8].Descriptor()
// promocode.DefaultUpdatedAt holds the default value on creation for the updated_at field.
promocode.DefaultUpdatedAt = promocodeDescUpdatedAt.Default.(func() time.Time)
// promocode.UpdateDefaultUpdatedAt holds the default value on update for the updated_at field.
promocode.UpdateDefaultUpdatedAt = promocodeDescUpdatedAt.UpdateDefault.(func() time.Time)
promocodeusageFields := schema.PromoCodeUsage{}.Fields()
_ = promocodeusageFields
// promocodeusageDescUsedAt is the schema descriptor for used_at field.
promocodeusageDescUsedAt := promocodeusageFields[3].Descriptor()
// promocodeusage.DefaultUsedAt holds the default value on creation for the used_at field.
promocodeusage.DefaultUsedAt = promocodeusageDescUsedAt.Default.(func() time.Time)
proxyMixin := schema.Proxy{}.Mixin()
proxyMixinHooks1 := proxyMixin[1].Hooks()
proxy.Hooks[0] = proxyMixinHooks1[0]
@@ -521,8 +585,24 @@ func init() {
usagelogDescStream := usagelogFields[21].Descriptor()
// usagelog.DefaultStream holds the default value on creation for the stream field.
usagelog.DefaultStream = usagelogDescStream.Default.(bool)
// usagelogDescUserAgent is the schema descriptor for user_agent field.
usagelogDescUserAgent := usagelogFields[24].Descriptor()
// usagelog.UserAgentValidator is a validator for the "user_agent" field. It is called by the builders before save.
usagelog.UserAgentValidator = usagelogDescUserAgent.Validators[0].(func(string) error)
// usagelogDescIPAddress is the schema descriptor for ip_address field.
usagelogDescIPAddress := usagelogFields[25].Descriptor()
// usagelog.IPAddressValidator is a validator for the "ip_address" field. It is called by the builders before save.
usagelog.IPAddressValidator = usagelogDescIPAddress.Validators[0].(func(string) error)
// usagelogDescImageCount is the schema descriptor for image_count field.
usagelogDescImageCount := usagelogFields[26].Descriptor()
// usagelog.DefaultImageCount holds the default value on creation for the image_count field.
usagelog.DefaultImageCount = usagelogDescImageCount.Default.(int)
// usagelogDescImageSize is the schema descriptor for image_size field.
usagelogDescImageSize := usagelogFields[27].Descriptor()
// usagelog.ImageSizeValidator is a validator for the "image_size" field. It is called by the builders before save.
usagelog.ImageSizeValidator = usagelogDescImageSize.Validators[0].(func(string) error)
// usagelogDescCreatedAt is the schema descriptor for created_at field.
usagelogDescCreatedAt := usagelogFields[24].Descriptor()
usagelogDescCreatedAt := usagelogFields[28].Descriptor()
// usagelog.DefaultCreatedAt holds the default value on creation for the created_at field.
usagelog.DefaultCreatedAt = usagelogDescCreatedAt.Default.(func() time.Time)
userMixin := schema.User{}.Mixin()

View File

@@ -118,6 +118,16 @@ func (Account) Fields() []ent.Field {
Optional().
Nillable().
SchemaType(map[string]string{dialect.Postgres: "timestamptz"}),
// expires_at: 账户过期时间(可为空)
field.Time("expires_at").
Optional().
Nillable().
Comment("Account expiration time (NULL means no expiration).").
SchemaType(map[string]string{dialect.Postgres: "timestamptz"}),
// auto_pause_on_expired: 过期后自动暂停调度
field.Bool("auto_pause_on_expired").
Default(true).
Comment("Auto pause scheduling when account expires."),
// ========== 调度和速率限制相关字段 ==========
// 这些字段在 migrations/005_schema_parity.sql 中添加

View File

@@ -46,6 +46,12 @@ func (APIKey) Fields() []ent.Field {
field.String("status").
MaxLen(20).
Default(service.StatusActive),
field.JSON("ip_whitelist", []string{}).
Optional().
Comment("Allowed IPs/CIDRs, e.g. [\"192.168.1.100\", \"10.0.0.0/8\"]"),
field.JSON("ip_blacklist", []string{}).
Optional().
Comment("Blocked IPs/CIDRs"),
}
}

View File

@@ -72,6 +72,29 @@ func (Group) Fields() []ent.Field {
SchemaType(map[string]string{dialect.Postgres: "decimal(20,8)"}),
field.Int("default_validity_days").
Default(30),
// 图片生成计费配置antigravity 和 gemini 平台使用)
field.Float("image_price_1k").
Optional().
Nillable().
SchemaType(map[string]string{dialect.Postgres: "decimal(20,8)"}),
field.Float("image_price_2k").
Optional().
Nillable().
SchemaType(map[string]string{dialect.Postgres: "decimal(20,8)"}),
field.Float("image_price_4k").
Optional().
Nillable().
SchemaType(map[string]string{dialect.Postgres: "decimal(20,8)"}),
// Claude Code 客户端限制 (added by migration 029)
field.Bool("claude_code_only").
Default(false).
Comment("是否仅允许 Claude Code 客户端"),
field.Int64("fallback_group_id").
Optional().
Nillable().
Comment("非 Claude Code 请求降级使用的分组 ID"),
}
}
@@ -87,6 +110,8 @@ func (Group) Edges() []ent.Edge {
edge.From("allowed_users", User.Type).
Ref("allowed_groups").
Through("user_allowed_groups", UserAllowedGroup.Type),
// 注意fallback_group_id 直接作为字段使用,不定义 edge
// 这样允许多个分组指向同一个降级分组M2O 关系)
}
}

View File

@@ -0,0 +1,87 @@
package schema
import (
"time"
"github.com/Wei-Shaw/sub2api/internal/service"
"entgo.io/ent"
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/entsql"
"entgo.io/ent/schema"
"entgo.io/ent/schema/edge"
"entgo.io/ent/schema/field"
"entgo.io/ent/schema/index"
)
// PromoCode holds the schema definition for the PromoCode entity.
//
// 注册优惠码:用户注册时使用,可获得赠送余额
// 与 RedeemCode 不同PromoCode 支持多次使用(有使用次数限制)
//
// 删除策略:硬删除
type PromoCode struct {
ent.Schema
}
func (PromoCode) Annotations() []schema.Annotation {
return []schema.Annotation{
entsql.Annotation{Table: "promo_codes"},
}
}
func (PromoCode) Fields() []ent.Field {
return []ent.Field{
field.String("code").
MaxLen(32).
NotEmpty().
Unique().
Comment("优惠码"),
field.Float("bonus_amount").
SchemaType(map[string]string{dialect.Postgres: "decimal(20,8)"}).
Default(0).
Comment("赠送余额金额"),
field.Int("max_uses").
Default(0).
Comment("最大使用次数0表示无限制"),
field.Int("used_count").
Default(0).
Comment("已使用次数"),
field.String("status").
MaxLen(20).
Default(service.PromoCodeStatusActive).
Comment("状态: active, disabled"),
field.Time("expires_at").
Optional().
Nillable().
SchemaType(map[string]string{dialect.Postgres: "timestamptz"}).
Comment("过期时间null表示永不过期"),
field.String("notes").
Optional().
Nillable().
SchemaType(map[string]string{dialect.Postgres: "text"}).
Comment("备注"),
field.Time("created_at").
Immutable().
Default(time.Now).
SchemaType(map[string]string{dialect.Postgres: "timestamptz"}),
field.Time("updated_at").
Default(time.Now).
UpdateDefault(time.Now).
SchemaType(map[string]string{dialect.Postgres: "timestamptz"}),
}
}
func (PromoCode) Edges() []ent.Edge {
return []ent.Edge{
edge.To("usage_records", PromoCodeUsage.Type),
}
}
func (PromoCode) Indexes() []ent.Index {
return []ent.Index{
// code 字段已在 Fields() 中声明 Unique(),无需重复索引
index.Fields("status"),
index.Fields("expires_at"),
}
}

View File

@@ -0,0 +1,66 @@
package schema
import (
"time"
"entgo.io/ent"
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/entsql"
"entgo.io/ent/schema"
"entgo.io/ent/schema/edge"
"entgo.io/ent/schema/field"
"entgo.io/ent/schema/index"
)
// PromoCodeUsage holds the schema definition for the PromoCodeUsage entity.
//
// 优惠码使用记录:记录每个用户使用优惠码的情况
type PromoCodeUsage struct {
ent.Schema
}
func (PromoCodeUsage) Annotations() []schema.Annotation {
return []schema.Annotation{
entsql.Annotation{Table: "promo_code_usages"},
}
}
func (PromoCodeUsage) Fields() []ent.Field {
return []ent.Field{
field.Int64("promo_code_id").
Comment("优惠码ID"),
field.Int64("user_id").
Comment("使用用户ID"),
field.Float("bonus_amount").
SchemaType(map[string]string{dialect.Postgres: "decimal(20,8)"}).
Comment("实际赠送金额"),
field.Time("used_at").
Default(time.Now).
SchemaType(map[string]string{dialect.Postgres: "timestamptz"}).
Comment("使用时间"),
}
}
func (PromoCodeUsage) Edges() []ent.Edge {
return []ent.Edge{
edge.From("promo_code", PromoCode.Type).
Ref("usage_records").
Field("promo_code_id").
Required().
Unique(),
edge.From("user", User.Type).
Ref("promo_code_usages").
Field("user_id").
Required().
Unique(),
}
}
func (PromoCodeUsage) Indexes() []ent.Index {
return []ent.Index{
index.Fields("promo_code_id"),
index.Fields("user_id"),
// 每个用户每个优惠码只能使用一次
index.Fields("promo_code_id", "user_id").Unique(),
}
}

View File

@@ -96,6 +96,22 @@ func (UsageLog) Fields() []ent.Field {
field.Int("first_token_ms").
Optional().
Nillable(),
field.String("user_agent").
MaxLen(512).
Optional().
Nillable(),
field.String("ip_address").
MaxLen(45). // 支持 IPv6
Optional().
Nillable(),
// 图片生成字段(仅 gemini-3-pro-image 等图片模型使用)
field.Int("image_count").
Default(0),
field.String("image_size").
MaxLen(10).
Optional().
Nillable(),
// 时间戳(只有 created_at日志不可修改
field.Time("created_at").

View File

@@ -74,6 +74,7 @@ func (User) Edges() []ent.Edge {
Through("user_allowed_groups", UserAllowedGroup.Type),
edge.To("usage_logs", UsageLog.Type),
edge.To("attribute_values", UserAttributeValue.Type),
edge.To("promo_code_usages", PromoCodeUsage.Type),
}
}

View File

@@ -8,6 +8,7 @@ import (
"math"
"entgo.io/ent"
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field"
@@ -22,6 +23,7 @@ type SettingQuery struct {
order []setting.OrderOption
inters []Interceptor
predicates []predicate.Setting
modifiers []func(*sql.Selector)
// intermediate query (i.e. traversal path).
sql *sql.Selector
path func(context.Context) (*sql.Selector, error)
@@ -343,6 +345,9 @@ func (_q *SettingQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*Sett
nodes = append(nodes, node)
return node.assignValues(columns, values)
}
if len(_q.modifiers) > 0 {
_spec.Modifiers = _q.modifiers
}
for i := range hooks {
hooks[i](ctx, _spec)
}
@@ -357,6 +362,9 @@ func (_q *SettingQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*Sett
func (_q *SettingQuery) sqlCount(ctx context.Context) (int, error) {
_spec := _q.querySpec()
if len(_q.modifiers) > 0 {
_spec.Modifiers = _q.modifiers
}
_spec.Node.Columns = _q.ctx.Fields
if len(_q.ctx.Fields) > 0 {
_spec.Unique = _q.ctx.Unique != nil && *_q.ctx.Unique
@@ -419,6 +427,9 @@ func (_q *SettingQuery) sqlQuery(ctx context.Context) *sql.Selector {
if _q.ctx.Unique != nil && *_q.ctx.Unique {
selector.Distinct()
}
for _, m := range _q.modifiers {
m(selector)
}
for _, p := range _q.predicates {
p(selector)
}
@@ -436,6 +447,32 @@ func (_q *SettingQuery) sqlQuery(ctx context.Context) *sql.Selector {
return selector
}
// ForUpdate locks the selected rows against concurrent updates, and prevent them from being
// updated, deleted or "selected ... for update" by other sessions, until the transaction is
// either committed or rolled-back.
func (_q *SettingQuery) ForUpdate(opts ...sql.LockOption) *SettingQuery {
if _q.driver.Dialect() == dialect.Postgres {
_q.Unique(false)
}
_q.modifiers = append(_q.modifiers, func(s *sql.Selector) {
s.ForUpdate(opts...)
})
return _q
}
// ForShare behaves similarly to ForUpdate, except that it acquires a shared mode lock
// on any rows that are read. Other sessions can read the rows, but cannot modify them
// until your transaction commits.
func (_q *SettingQuery) ForShare(opts ...sql.LockOption) *SettingQuery {
if _q.driver.Dialect() == dialect.Postgres {
_q.Unique(false)
}
_q.modifiers = append(_q.modifiers, func(s *sql.Selector) {
s.ForShare(opts...)
})
return _q
}
// SettingGroupBy is the group-by builder for Setting entities.
type SettingGroupBy struct {
selector

View File

@@ -22,6 +22,10 @@ type Tx struct {
AccountGroup *AccountGroupClient
// Group is the client for interacting with the Group builders.
Group *GroupClient
// PromoCode is the client for interacting with the PromoCode builders.
PromoCode *PromoCodeClient
// PromoCodeUsage is the client for interacting with the PromoCodeUsage builders.
PromoCodeUsage *PromoCodeUsageClient
// Proxy is the client for interacting with the Proxy builders.
Proxy *ProxyClient
// RedeemCode is the client for interacting with the RedeemCode builders.
@@ -175,6 +179,8 @@ func (tx *Tx) init() {
tx.Account = NewAccountClient(tx.config)
tx.AccountGroup = NewAccountGroupClient(tx.config)
tx.Group = NewGroupClient(tx.config)
tx.PromoCode = NewPromoCodeClient(tx.config)
tx.PromoCodeUsage = NewPromoCodeUsageClient(tx.config)
tx.Proxy = NewProxyClient(tx.config)
tx.RedeemCode = NewRedeemCodeClient(tx.config)
tx.Setting = NewSettingClient(tx.config)

View File

@@ -70,6 +70,14 @@ type UsageLog struct {
DurationMs *int `json:"duration_ms,omitempty"`
// FirstTokenMs holds the value of the "first_token_ms" field.
FirstTokenMs *int `json:"first_token_ms,omitempty"`
// UserAgent holds the value of the "user_agent" field.
UserAgent *string `json:"user_agent,omitempty"`
// IPAddress holds the value of the "ip_address" field.
IPAddress *string `json:"ip_address,omitempty"`
// ImageCount holds the value of the "image_count" field.
ImageCount int `json:"image_count,omitempty"`
// ImageSize holds the value of the "image_size" field.
ImageSize *string `json:"image_size,omitempty"`
// CreatedAt holds the value of the "created_at" field.
CreatedAt time.Time `json:"created_at,omitempty"`
// Edges holds the relations/edges for other nodes in the graph.
@@ -159,9 +167,9 @@ func (*UsageLog) scanValues(columns []string) ([]any, error) {
values[i] = new(sql.NullBool)
case usagelog.FieldInputCost, usagelog.FieldOutputCost, usagelog.FieldCacheCreationCost, usagelog.FieldCacheReadCost, usagelog.FieldTotalCost, usagelog.FieldActualCost, usagelog.FieldRateMultiplier:
values[i] = new(sql.NullFloat64)
case usagelog.FieldID, usagelog.FieldUserID, usagelog.FieldAPIKeyID, usagelog.FieldAccountID, usagelog.FieldGroupID, usagelog.FieldSubscriptionID, usagelog.FieldInputTokens, usagelog.FieldOutputTokens, usagelog.FieldCacheCreationTokens, usagelog.FieldCacheReadTokens, usagelog.FieldCacheCreation5mTokens, usagelog.FieldCacheCreation1hTokens, usagelog.FieldBillingType, usagelog.FieldDurationMs, usagelog.FieldFirstTokenMs:
case usagelog.FieldID, usagelog.FieldUserID, usagelog.FieldAPIKeyID, usagelog.FieldAccountID, usagelog.FieldGroupID, usagelog.FieldSubscriptionID, usagelog.FieldInputTokens, usagelog.FieldOutputTokens, usagelog.FieldCacheCreationTokens, usagelog.FieldCacheReadTokens, usagelog.FieldCacheCreation5mTokens, usagelog.FieldCacheCreation1hTokens, usagelog.FieldBillingType, usagelog.FieldDurationMs, usagelog.FieldFirstTokenMs, usagelog.FieldImageCount:
values[i] = new(sql.NullInt64)
case usagelog.FieldRequestID, usagelog.FieldModel:
case usagelog.FieldRequestID, usagelog.FieldModel, usagelog.FieldUserAgent, usagelog.FieldIPAddress, usagelog.FieldImageSize:
values[i] = new(sql.NullString)
case usagelog.FieldCreatedAt:
values[i] = new(sql.NullTime)
@@ -334,6 +342,33 @@ func (_m *UsageLog) assignValues(columns []string, values []any) error {
_m.FirstTokenMs = new(int)
*_m.FirstTokenMs = int(value.Int64)
}
case usagelog.FieldUserAgent:
if value, ok := values[i].(*sql.NullString); !ok {
return fmt.Errorf("unexpected type %T for field user_agent", values[i])
} else if value.Valid {
_m.UserAgent = new(string)
*_m.UserAgent = value.String
}
case usagelog.FieldIPAddress:
if value, ok := values[i].(*sql.NullString); !ok {
return fmt.Errorf("unexpected type %T for field ip_address", values[i])
} else if value.Valid {
_m.IPAddress = new(string)
*_m.IPAddress = value.String
}
case usagelog.FieldImageCount:
if value, ok := values[i].(*sql.NullInt64); !ok {
return fmt.Errorf("unexpected type %T for field image_count", values[i])
} else if value.Valid {
_m.ImageCount = int(value.Int64)
}
case usagelog.FieldImageSize:
if value, ok := values[i].(*sql.NullString); !ok {
return fmt.Errorf("unexpected type %T for field image_size", values[i])
} else if value.Valid {
_m.ImageSize = new(string)
*_m.ImageSize = value.String
}
case usagelog.FieldCreatedAt:
if value, ok := values[i].(*sql.NullTime); !ok {
return fmt.Errorf("unexpected type %T for field created_at", values[i])
@@ -481,6 +516,24 @@ func (_m *UsageLog) String() string {
builder.WriteString(fmt.Sprintf("%v", *v))
}
builder.WriteString(", ")
if v := _m.UserAgent; v != nil {
builder.WriteString("user_agent=")
builder.WriteString(*v)
}
builder.WriteString(", ")
if v := _m.IPAddress; v != nil {
builder.WriteString("ip_address=")
builder.WriteString(*v)
}
builder.WriteString(", ")
builder.WriteString("image_count=")
builder.WriteString(fmt.Sprintf("%v", _m.ImageCount))
builder.WriteString(", ")
if v := _m.ImageSize; v != nil {
builder.WriteString("image_size=")
builder.WriteString(*v)
}
builder.WriteString(", ")
builder.WriteString("created_at=")
builder.WriteString(_m.CreatedAt.Format(time.ANSIC))
builder.WriteByte(')')

View File

@@ -62,6 +62,14 @@ const (
FieldDurationMs = "duration_ms"
// FieldFirstTokenMs holds the string denoting the first_token_ms field in the database.
FieldFirstTokenMs = "first_token_ms"
// FieldUserAgent holds the string denoting the user_agent field in the database.
FieldUserAgent = "user_agent"
// FieldIPAddress holds the string denoting the ip_address field in the database.
FieldIPAddress = "ip_address"
// FieldImageCount holds the string denoting the image_count field in the database.
FieldImageCount = "image_count"
// FieldImageSize holds the string denoting the image_size field in the database.
FieldImageSize = "image_size"
// FieldCreatedAt holds the string denoting the created_at field in the database.
FieldCreatedAt = "created_at"
// EdgeUser holds the string denoting the user edge name in mutations.
@@ -140,6 +148,10 @@ var Columns = []string{
FieldStream,
FieldDurationMs,
FieldFirstTokenMs,
FieldUserAgent,
FieldIPAddress,
FieldImageCount,
FieldImageSize,
FieldCreatedAt,
}
@@ -188,6 +200,14 @@ var (
DefaultBillingType int8
// DefaultStream holds the default value on creation for the "stream" field.
DefaultStream bool
// UserAgentValidator is a validator for the "user_agent" field. It is called by the builders before save.
UserAgentValidator func(string) error
// IPAddressValidator is a validator for the "ip_address" field. It is called by the builders before save.
IPAddressValidator func(string) error
// DefaultImageCount holds the default value on creation for the "image_count" field.
DefaultImageCount int
// ImageSizeValidator is a validator for the "image_size" field. It is called by the builders before save.
ImageSizeValidator func(string) error
// DefaultCreatedAt holds the default value on creation for the "created_at" field.
DefaultCreatedAt func() time.Time
)
@@ -320,6 +340,26 @@ func ByFirstTokenMs(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldFirstTokenMs, opts...).ToFunc()
}
// ByUserAgent orders the results by the user_agent field.
func ByUserAgent(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldUserAgent, opts...).ToFunc()
}
// ByIPAddress orders the results by the ip_address field.
func ByIPAddress(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldIPAddress, opts...).ToFunc()
}
// ByImageCount orders the results by the image_count field.
func ByImageCount(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldImageCount, opts...).ToFunc()
}
// ByImageSize orders the results by the image_size field.
func ByImageSize(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldImageSize, opts...).ToFunc()
}
// ByCreatedAt orders the results by the created_at field.
func ByCreatedAt(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldCreatedAt, opts...).ToFunc()

View File

@@ -175,6 +175,26 @@ func FirstTokenMs(v int) predicate.UsageLog {
return predicate.UsageLog(sql.FieldEQ(FieldFirstTokenMs, v))
}
// UserAgent applies equality check predicate on the "user_agent" field. It's identical to UserAgentEQ.
func UserAgent(v string) predicate.UsageLog {
return predicate.UsageLog(sql.FieldEQ(FieldUserAgent, v))
}
// IPAddress applies equality check predicate on the "ip_address" field. It's identical to IPAddressEQ.
func IPAddress(v string) predicate.UsageLog {
return predicate.UsageLog(sql.FieldEQ(FieldIPAddress, v))
}
// ImageCount applies equality check predicate on the "image_count" field. It's identical to ImageCountEQ.
func ImageCount(v int) predicate.UsageLog {
return predicate.UsageLog(sql.FieldEQ(FieldImageCount, v))
}
// ImageSize applies equality check predicate on the "image_size" field. It's identical to ImageSizeEQ.
func ImageSize(v string) predicate.UsageLog {
return predicate.UsageLog(sql.FieldEQ(FieldImageSize, v))
}
// CreatedAt applies equality check predicate on the "created_at" field. It's identical to CreatedAtEQ.
func CreatedAt(v time.Time) predicate.UsageLog {
return predicate.UsageLog(sql.FieldEQ(FieldCreatedAt, v))
@@ -1100,6 +1120,271 @@ func FirstTokenMsNotNil() predicate.UsageLog {
return predicate.UsageLog(sql.FieldNotNull(FieldFirstTokenMs))
}
// UserAgentEQ applies the EQ predicate on the "user_agent" field.
func UserAgentEQ(v string) predicate.UsageLog {
return predicate.UsageLog(sql.FieldEQ(FieldUserAgent, v))
}
// UserAgentNEQ applies the NEQ predicate on the "user_agent" field.
func UserAgentNEQ(v string) predicate.UsageLog {
return predicate.UsageLog(sql.FieldNEQ(FieldUserAgent, v))
}
// UserAgentIn applies the In predicate on the "user_agent" field.
func UserAgentIn(vs ...string) predicate.UsageLog {
return predicate.UsageLog(sql.FieldIn(FieldUserAgent, vs...))
}
// UserAgentNotIn applies the NotIn predicate on the "user_agent" field.
func UserAgentNotIn(vs ...string) predicate.UsageLog {
return predicate.UsageLog(sql.FieldNotIn(FieldUserAgent, vs...))
}
// UserAgentGT applies the GT predicate on the "user_agent" field.
func UserAgentGT(v string) predicate.UsageLog {
return predicate.UsageLog(sql.FieldGT(FieldUserAgent, v))
}
// UserAgentGTE applies the GTE predicate on the "user_agent" field.
func UserAgentGTE(v string) predicate.UsageLog {
return predicate.UsageLog(sql.FieldGTE(FieldUserAgent, v))
}
// UserAgentLT applies the LT predicate on the "user_agent" field.
func UserAgentLT(v string) predicate.UsageLog {
return predicate.UsageLog(sql.FieldLT(FieldUserAgent, v))
}
// UserAgentLTE applies the LTE predicate on the "user_agent" field.
func UserAgentLTE(v string) predicate.UsageLog {
return predicate.UsageLog(sql.FieldLTE(FieldUserAgent, v))
}
// UserAgentContains applies the Contains predicate on the "user_agent" field.
func UserAgentContains(v string) predicate.UsageLog {
return predicate.UsageLog(sql.FieldContains(FieldUserAgent, v))
}
// UserAgentHasPrefix applies the HasPrefix predicate on the "user_agent" field.
func UserAgentHasPrefix(v string) predicate.UsageLog {
return predicate.UsageLog(sql.FieldHasPrefix(FieldUserAgent, v))
}
// UserAgentHasSuffix applies the HasSuffix predicate on the "user_agent" field.
func UserAgentHasSuffix(v string) predicate.UsageLog {
return predicate.UsageLog(sql.FieldHasSuffix(FieldUserAgent, v))
}
// UserAgentIsNil applies the IsNil predicate on the "user_agent" field.
func UserAgentIsNil() predicate.UsageLog {
return predicate.UsageLog(sql.FieldIsNull(FieldUserAgent))
}
// UserAgentNotNil applies the NotNil predicate on the "user_agent" field.
func UserAgentNotNil() predicate.UsageLog {
return predicate.UsageLog(sql.FieldNotNull(FieldUserAgent))
}
// UserAgentEqualFold applies the EqualFold predicate on the "user_agent" field.
func UserAgentEqualFold(v string) predicate.UsageLog {
return predicate.UsageLog(sql.FieldEqualFold(FieldUserAgent, v))
}
// UserAgentContainsFold applies the ContainsFold predicate on the "user_agent" field.
func UserAgentContainsFold(v string) predicate.UsageLog {
return predicate.UsageLog(sql.FieldContainsFold(FieldUserAgent, v))
}
// IPAddressEQ applies the EQ predicate on the "ip_address" field.
func IPAddressEQ(v string) predicate.UsageLog {
return predicate.UsageLog(sql.FieldEQ(FieldIPAddress, v))
}
// IPAddressNEQ applies the NEQ predicate on the "ip_address" field.
func IPAddressNEQ(v string) predicate.UsageLog {
return predicate.UsageLog(sql.FieldNEQ(FieldIPAddress, v))
}
// IPAddressIn applies the In predicate on the "ip_address" field.
func IPAddressIn(vs ...string) predicate.UsageLog {
return predicate.UsageLog(sql.FieldIn(FieldIPAddress, vs...))
}
// IPAddressNotIn applies the NotIn predicate on the "ip_address" field.
func IPAddressNotIn(vs ...string) predicate.UsageLog {
return predicate.UsageLog(sql.FieldNotIn(FieldIPAddress, vs...))
}
// IPAddressGT applies the GT predicate on the "ip_address" field.
func IPAddressGT(v string) predicate.UsageLog {
return predicate.UsageLog(sql.FieldGT(FieldIPAddress, v))
}
// IPAddressGTE applies the GTE predicate on the "ip_address" field.
func IPAddressGTE(v string) predicate.UsageLog {
return predicate.UsageLog(sql.FieldGTE(FieldIPAddress, v))
}
// IPAddressLT applies the LT predicate on the "ip_address" field.
func IPAddressLT(v string) predicate.UsageLog {
return predicate.UsageLog(sql.FieldLT(FieldIPAddress, v))
}
// IPAddressLTE applies the LTE predicate on the "ip_address" field.
func IPAddressLTE(v string) predicate.UsageLog {
return predicate.UsageLog(sql.FieldLTE(FieldIPAddress, v))
}
// IPAddressContains applies the Contains predicate on the "ip_address" field.
func IPAddressContains(v string) predicate.UsageLog {
return predicate.UsageLog(sql.FieldContains(FieldIPAddress, v))
}
// IPAddressHasPrefix applies the HasPrefix predicate on the "ip_address" field.
func IPAddressHasPrefix(v string) predicate.UsageLog {
return predicate.UsageLog(sql.FieldHasPrefix(FieldIPAddress, v))
}
// IPAddressHasSuffix applies the HasSuffix predicate on the "ip_address" field.
func IPAddressHasSuffix(v string) predicate.UsageLog {
return predicate.UsageLog(sql.FieldHasSuffix(FieldIPAddress, v))
}
// IPAddressIsNil applies the IsNil predicate on the "ip_address" field.
func IPAddressIsNil() predicate.UsageLog {
return predicate.UsageLog(sql.FieldIsNull(FieldIPAddress))
}
// IPAddressNotNil applies the NotNil predicate on the "ip_address" field.
func IPAddressNotNil() predicate.UsageLog {
return predicate.UsageLog(sql.FieldNotNull(FieldIPAddress))
}
// IPAddressEqualFold applies the EqualFold predicate on the "ip_address" field.
func IPAddressEqualFold(v string) predicate.UsageLog {
return predicate.UsageLog(sql.FieldEqualFold(FieldIPAddress, v))
}
// IPAddressContainsFold applies the ContainsFold predicate on the "ip_address" field.
func IPAddressContainsFold(v string) predicate.UsageLog {
return predicate.UsageLog(sql.FieldContainsFold(FieldIPAddress, v))
}
// ImageCountEQ applies the EQ predicate on the "image_count" field.
func ImageCountEQ(v int) predicate.UsageLog {
return predicate.UsageLog(sql.FieldEQ(FieldImageCount, v))
}
// ImageCountNEQ applies the NEQ predicate on the "image_count" field.
func ImageCountNEQ(v int) predicate.UsageLog {
return predicate.UsageLog(sql.FieldNEQ(FieldImageCount, v))
}
// ImageCountIn applies the In predicate on the "image_count" field.
func ImageCountIn(vs ...int) predicate.UsageLog {
return predicate.UsageLog(sql.FieldIn(FieldImageCount, vs...))
}
// ImageCountNotIn applies the NotIn predicate on the "image_count" field.
func ImageCountNotIn(vs ...int) predicate.UsageLog {
return predicate.UsageLog(sql.FieldNotIn(FieldImageCount, vs...))
}
// ImageCountGT applies the GT predicate on the "image_count" field.
func ImageCountGT(v int) predicate.UsageLog {
return predicate.UsageLog(sql.FieldGT(FieldImageCount, v))
}
// ImageCountGTE applies the GTE predicate on the "image_count" field.
func ImageCountGTE(v int) predicate.UsageLog {
return predicate.UsageLog(sql.FieldGTE(FieldImageCount, v))
}
// ImageCountLT applies the LT predicate on the "image_count" field.
func ImageCountLT(v int) predicate.UsageLog {
return predicate.UsageLog(sql.FieldLT(FieldImageCount, v))
}
// ImageCountLTE applies the LTE predicate on the "image_count" field.
func ImageCountLTE(v int) predicate.UsageLog {
return predicate.UsageLog(sql.FieldLTE(FieldImageCount, v))
}
// ImageSizeEQ applies the EQ predicate on the "image_size" field.
func ImageSizeEQ(v string) predicate.UsageLog {
return predicate.UsageLog(sql.FieldEQ(FieldImageSize, v))
}
// ImageSizeNEQ applies the NEQ predicate on the "image_size" field.
func ImageSizeNEQ(v string) predicate.UsageLog {
return predicate.UsageLog(sql.FieldNEQ(FieldImageSize, v))
}
// ImageSizeIn applies the In predicate on the "image_size" field.
func ImageSizeIn(vs ...string) predicate.UsageLog {
return predicate.UsageLog(sql.FieldIn(FieldImageSize, vs...))
}
// ImageSizeNotIn applies the NotIn predicate on the "image_size" field.
func ImageSizeNotIn(vs ...string) predicate.UsageLog {
return predicate.UsageLog(sql.FieldNotIn(FieldImageSize, vs...))
}
// ImageSizeGT applies the GT predicate on the "image_size" field.
func ImageSizeGT(v string) predicate.UsageLog {
return predicate.UsageLog(sql.FieldGT(FieldImageSize, v))
}
// ImageSizeGTE applies the GTE predicate on the "image_size" field.
func ImageSizeGTE(v string) predicate.UsageLog {
return predicate.UsageLog(sql.FieldGTE(FieldImageSize, v))
}
// ImageSizeLT applies the LT predicate on the "image_size" field.
func ImageSizeLT(v string) predicate.UsageLog {
return predicate.UsageLog(sql.FieldLT(FieldImageSize, v))
}
// ImageSizeLTE applies the LTE predicate on the "image_size" field.
func ImageSizeLTE(v string) predicate.UsageLog {
return predicate.UsageLog(sql.FieldLTE(FieldImageSize, v))
}
// ImageSizeContains applies the Contains predicate on the "image_size" field.
func ImageSizeContains(v string) predicate.UsageLog {
return predicate.UsageLog(sql.FieldContains(FieldImageSize, v))
}
// ImageSizeHasPrefix applies the HasPrefix predicate on the "image_size" field.
func ImageSizeHasPrefix(v string) predicate.UsageLog {
return predicate.UsageLog(sql.FieldHasPrefix(FieldImageSize, v))
}
// ImageSizeHasSuffix applies the HasSuffix predicate on the "image_size" field.
func ImageSizeHasSuffix(v string) predicate.UsageLog {
return predicate.UsageLog(sql.FieldHasSuffix(FieldImageSize, v))
}
// ImageSizeIsNil applies the IsNil predicate on the "image_size" field.
func ImageSizeIsNil() predicate.UsageLog {
return predicate.UsageLog(sql.FieldIsNull(FieldImageSize))
}
// ImageSizeNotNil applies the NotNil predicate on the "image_size" field.
func ImageSizeNotNil() predicate.UsageLog {
return predicate.UsageLog(sql.FieldNotNull(FieldImageSize))
}
// ImageSizeEqualFold applies the EqualFold predicate on the "image_size" field.
func ImageSizeEqualFold(v string) predicate.UsageLog {
return predicate.UsageLog(sql.FieldEqualFold(FieldImageSize, v))
}
// ImageSizeContainsFold applies the ContainsFold predicate on the "image_size" field.
func ImageSizeContainsFold(v string) predicate.UsageLog {
return predicate.UsageLog(sql.FieldContainsFold(FieldImageSize, v))
}
// CreatedAtEQ applies the EQ predicate on the "created_at" field.
func CreatedAtEQ(v time.Time) predicate.UsageLog {
return predicate.UsageLog(sql.FieldEQ(FieldCreatedAt, v))

View File

@@ -323,6 +323,62 @@ func (_c *UsageLogCreate) SetNillableFirstTokenMs(v *int) *UsageLogCreate {
return _c
}
// SetUserAgent sets the "user_agent" field.
func (_c *UsageLogCreate) SetUserAgent(v string) *UsageLogCreate {
_c.mutation.SetUserAgent(v)
return _c
}
// SetNillableUserAgent sets the "user_agent" field if the given value is not nil.
func (_c *UsageLogCreate) SetNillableUserAgent(v *string) *UsageLogCreate {
if v != nil {
_c.SetUserAgent(*v)
}
return _c
}
// SetIPAddress sets the "ip_address" field.
func (_c *UsageLogCreate) SetIPAddress(v string) *UsageLogCreate {
_c.mutation.SetIPAddress(v)
return _c
}
// SetNillableIPAddress sets the "ip_address" field if the given value is not nil.
func (_c *UsageLogCreate) SetNillableIPAddress(v *string) *UsageLogCreate {
if v != nil {
_c.SetIPAddress(*v)
}
return _c
}
// SetImageCount sets the "image_count" field.
func (_c *UsageLogCreate) SetImageCount(v int) *UsageLogCreate {
_c.mutation.SetImageCount(v)
return _c
}
// SetNillableImageCount sets the "image_count" field if the given value is not nil.
func (_c *UsageLogCreate) SetNillableImageCount(v *int) *UsageLogCreate {
if v != nil {
_c.SetImageCount(*v)
}
return _c
}
// SetImageSize sets the "image_size" field.
func (_c *UsageLogCreate) SetImageSize(v string) *UsageLogCreate {
_c.mutation.SetImageSize(v)
return _c
}
// SetNillableImageSize sets the "image_size" field if the given value is not nil.
func (_c *UsageLogCreate) SetNillableImageSize(v *string) *UsageLogCreate {
if v != nil {
_c.SetImageSize(*v)
}
return _c
}
// SetCreatedAt sets the "created_at" field.
func (_c *UsageLogCreate) SetCreatedAt(v time.Time) *UsageLogCreate {
_c.mutation.SetCreatedAt(v)
@@ -457,6 +513,10 @@ func (_c *UsageLogCreate) defaults() {
v := usagelog.DefaultStream
_c.mutation.SetStream(v)
}
if _, ok := _c.mutation.ImageCount(); !ok {
v := usagelog.DefaultImageCount
_c.mutation.SetImageCount(v)
}
if _, ok := _c.mutation.CreatedAt(); !ok {
v := usagelog.DefaultCreatedAt()
_c.mutation.SetCreatedAt(v)
@@ -535,6 +595,24 @@ func (_c *UsageLogCreate) check() error {
if _, ok := _c.mutation.Stream(); !ok {
return &ValidationError{Name: "stream", err: errors.New(`ent: missing required field "UsageLog.stream"`)}
}
if v, ok := _c.mutation.UserAgent(); ok {
if err := usagelog.UserAgentValidator(v); err != nil {
return &ValidationError{Name: "user_agent", err: fmt.Errorf(`ent: validator failed for field "UsageLog.user_agent": %w`, err)}
}
}
if v, ok := _c.mutation.IPAddress(); ok {
if err := usagelog.IPAddressValidator(v); err != nil {
return &ValidationError{Name: "ip_address", err: fmt.Errorf(`ent: validator failed for field "UsageLog.ip_address": %w`, err)}
}
}
if _, ok := _c.mutation.ImageCount(); !ok {
return &ValidationError{Name: "image_count", err: errors.New(`ent: missing required field "UsageLog.image_count"`)}
}
if v, ok := _c.mutation.ImageSize(); ok {
if err := usagelog.ImageSizeValidator(v); err != nil {
return &ValidationError{Name: "image_size", err: fmt.Errorf(`ent: validator failed for field "UsageLog.image_size": %w`, err)}
}
}
if _, ok := _c.mutation.CreatedAt(); !ok {
return &ValidationError{Name: "created_at", err: errors.New(`ent: missing required field "UsageLog.created_at"`)}
}
@@ -650,6 +728,22 @@ func (_c *UsageLogCreate) createSpec() (*UsageLog, *sqlgraph.CreateSpec) {
_spec.SetField(usagelog.FieldFirstTokenMs, field.TypeInt, value)
_node.FirstTokenMs = &value
}
if value, ok := _c.mutation.UserAgent(); ok {
_spec.SetField(usagelog.FieldUserAgent, field.TypeString, value)
_node.UserAgent = &value
}
if value, ok := _c.mutation.IPAddress(); ok {
_spec.SetField(usagelog.FieldIPAddress, field.TypeString, value)
_node.IPAddress = &value
}
if value, ok := _c.mutation.ImageCount(); ok {
_spec.SetField(usagelog.FieldImageCount, field.TypeInt, value)
_node.ImageCount = value
}
if value, ok := _c.mutation.ImageSize(); ok {
_spec.SetField(usagelog.FieldImageSize, field.TypeString, value)
_node.ImageSize = &value
}
if value, ok := _c.mutation.CreatedAt(); ok {
_spec.SetField(usagelog.FieldCreatedAt, field.TypeTime, value)
_node.CreatedAt = value
@@ -1199,6 +1293,78 @@ func (u *UsageLogUpsert) ClearFirstTokenMs() *UsageLogUpsert {
return u
}
// SetUserAgent sets the "user_agent" field.
func (u *UsageLogUpsert) SetUserAgent(v string) *UsageLogUpsert {
u.Set(usagelog.FieldUserAgent, v)
return u
}
// UpdateUserAgent sets the "user_agent" field to the value that was provided on create.
func (u *UsageLogUpsert) UpdateUserAgent() *UsageLogUpsert {
u.SetExcluded(usagelog.FieldUserAgent)
return u
}
// ClearUserAgent clears the value of the "user_agent" field.
func (u *UsageLogUpsert) ClearUserAgent() *UsageLogUpsert {
u.SetNull(usagelog.FieldUserAgent)
return u
}
// SetIPAddress sets the "ip_address" field.
func (u *UsageLogUpsert) SetIPAddress(v string) *UsageLogUpsert {
u.Set(usagelog.FieldIPAddress, v)
return u
}
// UpdateIPAddress sets the "ip_address" field to the value that was provided on create.
func (u *UsageLogUpsert) UpdateIPAddress() *UsageLogUpsert {
u.SetExcluded(usagelog.FieldIPAddress)
return u
}
// ClearIPAddress clears the value of the "ip_address" field.
func (u *UsageLogUpsert) ClearIPAddress() *UsageLogUpsert {
u.SetNull(usagelog.FieldIPAddress)
return u
}
// SetImageCount sets the "image_count" field.
func (u *UsageLogUpsert) SetImageCount(v int) *UsageLogUpsert {
u.Set(usagelog.FieldImageCount, v)
return u
}
// UpdateImageCount sets the "image_count" field to the value that was provided on create.
func (u *UsageLogUpsert) UpdateImageCount() *UsageLogUpsert {
u.SetExcluded(usagelog.FieldImageCount)
return u
}
// AddImageCount adds v to the "image_count" field.
func (u *UsageLogUpsert) AddImageCount(v int) *UsageLogUpsert {
u.Add(usagelog.FieldImageCount, v)
return u
}
// SetImageSize sets the "image_size" field.
func (u *UsageLogUpsert) SetImageSize(v string) *UsageLogUpsert {
u.Set(usagelog.FieldImageSize, v)
return u
}
// UpdateImageSize sets the "image_size" field to the value that was provided on create.
func (u *UsageLogUpsert) UpdateImageSize() *UsageLogUpsert {
u.SetExcluded(usagelog.FieldImageSize)
return u
}
// ClearImageSize clears the value of the "image_size" field.
func (u *UsageLogUpsert) ClearImageSize() *UsageLogUpsert {
u.SetNull(usagelog.FieldImageSize)
return u
}
// UpdateNewValues updates the mutable fields using the new values that were set on create.
// Using this option is equivalent to using:
//
@@ -1720,6 +1886,90 @@ func (u *UsageLogUpsertOne) ClearFirstTokenMs() *UsageLogUpsertOne {
})
}
// SetUserAgent sets the "user_agent" field.
func (u *UsageLogUpsertOne) SetUserAgent(v string) *UsageLogUpsertOne {
return u.Update(func(s *UsageLogUpsert) {
s.SetUserAgent(v)
})
}
// UpdateUserAgent sets the "user_agent" field to the value that was provided on create.
func (u *UsageLogUpsertOne) UpdateUserAgent() *UsageLogUpsertOne {
return u.Update(func(s *UsageLogUpsert) {
s.UpdateUserAgent()
})
}
// ClearUserAgent clears the value of the "user_agent" field.
func (u *UsageLogUpsertOne) ClearUserAgent() *UsageLogUpsertOne {
return u.Update(func(s *UsageLogUpsert) {
s.ClearUserAgent()
})
}
// SetIPAddress sets the "ip_address" field.
func (u *UsageLogUpsertOne) SetIPAddress(v string) *UsageLogUpsertOne {
return u.Update(func(s *UsageLogUpsert) {
s.SetIPAddress(v)
})
}
// UpdateIPAddress sets the "ip_address" field to the value that was provided on create.
func (u *UsageLogUpsertOne) UpdateIPAddress() *UsageLogUpsertOne {
return u.Update(func(s *UsageLogUpsert) {
s.UpdateIPAddress()
})
}
// ClearIPAddress clears the value of the "ip_address" field.
func (u *UsageLogUpsertOne) ClearIPAddress() *UsageLogUpsertOne {
return u.Update(func(s *UsageLogUpsert) {
s.ClearIPAddress()
})
}
// SetImageCount sets the "image_count" field.
func (u *UsageLogUpsertOne) SetImageCount(v int) *UsageLogUpsertOne {
return u.Update(func(s *UsageLogUpsert) {
s.SetImageCount(v)
})
}
// AddImageCount adds v to the "image_count" field.
func (u *UsageLogUpsertOne) AddImageCount(v int) *UsageLogUpsertOne {
return u.Update(func(s *UsageLogUpsert) {
s.AddImageCount(v)
})
}
// UpdateImageCount sets the "image_count" field to the value that was provided on create.
func (u *UsageLogUpsertOne) UpdateImageCount() *UsageLogUpsertOne {
return u.Update(func(s *UsageLogUpsert) {
s.UpdateImageCount()
})
}
// SetImageSize sets the "image_size" field.
func (u *UsageLogUpsertOne) SetImageSize(v string) *UsageLogUpsertOne {
return u.Update(func(s *UsageLogUpsert) {
s.SetImageSize(v)
})
}
// UpdateImageSize sets the "image_size" field to the value that was provided on create.
func (u *UsageLogUpsertOne) UpdateImageSize() *UsageLogUpsertOne {
return u.Update(func(s *UsageLogUpsert) {
s.UpdateImageSize()
})
}
// ClearImageSize clears the value of the "image_size" field.
func (u *UsageLogUpsertOne) ClearImageSize() *UsageLogUpsertOne {
return u.Update(func(s *UsageLogUpsert) {
s.ClearImageSize()
})
}
// Exec executes the query.
func (u *UsageLogUpsertOne) Exec(ctx context.Context) error {
if len(u.create.conflict) == 0 {
@@ -2407,6 +2657,90 @@ func (u *UsageLogUpsertBulk) ClearFirstTokenMs() *UsageLogUpsertBulk {
})
}
// SetUserAgent sets the "user_agent" field.
func (u *UsageLogUpsertBulk) SetUserAgent(v string) *UsageLogUpsertBulk {
return u.Update(func(s *UsageLogUpsert) {
s.SetUserAgent(v)
})
}
// UpdateUserAgent sets the "user_agent" field to the value that was provided on create.
func (u *UsageLogUpsertBulk) UpdateUserAgent() *UsageLogUpsertBulk {
return u.Update(func(s *UsageLogUpsert) {
s.UpdateUserAgent()
})
}
// ClearUserAgent clears the value of the "user_agent" field.
func (u *UsageLogUpsertBulk) ClearUserAgent() *UsageLogUpsertBulk {
return u.Update(func(s *UsageLogUpsert) {
s.ClearUserAgent()
})
}
// SetIPAddress sets the "ip_address" field.
func (u *UsageLogUpsertBulk) SetIPAddress(v string) *UsageLogUpsertBulk {
return u.Update(func(s *UsageLogUpsert) {
s.SetIPAddress(v)
})
}
// UpdateIPAddress sets the "ip_address" field to the value that was provided on create.
func (u *UsageLogUpsertBulk) UpdateIPAddress() *UsageLogUpsertBulk {
return u.Update(func(s *UsageLogUpsert) {
s.UpdateIPAddress()
})
}
// ClearIPAddress clears the value of the "ip_address" field.
func (u *UsageLogUpsertBulk) ClearIPAddress() *UsageLogUpsertBulk {
return u.Update(func(s *UsageLogUpsert) {
s.ClearIPAddress()
})
}
// SetImageCount sets the "image_count" field.
func (u *UsageLogUpsertBulk) SetImageCount(v int) *UsageLogUpsertBulk {
return u.Update(func(s *UsageLogUpsert) {
s.SetImageCount(v)
})
}
// AddImageCount adds v to the "image_count" field.
func (u *UsageLogUpsertBulk) AddImageCount(v int) *UsageLogUpsertBulk {
return u.Update(func(s *UsageLogUpsert) {
s.AddImageCount(v)
})
}
// UpdateImageCount sets the "image_count" field to the value that was provided on create.
func (u *UsageLogUpsertBulk) UpdateImageCount() *UsageLogUpsertBulk {
return u.Update(func(s *UsageLogUpsert) {
s.UpdateImageCount()
})
}
// SetImageSize sets the "image_size" field.
func (u *UsageLogUpsertBulk) SetImageSize(v string) *UsageLogUpsertBulk {
return u.Update(func(s *UsageLogUpsert) {
s.SetImageSize(v)
})
}
// UpdateImageSize sets the "image_size" field to the value that was provided on create.
func (u *UsageLogUpsertBulk) UpdateImageSize() *UsageLogUpsertBulk {
return u.Update(func(s *UsageLogUpsert) {
s.UpdateImageSize()
})
}
// ClearImageSize clears the value of the "image_size" field.
func (u *UsageLogUpsertBulk) ClearImageSize() *UsageLogUpsertBulk {
return u.Update(func(s *UsageLogUpsert) {
s.ClearImageSize()
})
}
// Exec executes the query.
func (u *UsageLogUpsertBulk) Exec(ctx context.Context) error {
if u.create.err != nil {

View File

@@ -8,6 +8,7 @@ import (
"math"
"entgo.io/ent"
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field"
@@ -32,6 +33,7 @@ type UsageLogQuery struct {
withAccount *AccountQuery
withGroup *GroupQuery
withSubscription *UserSubscriptionQuery
modifiers []func(*sql.Selector)
// intermediate query (i.e. traversal path).
sql *sql.Selector
path func(context.Context) (*sql.Selector, error)
@@ -531,6 +533,9 @@ func (_q *UsageLogQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*Usa
node.Edges.loadedTypes = loadedTypes
return node.assignValues(columns, values)
}
if len(_q.modifiers) > 0 {
_spec.Modifiers = _q.modifiers
}
for i := range hooks {
hooks[i](ctx, _spec)
}
@@ -727,6 +732,9 @@ func (_q *UsageLogQuery) loadSubscription(ctx context.Context, query *UserSubscr
func (_q *UsageLogQuery) sqlCount(ctx context.Context) (int, error) {
_spec := _q.querySpec()
if len(_q.modifiers) > 0 {
_spec.Modifiers = _q.modifiers
}
_spec.Node.Columns = _q.ctx.Fields
if len(_q.ctx.Fields) > 0 {
_spec.Unique = _q.ctx.Unique != nil && *_q.ctx.Unique
@@ -804,6 +812,9 @@ func (_q *UsageLogQuery) sqlQuery(ctx context.Context) *sql.Selector {
if _q.ctx.Unique != nil && *_q.ctx.Unique {
selector.Distinct()
}
for _, m := range _q.modifiers {
m(selector)
}
for _, p := range _q.predicates {
p(selector)
}
@@ -821,6 +832,32 @@ func (_q *UsageLogQuery) sqlQuery(ctx context.Context) *sql.Selector {
return selector
}
// ForUpdate locks the selected rows against concurrent updates, and prevent them from being
// updated, deleted or "selected ... for update" by other sessions, until the transaction is
// either committed or rolled-back.
func (_q *UsageLogQuery) ForUpdate(opts ...sql.LockOption) *UsageLogQuery {
if _q.driver.Dialect() == dialect.Postgres {
_q.Unique(false)
}
_q.modifiers = append(_q.modifiers, func(s *sql.Selector) {
s.ForUpdate(opts...)
})
return _q
}
// ForShare behaves similarly to ForUpdate, except that it acquires a shared mode lock
// on any rows that are read. Other sessions can read the rows, but cannot modify them
// until your transaction commits.
func (_q *UsageLogQuery) ForShare(opts ...sql.LockOption) *UsageLogQuery {
if _q.driver.Dialect() == dialect.Postgres {
_q.Unique(false)
}
_q.modifiers = append(_q.modifiers, func(s *sql.Selector) {
s.ForShare(opts...)
})
return _q
}
// UsageLogGroupBy is the group-by builder for UsageLog entities.
type UsageLogGroupBy struct {
selector

View File

@@ -504,6 +504,87 @@ func (_u *UsageLogUpdate) ClearFirstTokenMs() *UsageLogUpdate {
return _u
}
// SetUserAgent sets the "user_agent" field.
func (_u *UsageLogUpdate) SetUserAgent(v string) *UsageLogUpdate {
_u.mutation.SetUserAgent(v)
return _u
}
// SetNillableUserAgent sets the "user_agent" field if the given value is not nil.
func (_u *UsageLogUpdate) SetNillableUserAgent(v *string) *UsageLogUpdate {
if v != nil {
_u.SetUserAgent(*v)
}
return _u
}
// ClearUserAgent clears the value of the "user_agent" field.
func (_u *UsageLogUpdate) ClearUserAgent() *UsageLogUpdate {
_u.mutation.ClearUserAgent()
return _u
}
// SetIPAddress sets the "ip_address" field.
func (_u *UsageLogUpdate) SetIPAddress(v string) *UsageLogUpdate {
_u.mutation.SetIPAddress(v)
return _u
}
// SetNillableIPAddress sets the "ip_address" field if the given value is not nil.
func (_u *UsageLogUpdate) SetNillableIPAddress(v *string) *UsageLogUpdate {
if v != nil {
_u.SetIPAddress(*v)
}
return _u
}
// ClearIPAddress clears the value of the "ip_address" field.
func (_u *UsageLogUpdate) ClearIPAddress() *UsageLogUpdate {
_u.mutation.ClearIPAddress()
return _u
}
// SetImageCount sets the "image_count" field.
func (_u *UsageLogUpdate) SetImageCount(v int) *UsageLogUpdate {
_u.mutation.ResetImageCount()
_u.mutation.SetImageCount(v)
return _u
}
// SetNillableImageCount sets the "image_count" field if the given value is not nil.
func (_u *UsageLogUpdate) SetNillableImageCount(v *int) *UsageLogUpdate {
if v != nil {
_u.SetImageCount(*v)
}
return _u
}
// AddImageCount adds value to the "image_count" field.
func (_u *UsageLogUpdate) AddImageCount(v int) *UsageLogUpdate {
_u.mutation.AddImageCount(v)
return _u
}
// SetImageSize sets the "image_size" field.
func (_u *UsageLogUpdate) SetImageSize(v string) *UsageLogUpdate {
_u.mutation.SetImageSize(v)
return _u
}
// SetNillableImageSize sets the "image_size" field if the given value is not nil.
func (_u *UsageLogUpdate) SetNillableImageSize(v *string) *UsageLogUpdate {
if v != nil {
_u.SetImageSize(*v)
}
return _u
}
// ClearImageSize clears the value of the "image_size" field.
func (_u *UsageLogUpdate) ClearImageSize() *UsageLogUpdate {
_u.mutation.ClearImageSize()
return _u
}
// SetUser sets the "user" edge to the User entity.
func (_u *UsageLogUpdate) SetUser(v *User) *UsageLogUpdate {
return _u.SetUserID(v.ID)
@@ -603,6 +684,21 @@ func (_u *UsageLogUpdate) check() error {
return &ValidationError{Name: "model", err: fmt.Errorf(`ent: validator failed for field "UsageLog.model": %w`, err)}
}
}
if v, ok := _u.mutation.UserAgent(); ok {
if err := usagelog.UserAgentValidator(v); err != nil {
return &ValidationError{Name: "user_agent", err: fmt.Errorf(`ent: validator failed for field "UsageLog.user_agent": %w`, err)}
}
}
if v, ok := _u.mutation.IPAddress(); ok {
if err := usagelog.IPAddressValidator(v); err != nil {
return &ValidationError{Name: "ip_address", err: fmt.Errorf(`ent: validator failed for field "UsageLog.ip_address": %w`, err)}
}
}
if v, ok := _u.mutation.ImageSize(); ok {
if err := usagelog.ImageSizeValidator(v); err != nil {
return &ValidationError{Name: "image_size", err: fmt.Errorf(`ent: validator failed for field "UsageLog.image_size": %w`, err)}
}
}
if _u.mutation.UserCleared() && len(_u.mutation.UserIDs()) > 0 {
return errors.New(`ent: clearing a required unique edge "UsageLog.user"`)
}
@@ -738,6 +834,30 @@ func (_u *UsageLogUpdate) sqlSave(ctx context.Context) (_node int, err error) {
if _u.mutation.FirstTokenMsCleared() {
_spec.ClearField(usagelog.FieldFirstTokenMs, field.TypeInt)
}
if value, ok := _u.mutation.UserAgent(); ok {
_spec.SetField(usagelog.FieldUserAgent, field.TypeString, value)
}
if _u.mutation.UserAgentCleared() {
_spec.ClearField(usagelog.FieldUserAgent, field.TypeString)
}
if value, ok := _u.mutation.IPAddress(); ok {
_spec.SetField(usagelog.FieldIPAddress, field.TypeString, value)
}
if _u.mutation.IPAddressCleared() {
_spec.ClearField(usagelog.FieldIPAddress, field.TypeString)
}
if value, ok := _u.mutation.ImageCount(); ok {
_spec.SetField(usagelog.FieldImageCount, field.TypeInt, value)
}
if value, ok := _u.mutation.AddedImageCount(); ok {
_spec.AddField(usagelog.FieldImageCount, field.TypeInt, value)
}
if value, ok := _u.mutation.ImageSize(); ok {
_spec.SetField(usagelog.FieldImageSize, field.TypeString, value)
}
if _u.mutation.ImageSizeCleared() {
_spec.ClearField(usagelog.FieldImageSize, field.TypeString)
}
if _u.mutation.UserCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O,
@@ -1375,6 +1495,87 @@ func (_u *UsageLogUpdateOne) ClearFirstTokenMs() *UsageLogUpdateOne {
return _u
}
// SetUserAgent sets the "user_agent" field.
func (_u *UsageLogUpdateOne) SetUserAgent(v string) *UsageLogUpdateOne {
_u.mutation.SetUserAgent(v)
return _u
}
// SetNillableUserAgent sets the "user_agent" field if the given value is not nil.
func (_u *UsageLogUpdateOne) SetNillableUserAgent(v *string) *UsageLogUpdateOne {
if v != nil {
_u.SetUserAgent(*v)
}
return _u
}
// ClearUserAgent clears the value of the "user_agent" field.
func (_u *UsageLogUpdateOne) ClearUserAgent() *UsageLogUpdateOne {
_u.mutation.ClearUserAgent()
return _u
}
// SetIPAddress sets the "ip_address" field.
func (_u *UsageLogUpdateOne) SetIPAddress(v string) *UsageLogUpdateOne {
_u.mutation.SetIPAddress(v)
return _u
}
// SetNillableIPAddress sets the "ip_address" field if the given value is not nil.
func (_u *UsageLogUpdateOne) SetNillableIPAddress(v *string) *UsageLogUpdateOne {
if v != nil {
_u.SetIPAddress(*v)
}
return _u
}
// ClearIPAddress clears the value of the "ip_address" field.
func (_u *UsageLogUpdateOne) ClearIPAddress() *UsageLogUpdateOne {
_u.mutation.ClearIPAddress()
return _u
}
// SetImageCount sets the "image_count" field.
func (_u *UsageLogUpdateOne) SetImageCount(v int) *UsageLogUpdateOne {
_u.mutation.ResetImageCount()
_u.mutation.SetImageCount(v)
return _u
}
// SetNillableImageCount sets the "image_count" field if the given value is not nil.
func (_u *UsageLogUpdateOne) SetNillableImageCount(v *int) *UsageLogUpdateOne {
if v != nil {
_u.SetImageCount(*v)
}
return _u
}
// AddImageCount adds value to the "image_count" field.
func (_u *UsageLogUpdateOne) AddImageCount(v int) *UsageLogUpdateOne {
_u.mutation.AddImageCount(v)
return _u
}
// SetImageSize sets the "image_size" field.
func (_u *UsageLogUpdateOne) SetImageSize(v string) *UsageLogUpdateOne {
_u.mutation.SetImageSize(v)
return _u
}
// SetNillableImageSize sets the "image_size" field if the given value is not nil.
func (_u *UsageLogUpdateOne) SetNillableImageSize(v *string) *UsageLogUpdateOne {
if v != nil {
_u.SetImageSize(*v)
}
return _u
}
// ClearImageSize clears the value of the "image_size" field.
func (_u *UsageLogUpdateOne) ClearImageSize() *UsageLogUpdateOne {
_u.mutation.ClearImageSize()
return _u
}
// SetUser sets the "user" edge to the User entity.
func (_u *UsageLogUpdateOne) SetUser(v *User) *UsageLogUpdateOne {
return _u.SetUserID(v.ID)
@@ -1487,6 +1688,21 @@ func (_u *UsageLogUpdateOne) check() error {
return &ValidationError{Name: "model", err: fmt.Errorf(`ent: validator failed for field "UsageLog.model": %w`, err)}
}
}
if v, ok := _u.mutation.UserAgent(); ok {
if err := usagelog.UserAgentValidator(v); err != nil {
return &ValidationError{Name: "user_agent", err: fmt.Errorf(`ent: validator failed for field "UsageLog.user_agent": %w`, err)}
}
}
if v, ok := _u.mutation.IPAddress(); ok {
if err := usagelog.IPAddressValidator(v); err != nil {
return &ValidationError{Name: "ip_address", err: fmt.Errorf(`ent: validator failed for field "UsageLog.ip_address": %w`, err)}
}
}
if v, ok := _u.mutation.ImageSize(); ok {
if err := usagelog.ImageSizeValidator(v); err != nil {
return &ValidationError{Name: "image_size", err: fmt.Errorf(`ent: validator failed for field "UsageLog.image_size": %w`, err)}
}
}
if _u.mutation.UserCleared() && len(_u.mutation.UserIDs()) > 0 {
return errors.New(`ent: clearing a required unique edge "UsageLog.user"`)
}
@@ -1639,6 +1855,30 @@ func (_u *UsageLogUpdateOne) sqlSave(ctx context.Context) (_node *UsageLog, err
if _u.mutation.FirstTokenMsCleared() {
_spec.ClearField(usagelog.FieldFirstTokenMs, field.TypeInt)
}
if value, ok := _u.mutation.UserAgent(); ok {
_spec.SetField(usagelog.FieldUserAgent, field.TypeString, value)
}
if _u.mutation.UserAgentCleared() {
_spec.ClearField(usagelog.FieldUserAgent, field.TypeString)
}
if value, ok := _u.mutation.IPAddress(); ok {
_spec.SetField(usagelog.FieldIPAddress, field.TypeString, value)
}
if _u.mutation.IPAddressCleared() {
_spec.ClearField(usagelog.FieldIPAddress, field.TypeString)
}
if value, ok := _u.mutation.ImageCount(); ok {
_spec.SetField(usagelog.FieldImageCount, field.TypeInt, value)
}
if value, ok := _u.mutation.AddedImageCount(); ok {
_spec.AddField(usagelog.FieldImageCount, field.TypeInt, value)
}
if value, ok := _u.mutation.ImageSize(); ok {
_spec.SetField(usagelog.FieldImageSize, field.TypeString, value)
}
if _u.mutation.ImageSizeCleared() {
_spec.ClearField(usagelog.FieldImageSize, field.TypeString)
}
if _u.mutation.UserCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O,

View File

@@ -61,11 +61,13 @@ type UserEdges struct {
UsageLogs []*UsageLog `json:"usage_logs,omitempty"`
// AttributeValues holds the value of the attribute_values edge.
AttributeValues []*UserAttributeValue `json:"attribute_values,omitempty"`
// PromoCodeUsages holds the value of the promo_code_usages edge.
PromoCodeUsages []*PromoCodeUsage `json:"promo_code_usages,omitempty"`
// UserAllowedGroups holds the value of the user_allowed_groups edge.
UserAllowedGroups []*UserAllowedGroup `json:"user_allowed_groups,omitempty"`
// loadedTypes holds the information for reporting if a
// type was loaded (or requested) in eager-loading or not.
loadedTypes [8]bool
loadedTypes [9]bool
}
// APIKeysOrErr returns the APIKeys value or an error if the edge
@@ -131,10 +133,19 @@ func (e UserEdges) AttributeValuesOrErr() ([]*UserAttributeValue, error) {
return nil, &NotLoadedError{edge: "attribute_values"}
}
// PromoCodeUsagesOrErr returns the PromoCodeUsages value or an error if the edge
// was not loaded in eager-loading.
func (e UserEdges) PromoCodeUsagesOrErr() ([]*PromoCodeUsage, error) {
if e.loadedTypes[7] {
return e.PromoCodeUsages, nil
}
return nil, &NotLoadedError{edge: "promo_code_usages"}
}
// UserAllowedGroupsOrErr returns the UserAllowedGroups value or an error if the edge
// was not loaded in eager-loading.
func (e UserEdges) UserAllowedGroupsOrErr() ([]*UserAllowedGroup, error) {
if e.loadedTypes[7] {
if e.loadedTypes[8] {
return e.UserAllowedGroups, nil
}
return nil, &NotLoadedError{edge: "user_allowed_groups"}
@@ -289,6 +300,11 @@ func (_m *User) QueryAttributeValues() *UserAttributeValueQuery {
return NewUserClient(_m.config).QueryAttributeValues(_m)
}
// QueryPromoCodeUsages queries the "promo_code_usages" edge of the User entity.
func (_m *User) QueryPromoCodeUsages() *PromoCodeUsageQuery {
return NewUserClient(_m.config).QueryPromoCodeUsages(_m)
}
// QueryUserAllowedGroups queries the "user_allowed_groups" edge of the User entity.
func (_m *User) QueryUserAllowedGroups() *UserAllowedGroupQuery {
return NewUserClient(_m.config).QueryUserAllowedGroups(_m)

View File

@@ -51,6 +51,8 @@ const (
EdgeUsageLogs = "usage_logs"
// EdgeAttributeValues holds the string denoting the attribute_values edge name in mutations.
EdgeAttributeValues = "attribute_values"
// EdgePromoCodeUsages holds the string denoting the promo_code_usages edge name in mutations.
EdgePromoCodeUsages = "promo_code_usages"
// EdgeUserAllowedGroups holds the string denoting the user_allowed_groups edge name in mutations.
EdgeUserAllowedGroups = "user_allowed_groups"
// Table holds the table name of the user in the database.
@@ -102,6 +104,13 @@ const (
AttributeValuesInverseTable = "user_attribute_values"
// AttributeValuesColumn is the table column denoting the attribute_values relation/edge.
AttributeValuesColumn = "user_id"
// PromoCodeUsagesTable is the table that holds the promo_code_usages relation/edge.
PromoCodeUsagesTable = "promo_code_usages"
// PromoCodeUsagesInverseTable is the table name for the PromoCodeUsage entity.
// It exists in this package in order to avoid circular dependency with the "promocodeusage" package.
PromoCodeUsagesInverseTable = "promo_code_usages"
// PromoCodeUsagesColumn is the table column denoting the promo_code_usages relation/edge.
PromoCodeUsagesColumn = "user_id"
// UserAllowedGroupsTable is the table that holds the user_allowed_groups relation/edge.
UserAllowedGroupsTable = "user_allowed_groups"
// UserAllowedGroupsInverseTable is the table name for the UserAllowedGroup entity.
@@ -342,6 +351,20 @@ func ByAttributeValues(term sql.OrderTerm, terms ...sql.OrderTerm) OrderOption {
}
}
// ByPromoCodeUsagesCount orders the results by promo_code_usages count.
func ByPromoCodeUsagesCount(opts ...sql.OrderTermOption) OrderOption {
return func(s *sql.Selector) {
sqlgraph.OrderByNeighborsCount(s, newPromoCodeUsagesStep(), opts...)
}
}
// ByPromoCodeUsages orders the results by promo_code_usages terms.
func ByPromoCodeUsages(term sql.OrderTerm, terms ...sql.OrderTerm) OrderOption {
return func(s *sql.Selector) {
sqlgraph.OrderByNeighborTerms(s, newPromoCodeUsagesStep(), append([]sql.OrderTerm{term}, terms...)...)
}
}
// ByUserAllowedGroupsCount orders the results by user_allowed_groups count.
func ByUserAllowedGroupsCount(opts ...sql.OrderTermOption) OrderOption {
return func(s *sql.Selector) {
@@ -404,6 +427,13 @@ func newAttributeValuesStep() *sqlgraph.Step {
sqlgraph.Edge(sqlgraph.O2M, false, AttributeValuesTable, AttributeValuesColumn),
)
}
func newPromoCodeUsagesStep() *sqlgraph.Step {
return sqlgraph.NewStep(
sqlgraph.From(Table, FieldID),
sqlgraph.To(PromoCodeUsagesInverseTable, FieldID),
sqlgraph.Edge(sqlgraph.O2M, false, PromoCodeUsagesTable, PromoCodeUsagesColumn),
)
}
func newUserAllowedGroupsStep() *sqlgraph.Step {
return sqlgraph.NewStep(
sqlgraph.From(Table, FieldID),

View File

@@ -871,6 +871,29 @@ func HasAttributeValuesWith(preds ...predicate.UserAttributeValue) predicate.Use
})
}
// HasPromoCodeUsages applies the HasEdge predicate on the "promo_code_usages" edge.
func HasPromoCodeUsages() predicate.User {
return predicate.User(func(s *sql.Selector) {
step := sqlgraph.NewStep(
sqlgraph.From(Table, FieldID),
sqlgraph.Edge(sqlgraph.O2M, false, PromoCodeUsagesTable, PromoCodeUsagesColumn),
)
sqlgraph.HasNeighbors(s, step)
})
}
// HasPromoCodeUsagesWith applies the HasEdge predicate on the "promo_code_usages" edge with a given conditions (other predicates).
func HasPromoCodeUsagesWith(preds ...predicate.PromoCodeUsage) predicate.User {
return predicate.User(func(s *sql.Selector) {
step := newPromoCodeUsagesStep()
sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) {
for _, p := range preds {
p(s)
}
})
})
}
// HasUserAllowedGroups applies the HasEdge predicate on the "user_allowed_groups" edge.
func HasUserAllowedGroups() predicate.User {
return predicate.User(func(s *sql.Selector) {

View File

@@ -13,6 +13,7 @@ import (
"entgo.io/ent/schema/field"
"github.com/Wei-Shaw/sub2api/ent/apikey"
"github.com/Wei-Shaw/sub2api/ent/group"
"github.com/Wei-Shaw/sub2api/ent/promocodeusage"
"github.com/Wei-Shaw/sub2api/ent/redeemcode"
"github.com/Wei-Shaw/sub2api/ent/usagelog"
"github.com/Wei-Shaw/sub2api/ent/user"
@@ -271,6 +272,21 @@ func (_c *UserCreate) AddAttributeValues(v ...*UserAttributeValue) *UserCreate {
return _c.AddAttributeValueIDs(ids...)
}
// AddPromoCodeUsageIDs adds the "promo_code_usages" edge to the PromoCodeUsage entity by IDs.
func (_c *UserCreate) AddPromoCodeUsageIDs(ids ...int64) *UserCreate {
_c.mutation.AddPromoCodeUsageIDs(ids...)
return _c
}
// AddPromoCodeUsages adds the "promo_code_usages" edges to the PromoCodeUsage entity.
func (_c *UserCreate) AddPromoCodeUsages(v ...*PromoCodeUsage) *UserCreate {
ids := make([]int64, len(v))
for i := range v {
ids[i] = v[i].ID
}
return _c.AddPromoCodeUsageIDs(ids...)
}
// Mutation returns the UserMutation object of the builder.
func (_c *UserCreate) Mutation() *UserMutation {
return _c.mutation
@@ -593,6 +609,22 @@ func (_c *UserCreate) createSpec() (*User, *sqlgraph.CreateSpec) {
}
_spec.Edges = append(_spec.Edges, edge)
}
if nodes := _c.mutation.PromoCodeUsagesIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,
Inverse: false,
Table: user.PromoCodeUsagesTable,
Columns: []string{user.PromoCodeUsagesColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(promocodeusage.FieldID, field.TypeInt64),
},
}
for _, k := range nodes {
edge.Target.Nodes = append(edge.Target.Nodes, k)
}
_spec.Edges = append(_spec.Edges, edge)
}
return _node, _spec
}

View File

@@ -9,12 +9,14 @@ import (
"math"
"entgo.io/ent"
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field"
"github.com/Wei-Shaw/sub2api/ent/apikey"
"github.com/Wei-Shaw/sub2api/ent/group"
"github.com/Wei-Shaw/sub2api/ent/predicate"
"github.com/Wei-Shaw/sub2api/ent/promocodeusage"
"github.com/Wei-Shaw/sub2api/ent/redeemcode"
"github.com/Wei-Shaw/sub2api/ent/usagelog"
"github.com/Wei-Shaw/sub2api/ent/user"
@@ -37,7 +39,9 @@ type UserQuery struct {
withAllowedGroups *GroupQuery
withUsageLogs *UsageLogQuery
withAttributeValues *UserAttributeValueQuery
withPromoCodeUsages *PromoCodeUsageQuery
withUserAllowedGroups *UserAllowedGroupQuery
modifiers []func(*sql.Selector)
// intermediate query (i.e. traversal path).
sql *sql.Selector
path func(context.Context) (*sql.Selector, error)
@@ -228,6 +232,28 @@ func (_q *UserQuery) QueryAttributeValues() *UserAttributeValueQuery {
return query
}
// QueryPromoCodeUsages chains the current query on the "promo_code_usages" edge.
func (_q *UserQuery) QueryPromoCodeUsages() *PromoCodeUsageQuery {
query := (&PromoCodeUsageClient{config: _q.config}).Query()
query.path = func(ctx context.Context) (fromU *sql.Selector, err error) {
if err := _q.prepareQuery(ctx); err != nil {
return nil, err
}
selector := _q.sqlQuery(ctx)
if err := selector.Err(); err != nil {
return nil, err
}
step := sqlgraph.NewStep(
sqlgraph.From(user.Table, user.FieldID, selector),
sqlgraph.To(promocodeusage.Table, promocodeusage.FieldID),
sqlgraph.Edge(sqlgraph.O2M, false, user.PromoCodeUsagesTable, user.PromoCodeUsagesColumn),
)
fromU = sqlgraph.SetNeighbors(_q.driver.Dialect(), step)
return fromU, nil
}
return query
}
// QueryUserAllowedGroups chains the current query on the "user_allowed_groups" edge.
func (_q *UserQuery) QueryUserAllowedGroups() *UserAllowedGroupQuery {
query := (&UserAllowedGroupClient{config: _q.config}).Query()
@@ -449,6 +475,7 @@ func (_q *UserQuery) Clone() *UserQuery {
withAllowedGroups: _q.withAllowedGroups.Clone(),
withUsageLogs: _q.withUsageLogs.Clone(),
withAttributeValues: _q.withAttributeValues.Clone(),
withPromoCodeUsages: _q.withPromoCodeUsages.Clone(),
withUserAllowedGroups: _q.withUserAllowedGroups.Clone(),
// clone intermediate query.
sql: _q.sql.Clone(),
@@ -533,6 +560,17 @@ func (_q *UserQuery) WithAttributeValues(opts ...func(*UserAttributeValueQuery))
return _q
}
// WithPromoCodeUsages tells the query-builder to eager-load the nodes that are connected to
// the "promo_code_usages" edge. The optional arguments are used to configure the query builder of the edge.
func (_q *UserQuery) WithPromoCodeUsages(opts ...func(*PromoCodeUsageQuery)) *UserQuery {
query := (&PromoCodeUsageClient{config: _q.config}).Query()
for _, opt := range opts {
opt(query)
}
_q.withPromoCodeUsages = query
return _q
}
// WithUserAllowedGroups tells the query-builder to eager-load the nodes that are connected to
// the "user_allowed_groups" edge. The optional arguments are used to configure the query builder of the edge.
func (_q *UserQuery) WithUserAllowedGroups(opts ...func(*UserAllowedGroupQuery)) *UserQuery {
@@ -622,7 +660,7 @@ func (_q *UserQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*User, e
var (
nodes = []*User{}
_spec = _q.querySpec()
loadedTypes = [8]bool{
loadedTypes = [9]bool{
_q.withAPIKeys != nil,
_q.withRedeemCodes != nil,
_q.withSubscriptions != nil,
@@ -630,6 +668,7 @@ func (_q *UserQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*User, e
_q.withAllowedGroups != nil,
_q.withUsageLogs != nil,
_q.withAttributeValues != nil,
_q.withPromoCodeUsages != nil,
_q.withUserAllowedGroups != nil,
}
)
@@ -642,6 +681,9 @@ func (_q *UserQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*User, e
node.Edges.loadedTypes = loadedTypes
return node.assignValues(columns, values)
}
if len(_q.modifiers) > 0 {
_spec.Modifiers = _q.modifiers
}
for i := range hooks {
hooks[i](ctx, _spec)
}
@@ -702,6 +744,13 @@ func (_q *UserQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*User, e
return nil, err
}
}
if query := _q.withPromoCodeUsages; query != nil {
if err := _q.loadPromoCodeUsages(ctx, query, nodes,
func(n *User) { n.Edges.PromoCodeUsages = []*PromoCodeUsage{} },
func(n *User, e *PromoCodeUsage) { n.Edges.PromoCodeUsages = append(n.Edges.PromoCodeUsages, e) }); err != nil {
return nil, err
}
}
if query := _q.withUserAllowedGroups; query != nil {
if err := _q.loadUserAllowedGroups(ctx, query, nodes,
func(n *User) { n.Edges.UserAllowedGroups = []*UserAllowedGroup{} },
@@ -959,6 +1008,36 @@ func (_q *UserQuery) loadAttributeValues(ctx context.Context, query *UserAttribu
}
return nil
}
func (_q *UserQuery) loadPromoCodeUsages(ctx context.Context, query *PromoCodeUsageQuery, nodes []*User, init func(*User), assign func(*User, *PromoCodeUsage)) error {
fks := make([]driver.Value, 0, len(nodes))
nodeids := make(map[int64]*User)
for i := range nodes {
fks = append(fks, nodes[i].ID)
nodeids[nodes[i].ID] = nodes[i]
if init != nil {
init(nodes[i])
}
}
if len(query.ctx.Fields) > 0 {
query.ctx.AppendFieldOnce(promocodeusage.FieldUserID)
}
query.Where(predicate.PromoCodeUsage(func(s *sql.Selector) {
s.Where(sql.InValues(s.C(user.PromoCodeUsagesColumn), fks...))
}))
neighbors, err := query.All(ctx)
if err != nil {
return err
}
for _, n := range neighbors {
fk := n.UserID
node, ok := nodeids[fk]
if !ok {
return fmt.Errorf(`unexpected referenced foreign-key "user_id" returned %v for node %v`, fk, n.ID)
}
assign(node, n)
}
return nil
}
func (_q *UserQuery) loadUserAllowedGroups(ctx context.Context, query *UserAllowedGroupQuery, nodes []*User, init func(*User), assign func(*User, *UserAllowedGroup)) error {
fks := make([]driver.Value, 0, len(nodes))
nodeids := make(map[int64]*User)
@@ -992,6 +1071,9 @@ func (_q *UserQuery) loadUserAllowedGroups(ctx context.Context, query *UserAllow
func (_q *UserQuery) sqlCount(ctx context.Context) (int, error) {
_spec := _q.querySpec()
if len(_q.modifiers) > 0 {
_spec.Modifiers = _q.modifiers
}
_spec.Node.Columns = _q.ctx.Fields
if len(_q.ctx.Fields) > 0 {
_spec.Unique = _q.ctx.Unique != nil && *_q.ctx.Unique
@@ -1054,6 +1136,9 @@ func (_q *UserQuery) sqlQuery(ctx context.Context) *sql.Selector {
if _q.ctx.Unique != nil && *_q.ctx.Unique {
selector.Distinct()
}
for _, m := range _q.modifiers {
m(selector)
}
for _, p := range _q.predicates {
p(selector)
}
@@ -1071,6 +1156,32 @@ func (_q *UserQuery) sqlQuery(ctx context.Context) *sql.Selector {
return selector
}
// ForUpdate locks the selected rows against concurrent updates, and prevent them from being
// updated, deleted or "selected ... for update" by other sessions, until the transaction is
// either committed or rolled-back.
func (_q *UserQuery) ForUpdate(opts ...sql.LockOption) *UserQuery {
if _q.driver.Dialect() == dialect.Postgres {
_q.Unique(false)
}
_q.modifiers = append(_q.modifiers, func(s *sql.Selector) {
s.ForUpdate(opts...)
})
return _q
}
// ForShare behaves similarly to ForUpdate, except that it acquires a shared mode lock
// on any rows that are read. Other sessions can read the rows, but cannot modify them
// until your transaction commits.
func (_q *UserQuery) ForShare(opts ...sql.LockOption) *UserQuery {
if _q.driver.Dialect() == dialect.Postgres {
_q.Unique(false)
}
_q.modifiers = append(_q.modifiers, func(s *sql.Selector) {
s.ForShare(opts...)
})
return _q
}
// UserGroupBy is the group-by builder for User entities.
type UserGroupBy struct {
selector

View File

@@ -14,6 +14,7 @@ import (
"github.com/Wei-Shaw/sub2api/ent/apikey"
"github.com/Wei-Shaw/sub2api/ent/group"
"github.com/Wei-Shaw/sub2api/ent/predicate"
"github.com/Wei-Shaw/sub2api/ent/promocodeusage"
"github.com/Wei-Shaw/sub2api/ent/redeemcode"
"github.com/Wei-Shaw/sub2api/ent/usagelog"
"github.com/Wei-Shaw/sub2api/ent/user"
@@ -291,6 +292,21 @@ func (_u *UserUpdate) AddAttributeValues(v ...*UserAttributeValue) *UserUpdate {
return _u.AddAttributeValueIDs(ids...)
}
// AddPromoCodeUsageIDs adds the "promo_code_usages" edge to the PromoCodeUsage entity by IDs.
func (_u *UserUpdate) AddPromoCodeUsageIDs(ids ...int64) *UserUpdate {
_u.mutation.AddPromoCodeUsageIDs(ids...)
return _u
}
// AddPromoCodeUsages adds the "promo_code_usages" edges to the PromoCodeUsage entity.
func (_u *UserUpdate) AddPromoCodeUsages(v ...*PromoCodeUsage) *UserUpdate {
ids := make([]int64, len(v))
for i := range v {
ids[i] = v[i].ID
}
return _u.AddPromoCodeUsageIDs(ids...)
}
// Mutation returns the UserMutation object of the builder.
func (_u *UserUpdate) Mutation() *UserMutation {
return _u.mutation
@@ -443,6 +459,27 @@ func (_u *UserUpdate) RemoveAttributeValues(v ...*UserAttributeValue) *UserUpdat
return _u.RemoveAttributeValueIDs(ids...)
}
// ClearPromoCodeUsages clears all "promo_code_usages" edges to the PromoCodeUsage entity.
func (_u *UserUpdate) ClearPromoCodeUsages() *UserUpdate {
_u.mutation.ClearPromoCodeUsages()
return _u
}
// RemovePromoCodeUsageIDs removes the "promo_code_usages" edge to PromoCodeUsage entities by IDs.
func (_u *UserUpdate) RemovePromoCodeUsageIDs(ids ...int64) *UserUpdate {
_u.mutation.RemovePromoCodeUsageIDs(ids...)
return _u
}
// RemovePromoCodeUsages removes "promo_code_usages" edges to PromoCodeUsage entities.
func (_u *UserUpdate) RemovePromoCodeUsages(v ...*PromoCodeUsage) *UserUpdate {
ids := make([]int64, len(v))
for i := range v {
ids[i] = v[i].ID
}
return _u.RemovePromoCodeUsageIDs(ids...)
}
// Save executes the query and returns the number of nodes affected by the update operation.
func (_u *UserUpdate) Save(ctx context.Context) (int, error) {
if err := _u.defaults(); err != nil {
@@ -893,6 +930,51 @@ func (_u *UserUpdate) sqlSave(ctx context.Context) (_node int, err error) {
}
_spec.Edges.Add = append(_spec.Edges.Add, edge)
}
if _u.mutation.PromoCodeUsagesCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,
Inverse: false,
Table: user.PromoCodeUsagesTable,
Columns: []string{user.PromoCodeUsagesColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(promocodeusage.FieldID, field.TypeInt64),
},
}
_spec.Edges.Clear = append(_spec.Edges.Clear, edge)
}
if nodes := _u.mutation.RemovedPromoCodeUsagesIDs(); len(nodes) > 0 && !_u.mutation.PromoCodeUsagesCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,
Inverse: false,
Table: user.PromoCodeUsagesTable,
Columns: []string{user.PromoCodeUsagesColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(promocodeusage.FieldID, field.TypeInt64),
},
}
for _, k := range nodes {
edge.Target.Nodes = append(edge.Target.Nodes, k)
}
_spec.Edges.Clear = append(_spec.Edges.Clear, edge)
}
if nodes := _u.mutation.PromoCodeUsagesIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,
Inverse: false,
Table: user.PromoCodeUsagesTable,
Columns: []string{user.PromoCodeUsagesColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(promocodeusage.FieldID, field.TypeInt64),
},
}
for _, k := range nodes {
edge.Target.Nodes = append(edge.Target.Nodes, k)
}
_spec.Edges.Add = append(_spec.Edges.Add, edge)
}
if _node, err = sqlgraph.UpdateNodes(ctx, _u.driver, _spec); err != nil {
if _, ok := err.(*sqlgraph.NotFoundError); ok {
err = &NotFoundError{user.Label}
@@ -1170,6 +1252,21 @@ func (_u *UserUpdateOne) AddAttributeValues(v ...*UserAttributeValue) *UserUpdat
return _u.AddAttributeValueIDs(ids...)
}
// AddPromoCodeUsageIDs adds the "promo_code_usages" edge to the PromoCodeUsage entity by IDs.
func (_u *UserUpdateOne) AddPromoCodeUsageIDs(ids ...int64) *UserUpdateOne {
_u.mutation.AddPromoCodeUsageIDs(ids...)
return _u
}
// AddPromoCodeUsages adds the "promo_code_usages" edges to the PromoCodeUsage entity.
func (_u *UserUpdateOne) AddPromoCodeUsages(v ...*PromoCodeUsage) *UserUpdateOne {
ids := make([]int64, len(v))
for i := range v {
ids[i] = v[i].ID
}
return _u.AddPromoCodeUsageIDs(ids...)
}
// Mutation returns the UserMutation object of the builder.
func (_u *UserUpdateOne) Mutation() *UserMutation {
return _u.mutation
@@ -1322,6 +1419,27 @@ func (_u *UserUpdateOne) RemoveAttributeValues(v ...*UserAttributeValue) *UserUp
return _u.RemoveAttributeValueIDs(ids...)
}
// ClearPromoCodeUsages clears all "promo_code_usages" edges to the PromoCodeUsage entity.
func (_u *UserUpdateOne) ClearPromoCodeUsages() *UserUpdateOne {
_u.mutation.ClearPromoCodeUsages()
return _u
}
// RemovePromoCodeUsageIDs removes the "promo_code_usages" edge to PromoCodeUsage entities by IDs.
func (_u *UserUpdateOne) RemovePromoCodeUsageIDs(ids ...int64) *UserUpdateOne {
_u.mutation.RemovePromoCodeUsageIDs(ids...)
return _u
}
// RemovePromoCodeUsages removes "promo_code_usages" edges to PromoCodeUsage entities.
func (_u *UserUpdateOne) RemovePromoCodeUsages(v ...*PromoCodeUsage) *UserUpdateOne {
ids := make([]int64, len(v))
for i := range v {
ids[i] = v[i].ID
}
return _u.RemovePromoCodeUsageIDs(ids...)
}
// Where appends a list predicates to the UserUpdate builder.
func (_u *UserUpdateOne) Where(ps ...predicate.User) *UserUpdateOne {
_u.mutation.Where(ps...)
@@ -1802,6 +1920,51 @@ func (_u *UserUpdateOne) sqlSave(ctx context.Context) (_node *User, err error) {
}
_spec.Edges.Add = append(_spec.Edges.Add, edge)
}
if _u.mutation.PromoCodeUsagesCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,
Inverse: false,
Table: user.PromoCodeUsagesTable,
Columns: []string{user.PromoCodeUsagesColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(promocodeusage.FieldID, field.TypeInt64),
},
}
_spec.Edges.Clear = append(_spec.Edges.Clear, edge)
}
if nodes := _u.mutation.RemovedPromoCodeUsagesIDs(); len(nodes) > 0 && !_u.mutation.PromoCodeUsagesCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,
Inverse: false,
Table: user.PromoCodeUsagesTable,
Columns: []string{user.PromoCodeUsagesColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(promocodeusage.FieldID, field.TypeInt64),
},
}
for _, k := range nodes {
edge.Target.Nodes = append(edge.Target.Nodes, k)
}
_spec.Edges.Clear = append(_spec.Edges.Clear, edge)
}
if nodes := _u.mutation.PromoCodeUsagesIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,
Inverse: false,
Table: user.PromoCodeUsagesTable,
Columns: []string{user.PromoCodeUsagesColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(promocodeusage.FieldID, field.TypeInt64),
},
}
for _, k := range nodes {
edge.Target.Nodes = append(edge.Target.Nodes, k)
}
_spec.Edges.Add = append(_spec.Edges.Add, edge)
}
_node = &User{config: _u.config}
_spec.Assign = _node.assignValues
_spec.ScanValues = _node.scanValues

View File

@@ -8,6 +8,7 @@ import (
"math"
"entgo.io/ent"
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"github.com/Wei-Shaw/sub2api/ent/group"
@@ -25,6 +26,7 @@ type UserAllowedGroupQuery struct {
predicates []predicate.UserAllowedGroup
withUser *UserQuery
withGroup *GroupQuery
modifiers []func(*sql.Selector)
// intermediate query (i.e. traversal path).
sql *sql.Selector
path func(context.Context) (*sql.Selector, error)
@@ -347,6 +349,9 @@ func (_q *UserAllowedGroupQuery) sqlAll(ctx context.Context, hooks ...queryHook)
node.Edges.loadedTypes = loadedTypes
return node.assignValues(columns, values)
}
if len(_q.modifiers) > 0 {
_spec.Modifiers = _q.modifiers
}
for i := range hooks {
hooks[i](ctx, _spec)
}
@@ -432,6 +437,9 @@ func (_q *UserAllowedGroupQuery) loadGroup(ctx context.Context, query *GroupQuer
func (_q *UserAllowedGroupQuery) sqlCount(ctx context.Context) (int, error) {
_spec := _q.querySpec()
if len(_q.modifiers) > 0 {
_spec.Modifiers = _q.modifiers
}
_spec.Unique = false
_spec.Node.Columns = nil
return sqlgraph.CountNodes(ctx, _q.driver, _spec)
@@ -495,6 +503,9 @@ func (_q *UserAllowedGroupQuery) sqlQuery(ctx context.Context) *sql.Selector {
if _q.ctx.Unique != nil && *_q.ctx.Unique {
selector.Distinct()
}
for _, m := range _q.modifiers {
m(selector)
}
for _, p := range _q.predicates {
p(selector)
}
@@ -512,6 +523,32 @@ func (_q *UserAllowedGroupQuery) sqlQuery(ctx context.Context) *sql.Selector {
return selector
}
// ForUpdate locks the selected rows against concurrent updates, and prevent them from being
// updated, deleted or "selected ... for update" by other sessions, until the transaction is
// either committed or rolled-back.
func (_q *UserAllowedGroupQuery) ForUpdate(opts ...sql.LockOption) *UserAllowedGroupQuery {
if _q.driver.Dialect() == dialect.Postgres {
_q.Unique(false)
}
_q.modifiers = append(_q.modifiers, func(s *sql.Selector) {
s.ForUpdate(opts...)
})
return _q
}
// ForShare behaves similarly to ForUpdate, except that it acquires a shared mode lock
// on any rows that are read. Other sessions can read the rows, but cannot modify them
// until your transaction commits.
func (_q *UserAllowedGroupQuery) ForShare(opts ...sql.LockOption) *UserAllowedGroupQuery {
if _q.driver.Dialect() == dialect.Postgres {
_q.Unique(false)
}
_q.modifiers = append(_q.modifiers, func(s *sql.Selector) {
s.ForShare(opts...)
})
return _q
}
// UserAllowedGroupGroupBy is the group-by builder for UserAllowedGroup entities.
type UserAllowedGroupGroupBy struct {
selector

View File

@@ -9,6 +9,7 @@ import (
"math"
"entgo.io/ent"
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field"
@@ -25,6 +26,7 @@ type UserAttributeDefinitionQuery struct {
inters []Interceptor
predicates []predicate.UserAttributeDefinition
withValues *UserAttributeValueQuery
modifiers []func(*sql.Selector)
// intermediate query (i.e. traversal path).
sql *sql.Selector
path func(context.Context) (*sql.Selector, error)
@@ -384,6 +386,9 @@ func (_q *UserAttributeDefinitionQuery) sqlAll(ctx context.Context, hooks ...que
node.Edges.loadedTypes = loadedTypes
return node.assignValues(columns, values)
}
if len(_q.modifiers) > 0 {
_spec.Modifiers = _q.modifiers
}
for i := range hooks {
hooks[i](ctx, _spec)
}
@@ -436,6 +441,9 @@ func (_q *UserAttributeDefinitionQuery) loadValues(ctx context.Context, query *U
func (_q *UserAttributeDefinitionQuery) sqlCount(ctx context.Context) (int, error) {
_spec := _q.querySpec()
if len(_q.modifiers) > 0 {
_spec.Modifiers = _q.modifiers
}
_spec.Node.Columns = _q.ctx.Fields
if len(_q.ctx.Fields) > 0 {
_spec.Unique = _q.ctx.Unique != nil && *_q.ctx.Unique
@@ -498,6 +506,9 @@ func (_q *UserAttributeDefinitionQuery) sqlQuery(ctx context.Context) *sql.Selec
if _q.ctx.Unique != nil && *_q.ctx.Unique {
selector.Distinct()
}
for _, m := range _q.modifiers {
m(selector)
}
for _, p := range _q.predicates {
p(selector)
}
@@ -515,6 +526,32 @@ func (_q *UserAttributeDefinitionQuery) sqlQuery(ctx context.Context) *sql.Selec
return selector
}
// ForUpdate locks the selected rows against concurrent updates, and prevent them from being
// updated, deleted or "selected ... for update" by other sessions, until the transaction is
// either committed or rolled-back.
func (_q *UserAttributeDefinitionQuery) ForUpdate(opts ...sql.LockOption) *UserAttributeDefinitionQuery {
if _q.driver.Dialect() == dialect.Postgres {
_q.Unique(false)
}
_q.modifiers = append(_q.modifiers, func(s *sql.Selector) {
s.ForUpdate(opts...)
})
return _q
}
// ForShare behaves similarly to ForUpdate, except that it acquires a shared mode lock
// on any rows that are read. Other sessions can read the rows, but cannot modify them
// until your transaction commits.
func (_q *UserAttributeDefinitionQuery) ForShare(opts ...sql.LockOption) *UserAttributeDefinitionQuery {
if _q.driver.Dialect() == dialect.Postgres {
_q.Unique(false)
}
_q.modifiers = append(_q.modifiers, func(s *sql.Selector) {
s.ForShare(opts...)
})
return _q
}
// UserAttributeDefinitionGroupBy is the group-by builder for UserAttributeDefinition entities.
type UserAttributeDefinitionGroupBy struct {
selector

View File

@@ -8,6 +8,7 @@ import (
"math"
"entgo.io/ent"
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field"
@@ -26,6 +27,7 @@ type UserAttributeValueQuery struct {
predicates []predicate.UserAttributeValue
withUser *UserQuery
withDefinition *UserAttributeDefinitionQuery
modifiers []func(*sql.Selector)
// intermediate query (i.e. traversal path).
sql *sql.Selector
path func(context.Context) (*sql.Selector, error)
@@ -420,6 +422,9 @@ func (_q *UserAttributeValueQuery) sqlAll(ctx context.Context, hooks ...queryHoo
node.Edges.loadedTypes = loadedTypes
return node.assignValues(columns, values)
}
if len(_q.modifiers) > 0 {
_spec.Modifiers = _q.modifiers
}
for i := range hooks {
hooks[i](ctx, _spec)
}
@@ -505,6 +510,9 @@ func (_q *UserAttributeValueQuery) loadDefinition(ctx context.Context, query *Us
func (_q *UserAttributeValueQuery) sqlCount(ctx context.Context) (int, error) {
_spec := _q.querySpec()
if len(_q.modifiers) > 0 {
_spec.Modifiers = _q.modifiers
}
_spec.Node.Columns = _q.ctx.Fields
if len(_q.ctx.Fields) > 0 {
_spec.Unique = _q.ctx.Unique != nil && *_q.ctx.Unique
@@ -573,6 +581,9 @@ func (_q *UserAttributeValueQuery) sqlQuery(ctx context.Context) *sql.Selector {
if _q.ctx.Unique != nil && *_q.ctx.Unique {
selector.Distinct()
}
for _, m := range _q.modifiers {
m(selector)
}
for _, p := range _q.predicates {
p(selector)
}
@@ -590,6 +601,32 @@ func (_q *UserAttributeValueQuery) sqlQuery(ctx context.Context) *sql.Selector {
return selector
}
// ForUpdate locks the selected rows against concurrent updates, and prevent them from being
// updated, deleted or "selected ... for update" by other sessions, until the transaction is
// either committed or rolled-back.
func (_q *UserAttributeValueQuery) ForUpdate(opts ...sql.LockOption) *UserAttributeValueQuery {
if _q.driver.Dialect() == dialect.Postgres {
_q.Unique(false)
}
_q.modifiers = append(_q.modifiers, func(s *sql.Selector) {
s.ForUpdate(opts...)
})
return _q
}
// ForShare behaves similarly to ForUpdate, except that it acquires a shared mode lock
// on any rows that are read. Other sessions can read the rows, but cannot modify them
// until your transaction commits.
func (_q *UserAttributeValueQuery) ForShare(opts ...sql.LockOption) *UserAttributeValueQuery {
if _q.driver.Dialect() == dialect.Postgres {
_q.Unique(false)
}
_q.modifiers = append(_q.modifiers, func(s *sql.Selector) {
s.ForShare(opts...)
})
return _q
}
// UserAttributeValueGroupBy is the group-by builder for UserAttributeValue entities.
type UserAttributeValueGroupBy struct {
selector

View File

@@ -9,6 +9,7 @@ import (
"math"
"entgo.io/ent"
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field"
@@ -30,6 +31,7 @@ type UserSubscriptionQuery struct {
withGroup *GroupQuery
withAssignedByUser *UserQuery
withUsageLogs *UsageLogQuery
modifiers []func(*sql.Selector)
// intermediate query (i.e. traversal path).
sql *sql.Selector
path func(context.Context) (*sql.Selector, error)
@@ -494,6 +496,9 @@ func (_q *UserSubscriptionQuery) sqlAll(ctx context.Context, hooks ...queryHook)
node.Edges.loadedTypes = loadedTypes
return node.assignValues(columns, values)
}
if len(_q.modifiers) > 0 {
_spec.Modifiers = _q.modifiers
}
for i := range hooks {
hooks[i](ctx, _spec)
}
@@ -657,6 +662,9 @@ func (_q *UserSubscriptionQuery) loadUsageLogs(ctx context.Context, query *Usage
func (_q *UserSubscriptionQuery) sqlCount(ctx context.Context) (int, error) {
_spec := _q.querySpec()
if len(_q.modifiers) > 0 {
_spec.Modifiers = _q.modifiers
}
_spec.Node.Columns = _q.ctx.Fields
if len(_q.ctx.Fields) > 0 {
_spec.Unique = _q.ctx.Unique != nil && *_q.ctx.Unique
@@ -728,6 +736,9 @@ func (_q *UserSubscriptionQuery) sqlQuery(ctx context.Context) *sql.Selector {
if _q.ctx.Unique != nil && *_q.ctx.Unique {
selector.Distinct()
}
for _, m := range _q.modifiers {
m(selector)
}
for _, p := range _q.predicates {
p(selector)
}
@@ -745,6 +756,32 @@ func (_q *UserSubscriptionQuery) sqlQuery(ctx context.Context) *sql.Selector {
return selector
}
// ForUpdate locks the selected rows against concurrent updates, and prevent them from being
// updated, deleted or "selected ... for update" by other sessions, until the transaction is
// either committed or rolled-back.
func (_q *UserSubscriptionQuery) ForUpdate(opts ...sql.LockOption) *UserSubscriptionQuery {
if _q.driver.Dialect() == dialect.Postgres {
_q.Unique(false)
}
_q.modifiers = append(_q.modifiers, func(s *sql.Selector) {
s.ForUpdate(opts...)
})
return _q
}
// ForShare behaves similarly to ForUpdate, except that it acquires a shared mode lock
// on any rows that are read. Other sessions can read the rows, but cannot modify them
// until your transaction commits.
func (_q *UserSubscriptionQuery) ForShare(opts ...sql.LockOption) *UserSubscriptionQuery {
if _q.driver.Dialect() == dialect.Postgres {
_q.Unique(false)
}
_q.modifiers = append(_q.modifiers, func(s *sql.Selector) {
s.ForShare(opts...)
})
return _q
}
// UserSubscriptionGroupBy is the group-by builder for UserSubscription entities.
type UserSubscriptionGroupBy struct {
selector

View File

@@ -1,18 +1,18 @@
module github.com/Wei-Shaw/sub2api
go 1.24.0
toolchain go1.24.11
go 1.25.5
require (
entgo.io/ent v0.14.5
github.com/gin-gonic/gin v1.9.1
github.com/golang-jwt/jwt/v5 v5.2.0
github.com/golang-jwt/jwt/v5 v5.2.2
github.com/google/uuid v1.6.0
github.com/google/wire v0.7.0
github.com/imroc/req/v3 v3.56.0
github.com/gorilla/websocket v1.5.3
github.com/imroc/req/v3 v3.57.0
github.com/lib/pq v1.10.9
github.com/redis/go-redis/v9 v9.17.2
github.com/shirou/gopsutil/v4 v4.25.6
github.com/spf13/viper v1.18.2
github.com/stretchr/testify v1.11.1
github.com/testcontainers/testcontainers-go/modules/postgres v0.40.0
@@ -20,16 +20,16 @@ require (
github.com/tidwall/gjson v1.18.0
github.com/tidwall/sjson v1.2.5
github.com/zeromicro/go-zero v1.9.4
golang.org/x/crypto v0.44.0
golang.org/x/net v0.47.0
golang.org/x/term v0.37.0
golang.org/x/crypto v0.46.0
golang.org/x/net v0.48.0
golang.org/x/sync v0.19.0
golang.org/x/term v0.38.0
gopkg.in/yaml.v3 v3.0.1
)
require (
ariga.io/atlas v0.32.1-0.20250325101103-175b25e1c1b9 // indirect
dario.cat/mergo v1.0.2 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/agext/levenshtein v1.2.3 // indirect
@@ -46,11 +46,13 @@ require (
github.com/containerd/platforms v0.2.1 // indirect
github.com/cpuguy83/dockercfg v0.3.2 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dgraph-io/ristretto v0.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/docker v28.5.1+incompatible // indirect
github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ebitengine/purego v0.8.4 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
@@ -64,7 +66,6 @@ require (
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.14.0 // indirect
github.com/go-sql-driver/mysql v1.9.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
@@ -74,10 +75,8 @@ require (
github.com/hashicorp/hcl/v2 v2.18.1 // indirect
github.com/icholy/digest v1.1.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.1 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
@@ -105,13 +104,13 @@ require (
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.56.0 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.57.1 // indirect
github.com/refraction-networking/utls v1.8.1 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/shirou/gopsutil/v4 v4.25.6 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
@@ -141,16 +140,12 @@ require (
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/mod v0.29.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/tools v0.38.0 // indirect
golang.org/x/mod v0.30.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
golang.org/x/tools v0.39.0 // indirect
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated // indirect
google.golang.org/grpc v1.75.1 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gorm.io/datatypes v1.2.7 // indirect
gorm.io/driver/mysql v1.5.6 // indirect
gorm.io/gorm v1.30.0 // indirect
)

View File

@@ -4,8 +4,6 @@ dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
entgo.io/ent v0.14.5 h1:Rj2WOYJtCkWyFo6a+5wB3EfBRP0rnx1fMk6gGA0UUe4=
entgo.io/ent v0.14.5/go.mod h1:zTzLmWtPvGpmSwtkaayM2cm5m819NdM7z7tYPq3vN0U=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
@@ -53,6 +51,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgraph-io/ristretto v0.2.0 h1:XAfl+7cmoUDWW/2Lx8TGZQjjxIQ2Ley9DSf52dru4WE=
github.com/dgraph-io/ristretto v0.2.0/go.mod h1:8uBHCU/PBV4Ag0CJrP47b9Ofby5dqWNh4FicAdoqFNU=
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/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
@@ -63,6 +63,8 @@ github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pM
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
@@ -96,15 +98,12 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.9.0 h1:Y0zIbQXhQKmQgTp44Y1dp3wTXcn804QoTptLZT1vtvo=
github.com/go-sql-driver/mysql v1.9.0/go.mod h1:pDetrLJeA3oMujJuvXc8RJoasr589B6A9fwzD3QMrqw=
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
@@ -118,6 +117,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4=
github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
@@ -126,8 +127,8 @@ github.com/hashicorp/hcl/v2 v2.18.1 h1:6nxnOJFku1EuSawSD81fuviYUV8DxFr3fp2dUi3ZY
github.com/hashicorp/hcl/v2 v2.18.1/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE=
github.com/icholy/digest v1.1.0 h1:HfGg9Irj7i+IX1o1QAmPfIBNu/Q5A5Tu3n/MED9k9H4=
github.com/icholy/digest v1.1.0/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y=
github.com/imroc/req/v3 v3.56.0 h1:t6YdqqerYBXhZ9+VjqsQs5wlKxdUNEvsgBhxWc1AEEo=
github.com/imroc/req/v3 v3.56.0/go.mod h1:cUZSooE8hhzFNOrAbdxuemXDQxFXLQTnu3066jr7ZGk=
github.com/imroc/req/v3 v3.57.0 h1:LMTUjNRUybUkTPn8oJDq8Kg3JRBOBTcnDhKu7mzupKI=
github.com/imroc/req/v3 v3.57.0/go.mod h1:JL62ey1nvSLq81HORNcosvlf7SxZStONNqOprg0Pz00=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
@@ -138,14 +139,10 @@ github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg=
github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
@@ -219,16 +216,18 @@ github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.56.0 h1:q/TW+OLismmXAehgFLczhCDTYB3bFmua4D9lsNBWxvY=
github.com/quic-go/quic-go v0.56.0/go.mod h1:9gx5KsFQtw2oZ6GZTyh+7YEvOxWCL9WZAepnHxgAo6c=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10=
github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI=
github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
github.com/refraction-networking/utls v1.8.1 h1:yNY1kapmQU8JeM1sSw2H2asfTIwWxIkrMJI0pRUOCAo=
github.com/refraction-networking/utls v1.8.1/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
@@ -335,16 +334,16 @@ go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTV
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -354,16 +353,16 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
golang.org/x/tools/go/expect v0.1.0-deprecated h1:jY2C5HGYR5lqex3gEniOQL0r7Dq5+VGVgY1nudX5lXY=
golang.org/x/tools/go/expect v0.1.0-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY=
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM=
@@ -386,13 +385,6 @@ gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/datatypes v1.2.7 h1:ww9GAhF1aGXZY3EB3cJPJ7//JiuQo7DlQA7NNlVaTdk=
gorm.io/datatypes v1.2.7/go.mod h1:M2iO+6S3hhi4nAyYe444Pcb0dcIiOMJ7QHaUXxyiNZY=
gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8=
gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@@ -6,6 +6,8 @@ import (
"encoding/hex"
"fmt"
"log"
"net/url"
"os"
"strings"
"time"
@@ -17,7 +19,7 @@ const (
RunModeSimple = "simple"
)
const DefaultCSPPolicy = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https:; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
const DefaultCSPPolicy = "default-src 'self'; script-src 'self' https://challenges.cloudflare.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https:; frame-src https://challenges.cloudflare.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
// 连接池隔离策略常量
// 用于控制上游 HTTP 连接池的隔离粒度,影响连接复用和资源消耗
@@ -34,23 +36,29 @@ const (
)
type Config struct {
Server ServerConfig `mapstructure:"server"`
CORS CORSConfig `mapstructure:"cors"`
Security SecurityConfig `mapstructure:"security"`
Billing BillingConfig `mapstructure:"billing"`
Turnstile TurnstileConfig `mapstructure:"turnstile"`
Database DatabaseConfig `mapstructure:"database"`
Redis RedisConfig `mapstructure:"redis"`
JWT JWTConfig `mapstructure:"jwt"`
Default DefaultConfig `mapstructure:"default"`
RateLimit RateLimitConfig `mapstructure:"rate_limit"`
Pricing PricingConfig `mapstructure:"pricing"`
Gateway GatewayConfig `mapstructure:"gateway"`
Concurrency ConcurrencyConfig `mapstructure:"concurrency"`
TokenRefresh TokenRefreshConfig `mapstructure:"token_refresh"`
RunMode string `mapstructure:"run_mode" yaml:"run_mode"`
Timezone string `mapstructure:"timezone"` // e.g. "Asia/Shanghai", "UTC"
Gemini GeminiConfig `mapstructure:"gemini"`
Server ServerConfig `mapstructure:"server"`
CORS CORSConfig `mapstructure:"cors"`
Security SecurityConfig `mapstructure:"security"`
Billing BillingConfig `mapstructure:"billing"`
Turnstile TurnstileConfig `mapstructure:"turnstile"`
Database DatabaseConfig `mapstructure:"database"`
Redis RedisConfig `mapstructure:"redis"`
Ops OpsConfig `mapstructure:"ops"`
JWT JWTConfig `mapstructure:"jwt"`
LinuxDo LinuxDoConnectConfig `mapstructure:"linuxdo_connect"`
Default DefaultConfig `mapstructure:"default"`
RateLimit RateLimitConfig `mapstructure:"rate_limit"`
Pricing PricingConfig `mapstructure:"pricing"`
Gateway GatewayConfig `mapstructure:"gateway"`
APIKeyAuth APIKeyAuthCacheConfig `mapstructure:"api_key_auth_cache"`
Dashboard DashboardCacheConfig `mapstructure:"dashboard_cache"`
DashboardAgg DashboardAggregationConfig `mapstructure:"dashboard_aggregation"`
Concurrency ConcurrencyConfig `mapstructure:"concurrency"`
TokenRefresh TokenRefreshConfig `mapstructure:"token_refresh"`
RunMode string `mapstructure:"run_mode" yaml:"run_mode"`
Timezone string `mapstructure:"timezone"` // e.g. "Asia/Shanghai", "UTC"
Gemini GeminiConfig `mapstructure:"gemini"`
Update UpdateConfig `mapstructure:"update"`
}
type GeminiConfig struct {
@@ -75,6 +83,33 @@ type GeminiTierQuotaConfig struct {
CooldownMinutes *int `mapstructure:"cooldown_minutes" json:"cooldown_minutes"`
}
type UpdateConfig struct {
// ProxyURL 用于访问 GitHub 的代理地址
// 支持 http/https/socks5/socks5h 协议
// 例如: "http://127.0.0.1:7890", "socks5://127.0.0.1:1080"
ProxyURL string `mapstructure:"proxy_url"`
}
type LinuxDoConnectConfig struct {
Enabled bool `mapstructure:"enabled"`
ClientID string `mapstructure:"client_id"`
ClientSecret string `mapstructure:"client_secret"`
AuthorizeURL string `mapstructure:"authorize_url"`
TokenURL string `mapstructure:"token_url"`
UserInfoURL string `mapstructure:"userinfo_url"`
Scopes string `mapstructure:"scopes"`
RedirectURL string `mapstructure:"redirect_url"` // 后端回调地址(需在提供方后台登记)
FrontendRedirectURL string `mapstructure:"frontend_redirect_url"` // 前端接收 token 的路由(默认:/auth/linuxdo/callback
TokenAuthMethod string `mapstructure:"token_auth_method"` // client_secret_post / client_secret_basic / none
UsePKCE bool `mapstructure:"use_pkce"`
// 可选:用于从 userinfo JSON 中提取字段的 gjson 路径。
// 为空时,服务端会尝试一组常见字段名。
UserInfoEmailPath string `mapstructure:"userinfo_email_path"`
UserInfoIDPath string `mapstructure:"userinfo_id_path"`
UserInfoUsernamePath string `mapstructure:"userinfo_username_path"`
}
// TokenRefreshConfig OAuth token自动刷新配置
type TokenRefreshConfig struct {
// 是否启用自动刷新
@@ -147,7 +182,7 @@ type CSPConfig struct {
}
type ProxyProbeConfig struct {
InsecureSkipVerify bool `mapstructure:"insecure_skip_verify"`
InsecureSkipVerify bool `mapstructure:"insecure_skip_verify"` // 已禁用:禁止跳过 TLS 证书验证
}
type BillingConfig struct {
@@ -235,6 +270,29 @@ type GatewaySchedulingConfig struct {
// 过期槽位清理周期0 表示禁用)
SlotCleanupInterval time.Duration `mapstructure:"slot_cleanup_interval"`
// 受控回源配置
DbFallbackEnabled bool `mapstructure:"db_fallback_enabled"`
// 受控回源超时0 表示不额外收紧超时
DbFallbackTimeoutSeconds int `mapstructure:"db_fallback_timeout_seconds"`
// 受控回源限流(实例级 QPS0 表示不限制
DbFallbackMaxQPS int `mapstructure:"db_fallback_max_qps"`
// Outbox 轮询与滞后阈值配置
// Outbox 轮询周期(秒)
OutboxPollIntervalSeconds int `mapstructure:"outbox_poll_interval_seconds"`
// Outbox 滞后告警阈值(秒)
OutboxLagWarnSeconds int `mapstructure:"outbox_lag_warn_seconds"`
// Outbox 触发强制重建阈值(秒)
OutboxLagRebuildSeconds int `mapstructure:"outbox_lag_rebuild_seconds"`
// Outbox 连续滞后触发次数
OutboxLagRebuildFailures int `mapstructure:"outbox_lag_rebuild_failures"`
// Outbox 积压触发重建阈值(行数)
OutboxBacklogRebuildRows int `mapstructure:"outbox_backlog_rebuild_rows"`
// 全量重建周期配置
// 全量重建周期0 表示禁用
FullRebuildIntervalSeconds int `mapstructure:"full_rebuild_interval_seconds"`
}
func (s *ServerConfig) Address() string {
@@ -262,6 +320,13 @@ type DatabaseConfig struct {
}
func (d *DatabaseConfig) DSN() string {
// 当密码为空时不包含 password 参数,避免 libpq 解析错误
if d.Password == "" {
return fmt.Sprintf(
"host=%s port=%d user=%s dbname=%s sslmode=%s",
d.Host, d.Port, d.User, d.DBName, d.SSLMode,
)
}
return fmt.Sprintf(
"host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
d.Host, d.Port, d.User, d.Password, d.DBName, d.SSLMode,
@@ -273,6 +338,13 @@ func (d *DatabaseConfig) DSNWithTimezone(tz string) string {
if tz == "" {
tz = "Asia/Shanghai"
}
// 当密码为空时不包含 password 参数,避免 libpq 解析错误
if d.Password == "" {
return fmt.Sprintf(
"host=%s port=%d user=%s dbname=%s sslmode=%s TimeZone=%s",
d.Host, d.Port, d.User, d.DBName, d.SSLMode, tz,
)
}
return fmt.Sprintf(
"host=%s port=%d user=%s password=%s dbname=%s sslmode=%s TimeZone=%s",
d.Host, d.Port, d.User, d.Password, d.DBName, d.SSLMode, tz,
@@ -303,6 +375,47 @@ func (r *RedisConfig) Address() string {
return fmt.Sprintf("%s:%d", r.Host, r.Port)
}
type OpsConfig struct {
// Enabled controls whether ops features should run.
//
// NOTE: vNext still has a DB-backed feature flag (ops_monitoring_enabled) for runtime on/off.
// This config flag is the "hard switch" for deployments that want to disable ops completely.
Enabled bool `mapstructure:"enabled"`
// UsePreaggregatedTables prefers ops_metrics_hourly/daily for long-window dashboard queries.
UsePreaggregatedTables bool `mapstructure:"use_preaggregated_tables"`
// Cleanup controls periodic deletion of old ops data to prevent unbounded growth.
Cleanup OpsCleanupConfig `mapstructure:"cleanup"`
// MetricsCollectorCache controls Redis caching for expensive per-window collector queries.
MetricsCollectorCache OpsMetricsCollectorCacheConfig `mapstructure:"metrics_collector_cache"`
// Pre-aggregation configuration.
Aggregation OpsAggregationConfig `mapstructure:"aggregation"`
}
type OpsCleanupConfig struct {
Enabled bool `mapstructure:"enabled"`
Schedule string `mapstructure:"schedule"`
// Retention days (0 disables that cleanup target).
//
// vNext requirement: default 30 days across ops datasets.
ErrorLogRetentionDays int `mapstructure:"error_log_retention_days"`
MinuteMetricsRetentionDays int `mapstructure:"minute_metrics_retention_days"`
HourlyMetricsRetentionDays int `mapstructure:"hourly_metrics_retention_days"`
}
type OpsAggregationConfig struct {
Enabled bool `mapstructure:"enabled"`
}
type OpsMetricsCollectorCacheConfig struct {
Enabled bool `mapstructure:"enabled"`
TTL time.Duration `mapstructure:"ttl"`
}
type JWTConfig struct {
Secret string `mapstructure:"secret"`
ExpireHour int `mapstructure:"expire_hour"`
@@ -325,6 +438,55 @@ type RateLimitConfig struct {
OverloadCooldownMinutes int `mapstructure:"overload_cooldown_minutes"` // 529过载冷却时间(分钟)
}
// APIKeyAuthCacheConfig API Key 认证缓存配置
type APIKeyAuthCacheConfig struct {
L1Size int `mapstructure:"l1_size"`
L1TTLSeconds int `mapstructure:"l1_ttl_seconds"`
L2TTLSeconds int `mapstructure:"l2_ttl_seconds"`
NegativeTTLSeconds int `mapstructure:"negative_ttl_seconds"`
JitterPercent int `mapstructure:"jitter_percent"`
Singleflight bool `mapstructure:"singleflight"`
}
// DashboardCacheConfig 仪表盘统计缓存配置
type DashboardCacheConfig struct {
// Enabled: 是否启用仪表盘缓存
Enabled bool `mapstructure:"enabled"`
// KeyPrefix: Redis key 前缀,用于多环境隔离
KeyPrefix string `mapstructure:"key_prefix"`
// StatsFreshTTLSeconds: 缓存命中认为“新鲜”的时间窗口(秒)
StatsFreshTTLSeconds int `mapstructure:"stats_fresh_ttl_seconds"`
// StatsTTLSeconds: Redis 缓存总 TTL
StatsTTLSeconds int `mapstructure:"stats_ttl_seconds"`
// StatsRefreshTimeoutSeconds: 异步刷新超时(秒)
StatsRefreshTimeoutSeconds int `mapstructure:"stats_refresh_timeout_seconds"`
}
// DashboardAggregationConfig 仪表盘预聚合配置
type DashboardAggregationConfig struct {
// Enabled: 是否启用预聚合作业
Enabled bool `mapstructure:"enabled"`
// IntervalSeconds: 聚合刷新间隔(秒)
IntervalSeconds int `mapstructure:"interval_seconds"`
// LookbackSeconds: 回看窗口(秒)
LookbackSeconds int `mapstructure:"lookback_seconds"`
// BackfillEnabled: 是否允许全量回填
BackfillEnabled bool `mapstructure:"backfill_enabled"`
// BackfillMaxDays: 回填最大跨度(天)
BackfillMaxDays int `mapstructure:"backfill_max_days"`
// Retention: 各表保留窗口(天)
Retention DashboardAggregationRetentionConfig `mapstructure:"retention"`
// RecomputeDays: 启动时重算最近 N 天
RecomputeDays int `mapstructure:"recompute_days"`
}
// DashboardAggregationRetentionConfig 预聚合保留窗口
type DashboardAggregationRetentionConfig struct {
UsageLogsDays int `mapstructure:"usage_logs_days"`
HourlyDays int `mapstructure:"hourly_days"`
DailyDays int `mapstructure:"daily_days"`
}
func NormalizeRunMode(value string) string {
normalized := strings.ToLower(strings.TrimSpace(value))
switch normalized {
@@ -338,8 +500,19 @@ func NormalizeRunMode(value string) string {
func Load() (*Config, error) {
viper.SetConfigName("config")
viper.SetConfigType("yaml")
// Add config paths in priority order
// 1. DATA_DIR environment variable (highest priority)
if dataDir := os.Getenv("DATA_DIR"); dataDir != "" {
viper.AddConfigPath(dataDir)
}
// 2. Docker data directory
viper.AddConfigPath("/app/data")
// 3. Current directory
viper.AddConfigPath(".")
// 4. Config subdirectory
viper.AddConfigPath("./config")
// 5. System config directory
viper.AddConfigPath("/etc/sub2api")
// 环境变量支持
@@ -367,18 +540,31 @@ func Load() (*Config, error) {
cfg.Server.Mode = "debug"
}
cfg.JWT.Secret = strings.TrimSpace(cfg.JWT.Secret)
cfg.LinuxDo.ClientID = strings.TrimSpace(cfg.LinuxDo.ClientID)
cfg.LinuxDo.ClientSecret = strings.TrimSpace(cfg.LinuxDo.ClientSecret)
cfg.LinuxDo.AuthorizeURL = strings.TrimSpace(cfg.LinuxDo.AuthorizeURL)
cfg.LinuxDo.TokenURL = strings.TrimSpace(cfg.LinuxDo.TokenURL)
cfg.LinuxDo.UserInfoURL = strings.TrimSpace(cfg.LinuxDo.UserInfoURL)
cfg.LinuxDo.Scopes = strings.TrimSpace(cfg.LinuxDo.Scopes)
cfg.LinuxDo.RedirectURL = strings.TrimSpace(cfg.LinuxDo.RedirectURL)
cfg.LinuxDo.FrontendRedirectURL = strings.TrimSpace(cfg.LinuxDo.FrontendRedirectURL)
cfg.LinuxDo.TokenAuthMethod = strings.ToLower(strings.TrimSpace(cfg.LinuxDo.TokenAuthMethod))
cfg.LinuxDo.UserInfoEmailPath = strings.TrimSpace(cfg.LinuxDo.UserInfoEmailPath)
cfg.LinuxDo.UserInfoIDPath = strings.TrimSpace(cfg.LinuxDo.UserInfoIDPath)
cfg.LinuxDo.UserInfoUsernamePath = strings.TrimSpace(cfg.LinuxDo.UserInfoUsernamePath)
cfg.Dashboard.KeyPrefix = strings.TrimSpace(cfg.Dashboard.KeyPrefix)
cfg.CORS.AllowedOrigins = normalizeStringSlice(cfg.CORS.AllowedOrigins)
cfg.Security.ResponseHeaders.AdditionalAllowed = normalizeStringSlice(cfg.Security.ResponseHeaders.AdditionalAllowed)
cfg.Security.ResponseHeaders.ForceRemove = normalizeStringSlice(cfg.Security.ResponseHeaders.ForceRemove)
cfg.Security.CSP.Policy = strings.TrimSpace(cfg.Security.CSP.Policy)
if cfg.Server.Mode != "release" && cfg.JWT.Secret == "" {
if cfg.JWT.Secret == "" {
secret, err := generateJWTSecret(64)
if err != nil {
return nil, fmt.Errorf("generate jwt secret error: %w", err)
}
cfg.JWT.Secret = secret
log.Println("Warning: JWT secret auto-generated for non-release mode. Do not use in production.")
log.Println("Warning: JWT secret auto-generated. Consider setting a fixed secret for production.")
}
if err := cfg.Validate(); err != nil {
@@ -392,7 +578,7 @@ func Load() (*Config, error) {
log.Println("Warning: security.response_headers.enabled=false; configurable header filtering disabled (default allowlist only).")
}
if cfg.Server.Mode != "release" && cfg.JWT.Secret != "" && isWeakJWTSecret(cfg.JWT.Secret) {
if cfg.JWT.Secret != "" && isWeakJWTSecret(cfg.JWT.Secret) {
log.Println("Warning: JWT secret appears weak; use a 32+ character random secret in production.")
}
if len(cfg.Security.ResponseHeaders.AdditionalAllowed) > 0 || len(cfg.Security.ResponseHeaders.ForceRemove) > 0 {
@@ -436,8 +622,8 @@ func setDefaults() {
"raw.githubusercontent.com",
})
viper.SetDefault("security.url_allowlist.crs_hosts", []string{})
viper.SetDefault("security.url_allowlist.allow_private_hosts", false)
viper.SetDefault("security.url_allowlist.allow_insecure_http", false)
viper.SetDefault("security.url_allowlist.allow_private_hosts", true)
viper.SetDefault("security.url_allowlist.allow_insecure_http", true)
viper.SetDefault("security.response_headers.enabled", false)
viper.SetDefault("security.response_headers.additional_allowed", []string{})
viper.SetDefault("security.response_headers.force_remove", []string{})
@@ -454,6 +640,22 @@ func setDefaults() {
// Turnstile
viper.SetDefault("turnstile.required", false)
// LinuxDo Connect OAuth 登录
viper.SetDefault("linuxdo_connect.enabled", false)
viper.SetDefault("linuxdo_connect.client_id", "")
viper.SetDefault("linuxdo_connect.client_secret", "")
viper.SetDefault("linuxdo_connect.authorize_url", "https://connect.linux.do/oauth2/authorize")
viper.SetDefault("linuxdo_connect.token_url", "https://connect.linux.do/oauth2/token")
viper.SetDefault("linuxdo_connect.userinfo_url", "https://connect.linux.do/api/user")
viper.SetDefault("linuxdo_connect.scopes", "user")
viper.SetDefault("linuxdo_connect.redirect_url", "")
viper.SetDefault("linuxdo_connect.frontend_redirect_url", "/auth/linuxdo/callback")
viper.SetDefault("linuxdo_connect.token_auth_method", "client_secret_post")
viper.SetDefault("linuxdo_connect.use_pkce", false)
viper.SetDefault("linuxdo_connect.userinfo_email_path", "")
viper.SetDefault("linuxdo_connect.userinfo_id_path", "")
viper.SetDefault("linuxdo_connect.userinfo_username_path", "")
// Database
viper.SetDefault("database.host", "localhost")
viper.SetDefault("database.port", 5432)
@@ -477,6 +679,20 @@ func setDefaults() {
viper.SetDefault("redis.pool_size", 128)
viper.SetDefault("redis.min_idle_conns", 10)
// Ops (vNext)
viper.SetDefault("ops.enabled", true)
viper.SetDefault("ops.use_preaggregated_tables", false)
viper.SetDefault("ops.cleanup.enabled", true)
viper.SetDefault("ops.cleanup.schedule", "0 2 * * *")
// Retention days: vNext defaults to 30 days across ops datasets.
viper.SetDefault("ops.cleanup.error_log_retention_days", 30)
viper.SetDefault("ops.cleanup.minute_metrics_retention_days", 30)
viper.SetDefault("ops.cleanup.hourly_metrics_retention_days", 30)
viper.SetDefault("ops.aggregation.enabled", true)
viper.SetDefault("ops.metrics_collector_cache.enabled", true)
// TTL should be slightly larger than collection interval (1m) to maximize cross-replica cache hits.
viper.SetDefault("ops.metrics_collector_cache.ttl", 65*time.Second)
// JWT
viper.SetDefault("jwt.secret", "")
viper.SetDefault("jwt.expire_hour", 24)
@@ -505,9 +721,35 @@ func setDefaults() {
// Timezone (default to Asia/Shanghai for Chinese users)
viper.SetDefault("timezone", "Asia/Shanghai")
// API Key auth cache
viper.SetDefault("api_key_auth_cache.l1_size", 65535)
viper.SetDefault("api_key_auth_cache.l1_ttl_seconds", 15)
viper.SetDefault("api_key_auth_cache.l2_ttl_seconds", 300)
viper.SetDefault("api_key_auth_cache.negative_ttl_seconds", 30)
viper.SetDefault("api_key_auth_cache.jitter_percent", 10)
viper.SetDefault("api_key_auth_cache.singleflight", true)
// Dashboard cache
viper.SetDefault("dashboard_cache.enabled", true)
viper.SetDefault("dashboard_cache.key_prefix", "sub2api:")
viper.SetDefault("dashboard_cache.stats_fresh_ttl_seconds", 15)
viper.SetDefault("dashboard_cache.stats_ttl_seconds", 30)
viper.SetDefault("dashboard_cache.stats_refresh_timeout_seconds", 30)
// Dashboard aggregation
viper.SetDefault("dashboard_aggregation.enabled", true)
viper.SetDefault("dashboard_aggregation.interval_seconds", 60)
viper.SetDefault("dashboard_aggregation.lookback_seconds", 120)
viper.SetDefault("dashboard_aggregation.backfill_enabled", false)
viper.SetDefault("dashboard_aggregation.backfill_max_days", 31)
viper.SetDefault("dashboard_aggregation.retention.usage_logs_days", 90)
viper.SetDefault("dashboard_aggregation.retention.hourly_days", 180)
viper.SetDefault("dashboard_aggregation.retention.daily_days", 730)
viper.SetDefault("dashboard_aggregation.recompute_days", 2)
// Gateway
viper.SetDefault("gateway.response_header_timeout", 600) // 600秒(10分钟)等待上游响应头LLM高负载时可能排队较久
viper.SetDefault("gateway.log_upstream_error_body", false)
viper.SetDefault("gateway.log_upstream_error_body", true)
viper.SetDefault("gateway.log_upstream_error_body_max_bytes", 2048)
viper.SetDefault("gateway.inject_beta_for_apikey", false)
viper.SetDefault("gateway.failover_on_400", false)
@@ -525,11 +767,20 @@ func setDefaults() {
viper.SetDefault("gateway.stream_keepalive_interval", 10)
viper.SetDefault("gateway.max_line_size", 10*1024*1024)
viper.SetDefault("gateway.scheduling.sticky_session_max_waiting", 3)
viper.SetDefault("gateway.scheduling.sticky_session_wait_timeout", 45*time.Second)
viper.SetDefault("gateway.scheduling.sticky_session_wait_timeout", 120*time.Second)
viper.SetDefault("gateway.scheduling.fallback_wait_timeout", 30*time.Second)
viper.SetDefault("gateway.scheduling.fallback_max_waiting", 100)
viper.SetDefault("gateway.scheduling.load_batch_enabled", true)
viper.SetDefault("gateway.scheduling.slot_cleanup_interval", 30*time.Second)
viper.SetDefault("gateway.scheduling.db_fallback_enabled", true)
viper.SetDefault("gateway.scheduling.db_fallback_timeout_seconds", 0)
viper.SetDefault("gateway.scheduling.db_fallback_max_qps", 0)
viper.SetDefault("gateway.scheduling.outbox_poll_interval_seconds", 1)
viper.SetDefault("gateway.scheduling.outbox_lag_warn_seconds", 5)
viper.SetDefault("gateway.scheduling.outbox_lag_rebuild_seconds", 10)
viper.SetDefault("gateway.scheduling.outbox_lag_rebuild_failures", 3)
viper.SetDefault("gateway.scheduling.outbox_backlog_rebuild_rows", 10000)
viper.SetDefault("gateway.scheduling.full_rebuild_interval_seconds", 300)
viper.SetDefault("concurrency.ping_interval", 10)
// TokenRefresh
@@ -549,17 +800,6 @@ func setDefaults() {
}
func (c *Config) Validate() error {
if c.Server.Mode == "release" {
if c.JWT.Secret == "" {
return fmt.Errorf("jwt.secret is required in release mode")
}
if len(c.JWT.Secret) < 32 {
return fmt.Errorf("jwt.secret must be at least 32 characters")
}
if isWeakJWTSecret(c.JWT.Secret) {
return fmt.Errorf("jwt.secret is too weak")
}
}
if c.JWT.ExpireHour <= 0 {
return fmt.Errorf("jwt.expire_hour must be positive")
}
@@ -572,6 +812,61 @@ func (c *Config) Validate() error {
if c.Security.CSP.Enabled && strings.TrimSpace(c.Security.CSP.Policy) == "" {
return fmt.Errorf("security.csp.policy is required when CSP is enabled")
}
if c.LinuxDo.Enabled {
if strings.TrimSpace(c.LinuxDo.ClientID) == "" {
return fmt.Errorf("linuxdo_connect.client_id is required when linuxdo_connect.enabled=true")
}
if strings.TrimSpace(c.LinuxDo.AuthorizeURL) == "" {
return fmt.Errorf("linuxdo_connect.authorize_url is required when linuxdo_connect.enabled=true")
}
if strings.TrimSpace(c.LinuxDo.TokenURL) == "" {
return fmt.Errorf("linuxdo_connect.token_url is required when linuxdo_connect.enabled=true")
}
if strings.TrimSpace(c.LinuxDo.UserInfoURL) == "" {
return fmt.Errorf("linuxdo_connect.userinfo_url is required when linuxdo_connect.enabled=true")
}
if strings.TrimSpace(c.LinuxDo.RedirectURL) == "" {
return fmt.Errorf("linuxdo_connect.redirect_url is required when linuxdo_connect.enabled=true")
}
method := strings.ToLower(strings.TrimSpace(c.LinuxDo.TokenAuthMethod))
switch method {
case "", "client_secret_post", "client_secret_basic", "none":
default:
return fmt.Errorf("linuxdo_connect.token_auth_method must be one of: client_secret_post/client_secret_basic/none")
}
if method == "none" && !c.LinuxDo.UsePKCE {
return fmt.Errorf("linuxdo_connect.use_pkce must be true when linuxdo_connect.token_auth_method=none")
}
if (method == "" || method == "client_secret_post" || method == "client_secret_basic") &&
strings.TrimSpace(c.LinuxDo.ClientSecret) == "" {
return fmt.Errorf("linuxdo_connect.client_secret is required when linuxdo_connect.enabled=true and token_auth_method is client_secret_post/client_secret_basic")
}
if strings.TrimSpace(c.LinuxDo.FrontendRedirectURL) == "" {
return fmt.Errorf("linuxdo_connect.frontend_redirect_url is required when linuxdo_connect.enabled=true")
}
if err := ValidateAbsoluteHTTPURL(c.LinuxDo.AuthorizeURL); err != nil {
return fmt.Errorf("linuxdo_connect.authorize_url invalid: %w", err)
}
if err := ValidateAbsoluteHTTPURL(c.LinuxDo.TokenURL); err != nil {
return fmt.Errorf("linuxdo_connect.token_url invalid: %w", err)
}
if err := ValidateAbsoluteHTTPURL(c.LinuxDo.UserInfoURL); err != nil {
return fmt.Errorf("linuxdo_connect.userinfo_url invalid: %w", err)
}
if err := ValidateAbsoluteHTTPURL(c.LinuxDo.RedirectURL); err != nil {
return fmt.Errorf("linuxdo_connect.redirect_url invalid: %w", err)
}
if err := ValidateFrontendRedirectURL(c.LinuxDo.FrontendRedirectURL); err != nil {
return fmt.Errorf("linuxdo_connect.frontend_redirect_url invalid: %w", err)
}
warnIfInsecureURL("linuxdo_connect.authorize_url", c.LinuxDo.AuthorizeURL)
warnIfInsecureURL("linuxdo_connect.token_url", c.LinuxDo.TokenURL)
warnIfInsecureURL("linuxdo_connect.userinfo_url", c.LinuxDo.UserInfoURL)
warnIfInsecureURL("linuxdo_connect.redirect_url", c.LinuxDo.RedirectURL)
warnIfInsecureURL("linuxdo_connect.frontend_redirect_url", c.LinuxDo.FrontendRedirectURL)
}
if c.Billing.CircuitBreaker.Enabled {
if c.Billing.CircuitBreaker.FailureThreshold <= 0 {
return fmt.Errorf("billing.circuit_breaker.failure_threshold must be positive")
@@ -616,6 +911,78 @@ func (c *Config) Validate() error {
if c.Redis.MinIdleConns > c.Redis.PoolSize {
return fmt.Errorf("redis.min_idle_conns cannot exceed redis.pool_size")
}
if c.Dashboard.Enabled {
if c.Dashboard.StatsFreshTTLSeconds <= 0 {
return fmt.Errorf("dashboard_cache.stats_fresh_ttl_seconds must be positive")
}
if c.Dashboard.StatsTTLSeconds <= 0 {
return fmt.Errorf("dashboard_cache.stats_ttl_seconds must be positive")
}
if c.Dashboard.StatsRefreshTimeoutSeconds <= 0 {
return fmt.Errorf("dashboard_cache.stats_refresh_timeout_seconds must be positive")
}
if c.Dashboard.StatsFreshTTLSeconds > c.Dashboard.StatsTTLSeconds {
return fmt.Errorf("dashboard_cache.stats_fresh_ttl_seconds must be <= dashboard_cache.stats_ttl_seconds")
}
} else {
if c.Dashboard.StatsFreshTTLSeconds < 0 {
return fmt.Errorf("dashboard_cache.stats_fresh_ttl_seconds must be non-negative")
}
if c.Dashboard.StatsTTLSeconds < 0 {
return fmt.Errorf("dashboard_cache.stats_ttl_seconds must be non-negative")
}
if c.Dashboard.StatsRefreshTimeoutSeconds < 0 {
return fmt.Errorf("dashboard_cache.stats_refresh_timeout_seconds must be non-negative")
}
}
if c.DashboardAgg.Enabled {
if c.DashboardAgg.IntervalSeconds <= 0 {
return fmt.Errorf("dashboard_aggregation.interval_seconds must be positive")
}
if c.DashboardAgg.LookbackSeconds < 0 {
return fmt.Errorf("dashboard_aggregation.lookback_seconds must be non-negative")
}
if c.DashboardAgg.BackfillMaxDays < 0 {
return fmt.Errorf("dashboard_aggregation.backfill_max_days must be non-negative")
}
if c.DashboardAgg.BackfillEnabled && c.DashboardAgg.BackfillMaxDays == 0 {
return fmt.Errorf("dashboard_aggregation.backfill_max_days must be positive")
}
if c.DashboardAgg.Retention.UsageLogsDays <= 0 {
return fmt.Errorf("dashboard_aggregation.retention.usage_logs_days must be positive")
}
if c.DashboardAgg.Retention.HourlyDays <= 0 {
return fmt.Errorf("dashboard_aggregation.retention.hourly_days must be positive")
}
if c.DashboardAgg.Retention.DailyDays <= 0 {
return fmt.Errorf("dashboard_aggregation.retention.daily_days must be positive")
}
if c.DashboardAgg.RecomputeDays < 0 {
return fmt.Errorf("dashboard_aggregation.recompute_days must be non-negative")
}
} else {
if c.DashboardAgg.IntervalSeconds < 0 {
return fmt.Errorf("dashboard_aggregation.interval_seconds must be non-negative")
}
if c.DashboardAgg.LookbackSeconds < 0 {
return fmt.Errorf("dashboard_aggregation.lookback_seconds must be non-negative")
}
if c.DashboardAgg.BackfillMaxDays < 0 {
return fmt.Errorf("dashboard_aggregation.backfill_max_days must be non-negative")
}
if c.DashboardAgg.Retention.UsageLogsDays < 0 {
return fmt.Errorf("dashboard_aggregation.retention.usage_logs_days must be non-negative")
}
if c.DashboardAgg.Retention.HourlyDays < 0 {
return fmt.Errorf("dashboard_aggregation.retention.hourly_days must be non-negative")
}
if c.DashboardAgg.Retention.DailyDays < 0 {
return fmt.Errorf("dashboard_aggregation.retention.daily_days must be non-negative")
}
if c.DashboardAgg.RecomputeDays < 0 {
return fmt.Errorf("dashboard_aggregation.recompute_days must be non-negative")
}
}
if c.Gateway.MaxBodySize <= 0 {
return fmt.Errorf("gateway.max_body_size must be positive")
}
@@ -686,6 +1053,50 @@ func (c *Config) Validate() error {
if c.Gateway.Scheduling.SlotCleanupInterval < 0 {
return fmt.Errorf("gateway.scheduling.slot_cleanup_interval must be non-negative")
}
if c.Gateway.Scheduling.DbFallbackTimeoutSeconds < 0 {
return fmt.Errorf("gateway.scheduling.db_fallback_timeout_seconds must be non-negative")
}
if c.Gateway.Scheduling.DbFallbackMaxQPS < 0 {
return fmt.Errorf("gateway.scheduling.db_fallback_max_qps must be non-negative")
}
if c.Gateway.Scheduling.OutboxPollIntervalSeconds <= 0 {
return fmt.Errorf("gateway.scheduling.outbox_poll_interval_seconds must be positive")
}
if c.Gateway.Scheduling.OutboxLagWarnSeconds < 0 {
return fmt.Errorf("gateway.scheduling.outbox_lag_warn_seconds must be non-negative")
}
if c.Gateway.Scheduling.OutboxLagRebuildSeconds < 0 {
return fmt.Errorf("gateway.scheduling.outbox_lag_rebuild_seconds must be non-negative")
}
if c.Gateway.Scheduling.OutboxLagRebuildFailures <= 0 {
return fmt.Errorf("gateway.scheduling.outbox_lag_rebuild_failures must be positive")
}
if c.Gateway.Scheduling.OutboxBacklogRebuildRows < 0 {
return fmt.Errorf("gateway.scheduling.outbox_backlog_rebuild_rows must be non-negative")
}
if c.Gateway.Scheduling.FullRebuildIntervalSeconds < 0 {
return fmt.Errorf("gateway.scheduling.full_rebuild_interval_seconds must be non-negative")
}
if c.Gateway.Scheduling.OutboxLagWarnSeconds > 0 &&
c.Gateway.Scheduling.OutboxLagRebuildSeconds > 0 &&
c.Gateway.Scheduling.OutboxLagRebuildSeconds < c.Gateway.Scheduling.OutboxLagWarnSeconds {
return fmt.Errorf("gateway.scheduling.outbox_lag_rebuild_seconds must be >= outbox_lag_warn_seconds")
}
if c.Ops.MetricsCollectorCache.TTL < 0 {
return fmt.Errorf("ops.metrics_collector_cache.ttl must be non-negative")
}
if c.Ops.Cleanup.ErrorLogRetentionDays < 0 {
return fmt.Errorf("ops.cleanup.error_log_retention_days must be non-negative")
}
if c.Ops.Cleanup.MinuteMetricsRetentionDays < 0 {
return fmt.Errorf("ops.cleanup.minute_metrics_retention_days must be non-negative")
}
if c.Ops.Cleanup.HourlyMetricsRetentionDays < 0 {
return fmt.Errorf("ops.cleanup.hourly_metrics_retention_days must be non-negative")
}
if c.Ops.Cleanup.Enabled && strings.TrimSpace(c.Ops.Cleanup.Schedule) == "" {
return fmt.Errorf("ops.cleanup.schedule is required when ops.cleanup.enabled=true")
}
if c.Concurrency.PingInterval < 5 || c.Concurrency.PingInterval > 30 {
return fmt.Errorf("concurrency.ping_interval must be between 5-30 seconds")
}
@@ -762,3 +1173,77 @@ func GetServerAddress() string {
port := v.GetInt("server.port")
return fmt.Sprintf("%s:%d", host, port)
}
// ValidateAbsoluteHTTPURL 验证是否为有效的绝对 HTTP(S) URL
func ValidateAbsoluteHTTPURL(raw string) error {
raw = strings.TrimSpace(raw)
if raw == "" {
return fmt.Errorf("empty url")
}
u, err := url.Parse(raw)
if err != nil {
return err
}
if !u.IsAbs() {
return fmt.Errorf("must be absolute")
}
if !isHTTPScheme(u.Scheme) {
return fmt.Errorf("unsupported scheme: %s", u.Scheme)
}
if strings.TrimSpace(u.Host) == "" {
return fmt.Errorf("missing host")
}
if u.Fragment != "" {
return fmt.Errorf("must not include fragment")
}
return nil
}
// ValidateFrontendRedirectURL 验证前端重定向 URL可以是绝对 URL 或相对路径)
func ValidateFrontendRedirectURL(raw string) error {
raw = strings.TrimSpace(raw)
if raw == "" {
return fmt.Errorf("empty url")
}
if strings.ContainsAny(raw, "\r\n") {
return fmt.Errorf("contains invalid characters")
}
if strings.HasPrefix(raw, "/") {
if strings.HasPrefix(raw, "//") {
return fmt.Errorf("must not start with //")
}
return nil
}
u, err := url.Parse(raw)
if err != nil {
return err
}
if !u.IsAbs() {
return fmt.Errorf("must be absolute http(s) url or relative path")
}
if !isHTTPScheme(u.Scheme) {
return fmt.Errorf("unsupported scheme: %s", u.Scheme)
}
if strings.TrimSpace(u.Host) == "" {
return fmt.Errorf("missing host")
}
if u.Fragment != "" {
return fmt.Errorf("must not include fragment")
}
return nil
}
// isHTTPScheme 检查是否为 HTTP 或 HTTPS 协议
func isHTTPScheme(scheme string) bool {
return strings.EqualFold(scheme, "http") || strings.EqualFold(scheme, "https")
}
func warnIfInsecureURL(field, raw string) {
u, err := url.Parse(strings.TrimSpace(raw))
if err != nil {
return
}
if strings.EqualFold(u.Scheme, "http") {
log.Printf("Warning: %s uses http scheme; use https in production to avoid token leakage.", field)
}
}

View File

@@ -1,6 +1,7 @@
package config
import (
"strings"
"testing"
"time"
@@ -38,8 +39,8 @@ func TestLoadDefaultSchedulingConfig(t *testing.T) {
if cfg.Gateway.Scheduling.StickySessionMaxWaiting != 3 {
t.Fatalf("StickySessionMaxWaiting = %d, want 3", cfg.Gateway.Scheduling.StickySessionMaxWaiting)
}
if cfg.Gateway.Scheduling.StickySessionWaitTimeout != 45*time.Second {
t.Fatalf("StickySessionWaitTimeout = %v, want 45s", cfg.Gateway.Scheduling.StickySessionWaitTimeout)
if cfg.Gateway.Scheduling.StickySessionWaitTimeout != 120*time.Second {
t.Fatalf("StickySessionWaitTimeout = %v, want 120s", cfg.Gateway.Scheduling.StickySessionWaitTimeout)
}
if cfg.Gateway.Scheduling.FallbackWaitTimeout != 30*time.Second {
t.Fatalf("FallbackWaitTimeout = %v, want 30s", cfg.Gateway.Scheduling.FallbackWaitTimeout)
@@ -80,10 +81,202 @@ func TestLoadDefaultSecurityToggles(t *testing.T) {
if cfg.Security.URLAllowlist.Enabled {
t.Fatalf("URLAllowlist.Enabled = true, want false")
}
if cfg.Security.URLAllowlist.AllowInsecureHTTP {
t.Fatalf("URLAllowlist.AllowInsecureHTTP = true, want false")
if !cfg.Security.URLAllowlist.AllowInsecureHTTP {
t.Fatalf("URLAllowlist.AllowInsecureHTTP = false, want true")
}
if !cfg.Security.URLAllowlist.AllowPrivateHosts {
t.Fatalf("URLAllowlist.AllowPrivateHosts = false, want true")
}
if cfg.Security.ResponseHeaders.Enabled {
t.Fatalf("ResponseHeaders.Enabled = true, want false")
}
}
func TestValidateLinuxDoFrontendRedirectURL(t *testing.T) {
viper.Reset()
cfg, err := Load()
if err != nil {
t.Fatalf("Load() error: %v", err)
}
cfg.LinuxDo.Enabled = true
cfg.LinuxDo.ClientID = "test-client"
cfg.LinuxDo.ClientSecret = "test-secret"
cfg.LinuxDo.RedirectURL = "https://example.com/api/v1/auth/oauth/linuxdo/callback"
cfg.LinuxDo.TokenAuthMethod = "client_secret_post"
cfg.LinuxDo.UsePKCE = false
cfg.LinuxDo.FrontendRedirectURL = "javascript:alert(1)"
err = cfg.Validate()
if err == nil {
t.Fatalf("Validate() expected error for javascript scheme, got nil")
}
if !strings.Contains(err.Error(), "linuxdo_connect.frontend_redirect_url") {
t.Fatalf("Validate() expected frontend_redirect_url error, got: %v", err)
}
}
func TestValidateLinuxDoPKCERequiredForPublicClient(t *testing.T) {
viper.Reset()
cfg, err := Load()
if err != nil {
t.Fatalf("Load() error: %v", err)
}
cfg.LinuxDo.Enabled = true
cfg.LinuxDo.ClientID = "test-client"
cfg.LinuxDo.ClientSecret = ""
cfg.LinuxDo.RedirectURL = "https://example.com/api/v1/auth/oauth/linuxdo/callback"
cfg.LinuxDo.FrontendRedirectURL = "/auth/linuxdo/callback"
cfg.LinuxDo.TokenAuthMethod = "none"
cfg.LinuxDo.UsePKCE = false
err = cfg.Validate()
if err == nil {
t.Fatalf("Validate() expected error when token_auth_method=none and use_pkce=false, got nil")
}
if !strings.Contains(err.Error(), "linuxdo_connect.use_pkce") {
t.Fatalf("Validate() expected use_pkce error, got: %v", err)
}
}
func TestLoadDefaultDashboardCacheConfig(t *testing.T) {
viper.Reset()
cfg, err := Load()
if err != nil {
t.Fatalf("Load() error: %v", err)
}
if !cfg.Dashboard.Enabled {
t.Fatalf("Dashboard.Enabled = false, want true")
}
if cfg.Dashboard.KeyPrefix != "sub2api:" {
t.Fatalf("Dashboard.KeyPrefix = %q, want %q", cfg.Dashboard.KeyPrefix, "sub2api:")
}
if cfg.Dashboard.StatsFreshTTLSeconds != 15 {
t.Fatalf("Dashboard.StatsFreshTTLSeconds = %d, want 15", cfg.Dashboard.StatsFreshTTLSeconds)
}
if cfg.Dashboard.StatsTTLSeconds != 30 {
t.Fatalf("Dashboard.StatsTTLSeconds = %d, want 30", cfg.Dashboard.StatsTTLSeconds)
}
if cfg.Dashboard.StatsRefreshTimeoutSeconds != 30 {
t.Fatalf("Dashboard.StatsRefreshTimeoutSeconds = %d, want 30", cfg.Dashboard.StatsRefreshTimeoutSeconds)
}
}
func TestValidateDashboardCacheConfigEnabled(t *testing.T) {
viper.Reset()
cfg, err := Load()
if err != nil {
t.Fatalf("Load() error: %v", err)
}
cfg.Dashboard.Enabled = true
cfg.Dashboard.StatsFreshTTLSeconds = 10
cfg.Dashboard.StatsTTLSeconds = 5
err = cfg.Validate()
if err == nil {
t.Fatalf("Validate() expected error for stats_fresh_ttl_seconds > stats_ttl_seconds, got nil")
}
if !strings.Contains(err.Error(), "dashboard_cache.stats_fresh_ttl_seconds") {
t.Fatalf("Validate() expected stats_fresh_ttl_seconds error, got: %v", err)
}
}
func TestValidateDashboardCacheConfigDisabled(t *testing.T) {
viper.Reset()
cfg, err := Load()
if err != nil {
t.Fatalf("Load() error: %v", err)
}
cfg.Dashboard.Enabled = false
cfg.Dashboard.StatsTTLSeconds = -1
err = cfg.Validate()
if err == nil {
t.Fatalf("Validate() expected error for negative stats_ttl_seconds, got nil")
}
if !strings.Contains(err.Error(), "dashboard_cache.stats_ttl_seconds") {
t.Fatalf("Validate() expected stats_ttl_seconds error, got: %v", err)
}
}
func TestLoadDefaultDashboardAggregationConfig(t *testing.T) {
viper.Reset()
cfg, err := Load()
if err != nil {
t.Fatalf("Load() error: %v", err)
}
if !cfg.DashboardAgg.Enabled {
t.Fatalf("DashboardAgg.Enabled = false, want true")
}
if cfg.DashboardAgg.IntervalSeconds != 60 {
t.Fatalf("DashboardAgg.IntervalSeconds = %d, want 60", cfg.DashboardAgg.IntervalSeconds)
}
if cfg.DashboardAgg.LookbackSeconds != 120 {
t.Fatalf("DashboardAgg.LookbackSeconds = %d, want 120", cfg.DashboardAgg.LookbackSeconds)
}
if cfg.DashboardAgg.BackfillEnabled {
t.Fatalf("DashboardAgg.BackfillEnabled = true, want false")
}
if cfg.DashboardAgg.BackfillMaxDays != 31 {
t.Fatalf("DashboardAgg.BackfillMaxDays = %d, want 31", cfg.DashboardAgg.BackfillMaxDays)
}
if cfg.DashboardAgg.Retention.UsageLogsDays != 90 {
t.Fatalf("DashboardAgg.Retention.UsageLogsDays = %d, want 90", cfg.DashboardAgg.Retention.UsageLogsDays)
}
if cfg.DashboardAgg.Retention.HourlyDays != 180 {
t.Fatalf("DashboardAgg.Retention.HourlyDays = %d, want 180", cfg.DashboardAgg.Retention.HourlyDays)
}
if cfg.DashboardAgg.Retention.DailyDays != 730 {
t.Fatalf("DashboardAgg.Retention.DailyDays = %d, want 730", cfg.DashboardAgg.Retention.DailyDays)
}
if cfg.DashboardAgg.RecomputeDays != 2 {
t.Fatalf("DashboardAgg.RecomputeDays = %d, want 2", cfg.DashboardAgg.RecomputeDays)
}
}
func TestValidateDashboardAggregationConfigDisabled(t *testing.T) {
viper.Reset()
cfg, err := Load()
if err != nil {
t.Fatalf("Load() error: %v", err)
}
cfg.DashboardAgg.Enabled = false
cfg.DashboardAgg.IntervalSeconds = -1
err = cfg.Validate()
if err == nil {
t.Fatalf("Validate() expected error for negative dashboard_aggregation.interval_seconds, got nil")
}
if !strings.Contains(err.Error(), "dashboard_aggregation.interval_seconds") {
t.Fatalf("Validate() expected interval_seconds error, got: %v", err)
}
}
func TestValidateDashboardAggregationBackfillMaxDays(t *testing.T) {
viper.Reset()
cfg, err := Load()
if err != nil {
t.Fatalf("Load() error: %v", err)
}
cfg.DashboardAgg.BackfillEnabled = true
cfg.DashboardAgg.BackfillMaxDays = 0
err = cfg.Validate()
if err == nil {
t.Fatalf("Validate() expected error for dashboard_aggregation.backfill_max_days, got nil")
}
if !strings.Contains(err.Error(), "dashboard_aggregation.backfill_max_days") {
t.Fatalf("Validate() expected backfill_max_days error, got: %v", err)
}
}

View File

@@ -34,15 +34,16 @@ func NewOAuthHandler(oauthService *service.OAuthService) *OAuthHandler {
// AccountHandler handles admin account management
type AccountHandler struct {
adminService service.AdminService
oauthService *service.OAuthService
openaiOAuthService *service.OpenAIOAuthService
geminiOAuthService *service.GeminiOAuthService
rateLimitService *service.RateLimitService
accountUsageService *service.AccountUsageService
accountTestService *service.AccountTestService
concurrencyService *service.ConcurrencyService
crsSyncService *service.CRSSyncService
adminService service.AdminService
oauthService *service.OAuthService
openaiOAuthService *service.OpenAIOAuthService
geminiOAuthService *service.GeminiOAuthService
antigravityOAuthService *service.AntigravityOAuthService
rateLimitService *service.RateLimitService
accountUsageService *service.AccountUsageService
accountTestService *service.AccountTestService
concurrencyService *service.ConcurrencyService
crsSyncService *service.CRSSyncService
}
// NewAccountHandler creates a new admin account handler
@@ -51,6 +52,7 @@ func NewAccountHandler(
oauthService *service.OAuthService,
openaiOAuthService *service.OpenAIOAuthService,
geminiOAuthService *service.GeminiOAuthService,
antigravityOAuthService *service.AntigravityOAuthService,
rateLimitService *service.RateLimitService,
accountUsageService *service.AccountUsageService,
accountTestService *service.AccountTestService,
@@ -58,15 +60,16 @@ func NewAccountHandler(
crsSyncService *service.CRSSyncService,
) *AccountHandler {
return &AccountHandler{
adminService: adminService,
oauthService: oauthService,
openaiOAuthService: openaiOAuthService,
geminiOAuthService: geminiOAuthService,
rateLimitService: rateLimitService,
accountUsageService: accountUsageService,
accountTestService: accountTestService,
concurrencyService: concurrencyService,
crsSyncService: crsSyncService,
adminService: adminService,
oauthService: oauthService,
openaiOAuthService: openaiOAuthService,
geminiOAuthService: geminiOAuthService,
antigravityOAuthService: antigravityOAuthService,
rateLimitService: rateLimitService,
accountUsageService: accountUsageService,
accountTestService: accountTestService,
concurrencyService: concurrencyService,
crsSyncService: crsSyncService,
}
}
@@ -82,6 +85,8 @@ type CreateAccountRequest struct {
Concurrency int `json:"concurrency"`
Priority int `json:"priority"`
GroupIDs []int64 `json:"group_ids"`
ExpiresAt *int64 `json:"expires_at"`
AutoPauseOnExpired *bool `json:"auto_pause_on_expired"`
ConfirmMixedChannelRisk *bool `json:"confirm_mixed_channel_risk"` // 用户确认混合渠道风险
}
@@ -98,6 +103,8 @@ type UpdateAccountRequest struct {
Priority *int `json:"priority"`
Status string `json:"status" binding:"omitempty,oneof=active inactive"`
GroupIDs *[]int64 `json:"group_ids"`
ExpiresAt *int64 `json:"expires_at"`
AutoPauseOnExpired *bool `json:"auto_pause_on_expired"`
ConfirmMixedChannelRisk *bool `json:"confirm_mixed_channel_risk"` // 用户确认混合渠道风险
}
@@ -109,6 +116,7 @@ type BulkUpdateAccountsRequest struct {
Concurrency *int `json:"concurrency"`
Priority *int `json:"priority"`
Status string `json:"status" binding:"omitempty,oneof=active inactive error"`
Schedulable *bool `json:"schedulable"`
GroupIDs *[]int64 `json:"group_ids"`
Credentials map[string]any `json:"credentials"`
Extra map[string]any `json:"extra"`
@@ -129,6 +137,11 @@ func (h *AccountHandler) List(c *gin.Context) {
accountType := c.Query("type")
status := c.Query("status")
search := c.Query("search")
// 标准化和验证 search 参数
search = strings.TrimSpace(search)
if len(search) > 100 {
search = search[:100]
}
accounts, total, err := h.adminService.ListAccounts(c.Request.Context(), page, pageSize, platform, accountType, status, search)
if err != nil {
@@ -201,6 +214,8 @@ func (h *AccountHandler) Create(c *gin.Context) {
Concurrency: req.Concurrency,
Priority: req.Priority,
GroupIDs: req.GroupIDs,
ExpiresAt: req.ExpiresAt,
AutoPauseOnExpired: req.AutoPauseOnExpired,
SkipMixedChannelCheck: skipCheck,
})
if err != nil {
@@ -258,6 +273,8 @@ func (h *AccountHandler) Update(c *gin.Context) {
Priority: req.Priority, // 指针类型nil 表示未提供
Status: req.Status,
GroupIDs: req.GroupIDs,
ExpiresAt: req.ExpiresAt,
AutoPauseOnExpired: req.AutoPauseOnExpired,
SkipMixedChannelCheck: skipCheck,
})
if err != nil {
@@ -420,6 +437,19 @@ func (h *AccountHandler) Refresh(c *gin.Context) {
newCredentials[k] = v
}
}
} else if account.Platform == service.PlatformAntigravity {
tokenInfo, err := h.antigravityOAuthService.RefreshAccountToken(c.Request.Context(), account)
if err != nil {
response.ErrorFrom(c, err)
return
}
newCredentials = h.antigravityOAuthService.BuildAccountCredentials(tokenInfo)
for k, v := range account.Credentials {
if _, exists := newCredentials[k]; !exists {
newCredentials[k] = v
}
}
} else {
// Use Anthropic/Claude OAuth service to refresh token
tokenInfo, err := h.oauthService.RefreshAccountToken(c.Request.Context(), account)
@@ -631,6 +661,7 @@ func (h *AccountHandler) BulkUpdate(c *gin.Context) {
req.Concurrency != nil ||
req.Priority != nil ||
req.Status != "" ||
req.Schedulable != nil ||
req.GroupIDs != nil ||
len(req.Credentials) > 0 ||
len(req.Extra) > 0
@@ -647,6 +678,7 @@ func (h *AccountHandler) BulkUpdate(c *gin.Context) {
Concurrency: req.Concurrency,
Priority: req.Priority,
Status: req.Status,
Schedulable: req.Schedulable,
GroupIDs: req.GroupIDs,
Credentials: req.Credentials,
Extra: req.Extra,

View File

@@ -1,6 +1,7 @@
package admin
import (
"errors"
"strconv"
"time"
@@ -13,44 +14,48 @@ import (
// DashboardHandler handles admin dashboard statistics
type DashboardHandler struct {
dashboardService *service.DashboardService
startTime time.Time // Server start time for uptime calculation
dashboardService *service.DashboardService
aggregationService *service.DashboardAggregationService
startTime time.Time // Server start time for uptime calculation
}
// NewDashboardHandler creates a new admin dashboard handler
func NewDashboardHandler(dashboardService *service.DashboardService) *DashboardHandler {
func NewDashboardHandler(dashboardService *service.DashboardService, aggregationService *service.DashboardAggregationService) *DashboardHandler {
return &DashboardHandler{
dashboardService: dashboardService,
startTime: time.Now(),
dashboardService: dashboardService,
aggregationService: aggregationService,
startTime: time.Now(),
}
}
// parseTimeRange parses start_date, end_date query parameters
// Uses user's timezone if provided, otherwise falls back to server timezone
func parseTimeRange(c *gin.Context) (time.Time, time.Time) {
now := timezone.Now()
userTZ := c.Query("timezone") // Get user's timezone from request
now := timezone.NowInUserLocation(userTZ)
startDate := c.Query("start_date")
endDate := c.Query("end_date")
var startTime, endTime time.Time
if startDate != "" {
if t, err := timezone.ParseInLocation("2006-01-02", startDate); err == nil {
if t, err := timezone.ParseInUserLocation("2006-01-02", startDate, userTZ); err == nil {
startTime = t
} else {
startTime = timezone.StartOfDay(now.AddDate(0, 0, -7))
startTime = timezone.StartOfDayInUserLocation(now.AddDate(0, 0, -7), userTZ)
}
} else {
startTime = timezone.StartOfDay(now.AddDate(0, 0, -7))
startTime = timezone.StartOfDayInUserLocation(now.AddDate(0, 0, -7), userTZ)
}
if endDate != "" {
if t, err := timezone.ParseInLocation("2006-01-02", endDate); err == nil {
if t, err := timezone.ParseInUserLocation("2006-01-02", endDate, userTZ); err == nil {
endTime = t.Add(24 * time.Hour) // Include the end date
} else {
endTime = timezone.StartOfDay(now.AddDate(0, 0, 1))
endTime = timezone.StartOfDayInUserLocation(now.AddDate(0, 0, 1), userTZ)
}
} else {
endTime = timezone.StartOfDay(now.AddDate(0, 0, 1))
endTime = timezone.StartOfDayInUserLocation(now.AddDate(0, 0, 1), userTZ)
}
return startTime, endTime
@@ -112,6 +117,58 @@ func (h *DashboardHandler) GetStats(c *gin.Context) {
// 性能指标
"rpm": stats.Rpm,
"tpm": stats.Tpm,
// 预聚合新鲜度
"hourly_active_users": stats.HourlyActiveUsers,
"stats_updated_at": stats.StatsUpdatedAt,
"stats_stale": stats.StatsStale,
})
}
type DashboardAggregationBackfillRequest struct {
Start string `json:"start"`
End string `json:"end"`
}
// BackfillAggregation handles triggering aggregation backfill
// POST /api/v1/admin/dashboard/aggregation/backfill
func (h *DashboardHandler) BackfillAggregation(c *gin.Context) {
if h.aggregationService == nil {
response.InternalError(c, "Aggregation service not available")
return
}
var req DashboardAggregationBackfillRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "Invalid request body")
return
}
start, err := time.Parse(time.RFC3339, req.Start)
if err != nil {
response.BadRequest(c, "Invalid start time")
return
}
end, err := time.Parse(time.RFC3339, req.End)
if err != nil {
response.BadRequest(c, "Invalid end time")
return
}
if err := h.aggregationService.TriggerBackfill(start, end); err != nil {
if errors.Is(err, service.ErrDashboardBackfillDisabled) {
response.Forbidden(c, "Backfill is disabled")
return
}
if errors.Is(err, service.ErrDashboardBackfillTooLarge) {
response.BadRequest(c, "Backfill range too large")
return
}
response.InternalError(c, "Failed to trigger backfill")
return
}
response.Success(c, gin.H{
"status": "accepted",
})
}

View File

@@ -2,6 +2,7 @@ package admin
import (
"strconv"
"strings"
"github.com/Wei-Shaw/sub2api/internal/handler/dto"
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
@@ -33,6 +34,12 @@ type CreateGroupRequest struct {
DailyLimitUSD *float64 `json:"daily_limit_usd"`
WeeklyLimitUSD *float64 `json:"weekly_limit_usd"`
MonthlyLimitUSD *float64 `json:"monthly_limit_usd"`
// 图片生成计费配置antigravity 和 gemini 平台使用,负数表示清除配置)
ImagePrice1K *float64 `json:"image_price_1k"`
ImagePrice2K *float64 `json:"image_price_2k"`
ImagePrice4K *float64 `json:"image_price_4k"`
ClaudeCodeOnly bool `json:"claude_code_only"`
FallbackGroupID *int64 `json:"fallback_group_id"`
}
// UpdateGroupRequest represents update group request
@@ -47,6 +54,12 @@ type UpdateGroupRequest struct {
DailyLimitUSD *float64 `json:"daily_limit_usd"`
WeeklyLimitUSD *float64 `json:"weekly_limit_usd"`
MonthlyLimitUSD *float64 `json:"monthly_limit_usd"`
// 图片生成计费配置antigravity 和 gemini 平台使用,负数表示清除配置)
ImagePrice1K *float64 `json:"image_price_1k"`
ImagePrice2K *float64 `json:"image_price_2k"`
ImagePrice4K *float64 `json:"image_price_4k"`
ClaudeCodeOnly *bool `json:"claude_code_only"`
FallbackGroupID *int64 `json:"fallback_group_id"`
}
// List handles listing all groups with pagination
@@ -55,6 +68,12 @@ func (h *GroupHandler) List(c *gin.Context) {
page, pageSize := response.ParsePagination(c)
platform := c.Query("platform")
status := c.Query("status")
search := c.Query("search")
// 标准化和验证 search 参数
search = strings.TrimSpace(search)
if len(search) > 100 {
search = search[:100]
}
isExclusiveStr := c.Query("is_exclusive")
var isExclusive *bool
@@ -63,7 +82,7 @@ func (h *GroupHandler) List(c *gin.Context) {
isExclusive = &val
}
groups, total, err := h.adminService.ListGroups(c.Request.Context(), page, pageSize, platform, status, isExclusive)
groups, total, err := h.adminService.ListGroups(c.Request.Context(), page, pageSize, platform, status, search, isExclusive)
if err != nil {
response.ErrorFrom(c, err)
return
@@ -139,6 +158,11 @@ func (h *GroupHandler) Create(c *gin.Context) {
DailyLimitUSD: req.DailyLimitUSD,
WeeklyLimitUSD: req.WeeklyLimitUSD,
MonthlyLimitUSD: req.MonthlyLimitUSD,
ImagePrice1K: req.ImagePrice1K,
ImagePrice2K: req.ImagePrice2K,
ImagePrice4K: req.ImagePrice4K,
ClaudeCodeOnly: req.ClaudeCodeOnly,
FallbackGroupID: req.FallbackGroupID,
})
if err != nil {
response.ErrorFrom(c, err)
@@ -174,6 +198,11 @@ func (h *GroupHandler) Update(c *gin.Context) {
DailyLimitUSD: req.DailyLimitUSD,
WeeklyLimitUSD: req.WeeklyLimitUSD,
MonthlyLimitUSD: req.MonthlyLimitUSD,
ImagePrice1K: req.ImagePrice1K,
ImagePrice2K: req.ImagePrice2K,
ImagePrice4K: req.ImagePrice4K,
ClaudeCodeOnly: req.ClaudeCodeOnly,
FallbackGroupID: req.FallbackGroupID,
})
if err != nil {
response.ErrorFrom(c, err)

View File

@@ -0,0 +1,432 @@
package admin
import (
"encoding/json"
"fmt"
"math"
"net/http"
"strconv"
"strings"
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
)
var validOpsAlertMetricTypes = []string{
"success_rate",
"error_rate",
"upstream_error_rate",
"p95_latency_ms",
"p99_latency_ms",
"cpu_usage_percent",
"memory_usage_percent",
"concurrency_queue_depth",
}
var validOpsAlertMetricTypeSet = func() map[string]struct{} {
set := make(map[string]struct{}, len(validOpsAlertMetricTypes))
for _, v := range validOpsAlertMetricTypes {
set[v] = struct{}{}
}
return set
}()
var validOpsAlertOperators = []string{">", "<", ">=", "<=", "==", "!="}
var validOpsAlertOperatorSet = func() map[string]struct{} {
set := make(map[string]struct{}, len(validOpsAlertOperators))
for _, v := range validOpsAlertOperators {
set[v] = struct{}{}
}
return set
}()
var validOpsAlertSeverities = []string{"P0", "P1", "P2", "P3"}
var validOpsAlertSeveritySet = func() map[string]struct{} {
set := make(map[string]struct{}, len(validOpsAlertSeverities))
for _, v := range validOpsAlertSeverities {
set[v] = struct{}{}
}
return set
}()
type opsAlertRuleValidatedInput struct {
Name string
MetricType string
Operator string
Threshold float64
Severity string
WindowMinutes int
SustainedMinutes int
CooldownMinutes int
Enabled bool
NotifyEmail bool
WindowProvided bool
SustainedProvided bool
CooldownProvided bool
SeverityProvided bool
EnabledProvided bool
NotifyProvided bool
}
func isPercentOrRateMetric(metricType string) bool {
switch metricType {
case "success_rate",
"error_rate",
"upstream_error_rate",
"cpu_usage_percent",
"memory_usage_percent":
return true
default:
return false
}
}
func validateOpsAlertRulePayload(raw map[string]json.RawMessage) (*opsAlertRuleValidatedInput, error) {
if raw == nil {
return nil, fmt.Errorf("invalid request body")
}
requiredFields := []string{"name", "metric_type", "operator", "threshold"}
for _, field := range requiredFields {
if _, ok := raw[field]; !ok {
return nil, fmt.Errorf("%s is required", field)
}
}
var name string
if err := json.Unmarshal(raw["name"], &name); err != nil || strings.TrimSpace(name) == "" {
return nil, fmt.Errorf("name is required")
}
name = strings.TrimSpace(name)
var metricType string
if err := json.Unmarshal(raw["metric_type"], &metricType); err != nil || strings.TrimSpace(metricType) == "" {
return nil, fmt.Errorf("metric_type is required")
}
metricType = strings.TrimSpace(metricType)
if _, ok := validOpsAlertMetricTypeSet[metricType]; !ok {
return nil, fmt.Errorf("metric_type must be one of: %s", strings.Join(validOpsAlertMetricTypes, ", "))
}
var operator string
if err := json.Unmarshal(raw["operator"], &operator); err != nil || strings.TrimSpace(operator) == "" {
return nil, fmt.Errorf("operator is required")
}
operator = strings.TrimSpace(operator)
if _, ok := validOpsAlertOperatorSet[operator]; !ok {
return nil, fmt.Errorf("operator must be one of: %s", strings.Join(validOpsAlertOperators, ", "))
}
var threshold float64
if err := json.Unmarshal(raw["threshold"], &threshold); err != nil {
return nil, fmt.Errorf("threshold must be a number")
}
if math.IsNaN(threshold) || math.IsInf(threshold, 0) {
return nil, fmt.Errorf("threshold must be a finite number")
}
if isPercentOrRateMetric(metricType) {
if threshold < 0 || threshold > 100 {
return nil, fmt.Errorf("threshold must be between 0 and 100 for metric_type %s", metricType)
}
} else if threshold < 0 {
return nil, fmt.Errorf("threshold must be >= 0")
}
validated := &opsAlertRuleValidatedInput{
Name: name,
MetricType: metricType,
Operator: operator,
Threshold: threshold,
}
if v, ok := raw["severity"]; ok {
validated.SeverityProvided = true
var sev string
if err := json.Unmarshal(v, &sev); err != nil {
return nil, fmt.Errorf("severity must be a string")
}
sev = strings.ToUpper(strings.TrimSpace(sev))
if sev != "" {
if _, ok := validOpsAlertSeveritySet[sev]; !ok {
return nil, fmt.Errorf("severity must be one of: %s", strings.Join(validOpsAlertSeverities, ", "))
}
validated.Severity = sev
}
}
if validated.Severity == "" {
validated.Severity = "P2"
}
if v, ok := raw["enabled"]; ok {
validated.EnabledProvided = true
if err := json.Unmarshal(v, &validated.Enabled); err != nil {
return nil, fmt.Errorf("enabled must be a boolean")
}
} else {
validated.Enabled = true
}
if v, ok := raw["notify_email"]; ok {
validated.NotifyProvided = true
if err := json.Unmarshal(v, &validated.NotifyEmail); err != nil {
return nil, fmt.Errorf("notify_email must be a boolean")
}
} else {
validated.NotifyEmail = true
}
if v, ok := raw["window_minutes"]; ok {
validated.WindowProvided = true
if err := json.Unmarshal(v, &validated.WindowMinutes); err != nil {
return nil, fmt.Errorf("window_minutes must be an integer")
}
switch validated.WindowMinutes {
case 1, 5, 60:
default:
return nil, fmt.Errorf("window_minutes must be one of: 1, 5, 60")
}
} else {
validated.WindowMinutes = 1
}
if v, ok := raw["sustained_minutes"]; ok {
validated.SustainedProvided = true
if err := json.Unmarshal(v, &validated.SustainedMinutes); err != nil {
return nil, fmt.Errorf("sustained_minutes must be an integer")
}
if validated.SustainedMinutes < 1 || validated.SustainedMinutes > 1440 {
return nil, fmt.Errorf("sustained_minutes must be between 1 and 1440")
}
} else {
validated.SustainedMinutes = 1
}
if v, ok := raw["cooldown_minutes"]; ok {
validated.CooldownProvided = true
if err := json.Unmarshal(v, &validated.CooldownMinutes); err != nil {
return nil, fmt.Errorf("cooldown_minutes must be an integer")
}
if validated.CooldownMinutes < 0 || validated.CooldownMinutes > 1440 {
return nil, fmt.Errorf("cooldown_minutes must be between 0 and 1440")
}
} else {
validated.CooldownMinutes = 0
}
return validated, nil
}
// ListAlertRules returns all ops alert rules.
// GET /api/v1/admin/ops/alert-rules
func (h *OpsHandler) ListAlertRules(c *gin.Context) {
if h.opsService == nil {
response.Error(c, http.StatusServiceUnavailable, "Ops service not available")
return
}
if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil {
response.ErrorFrom(c, err)
return
}
rules, err := h.opsService.ListAlertRules(c.Request.Context())
if err != nil {
response.ErrorFrom(c, err)
return
}
response.Success(c, rules)
}
// CreateAlertRule creates an ops alert rule.
// POST /api/v1/admin/ops/alert-rules
func (h *OpsHandler) CreateAlertRule(c *gin.Context) {
if h.opsService == nil {
response.Error(c, http.StatusServiceUnavailable, "Ops service not available")
return
}
if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil {
response.ErrorFrom(c, err)
return
}
var raw map[string]json.RawMessage
if err := c.ShouldBindBodyWith(&raw, binding.JSON); err != nil {
response.BadRequest(c, "Invalid request body")
return
}
validated, err := validateOpsAlertRulePayload(raw)
if err != nil {
response.BadRequest(c, err.Error())
return
}
var rule service.OpsAlertRule
if err := c.ShouldBindBodyWith(&rule, binding.JSON); err != nil {
response.BadRequest(c, "Invalid request body")
return
}
rule.Name = validated.Name
rule.MetricType = validated.MetricType
rule.Operator = validated.Operator
rule.Threshold = validated.Threshold
rule.WindowMinutes = validated.WindowMinutes
rule.SustainedMinutes = validated.SustainedMinutes
rule.CooldownMinutes = validated.CooldownMinutes
rule.Severity = validated.Severity
rule.Enabled = validated.Enabled
rule.NotifyEmail = validated.NotifyEmail
created, err := h.opsService.CreateAlertRule(c.Request.Context(), &rule)
if err != nil {
response.ErrorFrom(c, err)
return
}
response.Success(c, created)
}
// UpdateAlertRule updates an existing ops alert rule.
// PUT /api/v1/admin/ops/alert-rules/:id
func (h *OpsHandler) UpdateAlertRule(c *gin.Context) {
if h.opsService == nil {
response.Error(c, http.StatusServiceUnavailable, "Ops service not available")
return
}
if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil {
response.ErrorFrom(c, err)
return
}
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil || id <= 0 {
response.BadRequest(c, "Invalid rule ID")
return
}
var raw map[string]json.RawMessage
if err := c.ShouldBindBodyWith(&raw, binding.JSON); err != nil {
response.BadRequest(c, "Invalid request body")
return
}
validated, err := validateOpsAlertRulePayload(raw)
if err != nil {
response.BadRequest(c, err.Error())
return
}
var rule service.OpsAlertRule
if err := c.ShouldBindBodyWith(&rule, binding.JSON); err != nil {
response.BadRequest(c, "Invalid request body")
return
}
rule.ID = id
rule.Name = validated.Name
rule.MetricType = validated.MetricType
rule.Operator = validated.Operator
rule.Threshold = validated.Threshold
rule.WindowMinutes = validated.WindowMinutes
rule.SustainedMinutes = validated.SustainedMinutes
rule.CooldownMinutes = validated.CooldownMinutes
rule.Severity = validated.Severity
rule.Enabled = validated.Enabled
rule.NotifyEmail = validated.NotifyEmail
updated, err := h.opsService.UpdateAlertRule(c.Request.Context(), &rule)
if err != nil {
response.ErrorFrom(c, err)
return
}
response.Success(c, updated)
}
// DeleteAlertRule deletes an ops alert rule.
// DELETE /api/v1/admin/ops/alert-rules/:id
func (h *OpsHandler) DeleteAlertRule(c *gin.Context) {
if h.opsService == nil {
response.Error(c, http.StatusServiceUnavailable, "Ops service not available")
return
}
if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil {
response.ErrorFrom(c, err)
return
}
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil || id <= 0 {
response.BadRequest(c, "Invalid rule ID")
return
}
if err := h.opsService.DeleteAlertRule(c.Request.Context(), id); err != nil {
response.ErrorFrom(c, err)
return
}
response.Success(c, gin.H{"deleted": true})
}
// ListAlertEvents lists recent ops alert events.
// GET /api/v1/admin/ops/alert-events
func (h *OpsHandler) ListAlertEvents(c *gin.Context) {
if h.opsService == nil {
response.Error(c, http.StatusServiceUnavailable, "Ops service not available")
return
}
if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil {
response.ErrorFrom(c, err)
return
}
limit := 100
if raw := strings.TrimSpace(c.Query("limit")); raw != "" {
n, err := strconv.Atoi(raw)
if err != nil || n <= 0 {
response.BadRequest(c, "Invalid limit")
return
}
limit = n
}
filter := &service.OpsAlertEventFilter{
Limit: limit,
Status: strings.TrimSpace(c.Query("status")),
Severity: strings.TrimSpace(c.Query("severity")),
}
// Optional global filter support (platform/group/time range).
if platform := strings.TrimSpace(c.Query("platform")); platform != "" {
filter.Platform = platform
}
if v := strings.TrimSpace(c.Query("group_id")); v != "" {
id, err := strconv.ParseInt(v, 10, 64)
if err != nil || id <= 0 {
response.BadRequest(c, "Invalid group_id")
return
}
filter.GroupID = &id
}
if startTime, endTime, err := parseOpsTimeRange(c, "24h"); err == nil {
// Only apply when explicitly provided to avoid surprising default narrowing.
if strings.TrimSpace(c.Query("start_time")) != "" || strings.TrimSpace(c.Query("end_time")) != "" || strings.TrimSpace(c.Query("time_range")) != "" {
filter.StartTime = &startTime
filter.EndTime = &endTime
}
} else {
response.BadRequest(c, err.Error())
return
}
events, err := h.opsService.ListAlertEvents(c.Request.Context(), filter)
if err != nil {
response.ErrorFrom(c, err)
return
}
response.Success(c, events)
}

View File

@@ -0,0 +1,243 @@
package admin
import (
"net/http"
"strconv"
"strings"
"time"
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/gin-gonic/gin"
)
// GetDashboardOverview returns vNext ops dashboard overview (raw path).
// GET /api/v1/admin/ops/dashboard/overview
func (h *OpsHandler) GetDashboardOverview(c *gin.Context) {
if h.opsService == nil {
response.Error(c, http.StatusServiceUnavailable, "Ops service not available")
return
}
if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil {
response.ErrorFrom(c, err)
return
}
startTime, endTime, err := parseOpsTimeRange(c, "1h")
if err != nil {
response.BadRequest(c, err.Error())
return
}
filter := &service.OpsDashboardFilter{
StartTime: startTime,
EndTime: endTime,
Platform: strings.TrimSpace(c.Query("platform")),
QueryMode: parseOpsQueryMode(c),
}
if v := strings.TrimSpace(c.Query("group_id")); v != "" {
id, err := strconv.ParseInt(v, 10, 64)
if err != nil || id <= 0 {
response.BadRequest(c, "Invalid group_id")
return
}
filter.GroupID = &id
}
data, err := h.opsService.GetDashboardOverview(c.Request.Context(), filter)
if err != nil {
response.ErrorFrom(c, err)
return
}
response.Success(c, data)
}
// GetDashboardThroughputTrend returns throughput time series (raw path).
// GET /api/v1/admin/ops/dashboard/throughput-trend
func (h *OpsHandler) GetDashboardThroughputTrend(c *gin.Context) {
if h.opsService == nil {
response.Error(c, http.StatusServiceUnavailable, "Ops service not available")
return
}
if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil {
response.ErrorFrom(c, err)
return
}
startTime, endTime, err := parseOpsTimeRange(c, "1h")
if err != nil {
response.BadRequest(c, err.Error())
return
}
filter := &service.OpsDashboardFilter{
StartTime: startTime,
EndTime: endTime,
Platform: strings.TrimSpace(c.Query("platform")),
QueryMode: parseOpsQueryMode(c),
}
if v := strings.TrimSpace(c.Query("group_id")); v != "" {
id, err := strconv.ParseInt(v, 10, 64)
if err != nil || id <= 0 {
response.BadRequest(c, "Invalid group_id")
return
}
filter.GroupID = &id
}
bucketSeconds := pickThroughputBucketSeconds(endTime.Sub(startTime))
data, err := h.opsService.GetThroughputTrend(c.Request.Context(), filter, bucketSeconds)
if err != nil {
response.ErrorFrom(c, err)
return
}
response.Success(c, data)
}
// GetDashboardLatencyHistogram returns the latency distribution histogram (success requests).
// GET /api/v1/admin/ops/dashboard/latency-histogram
func (h *OpsHandler) GetDashboardLatencyHistogram(c *gin.Context) {
if h.opsService == nil {
response.Error(c, http.StatusServiceUnavailable, "Ops service not available")
return
}
if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil {
response.ErrorFrom(c, err)
return
}
startTime, endTime, err := parseOpsTimeRange(c, "1h")
if err != nil {
response.BadRequest(c, err.Error())
return
}
filter := &service.OpsDashboardFilter{
StartTime: startTime,
EndTime: endTime,
Platform: strings.TrimSpace(c.Query("platform")),
QueryMode: parseOpsQueryMode(c),
}
if v := strings.TrimSpace(c.Query("group_id")); v != "" {
id, err := strconv.ParseInt(v, 10, 64)
if err != nil || id <= 0 {
response.BadRequest(c, "Invalid group_id")
return
}
filter.GroupID = &id
}
data, err := h.opsService.GetLatencyHistogram(c.Request.Context(), filter)
if err != nil {
response.ErrorFrom(c, err)
return
}
response.Success(c, data)
}
// GetDashboardErrorTrend returns error counts time series (raw path).
// GET /api/v1/admin/ops/dashboard/error-trend
func (h *OpsHandler) GetDashboardErrorTrend(c *gin.Context) {
if h.opsService == nil {
response.Error(c, http.StatusServiceUnavailable, "Ops service not available")
return
}
if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil {
response.ErrorFrom(c, err)
return
}
startTime, endTime, err := parseOpsTimeRange(c, "1h")
if err != nil {
response.BadRequest(c, err.Error())
return
}
filter := &service.OpsDashboardFilter{
StartTime: startTime,
EndTime: endTime,
Platform: strings.TrimSpace(c.Query("platform")),
QueryMode: parseOpsQueryMode(c),
}
if v := strings.TrimSpace(c.Query("group_id")); v != "" {
id, err := strconv.ParseInt(v, 10, 64)
if err != nil || id <= 0 {
response.BadRequest(c, "Invalid group_id")
return
}
filter.GroupID = &id
}
bucketSeconds := pickThroughputBucketSeconds(endTime.Sub(startTime))
data, err := h.opsService.GetErrorTrend(c.Request.Context(), filter, bucketSeconds)
if err != nil {
response.ErrorFrom(c, err)
return
}
response.Success(c, data)
}
// GetDashboardErrorDistribution returns error distribution by status code (raw path).
// GET /api/v1/admin/ops/dashboard/error-distribution
func (h *OpsHandler) GetDashboardErrorDistribution(c *gin.Context) {
if h.opsService == nil {
response.Error(c, http.StatusServiceUnavailable, "Ops service not available")
return
}
if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil {
response.ErrorFrom(c, err)
return
}
startTime, endTime, err := parseOpsTimeRange(c, "1h")
if err != nil {
response.BadRequest(c, err.Error())
return
}
filter := &service.OpsDashboardFilter{
StartTime: startTime,
EndTime: endTime,
Platform: strings.TrimSpace(c.Query("platform")),
QueryMode: parseOpsQueryMode(c),
}
if v := strings.TrimSpace(c.Query("group_id")); v != "" {
id, err := strconv.ParseInt(v, 10, 64)
if err != nil || id <= 0 {
response.BadRequest(c, "Invalid group_id")
return
}
filter.GroupID = &id
}
data, err := h.opsService.GetErrorDistribution(c.Request.Context(), filter)
if err != nil {
response.ErrorFrom(c, err)
return
}
response.Success(c, data)
}
func pickThroughputBucketSeconds(window time.Duration) int {
// Keep buckets predictable and avoid huge responses.
switch {
case window <= 2*time.Hour:
return 60
case window <= 24*time.Hour:
return 300
default:
return 3600
}
}
func parseOpsQueryMode(c *gin.Context) service.OpsQueryMode {
if c == nil {
return ""
}
raw := strings.TrimSpace(c.Query("mode"))
if raw == "" {
// Empty means "use server default" (DB setting ops_query_mode_default).
return ""
}
return service.ParseOpsQueryMode(raw)
}

View File

@@ -0,0 +1,364 @@
package admin
import (
"errors"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"time"
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
"github.com/Wei-Shaw/sub2api/internal/server/middleware"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/gin-gonic/gin"
)
type OpsHandler struct {
opsService *service.OpsService
}
func NewOpsHandler(opsService *service.OpsService) *OpsHandler {
return &OpsHandler{opsService: opsService}
}
// GetErrorLogs lists ops error logs.
// GET /api/v1/admin/ops/errors
func (h *OpsHandler) GetErrorLogs(c *gin.Context) {
if h.opsService == nil {
response.Error(c, http.StatusServiceUnavailable, "Ops service not available")
return
}
if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil {
response.ErrorFrom(c, err)
return
}
page, pageSize := response.ParsePagination(c)
// Ops list can be larger than standard admin tables.
if pageSize > 500 {
pageSize = 500
}
startTime, endTime, err := parseOpsTimeRange(c, "1h")
if err != nil {
response.BadRequest(c, err.Error())
return
}
filter := &service.OpsErrorLogFilter{
Page: page,
PageSize: pageSize,
}
if !startTime.IsZero() {
filter.StartTime = &startTime
}
if !endTime.IsZero() {
filter.EndTime = &endTime
}
if platform := strings.TrimSpace(c.Query("platform")); platform != "" {
filter.Platform = platform
}
if v := strings.TrimSpace(c.Query("group_id")); v != "" {
id, err := strconv.ParseInt(v, 10, 64)
if err != nil || id <= 0 {
response.BadRequest(c, "Invalid group_id")
return
}
filter.GroupID = &id
}
if v := strings.TrimSpace(c.Query("account_id")); v != "" {
id, err := strconv.ParseInt(v, 10, 64)
if err != nil || id <= 0 {
response.BadRequest(c, "Invalid account_id")
return
}
filter.AccountID = &id
}
if phase := strings.TrimSpace(c.Query("phase")); phase != "" {
filter.Phase = phase
}
if q := strings.TrimSpace(c.Query("q")); q != "" {
filter.Query = q
}
if statusCodesStr := strings.TrimSpace(c.Query("status_codes")); statusCodesStr != "" {
parts := strings.Split(statusCodesStr, ",")
out := make([]int, 0, len(parts))
for _, part := range parts {
p := strings.TrimSpace(part)
if p == "" {
continue
}
n, err := strconv.Atoi(p)
if err != nil || n < 0 {
response.BadRequest(c, "Invalid status_codes")
return
}
out = append(out, n)
}
filter.StatusCodes = out
}
result, err := h.opsService.GetErrorLogs(c.Request.Context(), filter)
if err != nil {
response.ErrorFrom(c, err)
return
}
response.Paginated(c, result.Errors, int64(result.Total), result.Page, result.PageSize)
}
// GetErrorLogByID returns a single error log detail.
// GET /api/v1/admin/ops/errors/:id
func (h *OpsHandler) GetErrorLogByID(c *gin.Context) {
if h.opsService == nil {
response.Error(c, http.StatusServiceUnavailable, "Ops service not available")
return
}
if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil {
response.ErrorFrom(c, err)
return
}
idStr := strings.TrimSpace(c.Param("id"))
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil || id <= 0 {
response.BadRequest(c, "Invalid error id")
return
}
detail, err := h.opsService.GetErrorLogByID(c.Request.Context(), id)
if err != nil {
response.ErrorFrom(c, err)
return
}
response.Success(c, detail)
}
// ListRequestDetails returns a request-level list (success + error) for drill-down.
// GET /api/v1/admin/ops/requests
func (h *OpsHandler) ListRequestDetails(c *gin.Context) {
if h.opsService == nil {
response.Error(c, http.StatusServiceUnavailable, "Ops service not available")
return
}
if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil {
response.ErrorFrom(c, err)
return
}
page, pageSize := response.ParsePagination(c)
if pageSize > 100 {
pageSize = 100
}
startTime, endTime, err := parseOpsTimeRange(c, "1h")
if err != nil {
response.BadRequest(c, err.Error())
return
}
filter := &service.OpsRequestDetailFilter{
Page: page,
PageSize: pageSize,
StartTime: &startTime,
EndTime: &endTime,
}
filter.Kind = strings.TrimSpace(c.Query("kind"))
filter.Platform = strings.TrimSpace(c.Query("platform"))
filter.Model = strings.TrimSpace(c.Query("model"))
filter.RequestID = strings.TrimSpace(c.Query("request_id"))
filter.Query = strings.TrimSpace(c.Query("q"))
filter.Sort = strings.TrimSpace(c.Query("sort"))
if v := strings.TrimSpace(c.Query("user_id")); v != "" {
id, err := strconv.ParseInt(v, 10, 64)
if err != nil || id <= 0 {
response.BadRequest(c, "Invalid user_id")
return
}
filter.UserID = &id
}
if v := strings.TrimSpace(c.Query("api_key_id")); v != "" {
id, err := strconv.ParseInt(v, 10, 64)
if err != nil || id <= 0 {
response.BadRequest(c, "Invalid api_key_id")
return
}
filter.APIKeyID = &id
}
if v := strings.TrimSpace(c.Query("account_id")); v != "" {
id, err := strconv.ParseInt(v, 10, 64)
if err != nil || id <= 0 {
response.BadRequest(c, "Invalid account_id")
return
}
filter.AccountID = &id
}
if v := strings.TrimSpace(c.Query("group_id")); v != "" {
id, err := strconv.ParseInt(v, 10, 64)
if err != nil || id <= 0 {
response.BadRequest(c, "Invalid group_id")
return
}
filter.GroupID = &id
}
if v := strings.TrimSpace(c.Query("min_duration_ms")); v != "" {
parsed, err := strconv.Atoi(v)
if err != nil || parsed < 0 {
response.BadRequest(c, "Invalid min_duration_ms")
return
}
filter.MinDurationMs = &parsed
}
if v := strings.TrimSpace(c.Query("max_duration_ms")); v != "" {
parsed, err := strconv.Atoi(v)
if err != nil || parsed < 0 {
response.BadRequest(c, "Invalid max_duration_ms")
return
}
filter.MaxDurationMs = &parsed
}
out, err := h.opsService.ListRequestDetails(c.Request.Context(), filter)
if err != nil {
// Invalid sort/kind/platform etc should be a bad request; keep it simple.
if strings.Contains(strings.ToLower(err.Error()), "invalid") {
response.BadRequest(c, err.Error())
return
}
response.Error(c, http.StatusInternalServerError, "Failed to list request details")
return
}
response.Paginated(c, out.Items, out.Total, out.Page, out.PageSize)
}
type opsRetryRequest struct {
Mode string `json:"mode"`
PinnedAccountID *int64 `json:"pinned_account_id"`
}
// RetryErrorRequest retries a failed request using stored request_body.
// POST /api/v1/admin/ops/errors/:id/retry
func (h *OpsHandler) RetryErrorRequest(c *gin.Context) {
if h.opsService == nil {
response.Error(c, http.StatusServiceUnavailable, "Ops service not available")
return
}
if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil {
response.ErrorFrom(c, err)
return
}
subject, ok := middleware.GetAuthSubjectFromContext(c)
if !ok || subject.UserID <= 0 {
response.Error(c, http.StatusUnauthorized, "Unauthorized")
return
}
idStr := strings.TrimSpace(c.Param("id"))
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil || id <= 0 {
response.BadRequest(c, "Invalid error id")
return
}
req := opsRetryRequest{Mode: service.OpsRetryModeClient}
if err := c.ShouldBindJSON(&req); err != nil && !errors.Is(err, io.EOF) {
response.BadRequest(c, "Invalid request: "+err.Error())
return
}
if strings.TrimSpace(req.Mode) == "" {
req.Mode = service.OpsRetryModeClient
}
result, err := h.opsService.RetryError(c.Request.Context(), subject.UserID, id, req.Mode, req.PinnedAccountID)
if err != nil {
response.ErrorFrom(c, err)
return
}
response.Success(c, result)
}
func parseOpsTimeRange(c *gin.Context, defaultRange string) (time.Time, time.Time, error) {
startStr := strings.TrimSpace(c.Query("start_time"))
endStr := strings.TrimSpace(c.Query("end_time"))
parseTS := func(s string) (time.Time, error) {
if s == "" {
return time.Time{}, nil
}
if t, err := time.Parse(time.RFC3339Nano, s); err == nil {
return t, nil
}
return time.Parse(time.RFC3339, s)
}
start, err := parseTS(startStr)
if err != nil {
return time.Time{}, time.Time{}, err
}
end, err := parseTS(endStr)
if err != nil {
return time.Time{}, time.Time{}, err
}
// start/end explicitly provided (even partially)
if startStr != "" || endStr != "" {
if end.IsZero() {
end = time.Now()
}
if start.IsZero() {
dur, _ := parseOpsDuration(defaultRange)
start = end.Add(-dur)
}
if start.After(end) {
return time.Time{}, time.Time{}, fmt.Errorf("invalid time range: start_time must be <= end_time")
}
if end.Sub(start) > 30*24*time.Hour {
return time.Time{}, time.Time{}, fmt.Errorf("invalid time range: max window is 30 days")
}
return start, end, nil
}
// time_range fallback
tr := strings.TrimSpace(c.Query("time_range"))
if tr == "" {
tr = defaultRange
}
dur, ok := parseOpsDuration(tr)
if !ok {
dur, _ = parseOpsDuration(defaultRange)
}
end = time.Now()
start = end.Add(-dur)
if end.Sub(start) > 30*24*time.Hour {
return time.Time{}, time.Time{}, fmt.Errorf("invalid time range: max window is 30 days")
}
return start, end, nil
}
func parseOpsDuration(v string) (time.Duration, bool) {
switch strings.TrimSpace(v) {
case "5m":
return 5 * time.Minute, true
case "30m":
return 30 * time.Minute, true
case "1h":
return time.Hour, true
case "6h":
return 6 * time.Hour, true
case "24h":
return 24 * time.Hour, true
default:
return 0, false
}
}

View File

@@ -0,0 +1,213 @@
package admin
import (
"net/http"
"strconv"
"strings"
"time"
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/gin-gonic/gin"
)
// GetConcurrencyStats returns real-time concurrency usage aggregated by platform/group/account.
// GET /api/v1/admin/ops/concurrency
func (h *OpsHandler) GetConcurrencyStats(c *gin.Context) {
if h.opsService == nil {
response.Error(c, http.StatusServiceUnavailable, "Ops service not available")
return
}
if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil {
response.ErrorFrom(c, err)
return
}
if !h.opsService.IsRealtimeMonitoringEnabled(c.Request.Context()) {
response.Success(c, gin.H{
"enabled": false,
"platform": map[string]*service.PlatformConcurrencyInfo{},
"group": map[int64]*service.GroupConcurrencyInfo{},
"account": map[int64]*service.AccountConcurrencyInfo{},
"timestamp": time.Now().UTC(),
})
return
}
platformFilter := strings.TrimSpace(c.Query("platform"))
var groupID *int64
if v := strings.TrimSpace(c.Query("group_id")); v != "" {
id, err := strconv.ParseInt(v, 10, 64)
if err != nil || id <= 0 {
response.BadRequest(c, "Invalid group_id")
return
}
groupID = &id
}
platform, group, account, collectedAt, err := h.opsService.GetConcurrencyStats(c.Request.Context(), platformFilter, groupID)
if err != nil {
response.ErrorFrom(c, err)
return
}
payload := gin.H{
"enabled": true,
"platform": platform,
"group": group,
"account": account,
}
if collectedAt != nil {
payload["timestamp"] = collectedAt.UTC()
}
response.Success(c, payload)
}
// GetAccountAvailability returns account availability statistics.
// GET /api/v1/admin/ops/account-availability
//
// Query params:
// - platform: optional
// - group_id: optional
func (h *OpsHandler) GetAccountAvailability(c *gin.Context) {
if h.opsService == nil {
response.Error(c, http.StatusServiceUnavailable, "Ops service not available")
return
}
if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil {
response.ErrorFrom(c, err)
return
}
if !h.opsService.IsRealtimeMonitoringEnabled(c.Request.Context()) {
response.Success(c, gin.H{
"enabled": false,
"platform": map[string]*service.PlatformAvailability{},
"group": map[int64]*service.GroupAvailability{},
"account": map[int64]*service.AccountAvailability{},
"timestamp": time.Now().UTC(),
})
return
}
platform := strings.TrimSpace(c.Query("platform"))
var groupID *int64
if v := strings.TrimSpace(c.Query("group_id")); v != "" {
id, err := strconv.ParseInt(v, 10, 64)
if err != nil || id <= 0 {
response.BadRequest(c, "Invalid group_id")
return
}
groupID = &id
}
platformStats, groupStats, accountStats, collectedAt, err := h.opsService.GetAccountAvailabilityStats(c.Request.Context(), platform, groupID)
if err != nil {
response.ErrorFrom(c, err)
return
}
payload := gin.H{
"enabled": true,
"platform": platformStats,
"group": groupStats,
"account": accountStats,
}
if collectedAt != nil {
payload["timestamp"] = collectedAt.UTC()
}
response.Success(c, payload)
}
func parseOpsRealtimeWindow(v string) (time.Duration, string, bool) {
switch strings.ToLower(strings.TrimSpace(v)) {
case "", "1min", "1m":
return 1 * time.Minute, "1min", true
case "5min", "5m":
return 5 * time.Minute, "5min", true
case "30min", "30m":
return 30 * time.Minute, "30min", true
case "1h", "60m", "60min":
return 1 * time.Hour, "1h", true
default:
return 0, "", false
}
}
// GetRealtimeTrafficSummary returns QPS/TPS current/peak/avg for the selected window.
// GET /api/v1/admin/ops/realtime-traffic
//
// Query params:
// - window: 1min|5min|30min|1h (default: 1min)
// - platform: optional
// - group_id: optional
func (h *OpsHandler) GetRealtimeTrafficSummary(c *gin.Context) {
if h.opsService == nil {
response.Error(c, http.StatusServiceUnavailable, "Ops service not available")
return
}
if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil {
response.ErrorFrom(c, err)
return
}
windowDur, windowLabel, ok := parseOpsRealtimeWindow(c.Query("window"))
if !ok {
response.BadRequest(c, "Invalid window")
return
}
platform := strings.TrimSpace(c.Query("platform"))
var groupID *int64
if v := strings.TrimSpace(c.Query("group_id")); v != "" {
id, err := strconv.ParseInt(v, 10, 64)
if err != nil || id <= 0 {
response.BadRequest(c, "Invalid group_id")
return
}
groupID = &id
}
endTime := time.Now().UTC()
startTime := endTime.Add(-windowDur)
if !h.opsService.IsRealtimeMonitoringEnabled(c.Request.Context()) {
disabledSummary := &service.OpsRealtimeTrafficSummary{
Window: windowLabel,
StartTime: startTime,
EndTime: endTime,
Platform: platform,
GroupID: groupID,
QPS: service.OpsRateSummary{},
TPS: service.OpsRateSummary{},
}
response.Success(c, gin.H{
"enabled": false,
"summary": disabledSummary,
"timestamp": endTime,
})
return
}
filter := &service.OpsDashboardFilter{
StartTime: startTime,
EndTime: endTime,
Platform: platform,
GroupID: groupID,
QueryMode: service.OpsQueryModeRaw,
}
summary, err := h.opsService.GetRealtimeTrafficSummary(c.Request.Context(), filter)
if err != nil {
response.ErrorFrom(c, err)
return
}
if summary != nil {
summary.Window = windowLabel
}
response.Success(c, gin.H{
"enabled": true,
"summary": summary,
"timestamp": endTime,
})
}

View File

@@ -0,0 +1,194 @@
package admin
import (
"net/http"
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/gin-gonic/gin"
)
// GetEmailNotificationConfig returns Ops email notification config (DB-backed).
// GET /api/v1/admin/ops/email-notification/config
func (h *OpsHandler) GetEmailNotificationConfig(c *gin.Context) {
if h.opsService == nil {
response.Error(c, http.StatusServiceUnavailable, "Ops service not available")
return
}
if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil {
response.ErrorFrom(c, err)
return
}
cfg, err := h.opsService.GetEmailNotificationConfig(c.Request.Context())
if err != nil {
response.Error(c, http.StatusInternalServerError, "Failed to get email notification config")
return
}
response.Success(c, cfg)
}
// UpdateEmailNotificationConfig updates Ops email notification config (DB-backed).
// PUT /api/v1/admin/ops/email-notification/config
func (h *OpsHandler) UpdateEmailNotificationConfig(c *gin.Context) {
if h.opsService == nil {
response.Error(c, http.StatusServiceUnavailable, "Ops service not available")
return
}
if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil {
response.ErrorFrom(c, err)
return
}
var req service.OpsEmailNotificationConfigUpdateRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "Invalid request body")
return
}
updated, err := h.opsService.UpdateEmailNotificationConfig(c.Request.Context(), &req)
if err != nil {
// Most failures here are validation errors from request payload; treat as 400.
response.Error(c, http.StatusBadRequest, err.Error())
return
}
response.Success(c, updated)
}
// GetAlertRuntimeSettings returns Ops alert evaluator runtime settings (DB-backed).
// GET /api/v1/admin/ops/runtime/alert
func (h *OpsHandler) GetAlertRuntimeSettings(c *gin.Context) {
if h.opsService == nil {
response.Error(c, http.StatusServiceUnavailable, "Ops service not available")
return
}
if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil {
response.ErrorFrom(c, err)
return
}
cfg, err := h.opsService.GetOpsAlertRuntimeSettings(c.Request.Context())
if err != nil {
response.Error(c, http.StatusInternalServerError, "Failed to get alert runtime settings")
return
}
response.Success(c, cfg)
}
// UpdateAlertRuntimeSettings updates Ops alert evaluator runtime settings (DB-backed).
// PUT /api/v1/admin/ops/runtime/alert
func (h *OpsHandler) UpdateAlertRuntimeSettings(c *gin.Context) {
if h.opsService == nil {
response.Error(c, http.StatusServiceUnavailable, "Ops service not available")
return
}
if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil {
response.ErrorFrom(c, err)
return
}
var req service.OpsAlertRuntimeSettings
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "Invalid request body")
return
}
updated, err := h.opsService.UpdateOpsAlertRuntimeSettings(c.Request.Context(), &req)
if err != nil {
response.Error(c, http.StatusBadRequest, err.Error())
return
}
response.Success(c, updated)
}
// GetAdvancedSettings returns Ops advanced settings (DB-backed).
// GET /api/v1/admin/ops/advanced-settings
func (h *OpsHandler) GetAdvancedSettings(c *gin.Context) {
if h.opsService == nil {
response.Error(c, http.StatusServiceUnavailable, "Ops service not available")
return
}
if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil {
response.ErrorFrom(c, err)
return
}
cfg, err := h.opsService.GetOpsAdvancedSettings(c.Request.Context())
if err != nil {
response.Error(c, http.StatusInternalServerError, "Failed to get advanced settings")
return
}
response.Success(c, cfg)
}
// UpdateAdvancedSettings updates Ops advanced settings (DB-backed).
// PUT /api/v1/admin/ops/advanced-settings
func (h *OpsHandler) UpdateAdvancedSettings(c *gin.Context) {
if h.opsService == nil {
response.Error(c, http.StatusServiceUnavailable, "Ops service not available")
return
}
if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil {
response.ErrorFrom(c, err)
return
}
var req service.OpsAdvancedSettings
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "Invalid request body")
return
}
updated, err := h.opsService.UpdateOpsAdvancedSettings(c.Request.Context(), &req)
if err != nil {
response.Error(c, http.StatusBadRequest, err.Error())
return
}
response.Success(c, updated)
}
// GetMetricThresholds returns Ops metric thresholds (DB-backed).
// GET /api/v1/admin/ops/settings/metric-thresholds
func (h *OpsHandler) GetMetricThresholds(c *gin.Context) {
if h.opsService == nil {
response.Error(c, http.StatusServiceUnavailable, "Ops service not available")
return
}
if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil {
response.ErrorFrom(c, err)
return
}
cfg, err := h.opsService.GetMetricThresholds(c.Request.Context())
if err != nil {
response.Error(c, http.StatusInternalServerError, "Failed to get metric thresholds")
return
}
response.Success(c, cfg)
}
// UpdateMetricThresholds updates Ops metric thresholds (DB-backed).
// PUT /api/v1/admin/ops/settings/metric-thresholds
func (h *OpsHandler) UpdateMetricThresholds(c *gin.Context) {
if h.opsService == nil {
response.Error(c, http.StatusServiceUnavailable, "Ops service not available")
return
}
if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil {
response.ErrorFrom(c, err)
return
}
var req service.OpsMetricThresholds
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "Invalid request body")
return
}
updated, err := h.opsService.UpdateMetricThresholds(c.Request.Context(), &req)
if err != nil {
response.Error(c, http.StatusBadRequest, err.Error())
return
}
response.Success(c, updated)
}

View File

@@ -0,0 +1,771 @@
package admin
import (
"context"
"encoding/json"
"log"
"math"
"net"
"net/http"
"net/netip"
"net/url"
"os"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
)
type OpsWSProxyConfig struct {
TrustProxy bool
TrustedProxies []netip.Prefix
OriginPolicy string
}
const (
envOpsWSTrustProxy = "OPS_WS_TRUST_PROXY"
envOpsWSTrustedProxies = "OPS_WS_TRUSTED_PROXIES"
envOpsWSOriginPolicy = "OPS_WS_ORIGIN_POLICY"
envOpsWSMaxConns = "OPS_WS_MAX_CONNS"
envOpsWSMaxConnsPerIP = "OPS_WS_MAX_CONNS_PER_IP"
)
const (
OriginPolicyStrict = "strict"
OriginPolicyPermissive = "permissive"
)
var opsWSProxyConfig = loadOpsWSProxyConfigFromEnv()
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return isAllowedOpsWSOrigin(r)
},
// Subprotocol negotiation:
// - The frontend passes ["sub2api-admin", "jwt.<token>"].
// - We always select "sub2api-admin" so the token is never echoed back in the handshake response.
Subprotocols: []string{"sub2api-admin"},
}
const (
qpsWSPushInterval = 2 * time.Second
qpsWSRefreshInterval = 5 * time.Second
qpsWSRequestCountWindow = 1 * time.Minute
defaultMaxWSConns = 100
defaultMaxWSConnsPerIP = 20
)
var wsConnCount atomic.Int32
var wsConnCountByIP sync.Map // map[string]*atomic.Int32
const qpsWSIdleStopDelay = 30 * time.Second
const (
opsWSCloseRealtimeDisabled = 4001
)
var qpsWSIdleStopMu sync.Mutex
var qpsWSIdleStopTimer *time.Timer
func cancelQPSWSIdleStop() {
qpsWSIdleStopMu.Lock()
if qpsWSIdleStopTimer != nil {
qpsWSIdleStopTimer.Stop()
qpsWSIdleStopTimer = nil
}
qpsWSIdleStopMu.Unlock()
}
func scheduleQPSWSIdleStop() {
qpsWSIdleStopMu.Lock()
if qpsWSIdleStopTimer != nil {
qpsWSIdleStopMu.Unlock()
return
}
qpsWSIdleStopTimer = time.AfterFunc(qpsWSIdleStopDelay, func() {
// Only stop if truly idle at fire time.
if wsConnCount.Load() == 0 {
qpsWSCache.Stop()
}
qpsWSIdleStopMu.Lock()
qpsWSIdleStopTimer = nil
qpsWSIdleStopMu.Unlock()
})
qpsWSIdleStopMu.Unlock()
}
type opsWSRuntimeLimits struct {
MaxConns int32
MaxConnsPerIP int32
}
var opsWSLimits = loadOpsWSRuntimeLimitsFromEnv()
const (
qpsWSWriteTimeout = 10 * time.Second
qpsWSPongWait = 60 * time.Second
qpsWSPingInterval = 30 * time.Second
// We don't expect clients to send application messages; we only read to process control frames (Pong/Close).
qpsWSMaxReadBytes = 1024
)
type opsWSQPSCache struct {
refreshInterval time.Duration
requestCountWindow time.Duration
lastUpdatedUnixNano atomic.Int64
payload atomic.Value // []byte
opsService *service.OpsService
cancel context.CancelFunc
done chan struct{}
mu sync.Mutex
running bool
}
var qpsWSCache = &opsWSQPSCache{
refreshInterval: qpsWSRefreshInterval,
requestCountWindow: qpsWSRequestCountWindow,
}
func (c *opsWSQPSCache) start(opsService *service.OpsService) {
if c == nil || opsService == nil {
return
}
for {
c.mu.Lock()
if c.running {
c.mu.Unlock()
return
}
// If a previous refresh loop is currently stopping, wait for it to fully exit.
done := c.done
if done != nil {
c.mu.Unlock()
<-done
c.mu.Lock()
if c.done == done && !c.running {
c.done = nil
}
c.mu.Unlock()
continue
}
c.opsService = opsService
ctx, cancel := context.WithCancel(context.Background())
c.cancel = cancel
c.done = make(chan struct{})
done = c.done
c.running = true
c.mu.Unlock()
go func() {
defer close(done)
c.refreshLoop(ctx)
}()
return
}
}
// Stop stops the background refresh loop.
// It is safe to call multiple times.
func (c *opsWSQPSCache) Stop() {
if c == nil {
return
}
c.mu.Lock()
if !c.running {
done := c.done
c.mu.Unlock()
if done != nil {
<-done
}
return
}
cancel := c.cancel
c.cancel = nil
c.running = false
c.opsService = nil
done := c.done
c.mu.Unlock()
if cancel != nil {
cancel()
}
if done != nil {
<-done
}
c.mu.Lock()
if c.done == done && !c.running {
c.done = nil
}
c.mu.Unlock()
}
func (c *opsWSQPSCache) refreshLoop(ctx context.Context) {
ticker := time.NewTicker(c.refreshInterval)
defer ticker.Stop()
c.refresh(ctx)
for {
select {
case <-ticker.C:
c.refresh(ctx)
case <-ctx.Done():
return
}
}
}
func (c *opsWSQPSCache) refresh(parentCtx context.Context) {
if c == nil {
return
}
c.mu.Lock()
opsService := c.opsService
c.mu.Unlock()
if opsService == nil {
return
}
if parentCtx == nil {
parentCtx = context.Background()
}
ctx, cancel := context.WithTimeout(parentCtx, 10*time.Second)
defer cancel()
now := time.Now().UTC()
stats, err := opsService.GetWindowStats(ctx, now.Add(-c.requestCountWindow), now)
if err != nil || stats == nil {
if err != nil {
log.Printf("[OpsWS] refresh: get window stats failed: %v", err)
}
return
}
requestCount := stats.SuccessCount + stats.ErrorCountTotal
qps := 0.0
tps := 0.0
if c.requestCountWindow > 0 {
seconds := c.requestCountWindow.Seconds()
qps = roundTo1DP(float64(requestCount) / seconds)
tps = roundTo1DP(float64(stats.TokenConsumed) / seconds)
}
payload := gin.H{
"type": "qps_update",
"timestamp": now.Format(time.RFC3339),
"data": gin.H{
"qps": qps,
"tps": tps,
"request_count": requestCount,
},
}
msg, err := json.Marshal(payload)
if err != nil {
log.Printf("[OpsWS] refresh: marshal payload failed: %v", err)
return
}
c.payload.Store(msg)
c.lastUpdatedUnixNano.Store(now.UnixNano())
}
func roundTo1DP(v float64) float64 {
return math.Round(v*10) / 10
}
func (c *opsWSQPSCache) getPayload() []byte {
if c == nil {
return nil
}
if cached, ok := c.payload.Load().([]byte); ok && cached != nil {
return cached
}
return nil
}
func closeWS(conn *websocket.Conn, code int, reason string) {
if conn == nil {
return
}
msg := websocket.FormatCloseMessage(code, reason)
_ = conn.WriteControl(websocket.CloseMessage, msg, time.Now().Add(qpsWSWriteTimeout))
_ = conn.Close()
}
// QPSWSHandler handles realtime QPS push via WebSocket.
// GET /api/v1/admin/ops/ws/qps
func (h *OpsHandler) QPSWSHandler(c *gin.Context) {
clientIP := requestClientIP(c.Request)
if h == nil || h.opsService == nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "ops service not initialized"})
return
}
// If realtime monitoring is disabled, prefer a successful WS upgrade followed by a clean close
// with a deterministic close code. This prevents clients from spinning on 404/1006 reconnect loops.
if !h.opsService.IsRealtimeMonitoringEnabled(c.Request.Context()) {
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "ops realtime monitoring is disabled"})
return
}
closeWS(conn, opsWSCloseRealtimeDisabled, "realtime_disabled")
return
}
cancelQPSWSIdleStop()
// Lazily start the background refresh loop so unit tests that never hit the
// websocket route don't spawn goroutines that depend on DB/Redis stubs.
qpsWSCache.start(h.opsService)
// Reserve a global slot before upgrading the connection to keep the limit strict.
if !tryAcquireOpsWSTotalSlot(opsWSLimits.MaxConns) {
log.Printf("[OpsWS] connection limit reached: %d/%d", wsConnCount.Load(), opsWSLimits.MaxConns)
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "too many connections"})
return
}
defer func() {
if wsConnCount.Add(-1) == 0 {
scheduleQPSWSIdleStop()
}
}()
if opsWSLimits.MaxConnsPerIP > 0 && clientIP != "" {
if !tryAcquireOpsWSIPSlot(clientIP, opsWSLimits.MaxConnsPerIP) {
log.Printf("[OpsWS] per-ip connection limit reached: ip=%s limit=%d", clientIP, opsWSLimits.MaxConnsPerIP)
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "too many connections"})
return
}
defer releaseOpsWSIPSlot(clientIP)
}
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
log.Printf("[OpsWS] upgrade failed: %v", err)
return
}
defer func() {
_ = conn.Close()
}()
handleQPSWebSocket(c.Request.Context(), conn)
}
func tryAcquireOpsWSTotalSlot(limit int32) bool {
if limit <= 0 {
return true
}
for {
current := wsConnCount.Load()
if current >= limit {
return false
}
if wsConnCount.CompareAndSwap(current, current+1) {
return true
}
}
}
func tryAcquireOpsWSIPSlot(clientIP string, limit int32) bool {
if strings.TrimSpace(clientIP) == "" || limit <= 0 {
return true
}
v, _ := wsConnCountByIP.LoadOrStore(clientIP, &atomic.Int32{})
counter, ok := v.(*atomic.Int32)
if !ok {
return false
}
for {
current := counter.Load()
if current >= limit {
return false
}
if counter.CompareAndSwap(current, current+1) {
return true
}
}
}
func releaseOpsWSIPSlot(clientIP string) {
if strings.TrimSpace(clientIP) == "" {
return
}
v, ok := wsConnCountByIP.Load(clientIP)
if !ok {
return
}
counter, ok := v.(*atomic.Int32)
if !ok {
return
}
next := counter.Add(-1)
if next <= 0 {
// Best-effort cleanup; safe even if a new slot was acquired concurrently.
wsConnCountByIP.Delete(clientIP)
}
}
func handleQPSWebSocket(parentCtx context.Context, conn *websocket.Conn) {
if conn == nil {
return
}
ctx, cancel := context.WithCancel(parentCtx)
defer cancel()
var closeOnce sync.Once
closeConn := func() {
closeOnce.Do(func() {
_ = conn.Close()
})
}
closeFrameCh := make(chan []byte, 1)
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
defer cancel()
conn.SetReadLimit(qpsWSMaxReadBytes)
if err := conn.SetReadDeadline(time.Now().Add(qpsWSPongWait)); err != nil {
log.Printf("[OpsWS] set read deadline failed: %v", err)
return
}
conn.SetPongHandler(func(string) error {
return conn.SetReadDeadline(time.Now().Add(qpsWSPongWait))
})
conn.SetCloseHandler(func(code int, text string) error {
select {
case closeFrameCh <- websocket.FormatCloseMessage(code, text):
default:
}
cancel()
return nil
})
for {
_, _, err := conn.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway, websocket.CloseNoStatusReceived) {
log.Printf("[OpsWS] read failed: %v", err)
}
return
}
}
}()
// Push QPS data every 2 seconds (values are globally cached and refreshed at most once per qpsWSRefreshInterval).
pushTicker := time.NewTicker(qpsWSPushInterval)
defer pushTicker.Stop()
// Heartbeat ping every 30 seconds.
pingTicker := time.NewTicker(qpsWSPingInterval)
defer pingTicker.Stop()
writeWithTimeout := func(messageType int, data []byte) error {
if err := conn.SetWriteDeadline(time.Now().Add(qpsWSWriteTimeout)); err != nil {
return err
}
return conn.WriteMessage(messageType, data)
}
sendClose := func(closeFrame []byte) {
if closeFrame == nil {
closeFrame = websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")
}
_ = writeWithTimeout(websocket.CloseMessage, closeFrame)
}
for {
select {
case <-pushTicker.C:
msg := qpsWSCache.getPayload()
if msg == nil {
continue
}
if err := writeWithTimeout(websocket.TextMessage, msg); err != nil {
log.Printf("[OpsWS] write failed: %v", err)
cancel()
closeConn()
wg.Wait()
return
}
case <-pingTicker.C:
if err := writeWithTimeout(websocket.PingMessage, nil); err != nil {
log.Printf("[OpsWS] ping failed: %v", err)
cancel()
closeConn()
wg.Wait()
return
}
case closeFrame := <-closeFrameCh:
sendClose(closeFrame)
closeConn()
wg.Wait()
return
case <-ctx.Done():
var closeFrame []byte
select {
case closeFrame = <-closeFrameCh:
default:
}
sendClose(closeFrame)
closeConn()
wg.Wait()
return
}
}
}
func isAllowedOpsWSOrigin(r *http.Request) bool {
if r == nil {
return false
}
origin := strings.TrimSpace(r.Header.Get("Origin"))
if origin == "" {
switch strings.ToLower(strings.TrimSpace(opsWSProxyConfig.OriginPolicy)) {
case OriginPolicyStrict:
return false
case OriginPolicyPermissive, "":
return true
default:
return true
}
}
parsed, err := url.Parse(origin)
if err != nil || parsed.Hostname() == "" {
return false
}
originHost := strings.ToLower(parsed.Hostname())
trustProxyHeaders := shouldTrustOpsWSProxyHeaders(r)
reqHost := hostWithoutPort(r.Host)
if trustProxyHeaders {
xfHost := strings.TrimSpace(r.Header.Get("X-Forwarded-Host"))
if xfHost != "" {
xfHost = strings.TrimSpace(strings.Split(xfHost, ",")[0])
if xfHost != "" {
reqHost = hostWithoutPort(xfHost)
}
}
}
reqHost = strings.ToLower(reqHost)
if reqHost == "" {
return false
}
return originHost == reqHost
}
func shouldTrustOpsWSProxyHeaders(r *http.Request) bool {
if r == nil {
return false
}
if !opsWSProxyConfig.TrustProxy {
return false
}
peerIP, ok := requestPeerIP(r)
if !ok {
return false
}
return isAddrInTrustedProxies(peerIP, opsWSProxyConfig.TrustedProxies)
}
func requestPeerIP(r *http.Request) (netip.Addr, bool) {
if r == nil {
return netip.Addr{}, false
}
host, _, err := net.SplitHostPort(strings.TrimSpace(r.RemoteAddr))
if err != nil {
host = strings.TrimSpace(r.RemoteAddr)
}
host = strings.TrimPrefix(host, "[")
host = strings.TrimSuffix(host, "]")
if host == "" {
return netip.Addr{}, false
}
addr, err := netip.ParseAddr(host)
if err != nil {
return netip.Addr{}, false
}
return addr.Unmap(), true
}
func requestClientIP(r *http.Request) string {
if r == nil {
return ""
}
trustProxyHeaders := shouldTrustOpsWSProxyHeaders(r)
if trustProxyHeaders {
xff := strings.TrimSpace(r.Header.Get("X-Forwarded-For"))
if xff != "" {
// Use the left-most entry (original client). If multiple proxies add values, they are comma-separated.
xff = strings.TrimSpace(strings.Split(xff, ",")[0])
xff = strings.TrimPrefix(xff, "[")
xff = strings.TrimSuffix(xff, "]")
if addr, err := netip.ParseAddr(xff); err == nil && addr.IsValid() {
return addr.Unmap().String()
}
}
}
if peer, ok := requestPeerIP(r); ok && peer.IsValid() {
return peer.String()
}
return ""
}
func isAddrInTrustedProxies(addr netip.Addr, trusted []netip.Prefix) bool {
if !addr.IsValid() {
return false
}
for _, p := range trusted {
if p.Contains(addr) {
return true
}
}
return false
}
func loadOpsWSProxyConfigFromEnv() OpsWSProxyConfig {
cfg := OpsWSProxyConfig{
TrustProxy: true,
TrustedProxies: defaultTrustedProxies(),
OriginPolicy: OriginPolicyPermissive,
}
if v := strings.TrimSpace(os.Getenv(envOpsWSTrustProxy)); v != "" {
if parsed, err := strconv.ParseBool(v); err == nil {
cfg.TrustProxy = parsed
} else {
log.Printf("[OpsWS] invalid %s=%q (expected bool); using default=%v", envOpsWSTrustProxy, v, cfg.TrustProxy)
}
}
if raw := strings.TrimSpace(os.Getenv(envOpsWSTrustedProxies)); raw != "" {
prefixes, invalid := parseTrustedProxyList(raw)
if len(invalid) > 0 {
log.Printf("[OpsWS] invalid %s entries ignored: %s", envOpsWSTrustedProxies, strings.Join(invalid, ", "))
}
cfg.TrustedProxies = prefixes
}
if v := strings.TrimSpace(os.Getenv(envOpsWSOriginPolicy)); v != "" {
normalized := strings.ToLower(v)
switch normalized {
case OriginPolicyStrict, OriginPolicyPermissive:
cfg.OriginPolicy = normalized
default:
log.Printf("[OpsWS] invalid %s=%q (expected %q or %q); using default=%q", envOpsWSOriginPolicy, v, OriginPolicyStrict, OriginPolicyPermissive, cfg.OriginPolicy)
}
}
return cfg
}
func loadOpsWSRuntimeLimitsFromEnv() opsWSRuntimeLimits {
cfg := opsWSRuntimeLimits{
MaxConns: defaultMaxWSConns,
MaxConnsPerIP: defaultMaxWSConnsPerIP,
}
if v := strings.TrimSpace(os.Getenv(envOpsWSMaxConns)); v != "" {
if parsed, err := strconv.Atoi(v); err == nil && parsed > 0 {
cfg.MaxConns = int32(parsed)
} else {
log.Printf("[OpsWS] invalid %s=%q (expected int>0); using default=%d", envOpsWSMaxConns, v, cfg.MaxConns)
}
}
if v := strings.TrimSpace(os.Getenv(envOpsWSMaxConnsPerIP)); v != "" {
if parsed, err := strconv.Atoi(v); err == nil && parsed >= 0 {
cfg.MaxConnsPerIP = int32(parsed)
} else {
log.Printf("[OpsWS] invalid %s=%q (expected int>=0); using default=%d", envOpsWSMaxConnsPerIP, v, cfg.MaxConnsPerIP)
}
}
return cfg
}
func defaultTrustedProxies() []netip.Prefix {
prefixes, _ := parseTrustedProxyList("127.0.0.0/8,::1/128")
return prefixes
}
func parseTrustedProxyList(raw string) (prefixes []netip.Prefix, invalid []string) {
for _, token := range strings.Split(raw, ",") {
item := strings.TrimSpace(token)
if item == "" {
continue
}
var (
p netip.Prefix
err error
)
if strings.Contains(item, "/") {
p, err = netip.ParsePrefix(item)
} else {
var addr netip.Addr
addr, err = netip.ParseAddr(item)
if err == nil {
addr = addr.Unmap()
bits := 128
if addr.Is4() {
bits = 32
}
p = netip.PrefixFrom(addr, bits)
}
}
if err != nil || !p.IsValid() {
invalid = append(invalid, item)
continue
}
prefixes = append(prefixes, p.Masked())
}
return prefixes, invalid
}
func hostWithoutPort(hostport string) string {
hostport = strings.TrimSpace(hostport)
if hostport == "" {
return ""
}
if host, _, err := net.SplitHostPort(hostport); err == nil {
return host
}
if strings.HasPrefix(hostport, "[") && strings.HasSuffix(hostport, "]") {
return strings.Trim(hostport, "[]")
}
parts := strings.Split(hostport, ":")
return parts[0]
}

View File

@@ -0,0 +1,209 @@
package admin
import (
"strconv"
"strings"
"time"
"github.com/Wei-Shaw/sub2api/internal/handler/dto"
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/gin-gonic/gin"
)
// PromoHandler handles admin promo code management
type PromoHandler struct {
promoService *service.PromoService
}
// NewPromoHandler creates a new admin promo handler
func NewPromoHandler(promoService *service.PromoService) *PromoHandler {
return &PromoHandler{
promoService: promoService,
}
}
// CreatePromoCodeRequest represents create promo code request
type CreatePromoCodeRequest struct {
Code string `json:"code"` // 可选,为空则自动生成
BonusAmount float64 `json:"bonus_amount" binding:"required,min=0"` // 赠送余额
MaxUses int `json:"max_uses" binding:"min=0"` // 最大使用次数0=无限
ExpiresAt *int64 `json:"expires_at"` // 过期时间戳(秒)
Notes string `json:"notes"` // 备注
}
// UpdatePromoCodeRequest represents update promo code request
type UpdatePromoCodeRequest struct {
Code *string `json:"code"`
BonusAmount *float64 `json:"bonus_amount" binding:"omitempty,min=0"`
MaxUses *int `json:"max_uses" binding:"omitempty,min=0"`
Status *string `json:"status" binding:"omitempty,oneof=active disabled"`
ExpiresAt *int64 `json:"expires_at"`
Notes *string `json:"notes"`
}
// List handles listing all promo codes with pagination
// GET /api/v1/admin/promo-codes
func (h *PromoHandler) List(c *gin.Context) {
page, pageSize := response.ParsePagination(c)
status := c.Query("status")
search := strings.TrimSpace(c.Query("search"))
if len(search) > 100 {
search = search[:100]
}
params := pagination.PaginationParams{
Page: page,
PageSize: pageSize,
}
codes, paginationResult, err := h.promoService.List(c.Request.Context(), params, status, search)
if err != nil {
response.ErrorFrom(c, err)
return
}
out := make([]dto.PromoCode, 0, len(codes))
for i := range codes {
out = append(out, *dto.PromoCodeFromService(&codes[i]))
}
response.Paginated(c, out, paginationResult.Total, page, pageSize)
}
// GetByID handles getting a promo code by ID
// GET /api/v1/admin/promo-codes/:id
func (h *PromoHandler) GetByID(c *gin.Context) {
codeID, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
response.BadRequest(c, "Invalid promo code ID")
return
}
code, err := h.promoService.GetByID(c.Request.Context(), codeID)
if err != nil {
response.ErrorFrom(c, err)
return
}
response.Success(c, dto.PromoCodeFromService(code))
}
// Create handles creating a new promo code
// POST /api/v1/admin/promo-codes
func (h *PromoHandler) Create(c *gin.Context) {
var req CreatePromoCodeRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "Invalid request: "+err.Error())
return
}
input := &service.CreatePromoCodeInput{
Code: req.Code,
BonusAmount: req.BonusAmount,
MaxUses: req.MaxUses,
Notes: req.Notes,
}
if req.ExpiresAt != nil {
t := time.Unix(*req.ExpiresAt, 0)
input.ExpiresAt = &t
}
code, err := h.promoService.Create(c.Request.Context(), input)
if err != nil {
response.ErrorFrom(c, err)
return
}
response.Success(c, dto.PromoCodeFromService(code))
}
// Update handles updating a promo code
// PUT /api/v1/admin/promo-codes/:id
func (h *PromoHandler) Update(c *gin.Context) {
codeID, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
response.BadRequest(c, "Invalid promo code ID")
return
}
var req UpdatePromoCodeRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "Invalid request: "+err.Error())
return
}
input := &service.UpdatePromoCodeInput{
Code: req.Code,
BonusAmount: req.BonusAmount,
MaxUses: req.MaxUses,
Status: req.Status,
Notes: req.Notes,
}
if req.ExpiresAt != nil {
if *req.ExpiresAt == 0 {
// 0 表示清除过期时间
input.ExpiresAt = nil
} else {
t := time.Unix(*req.ExpiresAt, 0)
input.ExpiresAt = &t
}
}
code, err := h.promoService.Update(c.Request.Context(), codeID, input)
if err != nil {
response.ErrorFrom(c, err)
return
}
response.Success(c, dto.PromoCodeFromService(code))
}
// Delete handles deleting a promo code
// DELETE /api/v1/admin/promo-codes/:id
func (h *PromoHandler) Delete(c *gin.Context) {
codeID, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
response.BadRequest(c, "Invalid promo code ID")
return
}
err = h.promoService.Delete(c.Request.Context(), codeID)
if err != nil {
response.ErrorFrom(c, err)
return
}
response.Success(c, gin.H{"message": "Promo code deleted successfully"})
}
// GetUsages handles getting usage records for a promo code
// GET /api/v1/admin/promo-codes/:id/usages
func (h *PromoHandler) GetUsages(c *gin.Context) {
codeID, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
response.BadRequest(c, "Invalid promo code ID")
return
}
page, pageSize := response.ParsePagination(c)
params := pagination.PaginationParams{
Page: page,
PageSize: pageSize,
}
usages, paginationResult, err := h.promoService.ListUsages(c.Request.Context(), codeID, params)
if err != nil {
response.ErrorFrom(c, err)
return
}
out := make([]dto.PromoCodeUsage, 0, len(usages))
for i := range usages {
out = append(out, *dto.PromoCodeUsageFromService(&usages[i]))
}
response.Paginated(c, out, paginationResult.Total, page, pageSize)
}

Some files were not shown because too many files have changed in this diff Show More