38 Commits
Author SHA1 Message Date
19904408334 2d4f8ca5d5 chore(media): 更新数据库连接凭据 2026-09-04 14:28:46 +08:00
19904408334 8083d3f2d2 feat(pricing): 支持 per_item 按逐条产出时长计费
- 新增 ChargeUsage.DurationItems 字段用于接收逐条时长明细
- 无明细时回落单条 DurationSec,兼容旧客户端和直接 /calc 调用
- 重构 charge 逻辑,按时长落档求和并支持上不封顶线性叠加
2026-09-03 15:46:16 +08:00
19904408334 136463f074 chore(docs): 移除 superpowers 设计与计划文档 2026-09-03 13:20:00 +08:00
19904408334 a52d66cd88 docs(pricing): 计划 Task 5 补设计文档 §5.2 修正步 + Self-Review 记录范围外死代码删除裁决 2026-09-01 15:10:32 +08:00
19904408334 7ff9743acc docs(pricing): 枚举设计 §4.2/§5.2/§8 同步 mediaPrices v0.4 + 设计文档 §5.2 语法修正 2026-09-01 15:00:32 +08:00
19904408334 1825ea5581 feat(pricing): pickModelPrice 按输入媒体取媒体价/默认价 2026-09-01 14:55:22 +08:00
19904408334 4260125040 feat(pricing): mediaPrices 键/值校验 + ModelMediaSet 白名单 2026-09-01 14:51:54 +08:00
19904408334 867f2ac40c docs(pricing): Task 4 追加既有行为回归测试(用户裁决 维持删除+补回归) 2026-09-01 14:49:55 +08:00
19904408334 4c54be1758 fix(pricing): validateTieredBands 按下界排序判档位相邻 + 修同起 0 漏检 2026-09-01 14:43:23 +08:00
19904408334 63bc74eca9 refactor(pricing): modelRule 加 mediaPrices + match 移除 mediaType 2026-09-01 14:38:49 +08:00
19904408334 11486c0647 docs(pricing): mediaPrices 实现计划 v0.4 2026-09-01 14:37:19 +08:00
19904408334 90ad1b86db docs(pricing): mediaPrices 媒体类型价与阶梯校验修复设计 v0.4 2026-09-01 14:25:07 +08:00
19904408334 f6ffb58794 feat(pricing): 按 subjectType+subjectId 查询可选计费方式接口 2026-09-01 13:08:03 +08:00
19904408334 1684c62032 feat(pricing): 计费方式中文名 + workflow 三模式常量 + charge-modes DTO 2026-09-01 13:03:44 +08:00
19904408334andClaude 72c3cb8d88 docs(pricing): charge-modes 查询接口实现计划
Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-01 12:01:28 +08:00
19904408334andClaude 9d06bbe9af docs(pricing): charge-modes 查询条件加 subjectId
Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-01 11:59:06 +08:00
19904408334andClaude d36b0c231f docs(pricing): 按 SubjectType 查询可选计费方式接口设计
Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-01 11:53:16 +08:00
19904408334 3eab69034c fix(pricing): settleOrder/Fail 锁内重读状态防并发重复扣费(跨库事务不可靠)
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-28 18:29:51 +08:00
19904408334andClaude 42ff4baac0 feat(pricing): init.sql 建表 subject 化 + config.yml 加 model-gateway 服务名
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-28 18:11:34 +08:00
19904408334 27bde0ee5b feat(pricing): 计价对象枚举接口(workflow+实时model-gateway系统模型+固定业务模块)
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-28 18:04:09 +08:00
19904408334andClaude e9da4c1d78 feat(pricing): OpenOrder/SaveConfig 按 subject 类型校验 + per_token 按模型实时价结算
- service 层全量切换 subject_type+subject_id 键(去除 BizKey 残留)
- OpenOrder 按 subject 类型 resolveChargeMode + 规则快照 + 门禁 + 幂等
- SaveConfig/GetConfig 按 subject 类型校验 rules 结构
- settleOrder 支持 per_token 分支(calcPerTokenOrder 按模型实时价)
- 修复 model 计算器:P1 nil price 校验、P2 unit/base 交叉校验、M3 cached>prompt 负成本钳位

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-28 17:56:00 +08:00
19904408334 2e8a3663a5 refactor(pricing): DAO 按 (subject_type, subject_id) 键查询/幂等
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-28 17:49:03 +08:00
19904408334 9b1aa1288d refactor(pricing): entity/DTO 换 subject_type+subject_id(含 subject_dto 枚举 DTO)
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-28 17:44:22 +08:00
19904408334 3e0b7bafc3 feat(pricing): 模型 token/单位计算器 + per_period + 按 subject_type 分表派发
pricing_service.go 三处调用同步改为 subject 签名(Task 5 将整体重构)。
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-28 17:33:59 +08:00
19904408334andClaude 0f8b9711ed feat(pricing): 扩展 charge_mode 枚举(token/单位/per_period)+ subject 常量
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-28 17:27:31 +08:00
19904408334andClaude 5d2b01e6f3 docs(pricing): 模型规则重设计(视频/推理/音频/图片自适应 schema + 预览)
- §4.2 model 规则改为模型类型自适应:unit 7 种(per_1K/per_1M/per_1/per_second/per_minute/per_hour/per_char)
  + tiered 阶梯(分档不累加)+ match 维度(mediaType/thinking/outputAudio/outputResolution/inputLength)+ price 收敛 input/output/cacheHit 与 unitPrice
- §5 计算器:token 计算器 + 单位计算器;ChargeUsage 扩展模型维度
- §7 枚举透传 modelType(100推理/200图片/300音频/600视频)
- preview HTML 重写模型编辑器为四类自适应 + 示例填充

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-28 17:06:45 +08:00
19904408334andClaude c3564df658 docs(pricing): spec 增补业务模块枚举与 per_period 周期计费;新增配置页原型
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-28 16:19:10 +08:00
19904408334andClaude 2b800544bc docs(pricing): 计价对象枚举化与模型计价设计 spec
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-28 15:36:24 +08:00
admin 4cf21df78e gitea,gitea-runner写入docker-compose 2026-08-06 09:40:31 +08:00
admin 9b15adbf7b common版本更新 2026-06-25 17:07:10 +08:00
admin 3f950c1731 common版本更新 2026-06-25 17:05:12 +08:00
admin 144373e3b1 common版本升级 2026-06-23 18:31:58 +08:00
admin cceb69b387 ci/cd调整 2026-06-10 16:39:09 +08:00
admin dd5df7f54f common版本更新 2026-04-22 18:00:31 +08:00
admin 78afa0cf26 增加dockerfile配置 2026-04-09 17:42:55 +08:00
admin 6dd40c3b24 增加dockerfile配置 2026-04-09 16:14:31 +08:00
admin bb1bd9becb golang版本升级1.26.0 2026-04-03 11:39:33 +08:00
admin 48b0765983 golang版本升级1.26.0 2026-04-03 11:05:03 +08:00
46 changed files with 2974 additions and 790 deletions
+1
View File
@@ -0,0 +1 @@
.git
+2
View File
@@ -1 +1,3 @@
/.idea/* /.idea/*
/.superpowers/
/docs/superpowers/
+25
View File
@@ -0,0 +1,25 @@
# 阶段1: 构建
FROM golang:alpine AS builder
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && \
apk add --no-cache git ca-certificates tzdata
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
ENV GO111MODULE=on
ENV GOPROXY=https://goproxy.cn,direct
ENV CGO_ENABLED=0
ENV GOTOOLCHAIN=auto
WORKDIR /build
COPY . .
RUN go mod download && go mod tidy
RUN go build -ldflags="-s -w" -o main ./main.go
EXPOSE 3007
CMD ["./main"]
+30 -9
View File
@@ -9,10 +9,10 @@ server:
database: database:
default: default:
- type: "pgsql" - type: "pgsql"
host: "116.204.74.41" host: "192.168.0.83"
port: "15432" port: "15432"
user: "postgres" user: "sql9f15b63fd203b36e"
pass: "Bjang09@686^*^" pass: "1ec94b1acdaf57b66030242d418fad5a"
name: "shop_user_trade" name: "shop_user_trade"
role: "master" role: "master"
debug: false debug: false
@@ -28,10 +28,10 @@ database:
deletedAt: "deleted_at" deletedAt: "deleted_at"
timeMaintainDisabled: false timeMaintainDisabled: false
- type: "pgsql" - type: "pgsql"
host: "116.204.74.41" host: "192.168.0.83"
port: "15432" port: "15432"
user: "postgres" user: "sql9f15b63fd203b36e"
pass: "Bjang09@686^*^" pass: "1ec94b1acdaf57b66030242d418fad5a"
name: "shop_user_trade" name: "shop_user_trade"
role: "slave" role: "slave"
debug: false debug: false
@@ -46,10 +46,31 @@ database:
updatedAt: "updated_at" updatedAt: "updated_at"
deletedAt: "deleted_at" deletedAt: "deleted_at"
timeMaintainDisabled: false timeMaintainDisabled: false
wallet:
- type: "pgsql"
host: "192.168.0.83"
port: "15432"
user: "sql9f15b63fd203b36e"
pass: "1ec94b1acdaf57b66030242d418fad5a"
name: "wallet"
prefix: "wallet_" # (可选)表名前缀
role: "master"
debug: true # (可选)开启调试模式
dryRun: false # (可选)ORM空跑(只读不写)
charset: "utf8" # (可选)数据库编码(如: utf8mb4/utf8/gbk/gb2312),一般设置为utf8mb4。默认为utf8。
timezone: "Asia/Shanghai" # (可选)时区配置,例如:Local
maxIdle: 5 # (可选)连接池最大闲置的连接数(默认10)
maxOpen: 20 # (可选)连接池最大打开的连接数(默认无限制)
maxLifetime: "30s" # (可选)连接对象可重复使用的时间长度(默认30秒)
maxIdleConnTime: "30s" # (可选,v2.10新增)连接池中空闲连接的最大生存时间(默认30秒)。可以通过配置文件或SetConnMaxIdleTime方法设置,避免长时间空闲连接占用资源。
createdAt: "created_at" # (可选)自动创建时间字段名称
updatedAt: "updated_at" # (可选)自动更新时间字段名称
deletedAt: "deleted_at" # (可选)软删除时间字段名称
timeMaintainDisabled: false # (可选)是否完全关闭时间更新特性,为true时CreatedAt/UpdatedAt/DeletedAt都将失效
redis: redis:
default: default:
address: 116.204.74.41:6379 address: 192.168.0.83:6379
db: 0 db: 0
idleTimeout: "60s" idleTimeout: "60s"
maxConnLifetime: "90s" maxConnLifetime: "90s"
@@ -60,7 +81,7 @@ redis:
maxActive: 100 maxActive: 100
consul: consul:
address: 116.204.74.41:8500 address: 192.168.0.83:8500
jaeger: jaeger:
addr: 116.204.74.41:4318 addr: 192.168.0.83:4318
+45
View File
@@ -0,0 +1,45 @@
package pricing
// ChargeMode 计费方式(全 DB 配置,代码不写死金额;新增方式在 charge_calc 计算器注册表加一项)
type ChargeMode string
const (
ChargeModePerItem ChargeMode = "per_item" // 按条:档位开放、上不封顶(rules.tiers + overflow
ChargeModePerSecond ChargeMode = "per_second" // 按秒(workflow:整秒向上取整;model:精确秒)
ChargeModePerToken ChargeMode = "per_token" // 按tokenworkflow 结算时按各模型 subject 实时价)
ChargeModePer1K ChargeMode = "per_1K" // 每千token(模型 token 计费)
ChargeModePer1M ChargeMode = "per_1M" // 每百万token(模型 token 计费)
ChargeModePer1 ChargeMode = "per_1" // 每个/每张图(图片按张数)
ChargeModePerMinute ChargeMode = "per_minute" // 每分钟(音频时长)
ChargeModePerHour ChargeMode = "per_hour" // 每小时(音频时长)
ChargeModePerChar ChargeMode = "per_char" // 每字(音频按字数)
ChargeModePerPeriod ChargeMode = "per_period" // 周期订阅(business 按年)
)
// ModelUnitSet model subject 允许的计费单位(spec §4.2)
var ModelUnitSet = map[ChargeMode]bool{
ChargeModePer1K: true, ChargeModePer1M: true, ChargeModePer1: true,
ChargeModePerSecond: true, ChargeModePerMinute: true, ChargeModePerHour: true, ChargeModePerChar: true,
}
// ModelMediaSet 模型费率 mediaPrices 允许的媒体类型键(输入媒体白名单,前端下拉同源)
var ModelMediaSet = map[string]struct{}{
"text": {}, "audio": {}, "video": {}, "image": {},
}
// PeriodSet 周期允许集合(per_period,扩月/季只加值)
var PeriodSet = map[string]bool{"year": true}
// ChargeModeNames 计费方式中文名(前端下拉渲染;全枚举覆盖,未命中回退 mode 本身)
var ChargeModeNames = map[ChargeMode]string{
ChargeModePerItem: "按条",
ChargeModePerSecond: "按秒",
ChargeModePerToken: "按token",
ChargeModePer1K: "每千token",
ChargeModePer1M: "每百万token",
ChargeModePer1: "每张/每个",
ChargeModePerMinute: "每分钟",
ChargeModePerHour: "每小时",
ChargeModePerChar: "每字",
ChargeModePerPeriod: "周期订阅",
}
+10
View File
@@ -0,0 +1,10 @@
package pricing
// ChargeOrderStatus 计费单状态(状态机单迁移:CREATED → SETTLED / FAILED
type ChargeOrderStatus int
const (
ChargeOrderStatusCreated ChargeOrderStatus = 1 // 已建单(未结算,不动钱)
ChargeOrderStatusSettled ChargeOrderStatus = 2 // 已结算(实收完成)
ChargeOrderStatusFailed ChargeOrderStatus = 3 // 已失败(不扣费)
)
+41
View File
@@ -0,0 +1,41 @@
package pricing
// SubjectType 计价对象类型(固定枚举 = pricing_config 唯一键第一部分)
type SubjectType string
const (
SubjectTypeWorkflow SubjectType = "workflow" // 工作流(固定单主体)
SubjectTypeModel SubjectType = "model" // 模型(model-gateway 系统模型,subject_id=模型ID
SubjectTypeBusiness SubjectType = "business" // 业务模块(固定写死列表)
)
// WorkflowSubjectID 工作流固定 subject_id
const WorkflowSubjectID = "workflow"
// BusinessSubject 业务模块枚举项(固定写死,新增业务模块改这里)
type BusinessSubject struct {
SubjectID string `json:"subjectId"`
Name string `json:"name"`
ChargeModes []ChargeMode `json:"chargeModes"`
}
// BusinessSubjects 业务模块固定列表(spec §7:与「固定枚举」一致)
var BusinessSubjects = []BusinessSubject{
{SubjectID: "ai_customer_service", Name: "AI客服", ChargeModes: []ChargeMode{ChargeModePerPeriod}},
}
// WorkflowChargeModes workflow 主体可选计费方式(与 charge_calc.go workflowCalculators 三键一致)
var WorkflowChargeModes = []ChargeMode{ChargeModePerItem, ChargeModePerSecond, ChargeModePerToken}
// ModelTypeCode 模型类型编码(透传 model-gateway 枚举,管理端按此分组渲染编辑器)
type ModelTypeCode = int
const (
ModelTypeReasoning ModelTypeCode = 100 // 推理
ModelTypeImage ModelTypeCode = 200 // 图片
ModelTypeAudio ModelTypeCode = 300 // 音频
ModelTypeVector ModelTypeCode = 400 // 向量
ModelTypeOmni ModelTypeCode = 500 // 多模态
ModelTypeVideo ModelTypeCode = 600 // 视频
ModelTypeCode2 ModelTypeCode = 700 // 代码
)
+11 -2
View File
@@ -1,11 +1,20 @@
package public package public
// 数据库名称
const (
DbNameWallet = "wallet"
)
// 数据库表名 // 数据库表名
const ( const (
TableNameKnapsack = "knapsack" // 背包表 TableNameKnapsack = "knapsack" // 背包表
TableNameKnapsackLog = "knapsack_log" // 背包日志表 TableNameKnapsackLog = "knapsack_log" // 背包日志表
TableNameMarket = "market" // 市场表 TableNameMarket = "market" // 市场表
TableNameMarketLog = "market_log" // 市场日志表 TableNameMarketLog = "market_log" // 市场日志表
TableNameWallet = "wallet" // 钱包表
TableNameWalletLog = "wallet_log" // 钱包日志 TableNameWalletAccount = "account" // 钱包表
TableNameWalletAccountLog = "account_log" // 钱包日志表
TableNamePricingConfig = "pricing_config" // 计价配置表
TableNameChargeOrder = "charge_order" // 计费单表
) )
+7 -5
View File
@@ -1,11 +1,13 @@
package wallet package wallet
// WalletLogType 钱包日志操作类型 // WalletLogType 钱包账务类型
// 语义按金额变动定义(与钱包 status 的整钱包冻结无关):
//
// income 收入/充值 balance += amount
// expense 支出(结算实扣) balance -= amount(允许为负=欠费)
type WalletLogType string type WalletLogType string
const ( const (
WalletLogTypeIncome WalletLogType = "income" // 收入 WalletLogTypeIncome WalletLogType = "income" // 收入/充值
WalletLogTypeExpense WalletLogType = "expense" // 支出 WalletLogTypeExpense WalletLogType = "expense" // 支出(结算实扣,允许负余额)
WalletLogTypeFreeze WalletLogType = "freeze" // 冻结
WalletLogTypeUnfreeze WalletLogType = "unfreeze" // 解冻
) )
+62
View File
@@ -0,0 +1,62 @@
package controller
import (
"context"
pricingDto "shop-user-trade/model/dto/pricing"
pricingService "shop-user-trade/service/pricing"
)
var Pricing = new(pricingController)
type pricingController struct{}
// OpenOrder 建单(不动钱)
func (c *pricingController) OpenOrder(ctx context.Context, req *pricingDto.OpenOrderReq) (*pricingDto.ChargeOrderInfo, error) {
return pricingService.Pricing.OpenOrder(ctx, req)
}
// Settle 结算(按最终产出)
func (c *pricingController) Settle(ctx context.Context, req *pricingDto.SettleReq) (*pricingDto.ChargeOrderInfo, error) {
return pricingService.Pricing.Settle(ctx, req)
}
// Cancel 取消(按已消耗用量结算)
func (c *pricingController) Cancel(ctx context.Context, req *pricingDto.CancelReq) (*pricingDto.ChargeOrderInfo, error) {
return pricingService.Pricing.Cancel(ctx, req)
}
// Fail 失败处理(不扣费)
func (c *pricingController) Fail(ctx context.Context, req *pricingDto.FailReq) (*pricingDto.ChargeOrderInfo, error) {
return pricingService.Pricing.Fail(ctx, req)
}
// GetOrder 查询计费单
func (c *pricingController) GetOrder(ctx context.Context, req *pricingDto.GetOrderInfoReq) (*pricingDto.ChargeOrderInfo, error) {
return pricingService.Pricing.GetOrderInfo(ctx, req)
}
// CalcFee 按用量计算费用(不建单不扣费)
func (c *pricingController) CalcFee(ctx context.Context, req *pricingDto.CalcFeeReq) (*pricingDto.CalcFeeRes, error) {
return pricingService.Pricing.CalcFee(ctx, req)
}
// SaveConfig 保存计价配置
func (c *pricingController) SaveConfig(ctx context.Context, req *pricingDto.SavePricingConfigReq) (*pricingDto.SavePricingConfigRes, error) {
return pricingService.Pricing.SavePricingConfig(ctx, req)
}
// GetConfig 查询计价配置
func (c *pricingController) GetConfig(ctx context.Context, req *pricingDto.GetPricingConfigReq) (*pricingDto.GetPricingConfigRes, error) {
return pricingService.Pricing.GetPricingConfig(ctx, req)
}
// Subjects 计价对象枚举
func (c *pricingController) Subjects(ctx context.Context, req *pricingDto.SubjectListReq) (*pricingDto.SubjectListRes, error) {
return pricingService.Pricing.ListSubjects(ctx, req)
}
// ChargeModes 查询主体可选计费方式
func (c *pricingController) ChargeModes(ctx context.Context, req *pricingDto.ChargeModeListReq) (*pricingDto.ChargeModeListRes, error) {
return pricingService.Pricing.ListChargeModes(ctx, req)
}
+30
View File
@@ -0,0 +1,30 @@
package controller
import (
"context"
walletDto "shop-user-trade/model/dto/wallet"
walletService "shop-user-trade/service/wallet"
"gitea.redpowerfuture.com/red-future/common/beans"
)
var Account = new(accountController)
type accountController struct{}
// Get 获取钱包
func (c *accountController) Get(ctx context.Context, req *walletDto.GetAccountByUserIdReq) (*walletDto.AccountInfo, error) {
return walletService.Account.GetAccountByUserId(ctx, req)
}
// Recharge 充值
func (c *accountController) Recharge(ctx context.Context, req *walletDto.RechargeReq) (res *beans.ResponseEmpty, err error) {
err = walletService.Account.Recharge(ctx, req)
return
}
// GetLogs 获取钱包流水
func (c *accountController) GetLogs(ctx context.Context, req *walletDto.GetAccountLogsReq) (*walletDto.AccountLogData, error) {
return walletService.Account.GetWalletLogs(ctx, req)
}
-32
View File
@@ -1,32 +0,0 @@
package controller
import (
"context"
walletDto "shop-user-trade/model/dto/wallet"
walletService "shop-user-trade/service/wallet"
)
var Wallet = new(walletController)
type walletController struct{}
// GetByUserId 根据用户ID获取钱包
func (c *walletController) GetByUserId(ctx context.Context, req *walletDto.GetWalletByUserIdReq) (res *walletDto.GetWalletByUserIdResp, err error) {
return walletService.Wallet.GetByUserId(ctx, req)
}
// Create 创建钱包
func (c *walletController) Create(ctx context.Context, req *walletDto.CreateWalletReq) (res *walletDto.CreateWalletResp, err error) {
return walletService.Wallet.Create(ctx, req)
}
// UpdateBalance 更新余额
func (c *walletController) UpdateBalance(ctx context.Context, req *walletDto.UpdateBalanceReq) (res *walletDto.UpdateBalanceResp, err error) {
return walletService.Wallet.UpdateBalance(ctx, req)
}
// GetWalletLogs 获取钱包日志
func (c *walletController) GetWalletLogs(ctx context.Context, req *walletDto.GetWalletLogsReq) (res *walletDto.GetWalletLogsResp, err error) {
return walletService.Wallet.GetWalletLogs(ctx, req)
}
+2 -2
View File
@@ -4,7 +4,7 @@ import (
"context" "context"
"time" "time"
"gitea.com/red-future/common/db/gfdb" "gitea.redpowerfuture.com/red-future/common/db/gfdb"
"github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/util/gconv" "github.com/gogf/gf/v2/util/gconv"
@@ -15,7 +15,7 @@ import (
knapsackEntity "shop-user-trade/model/entity/knapsack" knapsackEntity "shop-user-trade/model/entity/knapsack"
) )
var Knapsack = new(knapsackDao) var Knapsack = &knapsackDao{}
type knapsackDao struct{} type knapsackDao struct{}
+2 -2
View File
@@ -4,7 +4,7 @@ import (
"context" "context"
"time" "time"
"gitea.com/red-future/common/db/gfdb" "gitea.redpowerfuture.com/red-future/common/db/gfdb"
"github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/util/gconv" "github.com/gogf/gf/v2/util/gconv"
@@ -15,7 +15,7 @@ import (
marketEntity "shop-user-trade/model/entity/market" marketEntity "shop-user-trade/model/entity/market"
) )
var Market = new(marketDao) var Market = &marketDao{}
type marketDao struct{} type marketDao struct{}
+91
View File
@@ -0,0 +1,91 @@
package dao
import (
"context"
"gitea.redpowerfuture.com/red-future/common/db/gfdb"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/util/gconv"
pricingConsts "shop-user-trade/consts/pricing"
"shop-user-trade/consts/public"
pricingDto "shop-user-trade/model/dto/pricing"
pricingEntity "shop-user-trade/model/entity/pricing"
)
// ChargeOrder chargeOrderDao 计费单数据访问:薄查询/状态迁移原语(同 model-gateway dao 规范)。
// 方法入参统一吃 DTOmodel/dto/pricing);无锁、无事务、不定义业务错误。
var ChargeOrder = &chargeOrderDao{}
type chargeOrderDao struct{}
// GetOrder 按 ID 或 subject_type+subject_id+biz_order_no 查询计费单(Id>0 走 ID)。
// 幂等查询复用:工作流断点续跑/恢复 execId 不变,据此去重。
func (d *chargeOrderDao) GetOrder(ctx context.Context, req *pricingDto.GetChargeOrderReq) (res *pricingEntity.ChargeOrder, err error) {
m := gfdb.DB(ctx, public.DbNameWallet).Model(ctx, public.TableNameChargeOrder).Model
if req.Id > 0 {
m = m.Where(pricingEntity.ChargeOrderCol.Id, req.Id)
} else {
m = m.Where(pricingEntity.ChargeOrderCol.SubjectType, req.SubjectType).
Where(pricingEntity.ChargeOrderCol.SubjectID, req.SubjectID).
Where(pricingEntity.ChargeOrderCol.BizOrderNo, req.BizOrderNo)
}
r, err := m.One()
if err != nil {
return
}
if r.IsEmpty() {
return nil, nil
}
err = r.Struct(&res)
return
}
// Insert 创建计费单(事务透明:在调用方事务内执行;同 model-gateway dao 规范)
func (d *chargeOrderDao) Insert(ctx context.Context, req *pricingDto.CreateChargeOrderReq) (id int64, err error) {
var e = new(pricingEntity.ChargeOrder)
err = gconv.Struct(req, &e)
if err != nil {
return
}
r, err := gfdb.DB(ctx, public.DbNameWallet).Model(ctx, public.TableNameChargeOrder).Insert(e)
if err != nil {
return
}
return r.LastInsertId()
}
// UpdateToSettled 结算迁移:仅 CREATED → SETTLEDrows=0 表示已处理(幂等)
func (d *chargeOrderDao) UpdateToSettled(ctx context.Context, req *pricingDto.SettleChargeOrderReq) (bool, error) {
return d.migrateStatus(ctx, req.Id, gdb.Map{
pricingEntity.ChargeOrderCol.Status: int(pricingConsts.ChargeOrderStatusSettled),
pricingEntity.ChargeOrderCol.ActualAmount: req.ActualAmount,
pricingEntity.ChargeOrderCol.Usage: req.Usage,
pricingEntity.ChargeOrderCol.RuleSnapshot: req.RuleSnapshot,
pricingEntity.ChargeOrderCol.SettleTime: req.SettleTime,
})
}
// UpdateToFailed 失败迁移:仅 CREATED → FAILED(不扣费,不动钱)
func (d *chargeOrderDao) UpdateToFailed(ctx context.Context, req *pricingDto.FailChargeOrderReq) (bool, error) {
return d.migrateStatus(ctx, req.Id, gdb.Map{
pricingEntity.ChargeOrderCol.Status: int(pricingConsts.ChargeOrderStatusFailed),
})
}
// migrateStatus 条件状态迁移(幂等:非 CREATED 时 Update 影响 0 行返回 false
func (d *chargeOrderDao) migrateStatus(ctx context.Context, id int64, data gdb.Map) (bool, error) {
result, err := gfdb.DB(ctx, public.DbNameWallet).Model(ctx, public.TableNameChargeOrder).
Data(data).
Where(pricingEntity.ChargeOrderCol.Id, id).
Where(pricingEntity.ChargeOrderCol.Status, int(pricingConsts.ChargeOrderStatusCreated)).
Update()
if err != nil {
return false, err
}
rows, err := result.RowsAffected()
if err != nil {
return false, err
}
return rows > 0, nil
}
+63
View File
@@ -0,0 +1,63 @@
package dao
import (
"context"
"gitea.redpowerfuture.com/red-future/common/db/gfdb"
"github.com/gogf/gf/v2/util/gconv"
"shop-user-trade/consts/public"
pricingDto "shop-user-trade/model/dto/pricing"
pricingEntity "shop-user-trade/model/entity/pricing"
)
// PricingConfig pricingConfigDao 计价配置数据访问:薄 CRUD(同 model-gateway dao 规范)。
// 方法入参统一吃 DTOmodel/dto/pricing);无锁、无事务、不定义业务错误。
var PricingConfig = &pricingConfigDao{}
type pricingConfigDao struct{}
// Get 按计价对象(subject_type + subject_id)获取计价配置
func (d *pricingConfigDao) Get(ctx context.Context, req *pricingDto.GetPricingConfigReq) (res *pricingEntity.PricingConfig, err error) {
r, err := gfdb.DB(ctx, public.DbNameWallet).Model(ctx, public.TableNamePricingConfig).NoTenantId(ctx).
Where(pricingEntity.PricingConfigCol.SubjectType, req.SubjectType).
Where(pricingEntity.PricingConfigCol.SubjectID, req.SubjectID).
One()
if err != nil {
return
}
if r.IsEmpty() {
return nil, nil
}
err = r.Struct(&res)
return
}
// Insert 插入
func (d *pricingConfigDao) Insert(ctx context.Context, req *pricingDto.SavePricingConfigReq) (id int64, err error) {
var e = new(pricingEntity.PricingConfig)
err = gconv.Struct(req, &e)
if err != nil {
return
}
r, err := gfdb.DB(ctx, public.DbNameWallet).Model(ctx, public.TableNamePricingConfig).Insert(e)
if err != nil {
return
}
return r.LastInsertId()
}
// Update 更新
func (d *pricingConfigDao) Update(ctx context.Context, req *pricingDto.SavePricingConfigReq) (rows int64, err error) {
// 与 Insert 一致:DTO→entitymap[string]any),Data 以 map 落 JSONB 列
var e = new(pricingEntity.PricingConfig)
if err = gconv.Struct(req, &e); err != nil {
return
}
r, err := gfdb.DB(ctx, public.DbNameWallet).Model(ctx, public.TableNamePricingConfig).
OmitEmpty().Data(e).Where(pricingEntity.PricingConfigCol.Id, req.Id).Update()
if err != nil {
return
}
return r.RowsAffected()
}
+143
View File
@@ -0,0 +1,143 @@
package dao
import (
"context"
"errors"
"gitea.redpowerfuture.com/red-future/common/db/gfdb"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/util/gconv"
"github.com/google/uuid"
"shop-user-trade/consts/public"
walletConsts "shop-user-trade/consts/wallet"
walletDto "shop-user-trade/model/dto/wallet"
walletEntity "shop-user-trade/model/entity/wallet"
)
// Account AccountDao 钱包数据访问:薄查询/账务原语(同 model-gateway dao 规范)。
// 方法入参统一吃 DTOmodel/dto/wallet);无锁、无事务、不定义业务错误。
// 锁与事务由 service 编排;钱包是否存在/是否可用的业务校验在 service 层做。
// Change 为账务原子原语,事务透明,必须由调用方在事务内执行。
var Account = &accountDao{}
type accountDao struct{}
// ====================== 查询 ======================
// Get 根据用户ID获取钱包(查询用,不加锁)
func (d *accountDao) Get(ctx context.Context, req *walletDto.GetAccountByUserIdReq) (res *walletEntity.Account, err error) {
r, err := gfdb.DB(ctx, public.DbNameWallet).Model(ctx, public.TableNameWalletAccount).
Where(walletEntity.AccountCol.UserID, req.UserId).
One()
if err != nil {
return
}
if r.IsEmpty() {
return nil, nil
}
err = r.Struct(&res)
return
}
// GetForUpdate 行锁读取钱包(须在事务内调用),供账务变动使用
func (d *accountDao) GetForUpdate(ctx context.Context, req *walletDto.GetAccountByUserIdReq) (res *walletEntity.Account, err error) {
r, err := gfdb.DB(ctx, public.DbNameWallet).Model(ctx, public.TableNameWalletAccount).
Where(walletEntity.AccountCol.UserID, req.UserId).
LockUpdate().
One()
if err != nil {
return
}
if r.IsEmpty() {
return nil, nil
}
err = r.Struct(&res)
return
}
// Create 创建钱包(余额 0,默认启用;同 model-gateway dao 规范)
func (d *accountDao) Create(ctx context.Context, req *walletDto.CreateAccountReq) (id int64, err error) {
var e = new(walletEntity.Account)
err = gconv.Struct(req, &e)
if err != nil {
return
}
r, err := gfdb.DB(ctx, public.DbNameWallet).Model(ctx, public.TableNameWalletAccount).Insert(e)
if err != nil {
return
}
return r.LastInsertId()
}
// ====================== 原子账务 ======================
// Change 单次原子账务变动(事务透明:须在调用方事务内执行,gfdb.DB(ctx) 自动继承事务)。
// 行锁读取 → 校验状态 → 条件更新余额 → 写流水(transaction_no 唯一幂等)。
// 锁与事务由上层 service 编排(common 统一锁 + gfdb.DB(ctx).Transaction),本 DAO 不加锁、不开事务。
// ⚠️ 不做余额非负拦截:结算允许负余额(欠费,靠充值归正),见技术设计 §9。
func (d *accountDao) Change(ctx context.Context, req *walletDto.ChangeAccountReq) error {
w, err := d.GetForUpdate(ctx, &walletDto.GetAccountByUserIdReq{UserId: req.UserId})
if err != nil {
return err
}
if w == nil {
return errors.New("钱包不存在")
}
if w.Status != walletConsts.WalletStatusEnabled {
return errors.New("钱包状态不可用")
}
// 行锁(GetForUpdate 的 FOR UPDATE)在事务内持续持有,直接以 Go 侧计算结果写回,
// 避免浮点金额走字符串拼接进 SQL 引入精度误差;余额写入 NUMERIC(15,2) 列时由 DB 四舍五入。
updateData := gdb.Map{
walletEntity.AccountCol.Balance: w.Balance + req.DeltaBalance,
}
result, err := gfdb.DB(ctx, public.DbNameWallet).Model(ctx, public.TableNameWalletAccount).
Data(updateData).
Where(walletEntity.AccountCol.Id, w.Id).
Update()
if err != nil {
return err
}
affected, err := result.RowsAffected()
if err != nil {
return err
}
if affected == 0 {
return errors.New("钱包余额更新失败,请重试")
}
_, err = gfdb.DB(ctx, public.DbNameWallet).Model(ctx, public.TableNameWalletAccountLog).Data(&walletEntity.AccountLog{
UserID: req.UserId,
WalletID: w.Id,
OrderNo: req.OrderNo,
TransactionNo: uuid.NewString(),
Type: req.Type,
Amount: req.Amount,
BalanceBefore: w.Balance,
BalanceAfter: w.Balance + req.DeltaBalance,
Currency: w.Currency,
Description: req.Description,
ExtraData: req.ExtraData,
}).Insert()
return err
}
// ====================== 流水 ======================
// ListLogs 分页获取钱包流水
func (d *accountDao) ListLogs(ctx context.Context, req *walletDto.GetAccountLogsReq) (res []walletEntity.AccountLog, total int, err error) {
m := gfdb.DB(ctx, public.DbNameWallet).Model(ctx, public.TableNameWalletAccountLog).
Where(walletEntity.AccountLogCol.UserID, req.UserId).
OrderDesc(walletEntity.AccountLogCol.CreatedAt)
if req.Page != nil {
m.Page(int(req.Page.PageNum), int(req.Page.PageSize))
}
r, total, err := m.AllAndCount(false)
if err != nil {
return
}
err = r.Structs(&res)
return
}
-134
View File
@@ -1,134 +0,0 @@
package dao
import (
"context"
"errors"
"gitea.com/red-future/common/db/gfdb"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/util/gconv"
"shop-user-trade/consts/public"
walletConsts "shop-user-trade/consts/wallet"
walletDto "shop-user-trade/model/dto/wallet"
walletEntity "shop-user-trade/model/entity/wallet"
)
var Wallet = new(walletDao)
type walletDao struct{}
// GetByUserID 根据用户ID获取钱包
func (d *walletDao) GetByUserID(ctx context.Context, userID int64) (res *walletEntity.Wallet, err error) {
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameWallet).
Where(walletEntity.WalletCol.UserID, userID).
WhereNot(walletEntity.WalletCol.Status, int(walletConsts.WalletStatusFrozen)).
One()
if err != nil {
return
}
if r.IsEmpty() {
return nil, nil
}
err = r.Struct(&res)
return
}
// GetByID 根据ID获取钱包
func (d *walletDao) GetByID(ctx context.Context, walletID int64) (res *walletEntity.Wallet, err error) {
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameWallet).
Where(walletEntity.WalletCol.Id, walletID).
One()
if err != nil {
return
}
if r.IsEmpty() {
return nil, nil
}
err = r.Struct(&res)
return
}
// Create 创建钱包
func (d *walletDao) Create(ctx context.Context, req *walletDto.CreateWalletReq) (id int64, err error) {
entity := &walletEntity.Wallet{
UserID: req.UserId,
Balance: 0,
Currency: req.Currency,
Status: walletConsts.WalletStatusEnabled,
Version: 1,
}
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameWallet).Data(entity).Insert()
if err != nil {
return
}
return r.LastInsertId()
}
// UpdateBalance 更新余额(乐观锁)
func (d *walletDao) UpdateBalance(ctx context.Context, walletID int64, amount int64, opType string, version int64) (bool, error) {
var updateData gdb.Map
switch opType {
case "income":
updateData = gdb.Map{
"balance": gdb.Raw("balance + " + gconv.String(amount)),
"version": gdb.Raw("version + 1"),
}
case "expense":
updateData = gdb.Map{
"balance": gdb.Raw("balance - " + gconv.String(amount)),
"version": gdb.Raw("version + 1"),
}
case "freeze":
updateData = gdb.Map{
"status": int(walletConsts.WalletStatusFrozen),
}
case "unfreeze":
updateData = gdb.Map{
"status": int(walletConsts.WalletStatusEnabled),
}
default:
return false, errors.New("unsupported operation type")
}
result, err := gfdb.DB(ctx).Model(ctx, public.TableNameWallet).
Data(updateData).
Where(walletEntity.WalletCol.Id, walletID).
Where(walletEntity.WalletCol.Version, version).
Update()
if err != nil {
return false, err
}
rows, err := result.RowsAffected()
if err != nil {
return false, err
}
return rows > 0, nil
}
// CreateLog 创建钱包日志
func (d *walletDao) CreateLog(ctx context.Context, req *walletDto.CreateWalletLogReq) (id int64, err error) {
var entity *walletEntity.WalletLog
if err = gconv.Struct(req, &entity); err != nil {
return
}
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameWalletLog).Data(entity).Insert()
if err != nil {
return
}
return r.LastInsertId()
}
// ListLogs 获取钱包日志列表
func (d *walletDao) ListLogs(ctx context.Context, userID int64, page, pageSize int) (res []walletEntity.WalletLog, total int, err error) {
r, total, err := gfdb.DB(ctx).Model(ctx, public.TableNameWalletLog).
Where(walletEntity.WalletLogCol.UserID, userID).
OrderDesc(walletEntity.WalletLogCol.CreatedAt).
Page(page, pageSize).
AllAndCount(false)
if err != nil {
return
}
err = r.Structs(&res)
return
}
+48 -39
View File
@@ -1,33 +1,42 @@
module shop-user-trade module shop-user-trade
go 1.25.8 go 1.26.1
require ( require (
gitea.com/red-future/common v0.0.7 gitea.redpowerfuture.com/red-future/common v0.0.33
github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.10.0 github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.10.2
github.com/gogf/gf/v2 v2.10.0 github.com/gogf/gf/v2 v2.10.2
github.com/google/uuid v1.6.0
) )
//replace gitea.com/red-future/common v0.0.7 => ../common require (
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
)
require ( require (
github.com/BurntSushi/toml v1.5.0 // indirect github.com/BurntSushi/toml v1.6.0 // indirect
github.com/armon/go-metrics v0.4.1 // indirect github.com/armon/go-metrics v0.4.1 // indirect
github.com/bwmarrin/snowflake v0.3.0 // indirect github.com/bwmarrin/snowflake v0.3.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/clbanning/mxj/v2 v2.7.0 // indirect github.com/clbanning/mxj/v2 v2.7.0 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/dgraph-io/badger/v4 v4.2.0 // indirect github.com/dgraph-io/badger/v4 v4.2.0 // indirect
github.com/dgraph-io/ristretto v0.1.1 // indirect github.com/dgraph-io/ristretto v0.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/emirpasic/gods/v2 v2.0.0-alpha // indirect github.com/emirpasic/gods/v2 v2.0.0-alpha // indirect
github.com/fatih/color v1.19.0 // indirect github.com/fatih/color v1.19.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fsnotify/fsnotify v1.10.1 // indirect
github.com/go-ego/gse v1.0.2 // indirect
github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/gogf/gf/contrib/nosql/redis/v2 v2.9.1 // indirect github.com/goccy/go-json v0.10.6 // indirect
github.com/gogf/gf/contrib/nosql/redis/v2 v2.10.2
github.com/gogf/gf/contrib/registry/consul/v2 v2.9.5 // indirect github.com/gogf/gf/contrib/registry/consul/v2 v2.9.5 // indirect
github.com/gogf/gf/contrib/trace/otlphttp/v2 v2.9.5 // indirect github.com/gogf/gf/contrib/trace/otlphttp/v2 v2.9.5 // indirect
github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect
@@ -36,9 +45,9 @@ require (
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v1.0.0 // indirect github.com/golang/snappy v1.0.0 // indirect
github.com/google/flatbuffers v1.12.1 // indirect github.com/google/btree v1.1.3 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/flatbuffers v25.12.19+incompatible // indirect
github.com/gorilla/websocket v1.5.3 // indirect github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
github.com/grokify/html-strip-tags-go v0.1.0 // indirect github.com/grokify/html-strip-tags-go v0.1.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
github.com/hashicorp/consul/api v1.33.5 // indirect github.com/hashicorp/consul/api v1.33.5 // indirect
@@ -48,45 +57,45 @@ require (
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-rootcerts v1.0.2 // indirect github.com/hashicorp/go-rootcerts v1.0.2 // indirect
github.com/hashicorp/go-version v1.9.0 // indirect
github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect
github.com/hashicorp/serf v0.10.1 // indirect github.com/hashicorp/serf v0.10.1 // indirect
github.com/klauspost/compress v1.18.4 // indirect github.com/klauspost/compress v1.18.6 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/lib/pq v1.10.9 // indirect github.com/lib/pq v1.12.3 // indirect
github.com/magiconair/properties v1.8.10 // indirect github.com/magiconair/properties v1.8.10 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.15 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.22 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mattn/go-runewidth v0.0.24 // indirect
github.com/miekg/dns v1.1.72 // indirect github.com/miekg/dns v1.1.72 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/olekukonko/errors v1.1.0 // indirect github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
github.com/olekukonko/ll v0.0.9 // indirect github.com/olekukonko/errors v1.3.0 // indirect
github.com/olekukonko/tablewriter v1.1.0 // indirect github.com/olekukonko/ll v0.1.8 // indirect
github.com/olekukonko/tablewriter v1.1.4 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/redis/go-redis/v9 v9.18.0 // indirect github.com/r3labs/diff/v2 v2.15.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/redis/go-redis/v9 v9.21.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/tiger1103/gfast-token v1.0.10 // indirect github.com/tiger1103/gfast-token v1.0.10 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/vcaesar/cedar v0.30.0 // indirect
github.com/xdg-go/scram v1.2.0 // indirect github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect go.mongodb.org/mongo-driver/v2 v2.6.0 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect go.opencensus.io v0.24.0 // indirect
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
go.opencensus.io v0.23.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel v1.39.0 // indirect go.opentelemetry.io/otel v1.44.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect
go.opentelemetry.io/otel/metric v1.39.0 // indirect go.opentelemetry.io/otel/metric v1.44.0 // indirect
go.opentelemetry.io/otel/sdk v1.39.0 // indirect go.opentelemetry.io/otel/sdk v1.44.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect go.opentelemetry.io/otel/trace v1.44.0 // indirect
go.opentelemetry.io/proto/otlp v1.7.1 // indirect go.opentelemetry.io/proto/otlp v1.7.1 // indirect
go.uber.org/atomic v1.11.0 // indirect go.uber.org/atomic v1.11.0 // indirect
golang.org/x/crypto v0.49.0 // indirect golang.org/x/exp v0.0.0-20260611194520-c48552f49976 // indirect
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 // indirect golang.org/x/net v0.56.0 // indirect
golang.org/x/net v0.52.0 // indirect golang.org/x/sys v0.46.0 // indirect
golang.org/x/sync v0.20.0 // indirect golang.org/x/text v0.38.0 // indirect
golang.org/x/sys v0.42.0 // indirect google.golang.org/appengine v1.6.7 // indirect
golang.org/x/text v0.35.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
google.golang.org/grpc v1.79.3 // indirect google.golang.org/grpc v1.79.3 // indirect
+108 -102
View File
@@ -1,9 +1,13 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
gitea.com/red-future/common v0.0.7 h1:PbSazDCjmr+HZ4ckSSJYSCC+G/qwC5ZKbLEdsItanQ0= gitea.redpowerfuture.com/red-future/common v0.0.23 h1:xieoA00iKOCDm5SO9iXn+cSyMKBAlZwI0fuEVPWrHLg=
gitea.com/red-future/common v0.0.7/go.mod h1:JgBczlUF8xA8S0YbvDaMLxvvpmBGgjJA6po+EbodzZA= gitea.redpowerfuture.com/red-future/common v0.0.23/go.mod h1:50U1Xi+Ie56z09S5LQbZvaken0Mxv3OeS9LgR7U/ZRY=
gitea.redpowerfuture.com/red-future/common v0.0.29 h1:5McaN5pSewvrLUHQzWMX6EaUvD+B5I5bMYoU+clHJk4=
gitea.redpowerfuture.com/red-future/common v0.0.29/go.mod h1:50U1Xi+Ie56z09S5LQbZvaken0Mxv3OeS9LgR7U/ZRY=
gitea.redpowerfuture.com/red-future/common v0.0.33 h1:AhWJ6l9zrjc1U0UEfyIZu8wkkVFxNe0hfuA51vOnOIo=
gitea.redpowerfuture.com/red-future/common v0.0.33/go.mod h1:FtI9KJJSKo4/K0emjVkbL8yoSIPHJdZXr27vnScQpmM=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
@@ -36,6 +40,10 @@ github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp
github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME= github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -47,8 +55,6 @@ github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWa
github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
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/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 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/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
@@ -63,8 +69,10 @@ github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
github.com/go-ego/gse v1.0.2 h1:+27lYFPhQEhA9igtdOsJPRKYL/k3TwYsxBF5jr6KFv4=
github.com/go-ego/gse v1.0.2/go.mod h1:Fy35G+q7VV7Et1zIKO8o/sW1kkugV3znXap/lF/11zc=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
@@ -75,18 +83,20 @@ github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ4
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.10.0 h1:39+jbTenm7KBj4hO2C8ANAxVHpX/7OuRDs1VcGC9ylA= github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.10.0/go.mod h1:B0s0fVzn0W220E8UTpSGzrrGKsop5KcB90twBeLCiz0= github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gogf/gf/contrib/nosql/redis/v2 v2.9.1 h1:egobo4YfQX3C4NtrEFunBqMX3jsddagklgut9u91+BM= github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.10.2 h1:u8EpP24GkprogROnJ7htMov9Fc66pTP1eVYrWxiCYOs=
github.com/gogf/gf/contrib/nosql/redis/v2 v2.9.1/go.mod h1:YQ+u5Cs5N2ETCeQaLbv29z/UWYuxw27mJpUkOLj0kJ8= github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.10.2/go.mod h1:GmvM3r8GVByVMi4RD2+MCs5+CfxVXPMeT8mVDkAaAXE=
github.com/gogf/gf/contrib/nosql/redis/v2 v2.10.2 h1:iTQegT+lEg/wDKvj2mi3W1wrdrwFarjokf88EXVVgu4=
github.com/gogf/gf/contrib/nosql/redis/v2 v2.10.2/go.mod h1:ZRw3GNz5cq4uYrW4TPSVyrYWaoqzujKdWro/AOcGBaE=
github.com/gogf/gf/contrib/registry/consul/v2 v2.9.5 h1:eUqwJ/qNH8lJ6yssiqskazgp1ACQuNU6zXlLOZVuXTQ= github.com/gogf/gf/contrib/registry/consul/v2 v2.9.5 h1:eUqwJ/qNH8lJ6yssiqskazgp1ACQuNU6zXlLOZVuXTQ=
github.com/gogf/gf/contrib/registry/consul/v2 v2.9.5/go.mod h1:sjQyMry9+0POYZCA6lHXBxO77WoNKkruJpRB4xKqk5k= github.com/gogf/gf/contrib/registry/consul/v2 v2.9.5/go.mod h1:sjQyMry9+0POYZCA6lHXBxO77WoNKkruJpRB4xKqk5k=
github.com/gogf/gf/contrib/trace/otlphttp/v2 v2.9.5 h1:tHUEZYB5GTqEYYVDYnlGobf1xISARKDE4KHVlgjwTec= github.com/gogf/gf/contrib/trace/otlphttp/v2 v2.9.5 h1:tHUEZYB5GTqEYYVDYnlGobf1xISARKDE4KHVlgjwTec=
github.com/gogf/gf/contrib/trace/otlphttp/v2 v2.9.5/go.mod h1:cfzTn2HS9RDX8f5pUVkbGxUWcSosouqfNQ1G6cY0V88= github.com/gogf/gf/contrib/trace/otlphttp/v2 v2.9.5/go.mod h1:cfzTn2HS9RDX8f5pUVkbGxUWcSosouqfNQ1G6cY0V88=
github.com/gogf/gf/v2 v2.10.0 h1:rzDROlyqGMe/eM6dCalSR8dZOuMIdLhmxKSH1DGhbFs= github.com/gogf/gf/v2 v2.10.2 h1:46IO0Uc8e85/FqdftJFskfDejJLBL0JBnGS5qOftUu8=
github.com/gogf/gf/v2 v2.10.0/go.mod h1:Svl1N+E8G/QshU2DUbh/3J/AJauqCgUnxHurXWR4Qx0= github.com/gogf/gf/v2 v2.10.2/go.mod h1:Svl1N+E8G/QshU2DUbh/3J/AJauqCgUnxHurXWR4Qx0=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
@@ -114,10 +124,10 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw= github.com/google/flatbuffers v25.12.19+incompatible h1:haMV2JRRJCe1998HeW/p0X9UaMTK6SDo0ffLn2+DbLs=
github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/flatbuffers v25.12.19+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@@ -130,8 +140,8 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 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/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
github.com/grokify/html-strip-tags-go v0.1.0 h1:03UrQLjAny8xci+R+qjCce/MYnpNXCtgzltlQbOBae4= github.com/grokify/html-strip-tags-go v0.1.0 h1:03UrQLjAny8xci+R+qjCce/MYnpNXCtgzltlQbOBae4=
github.com/grokify/html-strip-tags-go v0.1.0/go.mod h1:ZdzgfHEzAfz9X6Xe5eBLVblWIxXfYSQ40S/VKrAOGpc= github.com/grokify/html-strip-tags-go v0.1.0/go.mod h1:ZdzgfHEzAfz9X6Xe5eBLVblWIxXfYSQ40S/VKrAOGpc=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
@@ -139,7 +149,6 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l8
github.com/hashicorp/consul/api v1.33.5 h1:Nn6q87zudRU1rLBTJEgaWxz9STCNadilLCD7B8OA5aI= github.com/hashicorp/consul/api v1.33.5 h1:Nn6q87zudRU1rLBTJEgaWxz9STCNadilLCD7B8OA5aI=
github.com/hashicorp/consul/api v1.33.5/go.mod h1:pa6fJOSHKLOzNHpUVeqLDtxA5+J1D7NNzLasuk8eRXA= github.com/hashicorp/consul/api v1.33.5/go.mod h1:pa6fJOSHKLOzNHpUVeqLDtxA5+J1D7NNzLasuk8eRXA=
github.com/hashicorp/consul/sdk v0.17.3 h1:oZMMxzQGSsiT+ToOH50y3Qcs0nc9Ud+7L5lRx+EmMU0= github.com/hashicorp/consul/sdk v0.17.3 h1:oZMMxzQGSsiT+ToOH50y3Qcs0nc9Ud+7L5lRx+EmMU0=
github.com/hashicorp/consul/sdk v0.17.3/go.mod h1:jnOmYjiNfVRpBaujQ1DFFVs0N6g3S1y6wygSjLTzYfc=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -169,8 +178,8 @@ github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI= github.com/hashicorp/go-version v1.9.0 h1:CeOIz6k+LoN3qX9Z0tyQrPtiB1DFYRPfCIBtaXPSCnA=
github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.9.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
@@ -185,8 +194,8 @@ github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -198,8 +207,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
@@ -207,17 +216,17 @@ github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVc
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
@@ -232,12 +241,14 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM= github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI= github.com/olekukonko/errors v1.3.0 h1:teJvgLGUEqMzBUms+Dj3/3szNqCG/Jdw9iDbum8fR6U=
github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g= github.com/olekukonko/errors v1.3.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
github.com/olekukonko/tablewriter v1.1.0 h1:N0LHrshF4T39KvI96fn6GT8HEjXRXYNDrDjKFDB7RIY= github.com/olekukonko/ll v0.1.8 h1:ysHCJRGHYKzmBSdz9w5AySztx7lG8SQY+naTGYUbsz8=
github.com/olekukonko/tablewriter v1.1.0/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo= github.com/olekukonko/ll v0.1.8/go.mod h1:RPRC6UcscfFZgjo1nulkfMH5IM0QAYim0LfnMvUuozw=
github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I=
github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
@@ -262,13 +273,11 @@ github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8b
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs= github.com/r3labs/diff/v2 v2.15.1 h1:EOrVqPUzi+njlumoqJwiS/TgGgmZo83619FNDB9xQUg=
github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0= github.com/r3labs/diff/v2 v2.15.1/go.mod h1:I8noH9Fc2fjSaMxqF3G2lhDdC0b+JXCfyx85tWFM9kc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/redis/go-redis/v9 v9.21.0 h1:FPBE4hhbAke+TLmcY3WkpbDffJEomdqPn3HYiqAtL9E=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/redis/go-redis/v9 v9.21.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
@@ -276,51 +285,61 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/tiger1103/gfast-token v1.0.10 h1:fNiBE/Dq5iTHvTGlCx3DmXa2o4hr0NtumFpffZ39k6s= github.com/tiger1103/gfast-token v1.0.10 h1:fNiBE/Dq5iTHvTGlCx3DmXa2o4hr0NtumFpffZ39k6s=
github.com/tiger1103/gfast-token v1.0.10/go.mod h1:a/21mxmj7zFeNvjhZSC0XpEAFHfb1aT2k6DXnufFU1s= github.com/tiger1103/gfast-token v1.0.10/go.mod h1:a/21mxmj7zFeNvjhZSC0XpEAFHfb1aT2k6DXnufFU1s=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/vcaesar/cedar v0.30.0 h1:9fSDpM7FTjjUdPiBUUa0MWYMRGSEcqgFXvppZcZ4d7Y=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/vcaesar/cedar v0.30.0/go.mod h1:lyuGvALuZZDPNXwpzv/9LyxW+8Y6faN7zauFezNsnik=
github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs= github.com/vcaesar/tt v0.20.1 h1:D/jUeeVCNbq3ad8M7hhtB3J9x5RZ6I1n1eZ0BJp7M+4=
github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8= github.com/vcaesar/tt v0.20.1/go.mod h1:cH2+AwGAJm19Wa6xvEa+0r+sXDJBT0QgNQey6mwqLeU=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= go.mongodb.org/mongo-driver/v2 v2.6.0 h1:b9sJOYrkmt4l8bY43ZenFBcPlhYIjaOfYHLtbB/5qi8=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= go.mongodb.org/mongo-driver/v2 v2.6.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
@@ -332,26 +351,22 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA= golang.org/x/exp v0.0.0-20260611194520-c48552f49976 h1:X8Hz2ImujgbmetVuW+w2YkyZChE3cBpZi2P158rTG9M=
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ= golang.org/x/exp v0.0.0-20260611194520-c48552f49976/go.mod h1:vnf4pv9iKZXY58sQE1L86zmNWJ4159e1RkcWiLCkeEY=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -360,9 +375,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -371,9 +385,7 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -392,27 +404,20 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -422,9 +427,7 @@ golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.46.0 h1:7jTurBkPZu4moS/Uy4OQT1M+QBlsj3wejyZwsT8Z7rk=
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -433,6 +436,9 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+310 -51
View File
@@ -1,16 +1,19 @@
-- shop_user_trade 数据库初始化脚本 -----------shop-user-trade 2026-08-28 12:00:00--------------
--------------------pgsql创建knapsack表语句---------------------------
-- 背包表 -- 背包表
CREATE TABLE IF NOT EXISTS knapsack ( CREATE TABLE IF NOT EXISTS knapsack (
-- 基础字段(继承 SQLBaseCol 通用字段,与 SQLBaseDO 对齐)
id BIGINT PRIMARY KEY, id BIGINT PRIMARY KEY,
tenant_id BIGINT NOT NULL DEFAULT 0, tenant_id BIGINT NOT NULL DEFAULT 0,
creator VARCHAR(64) NOT NULL DEFAULT '', creator VARCHAR(64) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updater VARCHAR(64) NOT NULL DEFAULT '', updater VARCHAR(64) NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMPTZ, deleted_at timestamp(6),
is_deleted BOOLEAN NOT NULL DEFAULT FALSE,
-- 背包核心字段
user_id BIGINT NOT NULL, user_id BIGINT NOT NULL,
asset_id BIGINT NOT NULL, asset_id BIGINT NOT NULL,
asset_name VARCHAR(255) NOT NULL DEFAULT '', asset_name VARCHAR(255) NOT NULL DEFAULT '',
@@ -31,22 +34,56 @@ CREATE TABLE IF NOT EXISTS knapsack (
virtual_asset_config JSONB virtual_asset_config JSONB
); );
-- 索引
CREATE INDEX IF NOT EXISTS idx_knapsack_user_id ON knapsack(user_id); CREATE INDEX IF NOT EXISTS idx_knapsack_user_id ON knapsack(user_id);
CREATE INDEX IF NOT EXISTS idx_knapsack_asset_id ON knapsack(asset_id); CREATE INDEX IF NOT EXISTS idx_knapsack_asset_id ON knapsack(asset_id);
CREATE INDEX IF NOT EXISTS idx_knapsack_status ON knapsack(status); CREATE INDEX IF NOT EXISTS idx_knapsack_status ON knapsack(status);
CREATE INDEX IF NOT EXISTS idx_knapsack_tenant_id ON knapsack(tenant_id); CREATE INDEX IF NOT EXISTS idx_knapsack_tenant_id ON knapsack(tenant_id);
-- 表和字段注释
COMMENT ON TABLE knapsack IS '背包表';
COMMENT ON COLUMN knapsack.id IS '主键ID(非自增)';
COMMENT ON COLUMN knapsack.tenant_id IS '租户ID';
COMMENT ON COLUMN knapsack.creator IS '创建人';
COMMENT ON COLUMN knapsack.created_at IS '创建时间';
COMMENT ON COLUMN knapsack.updater IS '更新人';
COMMENT ON COLUMN knapsack.updated_at IS '更新时间';
COMMENT ON COLUMN knapsack.deleted_at IS '删除时间(软删)';
COMMENT ON COLUMN knapsack.user_id IS '用户ID';
COMMENT ON COLUMN knapsack.asset_id IS '资产ID';
COMMENT ON COLUMN knapsack.asset_name IS '资产名称';
COMMENT ON COLUMN knapsack.sku_id IS 'SKU ID';
COMMENT ON COLUMN knapsack.sku_name IS 'SKU名称';
COMMENT ON COLUMN knapsack.spec_values IS '规格值(JSONB)';
COMMENT ON COLUMN knapsack.image_url IS '资产图片URL';
COMMENT ON COLUMN knapsack.type IS '资产类型:physical/virtual/service';
COMMENT ON COLUMN knapsack.stock_detail_id IS '库存明细ID(明细模式)';
COMMENT ON COLUMN knapsack.batch_id IS '批次ID(批次模式)';
COMMENT ON COLUMN knapsack.batch_no IS '批次号(批次模式)';
COMMENT ON COLUMN knapsack.stock_mode IS '库存管理模式:1-明细模式,2-批次模式';
COMMENT ON COLUMN knapsack.status IS '状态:0禁用/1启用/2已使用/3已上架/4已过期';
COMMENT ON COLUMN knapsack.used_at IS '使用时间(时间戳)';
COMMENT ON COLUMN knapsack.expire_at IS '过期时间(时间戳)';
COMMENT ON COLUMN knapsack.physical_asset_config IS '实物资产配置(JSONB)';
COMMENT ON COLUMN knapsack.service_asset_config IS '服务资产配置(JSONB)';
COMMENT ON COLUMN knapsack.virtual_asset_config IS '虚拟资产配置(JSONB)';
--------------------pgsql创建knapsack表语句---------------------------
--------------------pgsql创建knapsack_log表语句---------------------------
-- 背包日志表 -- 背包日志表
CREATE TABLE IF NOT EXISTS knapsack_log ( CREATE TABLE IF NOT EXISTS knapsack_log (
-- 基础字段(继承 SQLBaseCol 通用字段,与 SQLBaseDO 对齐)
id BIGINT PRIMARY KEY, id BIGINT PRIMARY KEY,
tenant_id BIGINT NOT NULL DEFAULT 0, tenant_id BIGINT NOT NULL DEFAULT 0,
creator VARCHAR(64) NOT NULL DEFAULT '', creator VARCHAR(64) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updater VARCHAR(64) NOT NULL DEFAULT '', updater VARCHAR(64) NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMPTZ, deleted_at timestamp(6),
is_deleted BOOLEAN NOT NULL DEFAULT FALSE,
-- 日志核心字段
knapsack_id BIGINT NOT NULL, knapsack_id BIGINT NOT NULL,
user_id BIGINT NOT NULL, user_id BIGINT NOT NULL,
action VARCHAR(32) NOT NULL, action VARCHAR(32) NOT NULL,
@@ -55,19 +92,41 @@ CREATE TABLE IF NOT EXISTS knapsack_log (
description TEXT NOT NULL DEFAULT '' description TEXT NOT NULL DEFAULT ''
); );
-- 索引
CREATE INDEX IF NOT EXISTS idx_knapsack_log_knapsack_id ON knapsack_log(knapsack_id); CREATE INDEX IF NOT EXISTS idx_knapsack_log_knapsack_id ON knapsack_log(knapsack_id);
-- 表和字段注释
COMMENT ON TABLE knapsack_log IS '背包日志表';
COMMENT ON COLUMN knapsack_log.id IS '主键ID(非自增)';
COMMENT ON COLUMN knapsack_log.tenant_id IS '租户ID';
COMMENT ON COLUMN knapsack_log.creator IS '创建人';
COMMENT ON COLUMN knapsack_log.created_at IS '创建时间';
COMMENT ON COLUMN knapsack_log.updater IS '更新人';
COMMENT ON COLUMN knapsack_log.updated_at IS '更新时间';
COMMENT ON COLUMN knapsack_log.deleted_at IS '删除时间(软删)';
COMMENT ON COLUMN knapsack_log.knapsack_id IS '背包项ID';
COMMENT ON COLUMN knapsack_log.user_id IS '用户ID';
COMMENT ON COLUMN knapsack_log.action IS '操作类型';
COMMENT ON COLUMN knapsack_log.operator_id IS '操作者ID';
COMMENT ON COLUMN knapsack_log.operator_name IS '操作者名称';
COMMENT ON COLUMN knapsack_log.description IS '操作描述';
--------------------pgsql创建knapsack_log表语句---------------------------
--------------------pgsql创建market表语句---------------------------
-- 市场表 -- 市场表
CREATE TABLE IF NOT EXISTS market ( CREATE TABLE IF NOT EXISTS market (
-- 基础字段(继承 SQLBaseCol 通用字段,与 SQLBaseDO 对齐)
id BIGINT PRIMARY KEY, id BIGINT PRIMARY KEY,
tenant_id BIGINT NOT NULL DEFAULT 0, tenant_id BIGINT NOT NULL DEFAULT 0,
creator VARCHAR(64) NOT NULL DEFAULT '', creator VARCHAR(64) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updater VARCHAR(64) NOT NULL DEFAULT '', updater VARCHAR(64) NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMPTZ, deleted_at timestamp(6),
is_deleted BOOLEAN NOT NULL DEFAULT FALSE,
-- 市场核心字段
knapsack_id BIGINT NOT NULL, knapsack_id BIGINT NOT NULL,
user_id BIGINT NOT NULL, user_id BIGINT NOT NULL,
asset_id BIGINT NOT NULL, asset_id BIGINT NOT NULL,
@@ -87,22 +146,55 @@ CREATE TABLE IF NOT EXISTS market (
list_expire_at BIGINT list_expire_at BIGINT
); );
-- 索引
CREATE INDEX IF NOT EXISTS idx_market_user_id ON market(user_id); CREATE INDEX IF NOT EXISTS idx_market_user_id ON market(user_id);
CREATE INDEX IF NOT EXISTS idx_market_knapsack_id ON market(knapsack_id); CREATE INDEX IF NOT EXISTS idx_market_knapsack_id ON market(knapsack_id);
CREATE INDEX IF NOT EXISTS idx_market_status ON market(status); CREATE INDEX IF NOT EXISTS idx_market_status ON market(status);
CREATE INDEX IF NOT EXISTS idx_market_tenant_id ON market(tenant_id); CREATE INDEX IF NOT EXISTS idx_market_tenant_id ON market(tenant_id);
-- 表和字段注释
COMMENT ON TABLE market IS '市场表';
COMMENT ON COLUMN market.id IS '主键ID(非自增)';
COMMENT ON COLUMN market.tenant_id IS '租户ID';
COMMENT ON COLUMN market.creator IS '创建人';
COMMENT ON COLUMN market.created_at IS '创建时间';
COMMENT ON COLUMN market.updater IS '更新人';
COMMENT ON COLUMN market.updated_at IS '更新时间';
COMMENT ON COLUMN market.deleted_at IS '删除时间(软删)';
COMMENT ON COLUMN market.knapsack_id IS '背包项ID';
COMMENT ON COLUMN market.user_id IS '卖家用户ID';
COMMENT ON COLUMN market.asset_id IS '资产ID';
COMMENT ON COLUMN market.asset_name IS '资产名称';
COMMENT ON COLUMN market.sku_id IS 'SKU ID';
COMMENT ON COLUMN market.sku_name IS 'SKU名称';
COMMENT ON COLUMN market.image_url IS '资产图片URL';
COMMENT ON COLUMN market.type IS '资产类型:physical/virtual/service';
COMMENT ON COLUMN market.stock_detail_id IS '库存明细ID(明细模式)';
COMMENT ON COLUMN market.batch_id IS '批次ID(批次模式)';
COMMENT ON COLUMN market.batch_no IS '批次号(批次模式)';
COMMENT ON COLUMN market.stock_mode IS '库存管理模式:1-明细模式,2-批次模式';
COMMENT ON COLUMN market.price IS '售价(分)';
COMMENT ON COLUMN market.original_price IS '原价(分)';
COMMENT ON COLUMN market.description IS '描述信息';
COMMENT ON COLUMN market.status IS '状态:1活跃/2已售/3下架/4过期';
COMMENT ON COLUMN market.list_expire_at IS '上架过期时间(时间戳)';
--------------------pgsql创建market表语句---------------------------
--------------------pgsql创建market_log表语句---------------------------
-- 市场日志表 -- 市场日志表
CREATE TABLE IF NOT EXISTS market_log ( CREATE TABLE IF NOT EXISTS market_log (
-- 基础字段(继承 SQLBaseCol 通用字段,与 SQLBaseDO 对齐)
id BIGINT PRIMARY KEY, id BIGINT PRIMARY KEY,
tenant_id BIGINT NOT NULL DEFAULT 0, tenant_id BIGINT NOT NULL DEFAULT 0,
creator VARCHAR(64) NOT NULL DEFAULT '', creator VARCHAR(64) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updater VARCHAR(64) NOT NULL DEFAULT '', updater VARCHAR(64) NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMPTZ, deleted_at timestamp(6),
is_deleted BOOLEAN NOT NULL DEFAULT FALSE,
-- 日志核心字段
market_id BIGINT NOT NULL, market_id BIGINT NOT NULL,
knapsack_id BIGINT NOT NULL, knapsack_id BIGINT NOT NULL,
user_id BIGINT NOT NULL, user_id BIGINT NOT NULL,
@@ -112,53 +204,220 @@ CREATE TABLE IF NOT EXISTS market_log (
description TEXT NOT NULL DEFAULT '' description TEXT NOT NULL DEFAULT ''
); );
-- 索引
CREATE INDEX IF NOT EXISTS idx_market_log_market_id ON market_log(market_id); CREATE INDEX IF NOT EXISTS idx_market_log_market_id ON market_log(market_id);
-- 表和字段注释
COMMENT ON TABLE market_log IS '市场日志表';
COMMENT ON COLUMN market_log.id IS '主键ID(非自增)';
COMMENT ON COLUMN market_log.tenant_id IS '租户ID';
COMMENT ON COLUMN market_log.creator IS '创建人';
COMMENT ON COLUMN market_log.created_at IS '创建时间';
COMMENT ON COLUMN market_log.updater IS '更新人';
COMMENT ON COLUMN market_log.updated_at IS '更新时间';
COMMENT ON COLUMN market_log.deleted_at IS '删除时间(软删)';
COMMENT ON COLUMN market_log.market_id IS '市场物品ID';
COMMENT ON COLUMN market_log.knapsack_id IS '背包项ID';
COMMENT ON COLUMN market_log.user_id IS '用户ID';
COMMENT ON COLUMN market_log.action IS '操作类型';
COMMENT ON COLUMN market_log.operator_id IS '操作者ID';
COMMENT ON COLUMN market_log.operator_name IS '操作者名称';
COMMENT ON COLUMN market_log.description IS '操作描述';
--------------------pgsql创建market_log表语句---------------------------
--------------------pgsql创建wallet_account表语句---------------------------
-- 钱包表 -- 钱包表
CREATE TABLE IF NOT EXISTS wallet ( CREATE TABLE IF NOT EXISTS wallet_account (
-- 基础字段(继承 SQLBaseCol 通用字段,与 SQLBaseDO 对齐)
id BIGINT PRIMARY KEY, id BIGINT PRIMARY KEY,
tenant_id BIGINT NOT NULL DEFAULT 0, tenant_id BIGINT NOT NULL DEFAULT 0,
creator VARCHAR(64) NOT NULL DEFAULT '', creator VARCHAR(64) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updater VARCHAR(64) NOT NULL DEFAULT '', updater VARCHAR(64) NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMPTZ, deleted_at timestamp(6),
is_deleted BOOLEAN NOT NULL DEFAULT FALSE,
user_id BIGINT NOT NULL UNIQUE, -- 钱包核心字段
balance BIGINT NOT NULL DEFAULT 0, user_id BIGINT NOT NULL,
user_name VARCHAR(64) NOT NULL DEFAULT '',
balance NUMERIC(15,2) NOT NULL DEFAULT 0,
currency VARCHAR(16) NOT NULL DEFAULT 'CNY', currency VARCHAR(16) NOT NULL DEFAULT 'CNY',
status SMALLINT NOT NULL DEFAULT 1, status SMALLINT NOT NULL DEFAULT 1
version BIGINT NOT NULL DEFAULT 1
); );
CREATE INDEX IF NOT EXISTS idx_wallet_user_id ON wallet(user_id); -- 索引
CREATE INDEX IF NOT EXISTS idx_wallet_tenant_id ON wallet(tenant_id); CREATE UNIQUE INDEX IF NOT EXISTS uk_account_user_id ON wallet_account(user_id);
CREATE INDEX IF NOT EXISTS idx_account_user_id ON wallet_account(user_id);
CREATE INDEX IF NOT EXISTS idx_account_tenant_id ON wallet_account(tenant_id);
-- 表和字段注释
COMMENT ON TABLE wallet_account IS '钱包表';
COMMENT ON COLUMN wallet_account.id IS '主键ID(非自增)';
COMMENT ON COLUMN wallet_account.tenant_id IS '租户ID';
COMMENT ON COLUMN wallet_account.creator IS '创建人';
COMMENT ON COLUMN wallet_account.created_at IS '创建时间';
COMMENT ON COLUMN wallet_account.updater IS '更新人';
COMMENT ON COLUMN wallet_account.updated_at IS '更新时间';
COMMENT ON COLUMN wallet_account.deleted_at IS '删除时间(软删)';
COMMENT ON COLUMN wallet_account.user_id IS '用户ID(唯一)';
COMMENT ON COLUMN wallet_account.user_name IS '用户名';
COMMENT ON COLUMN wallet_account.balance IS '可用余额(元,保留2位小数),可为负=欠费';
COMMENT ON COLUMN wallet_account.currency IS '货币类型:CNY-人民币';
COMMENT ON COLUMN wallet_account.status IS '状态:1启用/0禁用/-1冻结';
--------------------pgsql创建wallet_account表语句---------------------------
--------------------pgsql创建wallet_account_log表语句---------------------------
-- 钱包日志表 -- 钱包日志表
CREATE TABLE IF NOT EXISTS wallet_log ( CREATE TABLE IF NOT EXISTS wallet_account_log (
-- 基础字段(继承 SQLBaseCol 通用字段,与 SQLBaseDO 对齐)
id BIGINT PRIMARY KEY, id BIGINT PRIMARY KEY,
tenant_id BIGINT NOT NULL DEFAULT 0, tenant_id BIGINT NOT NULL DEFAULT 0,
creator VARCHAR(64) NOT NULL DEFAULT '', creator VARCHAR(64) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updater VARCHAR(64) NOT NULL DEFAULT '', updater VARCHAR(64) NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMPTZ, deleted_at timestamp(6),
is_deleted BOOLEAN NOT NULL DEFAULT FALSE,
-- 流水核心字段
user_id BIGINT NOT NULL, user_id BIGINT NOT NULL,
wallet_id BIGINT NOT NULL, wallet_id BIGINT NOT NULL,
order_no VARCHAR(64) NOT NULL DEFAULT '', order_no VARCHAR(64) NOT NULL DEFAULT '',
transaction_no VARCHAR(64) NOT NULL DEFAULT '', transaction_no VARCHAR(64) NOT NULL DEFAULT '',
type VARCHAR(16) NOT NULL, type VARCHAR(16) NOT NULL,
amount BIGINT NOT NULL DEFAULT 0, amount NUMERIC(15,2) NOT NULL DEFAULT 0,
balance_before BIGINT NOT NULL DEFAULT 0, balance_before NUMERIC(15,2) NOT NULL DEFAULT 0,
balance_after BIGINT NOT NULL DEFAULT 0, balance_after NUMERIC(15,2) NOT NULL DEFAULT 0,
currency VARCHAR(16) NOT NULL DEFAULT 'CNY', currency VARCHAR(16) NOT NULL DEFAULT 'CNY',
description TEXT NOT NULL DEFAULT '', description TEXT NOT NULL DEFAULT '',
extra_data JSONB extra_data JSONB
); );
CREATE INDEX IF NOT EXISTS idx_wallet_log_user_id ON wallet_log(user_id); -- 索引
CREATE INDEX IF NOT EXISTS idx_wallet_log_wallet_id ON wallet_log(wallet_id); CREATE INDEX IF NOT EXISTS idx_account_log_user_id ON wallet_account_log(user_id);
CREATE INDEX IF NOT EXISTS idx_wallet_log_order_no ON wallet_log(order_no); CREATE INDEX IF NOT EXISTS idx_account_log_wallet_id ON wallet_account_log(wallet_id);
CREATE INDEX IF NOT EXISTS idx_account_log_order_no ON wallet_account_log(order_no);
CREATE UNIQUE INDEX IF NOT EXISTS uk_account_log_transaction ON wallet_account_log(transaction_no) WHERE transaction_no <> '';
-- 表和字段注释
COMMENT ON TABLE wallet_account_log IS '钱包日志表';
COMMENT ON COLUMN wallet_account_log.id IS '主键ID(非自增)';
COMMENT ON COLUMN wallet_account_log.tenant_id IS '租户ID';
COMMENT ON COLUMN wallet_account_log.creator IS '创建人';
COMMENT ON COLUMN wallet_account_log.created_at IS '创建时间';
COMMENT ON COLUMN wallet_account_log.updater IS '更新人';
COMMENT ON COLUMN wallet_account_log.updated_at IS '更新时间';
COMMENT ON COLUMN wallet_account_log.deleted_at IS '删除时间(软删)';
COMMENT ON COLUMN wallet_account_log.user_id IS '用户ID';
COMMENT ON COLUMN wallet_account_log.wallet_id IS '钱包ID';
COMMENT ON COLUMN wallet_account_log.order_no IS '业务单据号(计费单号/充值单号)';
COMMENT ON COLUMN wallet_account_log.transaction_no IS '账务流水号(唯一,幂等)';
COMMENT ON COLUMN wallet_account_log.type IS '账务类型:income/expense';
COMMENT ON COLUMN wallet_account_log.amount IS '本次变动金额(元,保留2位小数,正数)';
COMMENT ON COLUMN wallet_account_log.balance_before IS '可用余额变动前(元)';
COMMENT ON COLUMN wallet_account_log.balance_after IS '可用余额变动后(元)';
COMMENT ON COLUMN wallet_account_log.currency IS '货币类型';
COMMENT ON COLUMN wallet_account_log.description IS '描述';
COMMENT ON COLUMN wallet_account_log.extra_data IS '额外数据(JSONB)';
--------------------pgsql创建wallet_account_log表语句---------------------------
--------------------pgsql创建wallet_pricing_config表语句---------------------------
-- 计价配置表(费率全 DB 配置,改价/加档只改 rules,代码不写死金额)
CREATE TABLE IF NOT EXISTS wallet_pricing_config (
-- 基础字段(继承 SQLBaseCol 通用字段,与 SQLBaseDO 对齐)
id BIGINT PRIMARY KEY,
tenant_id BIGINT NOT NULL DEFAULT 0,
creator VARCHAR(64) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updater VARCHAR(64) NOT NULL,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted_at timestamp(6),
-- 计价配置核心字段
subject_type VARCHAR(16) NOT NULL, -- workflow | model | business
subject_id VARCHAR(64) NOT NULL, -- workflow | 模型ID | 业务模块标识
rules JSONB NOT NULL DEFAULT '{}', -- 费率JSON对象(见设计§4,rules 自描述)
min_balance NUMERIC(15,2) NOT NULL DEFAULT 0,
currency VARCHAR(16) NOT NULL DEFAULT 'CNY',
enabled SMALLINT NOT NULL DEFAULT 1
);
-- 索引
CREATE UNIQUE INDEX IF NOT EXISTS uk_pricing_config_subject ON wallet_pricing_config(subject_type, subject_id);
CREATE INDEX IF NOT EXISTS idx_pricing_config_enabled ON wallet_pricing_config(enabled);
-- 表和字段注释
COMMENT ON TABLE wallet_pricing_config IS '计价配置表(计价对象固定枚举,费率全 DB 配置)';
COMMENT ON COLUMN wallet_pricing_config.id IS '主键ID(非自增)';
COMMENT ON COLUMN wallet_pricing_config.tenant_id IS '租户ID';
COMMENT ON COLUMN wallet_pricing_config.creator IS '创建人';
COMMENT ON COLUMN wallet_pricing_config.created_at IS '创建时间';
COMMENT ON COLUMN wallet_pricing_config.updater IS '更新人';
COMMENT ON COLUMN wallet_pricing_config.updated_at IS '更新时间';
COMMENT ON COLUMN wallet_pricing_config.deleted_at IS '删除时间(软删)';
COMMENT ON COLUMN wallet_pricing_config.subject_type IS '计价对象类型:workflow/model/business';
COMMENT ON COLUMN wallet_pricing_config.subject_id IS '计价对象IDworkflow/模型ID/业务模块标识)';
COMMENT ON COLUMN wallet_pricing_config.rules IS '费率JSONworkflow三模式容器/model自适应/business周期)';
COMMENT ON COLUMN wallet_pricing_config.min_balance IS '门禁:调用前可用余额须>=该值(元),0=不校验';
COMMENT ON COLUMN wallet_pricing_config.currency IS '货币类型';
COMMENT ON COLUMN wallet_pricing_config.enabled IS '1启用 0停用';
--------------------pgsql创建wallet_pricing_config表语句---------------------------
--------------------pgsql创建wallet_charge_order表语句---------------------------
-- 计费单表(一次计价调用 = 一张计费单,幂等键 subject_type + subject_id + biz_order_no
CREATE TABLE IF NOT EXISTS wallet_charge_order (
-- 基础字段(继承 SQLBaseCol 通用字段,与 SQLBaseDO 对齐)
id BIGINT PRIMARY KEY,
tenant_id BIGINT NOT NULL DEFAULT 0,
creator VARCHAR(64) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updater VARCHAR(64) NOT NULL,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted_at timestamp(6),
-- 计费单核心字段
subject_type VARCHAR(16) NOT NULL,
subject_id VARCHAR(64) NOT NULL,
biz_order_no VARCHAR(128) NOT NULL,
user_id BIGINT NOT NULL,
charge_mode VARCHAR(32) NOT NULL,
status SMALLINT NOT NULL DEFAULT 1,
actual_amount NUMERIC(15,2) NOT NULL DEFAULT 0,
usage JSONB NOT NULL DEFAULT '{}',
rule_snapshot JSONB NOT NULL DEFAULT '{}',
settle_time TIMESTAMP
);
-- 索引
CREATE UNIQUE INDEX IF NOT EXISTS uk_charge_order_subject ON wallet_charge_order(subject_type, subject_id, biz_order_no);
CREATE INDEX IF NOT EXISTS idx_charge_order_user_id ON wallet_charge_order(user_id);
CREATE INDEX IF NOT EXISTS idx_charge_order_status ON wallet_charge_order(status);
-- 表和字段注释
COMMENT ON TABLE wallet_charge_order IS '计费单表(一次计价调用 = 一张计费单)';
COMMENT ON COLUMN wallet_charge_order.id IS '主键ID(非自增)';
COMMENT ON COLUMN wallet_charge_order.tenant_id IS '租户ID';
COMMENT ON COLUMN wallet_charge_order.creator IS '创建人';
COMMENT ON COLUMN wallet_charge_order.created_at IS '创建时间';
COMMENT ON COLUMN wallet_charge_order.updater IS '更新人';
COMMENT ON COLUMN wallet_charge_order.updated_at IS '更新时间';
COMMENT ON COLUMN wallet_charge_order.deleted_at IS '删除时间(软删)';
COMMENT ON COLUMN wallet_charge_order.subject_type IS '计价对象类型';
COMMENT ON COLUMN wallet_charge_order.subject_id IS '计价对象ID';
COMMENT ON COLUMN wallet_charge_order.biz_order_no IS '业务单据号(幂等键,如工作流 execId)';
COMMENT ON COLUMN wallet_charge_order.user_id IS '扣费用户ID';
COMMENT ON COLUMN wallet_charge_order.charge_mode IS '计费方式(建单快照)';
COMMENT ON COLUMN wallet_charge_order.status IS '状态:1已建单 2已结算 3已失败';
COMMENT ON COLUMN wallet_charge_order.actual_amount IS '实收金额(元,保留2位小数)';
COMMENT ON COLUMN wallet_charge_order.usage IS '实际用量JSON';
COMMENT ON COLUMN wallet_charge_order.rule_snapshot IS '结算用费率快照JSON(防改价影响在途单)';
COMMENT ON COLUMN wallet_charge_order.settle_time IS '结算时间';
--------------------pgsql创建wallet_charge_order表语句---------------------------
+7 -4
View File
@@ -5,12 +5,14 @@ import (
knapsackController "shop-user-trade/controller/knapsack" knapsackController "shop-user-trade/controller/knapsack"
marketController "shop-user-trade/controller/market" marketController "shop-user-trade/controller/market"
pricingController "shop-user-trade/controller/pricing"
walletController "shop-user-trade/controller/wallet" walletController "shop-user-trade/controller/wallet"
"gitea.com/red-future/common/http" "gitea.redpowerfuture.com/red-future/common/http"
"gitea.com/red-future/common/jaeger" "gitea.redpowerfuture.com/red-future/common/jaeger"
_ "gitea.com/red-future/common/swagger" _ "gitea.redpowerfuture.com/red-future/common/swagger"
_ "github.com/gogf/gf/contrib/drivers/pgsql/v2" _ "github.com/gogf/gf/contrib/drivers/pgsql/v2"
_ "github.com/gogf/gf/contrib/nosql/redis/v2" // common utils.WithLock 需要 go-redis adapterWATCH/MULTI 归属释放)
) )
func main() { func main() {
@@ -20,7 +22,8 @@ func main() {
http.RouteRegister([]interface{}{ http.RouteRegister([]interface{}{
knapsackController.Knapsack, knapsackController.Knapsack,
marketController.Market, marketController.Market,
walletController.Wallet, walletController.Account,
pricingController.Pricing,
}) })
select {} select {}
} }
+1 -1
View File
@@ -4,7 +4,7 @@ import (
knapsackConsts "shop-user-trade/consts/knapsack" knapsackConsts "shop-user-trade/consts/knapsack"
"shop-user-trade/model/config" "shop-user-trade/model/config"
"gitea.com/red-future/common/beans" "gitea.redpowerfuture.com/red-future/common/beans"
"github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/frame/g"
) )
+1 -1
View File
@@ -3,7 +3,7 @@ package dto
import ( import (
marketConsts "shop-user-trade/consts/market" marketConsts "shop-user-trade/consts/market"
"gitea.com/red-future/common/beans" "gitea.redpowerfuture.com/red-future/common/beans"
"github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/frame/g"
) )
+140
View File
@@ -0,0 +1,140 @@
package dto
import (
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
pricingConsts "shop-user-trade/consts/pricing"
)
// ====================== 建单(不动钱) ======================
// OpenOrderReq 建单请求(执行开始调用,只建单不动钱),幂等键 subjectType+subjectId+bizOrderNo
type OpenOrderReq struct {
g.Meta `path:"/open_order" method:"post" tags:"计价扣费" summary:"建单" dc:"执行开始建计费单(不动钱),幂等键 subjectType+subjectId+bizOrderNo"`
UserId int64 `json:"userId" v:"required|min:1" dc:"用户ID"`
SubjectType string `json:"subjectType" v:"required|in:workflow,model,business" dc:"计价对象类型"`
SubjectID string `json:"subjectId" v:"required" dc:"计价对象ID"`
ChargeMode string `json:"chargeMode" dc:"计费方式:workflow 必填(rules 已配三模式之一);model/business 可空(默认取 unit/per_period"`
BizOrderNo string `json:"bizOrderNo" v:"required" dc:"业务单据号(幂等键,如工作流 execId)"`
}
// ====================== 结算(成功:按最终产出) ======================
// SettleReq 结算请求(按实际用量计价实收,余额不足允许为负)
type SettleReq struct {
g.Meta `path:"/settle" method:"post" tags:"计价扣费" summary:"结算" dc:"按实际用量计价实收(允许负余额),幂等"`
OrderId int64 `json:"orderId" dc:"计费单ID(与 subjectType+subjectId+bizOrderNo 二选一)"`
SubjectType string `json:"subjectType" dc:"计价对象类型(orderId 为空时必填)"`
SubjectID string `json:"subjectId" dc:"计价对象IDorderId 为空时必填)"`
BizOrderNo string `json:"bizOrderNo" dc:"业务单据号(orderId 为空时必填)"`
Usage map[string]any `json:"usage" v:"required" dc:"用量JSON对象(前端传对象)。工作流口径:{durationSec,feeByModel}——feeByModel 各模型=按次调 /calc 已记录费用(per_token 结算/时长取消补收据此合计实收);durationSec=生成视频总时长(per_item/per_second"`
}
// ====================== 取消(按已消耗用量结算) ======================
// CancelReq 取消请求(中途取消按已消耗用量计价扣费,非退款)
type CancelReq struct {
g.Meta `path:"/cancel" method:"post" tags:"计价扣费" summary:"取消" dc:"中途取消按已消耗用量计价实收(非退款),幂等"`
OrderId int64 `json:"orderId" dc:"计费单ID(与 subjectType+subjectId+bizOrderNo 二选一)"`
SubjectType string `json:"subjectType" dc:"计价对象类型(orderId 为空时必填)"`
SubjectID string `json:"subjectId" dc:"计价对象IDorderId 为空时必填)"`
BizOrderNo string `json:"bizOrderNo" dc:"业务单据号(orderId 为空时必填)"`
Usage map[string]any `json:"usage" v:"required" dc:"已消耗用量JSON对象(前端传对象)"`
}
// ====================== 失败(不扣费) ======================
// FailReq 失败请求(执行失败/无产出,不扣费)
type FailReq struct {
g.Meta `path:"/fail" method:"post" tags:"计价扣费" summary:"失败处理" dc:"执行失败/无产出,不扣费,幂等"`
OrderId int64 `json:"orderId" dc:"计费单ID(与 subjectType+subjectId+bizOrderNo 二选一)"`
SubjectType string `json:"subjectType" dc:"计价对象类型(orderId 为空时必填)"`
SubjectID string `json:"subjectId" dc:"计价对象IDorderId 为空时必填)"`
BizOrderNo string `json:"bizOrderNo" dc:"业务单据号(orderId 为空时必填)"`
Reason string `json:"reason" dc:"失败原因"`
}
// ====================== 查询 ======================
// ChargeOrderInfo 计费单信息
type ChargeOrderInfo struct {
ID int64 `json:"id"`
SubjectType string `json:"subjectType"`
SubjectID string `json:"subjectId"`
BizOrderNo string `json:"bizOrderNo"`
UserId int64 `json:"userId"`
ChargeMode string `json:"chargeMode"`
Status int `json:"status"` // 1已建单 2已结算 3已失败
ActualAmount float64 `json:"actualAmount"`
Usage map[string]any `json:"usage"`
RuleSnapshot map[string]any `json:"ruleSnapshot"`
SettleTime string `json:"settleTime"`
CreatedAt string `json:"createdAt"`
}
// GetOrderInfoReq 查询计费单(工作流结算前取 CreatedAt 算时长)
type GetOrderInfoReq struct {
g.Meta `path:"/order" method:"get" tags:"计价扣费" summary:"查询计费单" dc:"按ID或subjectType+subjectId+bizOrderNo查询计费单(工作流结算前取CreatedAt算时长)"`
Id int64 `json:"id" dc:"计费单ID(与 subjectType+subjectId+bizOrderNo 二选一)"`
SubjectType string `json:"subjectType" dc:"计价对象类型"`
SubjectID string `json:"subjectId" dc:"计价对象ID"`
BizOrderNo string `json:"bizOrderNo" dc:"业务单据号"`
}
// ====================== 算费(不建单不扣费,model-gateway 调用) ======================
// CalcFeeReq 按用量计算费用(不建单不扣费;未配置/未启用计价即报错)
type CalcFeeReq struct {
g.Meta `path:"/calc" method:"post" tags:"计价扣费" summary:"按用量计算费用" dc:"按计价配置与用量计算费用(不建单不扣费;model-gateway 调用)"`
SubjectType string `json:"subjectType" v:"required" dc:"计价对象类型"`
SubjectID string `json:"subjectId" v:"required" dc:"计价对象ID"`
ChargeMode string `json:"chargeMode" dc:"计费方式(model 可空=配置 unit"`
Usage map[string]any `json:"usage" v:"required" dc:"用量JSON对象(ChargeUsage 形状:promptTokens/completionTokens/cachedTokens/mediaType/durationSec 等)"`
}
// CalcFeeRes 算费结果
type CalcFeeRes struct {
Cost float64 `json:"cost" dc:"费用(元,不足1分向上取整)"`
}
// ====================== DAO 入参(统一吃 DTO,同 model-gateway ======================
// GetChargeOrderReq DAO 查询计费单:Id 与 SubjectType+SubjectID+BizOrderNo 二选一
type GetChargeOrderReq struct {
Id int64 `json:"id"`
SubjectType string `json:"subjectType"`
SubjectID string `json:"subjectId"`
BizOrderNo string `json:"bizOrderNo"`
}
// CreateChargeOrderReq DAO 创建计费单
type CreateChargeOrderReq struct {
SubjectType pricingConsts.SubjectType `json:"subjectType"`
SubjectID string `json:"subjectId"`
BizOrderNo string `json:"bizOrderNo"`
UserId int64 `json:"userId"`
ChargeMode pricingConsts.ChargeMode `json:"chargeMode"`
Status pricingConsts.ChargeOrderStatus `json:"status"`
RuleSnapshot map[string]any `json:"ruleSnapshot"`
}
// SettleChargeOrderReq DAO 结算迁移(CREATED→SETTLED,条件更新幂等)
type SettleChargeOrderReq struct {
Id int64 `json:"id"`
ActualAmount float64 `json:"actualAmount"`
Usage map[string]any `json:"usage"`
RuleSnapshot map[string]any `json:"ruleSnapshot"`
SettleTime *gtime.Time `json:"settleTime"`
}
// FailChargeOrderReq DAO 失败迁移(CREATED→FAILED,条件更新幂等)
type FailChargeOrderReq struct {
Id int64 `json:"id"`
}
+46
View File
@@ -0,0 +1,46 @@
package dto
import (
"github.com/gogf/gf/v2/frame/g"
)
// ====================== 配置管理 ======================
// SavePricingConfigReq 新增/更新计价配置请求(费率全 DB 配置,改价/加档只改 rules)
type SavePricingConfigReq struct {
g.Meta `path:"/config/save" method:"post" tags:"计价配置" summary:"保存计价配置" dc:"新增或更新计价配置(subject_type+subject_id 唯一)"`
Id int64 `json:"id" dc:"配置ID>0 更新,0 新增)"`
SubjectType string `json:"subjectType" v:"required|in:workflow,model,business" dc:"计价对象类型"`
SubjectID string `json:"subjectId" v:"required" dc:"计价对象ID"`
Rules map[string]any `json:"rules" v:"required" dc:"费率JSON对象(见设计§4;前端传对象)"`
MinBalance *float64 `json:"minBalance" dc:"门禁:调用前可用余额须>=该值(元),0=不校验"`
Currency string `json:"currency" dc:"货币类型,默认CNY"`
Enabled *int `json:"enabled" dc:"1启用 0停用"`
}
// SavePricingConfigRes 保存计价配置数据
type SavePricingConfigRes struct {
Id int64 `json:"id"`
}
// GetPricingConfigReq 查询计价配置请求
type GetPricingConfigReq struct {
g.Meta `path:"/config/get" method:"get" tags:"计价配置" summary:"查询计价配置" dc:"按 subjectType+subjectId 查询计价配置"`
SubjectType string `json:"subjectType" v:"required" dc:"计价对象类型"`
SubjectID string `json:"subjectId" v:"required" dc:"计价对象ID"`
}
// GetPricingConfigRes 计价配置信息
type GetPricingConfigRes struct {
ID int64 `json:"id,string"`
SubjectType string `json:"subjectType" dc:"计价对象类型"`
SubjectID string `json:"subjectId" dc:"计价对象ID"`
Rules map[string]any `json:"rules" dc:"费率JSON对象(见设计§4"`
MinBalance float64 `json:"minBalance" dc:"门禁:调用前可用余额须>=该值(元),0=不校验"`
Currency string `json:"currency" dc:"货币类型,默认CNY"`
Enabled int `json:"enabled" dc:"1启用 0停用"`
CreatedAt string `json:"createdAt" dc:"创建时间"`
UpdatedAt string `json:"updatedAt" dc:"更新时间"`
}
+46
View File
@@ -0,0 +1,46 @@
package dto
import (
"github.com/gogf/gf/v2/frame/g"
)
// SubjectListReq 计价对象枚举请求
type SubjectListReq struct {
g.Meta `path:"/subjects" method:"get" tags:"计价配置" summary:"计价对象枚举" dc:"workflow + 实时model-gateway系统模型 + 固定业务模块"`
}
// SubjectListRes 计价对象枚举响应
type SubjectListRes struct {
Subjects []SubjectInfo `json:"subjects"`
}
// SubjectInfo 计价对象枚举项
type SubjectInfo struct {
SubjectType string `json:"subjectType"`
SubjectID string `json:"subjectId"`
Name string `json:"name"`
ModelType string `json:"modelType,omitempty"` // 模型类型名(管理端分组展示,如 推理/视频)
ModelTypeCode int `json:"modelTypeCode,omitempty"` // 模型类型编码:100推理/200图片/300音频/600视频
ChargeModes []string `json:"chargeModes,omitempty"` // workflow/business 用(可选的计费方式)
}
// ChargeModeListReq 按主体查询可选计费方式请求
type ChargeModeListReq struct {
g.Meta `path:"/subjects/charge-modes" method:"get" tags:"计价配置" summary:"查询主体可选计费方式" dc:"按 subjectType+subjectId 返回该主体可选计费方式(workflow/businessmodel 无需选择不入参)"`
SubjectType string `json:"subjectType" v:"required|in:workflow,business" dc:"计价对象类型(model 非法:模型计费方式无需选择)"`
SubjectID string `json:"subjectId" v:"required" dc:"计价对象IDworkflow→workflowbusiness→业务模块标识"`
}
// ChargeModeListRes 可选计费方式响应
type ChargeModeListRes struct {
SubjectType string `json:"subjectType" dc:"计价对象类型"`
SubjectID string `json:"subjectId" dc:"计价对象ID"`
ChargeModes []ChargeModeInfo `json:"chargeModes" dc:"可选计费方式列表"`
}
// ChargeModeInfo 计费方式项
type ChargeModeInfo struct {
Mode string `json:"mode" dc:"提交值:per_item/per_second/per_token/per_1K/per_1M/per_1/per_minute/per_hour/per_char/per_period"`
Name string `json:"name" dc:"展示名:按条/按秒/按token/每千token/每百万token/每张·每个/每分钟/每小时/每字/周期订阅"`
}
+85
View File
@@ -0,0 +1,85 @@
package dto
import (
"gitea.redpowerfuture.com/red-future/common/beans"
"github.com/gogf/gf/v2/frame/g"
walletConsts "shop-user-trade/consts/wallet"
)
// GetAccountByUserIdReq 获取钱包请求
type GetAccountByUserIdReq struct {
g.Meta `path:"/get" method:"get" tags:"钱包管理" summary:"获取钱包" dc:"根据用户ID获取钱包信息"`
UserId int64 `json:"userId" v:"required|min:1" dc:"用户ID"`
}
// AccountInfo 钱包信息
type AccountInfo struct {
ID int64 `json:"id" dc:"钱包ID"`
UserID int64 `json:"userId" dc:"用户ID"`
Balance float64 `json:"balance" dc:"可用余额(元),负值=欠费"`
Currency string `json:"currency" dc:"货币类型"`
Status int `json:"status" dc:"状态:1启用 0禁用 -1冻结"`
}
// RechargeReq 充值请求
type RechargeReq struct {
g.Meta `path:"/recharge" method:"post" tags:"钱包管理" summary:"充值" dc:"为用户钱包充值"`
UserId int64 `json:"userId" v:"required|min:1" dc:"用户ID"`
UserName string `json:"userName" v:"required" dc:"账户名"`
Amount float64 `json:"amount" v:"required|min:0.01" dc:"充值金额(元,保留2位小数)"`
Currency string `json:"currency" dc:"货币类型,默认CNY"`
OrderNo string `json:"orderNo" dc:"外部订单号"`
Description string `json:"description" dc:"描述"`
}
// GetAccountLogsReq 获取钱包流水请求
type GetAccountLogsReq struct {
g.Meta `path:"/logs" method:"get" tags:"钱包管理" summary:"获取钱包流水" dc:"分页获取钱包账务流水"`
UserId int64 `json:"userId" v:"required|min:1" dc:"用户ID"`
*beans.Page `json:"page"`
}
// AccountLogData 钱包流水数据
type AccountLogData struct {
Logs []AccountLogInfo `json:"logs"`
Total int `json:"total"`
}
// AccountLogInfo 钱包流水信息
type AccountLogInfo struct {
ID int64 `json:"id"`
OrderNo string `json:"orderNo" dc:"业务单据号(计费单号/充值单号)"`
TransactionNo string `json:"transactionNo" dc:"账务流水号(唯一,幂等)"`
Type string `json:"type" dc:"账务类型:income/expense"`
Amount float64 `json:"amount" dc:"本次变动金额(元,正数)"`
BalanceBefore float64 `json:"balanceBefore" dc:"可用余额变动前(元)"`
BalanceAfter float64 `json:"balanceAfter" dc:"可用余额变动后(元)"`
Currency string `json:"currency" dc:"货币类型"`
Description string `json:"description" dc:"描述"`
CreatedAt string `json:"createdAt" dc:"创建时间"`
}
// ====================== DAO 入参(统一吃 DTO,同 model-gateway ======================
// CreateAccountReq 创建钱包请求(钱包不存在时自动创建,余额 0)
type CreateAccountReq struct {
UserId int64 `json:"userId"`
UserName string `json:"userName"`
Status int `json:"status"`
Currency string `json:"currency"`
}
// ChangeAccountReq 单次账务变动请求(事务透明,由调用方在 service 事务内使用)
type ChangeAccountReq struct {
UserId int64 `json:"userId"`
DeltaBalance float64 `json:"deltaBalance"` // 余额增量(元,可为负=扣款)
Type walletConsts.WalletLogType `json:"type"` // income/expense
Amount float64 `json:"amount"` // 记账金额(元,正数)
OrderNo string `json:"orderNo"`
Description string `json:"description"`
ExtraData map[string]interface{} `json:"extraData"`
}
-112
View File
@@ -1,112 +0,0 @@
package dto
import (
"github.com/gogf/gf/v2/frame/g"
)
// GetWalletByUserIdReq 根据用户ID获取钱包请求
type GetWalletByUserIdReq struct {
g.Meta `path:"/getWallet" method:"get" tags:"钱包管理" summary:"获取钱包" dc:"根据用户ID获取钱包信息"`
UserId int64 `json:"userId" v:"required|min:1" dc:"用户ID"`
}
// GetWalletByUserIdResp 根据用户ID获取钱包响应
type GetWalletByUserIdResp struct {
Data WalletInfo `json:"data"`
}
// WalletInfo 钱包信息
type WalletInfo struct {
ID int64 `json:"id" dc:"钱包ID"`
UserID int64 `json:"userId" dc:"用户ID"`
Balance int64 `json:"balance" dc:"余额(分)"`
Currency string `json:"currency" dc:"货币类型"`
Status int `json:"status" dc:"状态"`
}
// CreateWalletReq 创建钱包请求
type CreateWalletReq struct {
g.Meta `path:"/createWallet" method:"post" tags:"钱包管理" summary:"创建钱包" dc:"为用户创建钱包"`
UserId int64 `json:"userId" v:"required|min:1" dc:"用户ID"`
Currency string `json:"currency" v:"required" dc:"货币类型"`
}
// CreateWalletResp 创建钱包响应
type CreateWalletResp struct {
Data CreateWalletData `json:"data"`
}
// CreateWalletData 创建钱包数据
type CreateWalletData struct {
WalletID int64 `json:"walletId" dc:"钱包ID"`
}
// UpdateBalanceReq 更新余额请求
type UpdateBalanceReq struct {
g.Meta `path:"/updateBalance" method:"post" tags:"钱包管理" summary:"更新余额" dc:"对钱包余额进行收入/支出/冻结/解冻操作"`
WalletID int64 `json:"walletId" v:"required" dc:"钱包ID"`
Amount int64 `json:"amount" v:"required|min:1" dc:"金额(分)"`
Type string `json:"type" v:"required|in:income,expense,freeze,unfreeze" dc:"操作类型"`
OrderNo string `json:"orderNo" dc:"业务订单号"`
TransactionNo string `json:"transactionNo" dc:"钱包交易流水号"`
Description string `json:"description" dc:"描述"`
ExtraData map[string]interface{} `json:"extraData" dc:"额外数据"`
}
// UpdateBalanceResp 更新余额响应
type UpdateBalanceResp struct {
Success bool `json:"success"`
Message string `json:"message"`
}
// GetWalletLogsReq 获取钱包日志请求
type GetWalletLogsReq struct {
g.Meta `path:"/getWalletLogs" method:"get" tags:"钱包管理" summary:"获取钱包日志" dc:"分页获取钱包操作日志"`
UserId int64 `json:"userId" v:"required|min:1" dc:"用户ID"`
Page int `json:"page" v:"required|min:1" dc:"页码"`
PageSize int `json:"pageSize" v:"required|min:1|max:100" dc:"每页大小"`
}
// GetWalletLogsResp 获取钱包日志响应
type GetWalletLogsResp struct {
Data WalletLogData `json:"data"`
}
// WalletLogData 钱包日志数据
type WalletLogData struct {
Logs []WalletLogInfo `json:"logs"`
Total int `json:"total"`
}
// WalletLogInfo 钱包日志信息
type WalletLogInfo struct {
ID int64 `json:"id"`
OrderNo string `json:"orderNo"`
TransactionNo string `json:"transactionNo"`
Type string `json:"type"`
Amount int64 `json:"amount"`
BalanceBefore int64 `json:"balanceBefore"`
BalanceAfter int64 `json:"balanceAfter"`
Currency string `json:"currency"`
Description string `json:"description"`
CreatedAt string `json:"createdAt"`
}
// CreateWalletLogReq 创建钱包日志请求(内部使用)
type CreateWalletLogReq struct {
UserID int64 `json:"userId"`
WalletID int64 `json:"walletId"`
OrderNo string `json:"orderNo"`
TransactionNo string `json:"transactionNo"`
Type string `json:"type"`
Amount int64 `json:"amount"`
BalanceBefore int64 `json:"balanceBefore"`
BalanceAfter int64 `json:"balanceAfter"`
Currency string `json:"currency"`
Description string `json:"description"`
ExtraData map[string]interface{} `json:"extraData"`
}
+1 -1
View File
@@ -4,7 +4,7 @@ import (
knapsackConsts "shop-user-trade/consts/knapsack" knapsackConsts "shop-user-trade/consts/knapsack"
"shop-user-trade/model/config" "shop-user-trade/model/config"
"gitea.com/red-future/common/beans" "gitea.redpowerfuture.com/red-future/common/beans"
) )
type knapsackCol struct { type knapsackCol struct {
+1 -1
View File
@@ -3,7 +3,7 @@ package entity
import ( import (
knapsackConsts "shop-user-trade/consts/knapsack" knapsackConsts "shop-user-trade/consts/knapsack"
"gitea.com/red-future/common/beans" "gitea.redpowerfuture.com/red-future/common/beans"
) )
// KnapsackLog 背包操作日志实体 // KnapsackLog 背包操作日志实体
+1 -1
View File
@@ -3,7 +3,7 @@ package entity
import ( import (
marketConsts "shop-user-trade/consts/market" marketConsts "shop-user-trade/consts/market"
"gitea.com/red-future/common/beans" "gitea.redpowerfuture.com/red-future/common/beans"
) )
type marketCol struct { type marketCol struct {
+1 -1
View File
@@ -3,7 +3,7 @@ package entity
import ( import (
marketConsts "shop-user-trade/consts/market" marketConsts "shop-user-trade/consts/market"
"gitea.com/red-future/common/beans" "gitea.redpowerfuture.com/red-future/common/beans"
) )
// MarketLog 市场操作日志实体 // MarketLog 市场操作日志实体
+53
View File
@@ -0,0 +1,53 @@
package entity
import (
pricingConsts "shop-user-trade/consts/pricing"
"gitea.redpowerfuture.com/red-future/common/beans"
"github.com/gogf/gf/v2/os/gtime"
)
type chargeOrderCol struct {
beans.SQLBaseCol
SubjectType string
SubjectID string
BizOrderNo string
UserId string
ChargeMode string
Status string
ActualAmount string
Usage string
RuleSnapshot string
SettleTime string
}
var ChargeOrderCol = chargeOrderCol{
SQLBaseCol: beans.DefSQLBaseCol,
SubjectType: "subject_type",
SubjectID: "subject_id",
BizOrderNo: "biz_order_no",
UserId: "user_id",
ChargeMode: "charge_mode",
Status: "status",
ActualAmount: "actual_amount",
Usage: "usage",
RuleSnapshot: "rule_snapshot",
SettleTime: "settle_time",
}
// ChargeOrder 计费单实体:一次计价调用 = 一张计费单(幂等键 subject_type+subject_id+biz_order_no
// 状态机:CREATED(已建单,不动钱) → SETTLED(已结算) / FAILED(已失败,不扣费)
type ChargeOrder struct {
beans.SQLBaseDO `orm:",inherit"`
SubjectType pricingConsts.SubjectType `orm:"subject_type" json:"subjectType" description:"计价对象类型"`
SubjectID string `orm:"subject_id" json:"subjectId" description:"计价对象ID"`
BizOrderNo string `orm:"biz_order_no" json:"bizOrderNo" description:"业务单据号(幂等键,如工作流 execId)"`
UserId int64 `orm:"user_id" json:"userId,string" description:"扣费用户ID"`
ChargeMode pricingConsts.ChargeMode `orm:"charge_mode" json:"chargeMode" description:"计费方式(建单快照)"`
Status pricingConsts.ChargeOrderStatus `orm:"status" json:"status" description:"状态:1已建单 2已结算 3已失败"`
ActualAmount float64 `orm:"actual_amount" json:"actualAmount" description:"实收金额(元,保留2位小数)"`
Usage map[string]any `orm:"usage" json:"usage" description:"实际用量JSON对象"`
RuleSnapshot map[string]any `orm:"rule_snapshot" json:"ruleSnapshot" description:"结算用费率快照JSON对象(防改价影响在途单)"`
SettleTime *gtime.Time `orm:"settle_time" json:"settleTime" description:"结算时间"`
}
+48
View File
@@ -0,0 +1,48 @@
package entity
import (
pricingConsts "shop-user-trade/consts/pricing"
"gitea.redpowerfuture.com/red-future/common/beans"
)
type pricingConfigCol struct {
beans.SQLBaseCol
SubjectType string
SubjectID string
Rules string
MinBalance string
Currency string
Enabled string
}
var PricingConfigCol = pricingConfigCol{
SQLBaseCol: beans.DefSQLBaseCol,
SubjectType: "subject_type",
SubjectID: "subject_id",
Rules: "rules",
MinBalance: "min_balance",
Currency: "currency",
Enabled: "enabled",
}
// PricingConfig 计价配置实体(费率全 DB 配置,改价/加档只改 rules,代码不写死金额)
type PricingConfig struct {
beans.SQLBaseDO `orm:",inherit"`
SubjectType pricingConsts.SubjectType `orm:"subject_type" json:"subjectType" description:"计价对象类型:workflow/model/business"`
SubjectID string `orm:"subject_id" json:"subjectId" description:"计价对象IDworkflow/模型ID/业务模块标识)"`
Rules map[string]any `orm:"rules" json:"rules" description:"费率JSON对象(见设计§4rules 自描述不再有 charge_mode 列)"`
MinBalance float64 `orm:"min_balance" json:"minBalance" description:"门禁:调用前可用余额须>=该值(元),0=不校验"`
Currency string `orm:"currency" json:"currency" description:"货币类型"`
Enabled int `orm:"enabled" json:"enabled" description:"1启用 0停用"`
}
// Unit 从 rules 中解析模型计费单位(仅 subject_type=model 有效,其余返回空)。
// service 层 per_token 结算/建单解析计费方式时使用,避免重复解析 rules。
func (e *PricingConfig) Unit() pricingConsts.ChargeMode {
if unit, ok := e.Rules["unit"].(string); ok {
return pricingConsts.ChargeMode(unit)
}
return ""
}
@@ -3,34 +3,36 @@ package entity
import ( import (
walletConsts "shop-user-trade/consts/wallet" walletConsts "shop-user-trade/consts/wallet"
"gitea.com/red-future/common/beans" "gitea.redpowerfuture.com/red-future/common/beans"
) )
type walletCol struct { type accountCol struct {
beans.SQLBaseCol beans.SQLBaseCol
UserID string UserID string
UserName string
Balance string Balance string
Currency string Currency string
Status string Status string
Version string
} }
var WalletCol = walletCol{ var AccountCol = accountCol{
SQLBaseCol: beans.DefSQLBaseCol, SQLBaseCol: beans.DefSQLBaseCol,
UserID: "user_id", UserID: "user_id",
UserName: "user_name",
Balance: "balance", Balance: "balance",
Currency: "currency", Currency: "currency",
Status: "status", Status: "status",
Version: "version",
} }
// Wallet 钱包实体 // Account 钱包实体
type Wallet struct { // 单余额模型:balance 可用余额(元),可为负(欠费,靠充值归正)。
// status 为整钱包状态(1启用/0禁用/-1冻结,风控用)。
type Account struct {
beans.SQLBaseDO `orm:",inherit"` beans.SQLBaseDO `orm:",inherit"`
UserID int64 `orm:"user_id" json:"userId,string" description:"用户ID"` UserID int64 `orm:"user_id" json:"userId,string" description:"用户ID"`
Balance int64 `orm:"balance" json:"balance" description:"余额(分)"` UserName string `orm:"user_name" json:"userName" description:"用户名"`
Balance float64 `orm:"balance" json:"balance" description:"可用余额(元,保留2位小数),可为负=欠费"`
Currency string `orm:"currency" json:"currency" description:"货币类型:CNY-人民币"` Currency string `orm:"currency" json:"currency" description:"货币类型:CNY-人民币"`
Status walletConsts.WalletStatus `orm:"status" json:"status" description:"状态:1启用/0禁用/-1冻结"` Status walletConsts.WalletStatus `orm:"status" json:"status" description:"状态:1启用/0禁用/-1冻结"`
Version int64 `orm:"version" json:"version" description:"乐观锁版本号"`
} }
+54
View File
@@ -0,0 +1,54 @@
package entity
import (
walletConsts "shop-user-trade/consts/wallet"
"gitea.redpowerfuture.com/red-future/common/beans"
)
// AccountLog 钱包操作日志实体
type AccountLog struct {
beans.SQLBaseDO `orm:",inherit"`
UserID int64 `orm:"user_id" json:"userId,string" description:"用户ID"`
WalletID int64 `orm:"wallet_id" json:"walletId,string" description:"钱包ID"`
OrderNo string `orm:"order_no" json:"orderNo" description:"业务单据号(计费单号/充值单号)"`
TransactionNo string `orm:"transaction_no" json:"transactionNo" description:"账务流水号(唯一,幂等)"`
Type walletConsts.WalletLogType `orm:"type" json:"type" description:"账务类型:income/expense"`
Amount float64 `orm:"amount" json:"amount" description:"本次变动金额(元,保留2位小数,正数)"`
BalanceBefore float64 `orm:"balance_before" json:"balanceBefore" description:"可用余额变动前(元)"`
BalanceAfter float64 `orm:"balance_after" json:"balanceAfter" description:"可用余额变动后(元)"`
Currency string `orm:"currency" json:"currency" description:"货币类型"`
Description string `orm:"description" json:"description" description:"描述"`
ExtraData map[string]interface{} `orm:"extra_data" json:"extraData" description:"额外数据(JSONB)"`
}
type accountLogCol struct {
beans.SQLBaseCol
UserID string
WalletID string
OrderNo string
TransactionNo string
Type string
Amount string
BalanceBefore string
BalanceAfter string
Currency string
Description string
ExtraData string
}
var AccountLogCol = accountLogCol{
SQLBaseCol: beans.DefSQLBaseCol,
UserID: "user_id",
WalletID: "wallet_id",
OrderNo: "order_no",
TransactionNo: "transaction_no",
Type: "type",
Amount: "amount",
BalanceBefore: "balance_before",
BalanceAfter: "balance_after",
Currency: "currency",
Description: "description",
ExtraData: "extra_data",
}
-24
View File
@@ -1,24 +0,0 @@
package entity
import (
walletConsts "shop-user-trade/consts/wallet"
"gitea.com/red-future/common/beans"
)
// WalletLog 钱包操作日志实体
type WalletLog struct {
beans.SQLBaseDO `orm:",inherit"`
UserID int64 `orm:"user_id" json:"userId,string" description:"用户ID"`
WalletID int64 `orm:"wallet_id" json:"walletId,string" description:"钱包ID"`
OrderNo string `orm:"order_no" json:"orderNo" description:"业务订单号"`
TransactionNo string `orm:"transaction_no" json:"transactionNo" description:"钱包交易流水号"`
Type walletConsts.WalletLogType `orm:"type" json:"type" description:"操作类型"`
Amount int64 `orm:"amount" json:"amount" description:"金额(分)"`
BalanceBefore int64 `orm:"balance_before" json:"balanceBefore" description:"操作前余额"`
BalanceAfter int64 `orm:"balance_after" json:"balanceAfter" description:"操作后余额"`
Currency string `orm:"currency" json:"currency" description:"货币类型"`
Description string `orm:"description" json:"description" description:"描述"`
ExtraData map[string]interface{} `orm:"extra_data" json:"extraData" description:"额外数据(JSONB)"`
}
-33
View File
@@ -1,33 +0,0 @@
package entity
import "gitea.com/red-future/common/beans"
type walletLogCol struct {
beans.SQLBaseCol
UserID string
WalletID string
OrderNo string
TransactionNo string
Type string
Amount string
BalanceBefore string
BalanceAfter string
Currency string
Description string
ExtraData string
}
var WalletLogCol = walletLogCol{
SQLBaseCol: beans.DefSQLBaseCol,
UserID: "user_id",
WalletID: "wallet_id",
OrderNo: "order_no",
TransactionNo: "transaction_no",
Type: "type",
Amount: "amount",
BalanceBefore: "balance_before",
BalanceAfter: "balance_after",
Currency: "currency",
Description: "description",
ExtraData: "extra_data",
}
+2 -2
View File
@@ -6,8 +6,8 @@ import (
"fmt" "fmt"
"time" "time"
"gitea.com/red-future/common/consul" "gitea.redpowerfuture.com/red-future/common/consul"
"gitea.com/red-future/common/http" "gitea.redpowerfuture.com/red-future/common/http"
"github.com/gogf/gf/v2/util/gconv" "github.com/gogf/gf/v2/util/gconv"
marketConsts "shop-user-trade/consts/market" marketConsts "shop-user-trade/consts/market"
+480
View File
@@ -0,0 +1,480 @@
package service
import (
"encoding/json"
"errors"
"fmt"
"math"
"sort"
pricingConsts "shop-user-trade/consts/pricing"
)
// ====================== 用量结构 ======================
// ChargeUsage 实际用量(结算入参,工作流/调用方上报)。
// DurationSec 秒;工作流 token 计费按 FeeByModel 合计实收(各模型=调用方按次调 shop /calc 返回费求和,
// 每次调用已 ceilFen 到分、媒体/费率按调用时快照)。逐模型 token/媒体明细由调用方保留(ai-agent
// node_execution.token_info),本结构解析只取计价字段,未知键原样落库 order.usage 作审计(见 settleOrder)。
// 模型计价字段见各计算器注释(spec §5.3)。
type ChargeUsage struct {
DurationSec float64 `json:"durationSec"` // 时长(秒):per_second 工作流结算、model 单位计价用;per_item 无 durationItems 时回落单条
// DurationItems 逐条产出时长明细(秒,per_item 工作流结算用):一条 = 一次视频节点执行产出的一条成片
// ai-agent 按 node_execution 记录收集,各条独立时长)。per_item 逐条各自落档求和(2×4s→2×29);
// 为空回落单条 DurationSec(旧客户端 / 直接 /calc 调用,单条数值与旧口径一致)。
DurationItems []float64 `json:"durationItems"`
// FeeByModel 各模型已消耗费用(= 调用方按次调 shop /calc 返回费的合计,每次调用已 ceilFen 到分、各次媒体/费率
// 按调用时快照)。per_item/per_second 取消补收、per_token 结算的 token 部分按此合计实收(不再按聚合 token
// 重算,避免丢失「每次调用不足1分按1分」的兜底与逐调用媒体价——聚合重算会把两笔 0.01 合并成 0.01 且拿错媒体价)。
FeeByModel map[string]float64 `json:"feeByModel"`
// —— 模型计价扩展(spec §5.3)——
PromptTokens int64 `json:"promptTokens"` // token 计费用:输入
CompletionTokens int64 `json:"completionTokens"` // token 计费用:输出;per_char 模型=输出字数(调用方把字数映射到该字段传输)
CachedTokens int64 `json:"cachedTokens"` // token 计费用:缓存命中
ImageCount int64 `json:"imageCount"` // per_1 用(图片张数)
MediaType string `json:"mediaType"` // text/audio/video/image(输入媒体)
Thinking *bool `json:"thinking"` // 推理 思考/非思考
OutputAudio *bool `json:"outputAudio"` // 视频 输出有声/无声
OutputResolution string `json:"outputResolution"` // 视频/图片 输出分辨率
}
// ====================== 各计费方式费率 JSON 结构(全 DB 配置) ======================
// per_item 按条:开放档位(上不封顶),超出最大档位按 overflowUnitPrice 线性叠加。
// {"tiers":[{"maxSec":15,"price":29},{"maxSec":30,"price":49},{"maxSec":60,"price":89}],"overflowUnitPrice":0.9}
type perItemRules struct {
Tiers []perItemTier `json:"tiers"`
OverflowUnitPrice float64 `json:"overflowUnitPrice"` // 元/秒,超出最大档位后线性计费
}
type perItemTier struct {
MaxSec float64 `json:"maxSec"` // 该档位最大时长(秒),open-ended
Price float64 `json:"price"` // 该档位价格(元)
}
// per_second 按秒:单位时长单价。
// {"unitPrice":0.9}0.9元/秒)。
type perSecondRules struct {
UnitPrice float64 `json:"unitPrice"` // 元/秒
}
// ====================== 计算器注册表 ======================
// calculator 计费方式计算器(新增方式:实现该接口并按 subject_type 注册进对应分表)。
// rules 为 rulesJSON 解析后的强类型费率结构;所有金额单位均为元(保留2位小数)。
type calculator interface {
// validate 解析并校验 rulesJSONSaveConfig 与结算时调用)
validate(rulesJSON string) (interface{}, error)
// charge 结算实际金额(元),不足1分向上取整
charge(rules interface{}, usage *ChargeUsage) (float64, error)
}
// ceilFen 金额向上取整到分(1分=0.01元),保证结果保留2位小数(不足1分按1分收)。
func ceilFen(v float64) float64 {
return math.Ceil(v*100) / 100
}
// roundCost 去浮点噪声:乘法累积误差 ~1e-16,四舍五入到 9 位小数后再 ceilFen,
// 避免 2.2×0.9=1.9800000000000002 在分位边界被错误进位(1.98 → 1.99)。
func roundCost(v float64) float64 {
return math.Round(v*1e9) / 1e9
}
// chargeCalculators 计费方式注册表(按 subject_type 分表;代码不写死金额,只挂计算器)
var (
// workflow 计算器(spec §4.1 三模式容器,per_token 为空标记)
workflowCalculators = map[pricingConsts.ChargeMode]calculator{
pricingConsts.ChargeModePerItem: perItemCalculator{},
pricingConsts.ChargeModePerSecond: perSecondCalculator{},
pricingConsts.ChargeModePerToken: perTokenCalculator{},
}
// model 计算器(spec §4.2 类型自适应)
modelCalculators = map[pricingConsts.ChargeMode]calculator{
pricingConsts.ChargeModePer1K: modelTokenCalculator{base: 1000},
pricingConsts.ChargeModePer1M: modelTokenCalculator{base: 1e6},
pricingConsts.ChargeModePer1: modelUnitCalculator{base: 1, usage: usageImageCount},
pricingConsts.ChargeModePerSecond: modelUnitCalculator{base: 1, usage: usageDurationSec},
pricingConsts.ChargeModePerMinute: modelUnitCalculator{base: 60, usage: usageDurationSec},
pricingConsts.ChargeModePerHour: modelUnitCalculator{base: 3600, usage: usageDurationSec},
pricingConsts.ChargeModePerChar: modelUnitCalculator{base: 1, usage: usageCompletionTokens}, // 输出字数经 completionTokens 传输
}
// business 计算器(spec §4.3 周期订阅)
businessCalculators = map[pricingConsts.ChargeMode]calculator{
pricingConsts.ChargeModePerPeriod: perPeriodCalculator{},
}
)
// getCalculator 获取计算器(按 subject_type 分表)
func getCalculator(subjectType pricingConsts.SubjectType, mode pricingConsts.ChargeMode) (calculator, error) {
calcs := workflowCalculators
switch subjectType {
case pricingConsts.SubjectTypeModel:
calcs = modelCalculators
case pricingConsts.SubjectTypeBusiness:
calcs = businessCalculators
}
c, ok := calcs[mode]
if !ok {
return nil, fmt.Errorf("不支持的计费方式: %s", mode)
}
return c, nil
}
// calcCharge 结算实际金额(元)
func calcCharge(subjectType pricingConsts.SubjectType, mode pricingConsts.ChargeMode, rulesJSON string, usage *ChargeUsage) (float64, error) {
c, err := getCalculator(subjectType, mode)
if err != nil {
return 0, err
}
rules, err := c.validate(rulesJSON)
if err != nil {
return 0, err
}
return c.charge(rules, usage)
}
// ====================== per_item 按条 ======================
type perItemCalculator struct{}
func (perItemCalculator) validate(rulesJSON string) (interface{}, error) {
var rules perItemRules
if err := json.Unmarshal([]byte(rulesJSON), &rules); err != nil {
return nil, fmt.Errorf("per_item 费率JSON解析失败: %v", err)
}
if len(rules.Tiers) == 0 {
return nil, errors.New("per_item 费率至少需要一个档位 tiers")
}
return &rules, nil
}
func (perItemCalculator) charge(rules interface{}, usage *ChargeUsage) (float64, error) {
r := rules.(*perItemRules)
// 逐条明细(durationItems:多产出,一条=一次视频节点执行产出的一条成片)为空时回落单条 durationSec
// 兼容旧客户端与直接 /calc 调用(单条数值与旧口径完全一致)。
items := usage.DurationItems
if len(items) == 0 {
if usage.DurationSec <= 0 {
return 0, nil // 0秒无产出(中途取消/未生成视频):按已消耗=0 计费,不落入第一档位价
}
items = []float64{usage.DurationSec}
}
// 每条按时长落档求和(chargePerItem 单条口径与旧 per_item 单条完全一致)
var total float64
for _, dur := range items {
if dur <= 0 {
continue
}
total += chargePerItem(r, dur)
}
if total <= 0 {
return 0, nil
}
return ceilFen(roundCost(total)), nil
}
// chargePerItem 单条按时长落档:命中 tier → tier.Price;超出最大档位(上不封顶)→ 最大档价 + 超出秒数×overflow 线性叠加(元)。
func chargePerItem(r *perItemRules, dur float64) float64 {
last := r.Tiers[len(r.Tiers)-1]
for _, t := range r.Tiers {
if dur <= t.MaxSec {
return t.Price
}
}
overflow := ceilFen((dur - last.MaxSec) * r.OverflowUnitPrice)
return last.Price + overflow
}
// ====================== per_second 按秒 ======================
type perSecondCalculator struct{}
func (perSecondCalculator) validate(rulesJSON string) (interface{}, error) {
var rules perSecondRules
if err := json.Unmarshal([]byte(rulesJSON), &rules); err != nil {
return nil, fmt.Errorf("per_second 费率JSON解析失败: %v", err)
}
if rules.UnitPrice <= 0 {
return nil, errors.New("per_second 费率须配置 unitPrice(元/秒)")
}
return &rules, nil
}
func (perSecondCalculator) charge(rules interface{}, usage *ChargeUsage) (float64, error) {
r := rules.(*perSecondRules)
// 按秒向上取整(不满1秒按1秒计),再乘单价(元/秒)
return ceilFen(math.Ceil(usage.DurationSec) * r.UnitPrice), nil
}
// ====================== per_token 按token ======================
type perTokenCalculator struct{}
func (perTokenCalculator) validate(rulesJSON string) (interface{}, error) {
// per_token 为 {} 仅启用标记;费率不落槽位(建单无快照),结算按调用方上报的按次费用(FeeByModel)合计实收(spec §5.4
var container map[string]interface{}
if err := json.Unmarshal([]byte(rulesJSON), &container); err != nil {
return nil, fmt.Errorf("per_token 费率JSON解析失败: %v", err)
}
return &container, nil
}
func (perTokenCalculator) charge(rules interface{}, usage *ChargeUsage) (float64, error) {
// 不在此计价:per_token 无费率档位(建单槽位 {}),按调用方上报的按次已记录费用(FeeByModel)合计实收,
// 由 service 层 sumRecordedFee 处理
return 0, errors.New("per_token 计价在 service 层按调用方上报的按次费用(FeeByModel)合计实收")
}
// ====================== 模型计费 JSON 结构(model subject rulesspec §4.2 ======================
// modelRules 模型计费规则:unit + tiered + rulesrules 按序首条命中)
type modelRules struct {
Unit pricingConsts.ChargeMode `json:"unit"`
Tiered bool `json:"tiered"`
Rules []modelRule `json:"rules"`
Currency string `json:"currency"`
}
type modelRule struct {
Name string `json:"name"`
Match *modelMatch `json:"match,omitempty"` // 空=任意调用
Price *modelPrice `json:"price"` // 默认价:输入不含以下媒体
MediaPrices map[string]*modelPrice `json:"mediaPrices,omitempty"` // 媒体类型→该媒体价(输入含该媒体时)
}
// modelMatch 命中条件(全可选,命中=全部满足)
type modelMatch struct {
Thinking *bool `json:"thinking,omitempty"` // 思考/非思考
OutputAudio *bool `json:"outputAudio,omitempty"` // 输出有声/无声
OutputResolution string `json:"outputResolution,omitempty"` // 输出分辨率
InputLengthMin int64 `json:"inputLengthMin,omitempty"` // 输入token下界
InputLengthMax int64 `json:"inputLengthMax,omitempty"` // 输入token上界
}
// modelPrice 计价项:token 单位用 input/output/cacheHit;非 token 单位用 unitPrice
type modelPrice struct {
Input float64 `json:"input,omitempty"`
Output float64 `json:"output,omitempty"`
CacheHit float64 `json:"cacheHit,omitempty"`
UnitPrice float64 `json:"unitPrice,omitempty"`
}
// perPeriodRules 周期订阅规则(business subjectspec §4.3
type perPeriodRules struct {
Period string `json:"period"` // year(扩月/季只加 PeriodSet
Price float64 `json:"price"` // 每周期价格(元)
}
// modelRuleMatch 单条 match 判定(nil match=无条件命中)
func modelRuleMatch(m *modelMatch, u *ChargeUsage) bool {
if m == nil {
return true
}
if m.Thinking != nil && (u.Thinking == nil || *m.Thinking != *u.Thinking) {
return false
}
if m.OutputAudio != nil && (u.OutputAudio == nil || *m.OutputAudio != *u.OutputAudio) {
return false
}
if m.OutputResolution != "" && m.OutputResolution != u.OutputResolution {
return false
}
if m.InputLengthMin > 0 && u.PromptTokens < m.InputLengthMin {
return false
}
if m.InputLengthMax > 0 && u.PromptTokens > m.InputLengthMax {
return false
}
return true
}
// validateMediaPrices mediaPrices 校验:键须 ∈ ModelMediaSet,值须配置 pricetoken/unit 计算器共用)
func validateMediaPrices(mp map[string]*modelPrice) error {
for mt, p := range mp {
if _, ok := pricingConsts.ModelMediaSet[mt]; !ok {
return errors.New("model 费率 mediaPrices 键须为 text/audio/video/image")
}
if p == nil {
return errors.New("model 费率 mediaPrices 值须配置 price")
}
}
return nil
}
// matchModelRule 按序返回首条命中规则(先写优先级高)
func matchModelRule(r *modelRules, u *ChargeUsage) *modelRule {
for i := range r.Rules {
if modelRuleMatch(r.Rules[i].Match, u) {
return &r.Rules[i]
}
}
return nil
}
// pickModelPrice 命中规则后按输入媒体取价:mediaPrices 命中使用媒体价,否则默认 price。
func pickModelPrice(rule *modelRule, usage *ChargeUsage) *modelPrice {
if len(rule.MediaPrices) > 0 {
if p, ok := rule.MediaPrices[usage.MediaType]; ok {
return p
}
}
return rule.Price
}
// ====================== model token 计算器(per_1K / per_1M ======================
// modelTokenCalculator 模型 token 计价:首条命中规则后按基准换算。
// cost = (prompt-cached)/base×input + cached/base×cacheHit + completion/base×output
type modelTokenCalculator struct{ base float64 }
func (c modelTokenCalculator) validate(rulesJSON string) (interface{}, error) {
var r modelRules
if err := json.Unmarshal([]byte(rulesJSON), &r); err != nil {
return nil, fmt.Errorf("model token 费率JSON解析失败: %v", err)
}
if r.Unit != pricingConsts.ChargeModePer1K && r.Unit != pricingConsts.ChargeModePer1M {
return nil, errors.New("model token 费率 unit 须为 per_1K/per_1M")
}
// 实例基准与 JSON unit 交叉校验:per_1K 实例 base=1000per_1M 实例 base=1e6
// 防止 per_1M 配置配 per_1K 单位导致结算 1000 倍价差(P2)。
if (c.base == 1000) != (r.Unit == pricingConsts.ChargeModePer1K) {
return nil, errors.New("model token 费率 unit 与实例基准不匹配")
}
if len(r.Rules) == 0 {
return nil, errors.New("model 费率至少一条 rules")
}
for i := range r.Rules {
if r.Rules[i].Price == nil {
return nil, errors.New("model 费率规则须配置 price")
}
if err := validateMediaPrices(r.Rules[i].MediaPrices); err != nil {
return nil, err
}
}
if r.Tiered {
if err := validateTieredBands(r.Rules); err != nil {
return nil, err
}
}
return &r, nil
}
// validateTieredBands 阶梯分档校验:带长度区间的规则按下界升序后相邻不重叠。
// 无长度区间规则不参与;上不封顶档(max=0)须为最后一个长度档。
func validateTieredBands(rules []modelRule) error {
var bands []modelRule
for _, r := range rules {
if r.Match != nil && (r.Match.InputLengthMin > 0 || r.Match.InputLengthMax > 0) {
bands = append(bands, r)
}
}
sort.Slice(bands, func(i, j int) bool {
return bands[i].Match.InputLengthMin < bands[j].Match.InputLengthMin
})
var prevMax int64 = -1
openEnded := false
for _, r := range bands {
m := r.Match
if openEnded {
return errors.New("阶梯档位区间重叠:上不封顶档后不能再有档位")
}
if m.InputLengthMin <= prevMax {
return errors.New("阶梯档位区间重叠:规则间 InputLengthMin 须 > 前一档 InputLengthMax")
}
if m.InputLengthMax == 0 {
openEnded = true // 上不封顶,之后不可再有长度档
} else {
prevMax = m.InputLengthMax
}
}
return nil
}
func (c modelTokenCalculator) charge(rules interface{}, usage *ChargeUsage) (float64, error) {
r := rules.(*modelRules)
rule := matchModelRule(r, usage)
if rule == nil {
return 0, errors.New("无匹配计费规则")
}
p := pickModelPrice(rule, usage)
// 防负成本:异常/边界用量报 cached > prompt 时钳为 0,避免结算出现负数(M3)。
promptInput := usage.PromptTokens - usage.CachedTokens
if promptInput < 0 {
promptInput = 0
}
cost := float64(promptInput)/c.base*p.Input +
float64(usage.CachedTokens)/c.base*p.CacheHit +
float64(usage.CompletionTokens)/c.base*p.Output
return ceilFen(roundCost(cost)), nil
}
// ====================== model 单位计算器(per_1 / per_second / per_minute / per_hour / per_char ======================
// modelUnitCalculator 模型非 token 计价:用量/base×unitPrice。
// usage 为用量提取函数(张数/秒/秒/秒/字数),base 为基准(1/1/60/3600/1)。
type modelUnitCalculator struct {
base float64
usage func(*ChargeUsage) float64
}
func (c modelUnitCalculator) validate(rulesJSON string) (interface{}, error) {
var r modelRules
if err := json.Unmarshal([]byte(rulesJSON), &r); err != nil {
return nil, fmt.Errorf("model 单位费率JSON解析失败: %v", err)
}
if !pricingConsts.ModelUnitSet[r.Unit] {
return nil, errors.New("model 单位费率 unit 不在允许集合内")
}
if len(r.Rules) == 0 {
return nil, errors.New("model 费率至少一条 rules")
}
for i := range r.Rules {
if r.Rules[i].Price == nil {
return nil, errors.New("model 费率规则须配置 price")
}
if err := validateMediaPrices(r.Rules[i].MediaPrices); err != nil {
return nil, err
}
}
return &r, nil
}
func (c modelUnitCalculator) charge(rules interface{}, usage *ChargeUsage) (float64, error) {
r := rules.(*modelRules)
rule := matchModelRule(r, usage)
if rule == nil {
return 0, errors.New("无匹配计费规则")
}
p := pickModelPrice(rule, usage)
return ceilFen(roundCost(c.usage(usage) / c.base * p.UnitPrice)), nil
}
// 用量提取函数
func usageDurationSec(u *ChargeUsage) float64 { return u.DurationSec }
// usageCompletionTokens per_char 用:调用方把输出字数映射到 CompletionTokens 传输(无独立 charCount 字段)
func usageCompletionTokens(u *ChargeUsage) float64 { return float64(u.CompletionTokens) }
func usageImageCount(u *ChargeUsage) float64 { return float64(u.ImageCount) }
// ====================== per_period 周期订阅(business ======================
type perPeriodCalculator struct{}
func (perPeriodCalculator) validate(rulesJSON string) (interface{}, error) {
var r perPeriodRules
if err := json.Unmarshal([]byte(rulesJSON), &r); err != nil {
return nil, fmt.Errorf("per_period 费率JSON解析失败: %v", err)
}
if !pricingConsts.PeriodSet[r.Period] {
return nil, fmt.Errorf("不支持的周期: %s", r.Period)
}
if r.Price <= 0 {
return nil, errors.New("per_period 费率须配置 price>0")
}
return &r, nil
}
func (perPeriodCalculator) charge(rules interface{}, usage *ChargeUsage) (float64, error) {
r := rules.(*perPeriodRules)
return ceilFen(r.Price), nil
}
+166
View File
@@ -0,0 +1,166 @@
package service
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/util/gconv"
pricingConsts "shop-user-trade/consts/pricing"
chargeDao "shop-user-trade/dao/pricing"
pricingDto "shop-user-trade/model/dto/pricing"
pricingEntity "shop-user-trade/model/entity/pricing"
)
// ====================== 配置管理 ======================
// SavePricingConfig 新增/更新计价配置(费率全 DB 配置;按 subject 类型校验 rules 结构,防止脏配置上线后结算报错)
func (s *pricing) SavePricingConfig(ctx context.Context, req *pricingDto.SavePricingConfigReq) (*pricingDto.SavePricingConfigRes, error) {
subjectType := pricingConsts.SubjectType(req.SubjectType)
rulesBytes, marshalErr := json.Marshal(req.Rules)
if marshalErr != nil {
return nil, fmt.Errorf("费率JSON序列化失败: %v", marshalErr)
}
if err := s.validateConfigRules(subjectType, string(rulesBytes)); err != nil {
return nil, err
}
if req.Currency == "" {
req.Currency = "CNY"
}
if req.Id == 0 && g.IsEmpty(req.Enabled) {
req.Enabled = new(1)
}
var id int64
var err error
if req.Id > 0 {
// Update 返回受影响行数;配置 ID 即 req.Id
if _, err = chargeDao.PricingConfig.Update(ctx, req); err != nil {
return nil, err
}
id = req.Id
} else {
id, err = chargeDao.PricingConfig.Insert(ctx, req)
if err != nil {
return nil, err
}
}
return &pricingDto.SavePricingConfigRes{Id: id}, nil
}
// GetPricingConfig 查询计价配置
func (s *pricing) GetPricingConfig(ctx context.Context, req *pricingDto.GetPricingConfigReq) (res *pricingDto.GetPricingConfigRes, err error) {
cfg, err := chargeDao.PricingConfig.Get(ctx, req)
if err != nil {
return nil, err
}
if cfg == nil {
return nil, ErrConfigNotFound
}
res = new(pricingDto.GetPricingConfigRes)
if err = gconv.Struct(cfg, res); err != nil {
return
}
return
}
// validateConfigRules 按 subject 类型校验 rules JSONspec §4):
//
// workflow:三模式容器逐个校验已配模式;model:先解析 unit 再用对应计算器;businessper_period 计算器
func (s *pricing) validateConfigRules(subjectType pricingConsts.SubjectType, rulesJSON string) error {
switch subjectType {
case pricingConsts.SubjectTypeWorkflow:
return s.validateWorkflowRules(rulesJSON)
case pricingConsts.SubjectTypeModel:
var cfg struct {
Unit pricingConsts.ChargeMode `json:"unit"`
}
if err := json.Unmarshal([]byte(rulesJSON), &cfg); err != nil {
return fmt.Errorf("model 费率JSON解析失败: %v", err)
}
c, err := getCalculator(subjectType, cfg.Unit)
if err != nil {
return err
}
_, err = c.validate(rulesJSON)
return err
case pricingConsts.SubjectTypeBusiness:
c, err := getCalculator(subjectType, pricingConsts.ChargeModePerPeriod)
if err != nil {
return err
}
_, err = c.validate(rulesJSON)
return err
default:
return errors.New("未知计价对象类型: " + string(subjectType))
}
}
// validateWorkflowRules 校验三模式容器:逐个校验已配模式(spec §4.1)
func (s *pricing) validateWorkflowRules(rulesJSON string) error {
var container map[string]json.RawMessage
if err := json.Unmarshal([]byte(rulesJSON), &container); err != nil {
return fmt.Errorf("workflow 费率JSON解析失败: %v", err)
}
if len(container) == 0 {
return errors.New("workflow 费率至少配置一种模式")
}
for mode, raw := range container {
chargeMode := pricingConsts.ChargeMode(mode)
c, err := getCalculator(pricingConsts.SubjectTypeWorkflow, chargeMode)
if err != nil {
return err
}
if _, err = c.validate(string(raw)); err != nil {
return fmt.Errorf("workflow %s 模式校验失败: %v", mode, err)
}
}
return nil
}
// getEnabledConfig 获取启用中的计价配置
func (s *pricing) getEnabledConfig(ctx context.Context, subjectType pricingConsts.SubjectType, subjectID string) (*pricingEntity.PricingConfig, error) {
cfg, err := chargeDao.PricingConfig.Get(ctx, &pricingDto.GetPricingConfigReq{SubjectType: string(subjectType), SubjectID: subjectID})
if err != nil {
return nil, err
}
if cfg == nil {
return nil, ErrConfigNotFound
}
if cfg.Enabled != 1 {
return nil, errors.New("计价配置未启用: " + subjectID)
}
return cfg, nil
}
// resolveChargeMode 建单时解析实际计费方式与规则快照(spec §6 OpenOrder 校验):
//
// workflowchargeMode 必填 ∈ rules 已配模式;快照 = rules[chargeMode]
// modelchargeMode 可空 = config.unit;快照 = 整个 modelCfg
// businesschargeMode 可空 = per_period;快照 = 周期规则
func (s *pricing) resolveChargeMode(ctx context.Context, cfg *pricingEntity.PricingConfig, req *pricingDto.OpenOrderReq) (pricingConsts.ChargeMode, map[string]any, error) {
switch cfg.SubjectType {
case pricingConsts.SubjectTypeWorkflow:
if req.ChargeMode == "" {
return "", nil, errors.New("workflow 建单须指定 chargeMode")
}
return pricingConsts.ChargeMode(req.ChargeMode), s.workflowRuleSnapshot(cfg.Rules, req.ChargeMode), nil
case pricingConsts.SubjectTypeModel:
// model 计费方式固定取配置 unitcfg.Unit() 见 entity);快照 = 整个 rules 对象
return cfg.Unit(), cfg.Rules, nil
case pricingConsts.SubjectTypeBusiness:
return pricingConsts.ChargeModePerPeriod, cfg.Rules, nil
default:
return "", nil, errors.New("未知计价对象类型: " + string(cfg.SubjectType))
}
}
// workflowRuleSnapshot 从三模式容器取选中模式的规则片段对象(spec §4.1)
func (s *pricing) workflowRuleSnapshot(rules map[string]any, mode string) map[string]any {
if snapshot, ok := rules[mode].(map[string]any); ok {
return snapshot
}
return nil
}
+426
View File
@@ -0,0 +1,426 @@
package service
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/util/gconv"
"gitea.redpowerfuture.com/red-future/common/db/gfdb"
pricingConsts "shop-user-trade/consts/pricing"
"shop-user-trade/consts/public"
walletConsts "shop-user-trade/consts/wallet"
chargeDao "shop-user-trade/dao/pricing"
walletDao "shop-user-trade/dao/wallet"
pricingDto "shop-user-trade/model/dto/pricing"
walletDto "shop-user-trade/model/dto/wallet"
pricingEntity "shop-user-trade/model/entity/pricing"
walletService "shop-user-trade/service/wallet"
)
var (
// ErrOrderAlreadyHandled 计费单已被处理(并发幂等:让事务回滚,避免账务重复)
ErrOrderAlreadyHandled = errors.New("计费单已被处理")
// ErrConfigNotFound 计价配置不存在(service 层业务错误)
ErrConfigNotFound = errors.New("计价配置不存在")
)
// Pricing pricing 计价服务:算钱(定价+计费单生命周期+门禁)。
// 职责:建单(不动钱)→ 结算/取消(实收)→ 失败(不扣费)。
// 锁经 wallet servicecommon 统一锁),账务经钱包 dao 的 Change 原语;
// 钱包与计费单状态在同一事务内原子更新,不直接操作钱包表。
var Pricing = new(pricing)
type pricing struct{}
// ====================== 建单(不动钱) ======================
// OpenOrder 建单:校验配置 + 解析计费方式 + 门禁(余额 >= min_balance,非冻结),建 CREATED 计费单,不动钱。
// 幂等键 subject_type + subject_id + biz_order_no(工作流 execId),重复调用返回既有单。
func (s *pricing) OpenOrder(ctx context.Context, req *pricingDto.OpenOrderReq) (*pricingDto.ChargeOrderInfo, error) {
cfg, err := s.getEnabledConfig(ctx, pricingConsts.SubjectType(req.SubjectType), req.SubjectID)
if err != nil {
return nil, err
}
// 幂等快路径:同 subject + biz_order_no 已建单直接返回
if exist, err := chargeDao.ChargeOrder.GetOrder(ctx, &pricingDto.GetChargeOrderReq{
SubjectType: req.SubjectType, SubjectID: req.SubjectID, BizOrderNo: req.BizOrderNo,
}); err != nil {
return nil, err
} else if exist != nil {
info := s.toOrderInfo(exist)
return &info, nil
}
// 解析计费方式与规则快照(含 chargeMode 校验)
chargeMode, ruleSnapshot, err := s.resolveChargeMode(ctx, cfg, req)
if err != nil {
return nil, err
}
// 建单前校验费率 JSON(脏配置在建单即暴露,不拖到结算)
c, err := getCalculator(cfg.SubjectType, chargeMode)
if err != nil {
return nil, err
}
ruleJSON, err := json.Marshal(ruleSnapshot)
if err != nil {
return nil, fmt.Errorf("费率快照序列化失败: %v", err)
}
if _, err = c.validate(string(ruleJSON)); err != nil {
return nil, err
}
// 门禁(非冻结):调用前可用余额须 >= min_balance。
// 不动钱,普通读即可,无需锁钱包;真正的扣费发生在结算时(允许负余额兜底)。
w, err := walletDao.Account.Get(ctx, &walletDto.GetAccountByUserIdReq{UserId: req.UserId})
if err != nil {
return nil, err
}
if w == nil {
return nil, walletService.ErrWalletNotFound
}
if w.Status != walletConsts.WalletStatusEnabled {
return nil, walletService.ErrWalletDisabled
}
if cfg.MinBalance > 0 && w.Balance < cfg.MinBalance {
return nil, fmt.Errorf("余额不足:可用余额须不低于 %.2f 元才能发起", cfg.MinBalance)
}
id, err := chargeDao.ChargeOrder.Insert(ctx, &pricingDto.CreateChargeOrderReq{
SubjectType: cfg.SubjectType,
SubjectID: cfg.SubjectID,
BizOrderNo: req.BizOrderNo,
UserId: req.UserId,
ChargeMode: chargeMode,
Status: pricingConsts.ChargeOrderStatusCreated,
RuleSnapshot: ruleSnapshot, // 费率快照,结算据此计价,防改价影响在途单
})
if err != nil {
// 唯一键冲突:并发已建单,幂等返回既有单
if exist, e2 := chargeDao.ChargeOrder.GetOrder(ctx, &pricingDto.GetChargeOrderReq{
SubjectType: req.SubjectType, SubjectID: req.SubjectID, BizOrderNo: req.BizOrderNo,
}); e2 == nil && exist != nil {
info := s.toOrderInfo(exist)
return &info, nil
}
return nil, err
}
entity := &pricingEntity.ChargeOrder{
SubjectType: cfg.SubjectType,
SubjectID: cfg.SubjectID,
BizOrderNo: req.BizOrderNo,
UserId: req.UserId,
ChargeMode: chargeMode,
Status: pricingConsts.ChargeOrderStatusCreated,
RuleSnapshot: ruleSnapshot,
}
entity.Id = id
info := s.toOrderInfo(entity)
return &info, nil
}
// ====================== 结算(成功:按最终产出;取消:按已消耗) ======================
// Settle 结算:按实际用量计价实收(余额不足允许为负=欠费),幂等。
func (s *pricing) Settle(ctx context.Context, req *pricingDto.SettleReq) (*pricingDto.ChargeOrderInfo, error) {
order, err := s.resolveOrder(ctx, req.OrderId, req.SubjectType, req.SubjectID, req.BizOrderNo)
if err != nil {
return nil, err
}
if _, err = s.settleOrder(ctx, order, req.Usage, "结算:"); err != nil {
return nil, err
}
info := s.toOrderInfo(order)
return &info, nil
}
// Cancel 取消:中途取消按已消耗用量计价实收(非退款),幂等。
func (s *pricing) Cancel(ctx context.Context, req *pricingDto.CancelReq) (*pricingDto.ChargeOrderInfo, error) {
order, err := s.resolveOrder(ctx, req.OrderId, req.SubjectType, req.SubjectID, req.BizOrderNo)
if err != nil {
return nil, err
}
if _, err = s.settleOrder(ctx, order, req.Usage, "取消结算(已消耗):"); err != nil {
return nil, err
}
info := s.toOrderInfo(order)
return &info, nil
}
// settleOrder 核心结算:按 rule_snapshot 计价实收(允许负余额),状态 CREATED → SETTLED。
// 钱包写入在独立 DB group,无法随 default 事务回滚;故持用户锁内权威重读状态为 CREATED 才扣费,
// 条件状态迁移(CAS)作最后防线。重复结算返回既有结果。
func (s *pricing) settleOrder(ctx context.Context, order *pricingEntity.ChargeOrder, usage map[string]any, desc string) (float64, error) {
// 快路径(非权威):锁外状态仅供提前返回,扣费与否以锁内重读为准
if order.Status == pricingConsts.ChargeOrderStatusSettled {
return order.ActualAmount, nil
}
if order.Status != pricingConsts.ChargeOrderStatusCreated {
return 0, errors.New("计费单状态不可结算(已失败或状态异常)")
}
var usageStruct ChargeUsage
if err := gconv.Struct(usage, &usageStruct); err != nil {
return 0, fmt.Errorf("用量解析失败: %v", err)
}
// 用建单时费率快照计价(防改价影响在途单);per_token 无快照(槽位 {}),按调用方上报的
// 「按次已记录费用」(feeByModel)合计实收——每次模型调用在发生时已由 shop /calc 计价
// calculator.charge 内部 ceilFen,「不足1分按1分」按调用次生效 + 调用时媒体/费率快照),
// 直接合计即实收,不再按聚合 token 重算(两笔各 0.01 合并重算成 0.01 的误收根因)。
// 调用方原始上报的用量整份落库 order.usage 作审计(本处 gconv 解析只取计价字段,未知键忽略,不影响落库)。
var actualAmount float64
var err error
if order.SubjectType == pricingConsts.SubjectTypeWorkflow && order.ChargeMode == pricingConsts.ChargeModePerToken {
actualAmount = sumRecordedFee(usageStruct.FeeByModel)
} else {
var ruleJSON []byte
ruleJSON, err = json.Marshal(order.RuleSnapshot)
if err == nil {
actualAmount, err = calcCharge(order.SubjectType, order.ChargeMode, string(ruleJSON), &usageStruct)
}
// per_item / per_second 取消补收:中途取消通常无视频时长产出(时长计价=0),但已完成节点可能已消耗
// token。调用方在 cancel 场景附 feeByModel(不含视频节点,其消耗由时长计价覆盖),与时长计价相加;
// 正常结算调用方不附费用拆分 → 仅时长,行为不变。
if err == nil && order.SubjectType == pricingConsts.SubjectTypeWorkflow && len(usageStruct.FeeByModel) > 0 {
actualAmount = ceilFen(roundCost(actualAmount + sumRecordedFee(usageStruct.FeeByModel)))
}
}
if err != nil {
return 0, err
}
var alreadySettled bool
var settledAmount float64
err = walletService.Account.WithUserLock(ctx, order.UserId, func(ctx context.Context) error {
// 锁内权威重读:并发 Settle/Cancel 经同一用户锁串行,锁内状态才是最终判定。
// 钱包 account 与计费单同处 wallet DB group,事务须开在 wallet 组(gfdb.DB(ctx) 无参 = default 组,
// 而 shop-user-trade 的 default 组指向 shop_user_trade 库、无钱包表,会在 BEGIN 即失败),
// 钱包扣款与订单状态迁移才能在同一事务内原子生效。
fresh, err := chargeDao.ChargeOrder.GetOrder(ctx, &pricingDto.GetChargeOrderReq{Id: order.Id})
if err != nil {
return err
}
if fresh == nil {
return errors.New("计费单不存在")
}
if fresh.Status == pricingConsts.ChargeOrderStatusSettled {
alreadySettled = true
settledAmount = fresh.ActualAmount
return nil
}
if fresh.Status != pricingConsts.ChargeOrderStatusCreated {
return errors.New("计费单状态不可结算(已失败或状态异常)")
}
return gfdb.DB(ctx, public.DbNameWallet).Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
// 实收扣款(允许负余额=欠费,靠充值归正,见技术设计 §9)。
if err := walletDao.Account.Change(ctx, &walletDto.ChangeAccountReq{
UserId: order.UserId,
DeltaBalance: -actualAmount,
Type: walletConsts.WalletLogTypeExpense,
Amount: actualAmount,
OrderNo: fmt.Sprintf("charge:%d", order.Id),
Description: desc + string(order.SubjectType) + ":" + order.SubjectID,
}); err != nil {
return err
}
// 条件状态迁移:仅 CREATED → SETTLEDrows=0 说明已被并发处理(锁内不应发生,作最后防线)
ok, err := chargeDao.ChargeOrder.UpdateToSettled(ctx, &pricingDto.SettleChargeOrderReq{
Id: order.Id,
ActualAmount: actualAmount,
Usage: usage,
RuleSnapshot: order.RuleSnapshot,
SettleTime: gtime.Now(),
})
if err != nil {
return err
}
if !ok {
return ErrOrderAlreadyHandled
}
return nil
})
})
if err != nil {
return 0, err
}
if alreadySettled {
order.Status = pricingConsts.ChargeOrderStatusSettled
order.ActualAmount = settledAmount
order.Usage = usage
return settledAmount, nil
}
order.Status = pricingConsts.ChargeOrderStatusSettled
order.ActualAmount = actualAmount
order.Usage = usage
return actualAmount, nil
}
// sumRecordedFee 按次已记录费用合计:feeByModel 各值为调用方按次调 shop /calc 的返回费
// calculator.charge 内部已 ceilFen 到分),直接相加再归一化到分(去浮点噪声)。
// 负值防御性忽略(异常上报不抵减)。per_item/per_second 取消补收与 per_token 结算的 token 金额据此实收。
func sumRecordedFee(feeByModel map[string]float64) float64 {
var total float64
for _, fee := range feeByModel {
if fee < 0 {
continue
}
total += fee
}
return ceilFen(roundCost(total))
}
// ====================== 失败(不扣费) ======================
// Fail 失败处理:执行失败/无产出,不扣费、不动钱,仅状态迁移 CREATED → FAILED。幂等。
func (s *pricing) Fail(ctx context.Context, req *pricingDto.FailReq) (*pricingDto.ChargeOrderInfo, error) {
order, err := s.resolveOrder(ctx, req.OrderId, req.SubjectType, req.SubjectID, req.BizOrderNo)
if err != nil {
return nil, err
}
// 幂等:已失败直接返回
if order.Status == pricingConsts.ChargeOrderStatusFailed {
info := s.toOrderInfo(order)
return &info, nil
}
if order.Status != pricingConsts.ChargeOrderStatusCreated {
return nil, errors.New("计费单状态不可转失败(已结算或状态异常)")
}
// 与 Settle/Cancel 同一用户锁串行,防止结算/失败竞争(锁内重读为权威判定)
err = walletService.Account.WithUserLock(ctx, order.UserId, func(ctx context.Context) error {
fresh, err := chargeDao.ChargeOrder.GetOrder(ctx, &pricingDto.GetChargeOrderReq{Id: order.Id})
if err != nil {
return err
}
if fresh == nil {
return errors.New("计费单不存在")
}
if fresh.Status == pricingConsts.ChargeOrderStatusFailed {
return nil // 已失败(幂等)
}
if fresh.Status != pricingConsts.ChargeOrderStatusCreated {
return errors.New("计费单状态不可转失败(已结算或状态异常)")
}
ok, err := chargeDao.ChargeOrder.UpdateToFailed(ctx, &pricingDto.FailChargeOrderReq{Id: order.Id})
if err != nil {
return err
}
if !ok {
return ErrOrderAlreadyHandled
}
return nil
})
if err != nil {
return nil, err
}
order.Status = pricingConsts.ChargeOrderStatusFailed
info := s.toOrderInfo(order)
return &info, nil
}
// ====================== 算费(不建单不扣费) ======================
// CalcFee 按计价配置与用量计算费用(不建单不扣费)。未配置/未启用 → 报错(model-gateway 据此阻塞调用)。
// model subjectchargeMode 可空 = 配置 unit;快照 = 整个 rules。复用 getEnabledConfig + resolveChargeMode + 计算器。
func (s *pricing) CalcFee(ctx context.Context, req *pricingDto.CalcFeeReq) (*pricingDto.CalcFeeRes, error) {
cfg, err := s.getEnabledConfig(ctx, pricingConsts.SubjectType(req.SubjectType), req.SubjectID)
if err != nil {
return nil, err
}
chargeMode, ruleSnapshot, err := s.resolveChargeMode(ctx, cfg, &pricingDto.OpenOrderReq{
SubjectType: req.SubjectType, SubjectID: req.SubjectID, ChargeMode: req.ChargeMode,
})
if err != nil {
return nil, err
}
var usage ChargeUsage
if err := gconv.Struct(req.Usage, &usage); err != nil {
return nil, fmt.Errorf("用量解析失败: %v", err)
}
c, err := getCalculator(cfg.SubjectType, chargeMode)
if err != nil {
return nil, err
}
ruleJSON, err := json.Marshal(ruleSnapshot)
if err != nil {
return nil, fmt.Errorf("费率快照序列化失败: %v", err)
}
rules, err := c.validate(string(ruleJSON))
if err != nil {
return nil, err
}
cost, err := c.charge(rules, &usage)
if err != nil {
return nil, err
}
return &pricingDto.CalcFeeRes{Cost: cost}, nil
}
// ====================== 查询 ======================
// GetOrderInfo 查询计费单(复用 resolveOrder + toOrderInfo;工作流结算前取 CreatedAt 算时长)
func (s *pricing) GetOrderInfo(ctx context.Context, req *pricingDto.GetOrderInfoReq) (*pricingDto.ChargeOrderInfo, error) {
order, err := s.resolveOrder(ctx, req.Id, req.SubjectType, req.SubjectID, req.BizOrderNo)
if err != nil {
return nil, err
}
info := s.toOrderInfo(order)
return &info, nil
}
// ====================== 内部辅助 ======================
// resolveOrder 按ID或 subject_type+subject_id+biz_order_no 定位计费单
func (s *pricing) resolveOrder(ctx context.Context, orderId int64, subjectType, subjectID, bizOrderNo string) (*pricingEntity.ChargeOrder, error) {
if orderId > 0 {
order, err := chargeDao.ChargeOrder.GetOrder(ctx, &pricingDto.GetChargeOrderReq{Id: orderId})
if err != nil {
return nil, err
}
if order == nil {
return nil, errors.New("计费单不存在")
}
return order, nil
}
if subjectType == "" || subjectID == "" || bizOrderNo == "" {
return nil, errors.New("orderId 与 subjectType/subjectId/bizOrderNo 须二选一")
}
order, err := chargeDao.ChargeOrder.GetOrder(ctx, &pricingDto.GetChargeOrderReq{SubjectType: subjectType, SubjectID: subjectID, BizOrderNo: bizOrderNo})
if err != nil {
return nil, err
}
if order == nil {
return nil, errors.New("计费单不存在")
}
return order, nil
}
// toOrderInfo 计费单实体转响应
func (s *pricing) toOrderInfo(e *pricingEntity.ChargeOrder) pricingDto.ChargeOrderInfo {
info := pricingDto.ChargeOrderInfo{
ID: e.Id,
SubjectType: string(e.SubjectType),
SubjectID: e.SubjectID,
BizOrderNo: e.BizOrderNo,
UserId: e.UserId,
ChargeMode: string(e.ChargeMode),
Status: int(e.Status),
ActualAmount: e.ActualAmount,
Usage: e.Usage,
RuleSnapshot: e.RuleSnapshot,
}
if e.CreatedAt != nil {
info.CreatedAt = e.CreatedAt.String()
}
if e.SettleTime != nil {
info.SettleTime = e.SettleTime.String()
}
return info
}
+176
View File
@@ -0,0 +1,176 @@
package service
import (
"context"
"fmt"
"gitea.redpowerfuture.com/red-future/common/beans"
"gitea.redpowerfuture.com/red-future/common/http"
pricingConsts "shop-user-trade/consts/pricing"
pricingDto "shop-user-trade/model/dto/pricing"
)
// modelGatewayModelItem 解析 /listModelManage 响应单条(仅取枚举所需字段)。
// model-gateway 的 ModelType 为 *intconsts/model/model_type.go:59 `type ModelType = *int`),可为 null → 用 *int 安全解析。
type modelGatewayModelItem struct {
Id int64 `json:"id"`
ModelName string `json:"modelName"`
ModelType *int `json:"modelType"` // 编码:100推理/200图片/300音频/600视频(*int,可能 null
SystemModel *bool `json:"systemModel"`
}
// ListSubjects 计价对象枚举:固定 workflow + 实时 model-gateway 系统模型 + 固定业务模块(spec §7)。
// 每项查 pricing_config 是否已配置(hasConfig/enabled);model-gateway 不可用时直接报错。
func (s *pricing) ListSubjects(ctx context.Context, req *pricingDto.SubjectListReq) (*pricingDto.SubjectListRes, error) {
res := &pricingDto.SubjectListRes{Subjects: []pricingDto.SubjectInfo{}}
// 1. workflow(固定)
res.Subjects = append(res.Subjects, pricingDto.SubjectInfo{
SubjectType: string(pricingConsts.SubjectTypeWorkflow),
SubjectID: pricingConsts.WorkflowSubjectID,
Name: "工作流",
ChargeModes: []string{string(pricingConsts.ChargeModePerItem), string(pricingConsts.ChargeModePerSecond), string(pricingConsts.ChargeModePerToken)},
})
// 2. 业务模块(固定写死)
for _, b := range pricingConsts.BusinessSubjects {
modes := make([]string, 0, len(b.ChargeModes))
for _, m := range b.ChargeModes {
modes = append(modes, string(m))
}
res.Subjects = append(res.Subjects, pricingDto.SubjectInfo{
SubjectType: string(pricingConsts.SubjectTypeBusiness),
SubjectID: b.SubjectID,
Name: b.Name,
ChargeModes: modes,
})
}
// 3. model-gateway 系统模型(实时;不可用直接报错)
models, err := s.listSystemModels(ctx)
if err != nil {
return nil, err
}
for _, m := range models {
typeCode := 0
if m.ModelType != nil {
typeCode = *m.ModelType
}
res.Subjects = append(res.Subjects, pricingDto.SubjectInfo{
SubjectType: string(pricingConsts.SubjectTypeModel),
SubjectID: fmt.Sprintf("%d", m.Id),
Name: m.ModelName,
ModelType: modelTypeName(typeCode),
ModelTypeCode: typeCode,
//HasConfig: false, Enabled: 0,
})
}
// 填充 hasConfig/enabled(批量查 pricing_config
//if err := s.fillSubjectConfig(ctx, res.Subjects); err != nil {
// return nil, err
//}
return res, nil
}
// listSystemModels 调 model-gateway /listModelManagesystem_model=true)拉取系统模型
func (s *pricing) listSystemModels(ctx context.Context) ([]modelGatewayModelItem, error) {
// ⚠️ http.Get 内部已剥离 {code,message,data} 包裹层并 gconv.Struct(Data, target)
// 故 target 直接是 data 载荷 {list,total}controller 修正 2026-08-28,原稿误传包裹结构会静默空列表)
res := &struct {
List []modelGatewayModelItem `json:"list"`
Total int `json:"total"`
}{}
// GET 查询参数:偶数对 key/value;分页拉全量(总条数少,一次取大页)。
err := http.Get(ctx, "model-gateway/model/manage/listModelManage", nil, res, beans.Page{PageNum: 1, PageSize: 1000})
if err != nil {
return nil, fmt.Errorf("调用model-gateway模型列表失败: %w", err)
}
var system []modelGatewayModelItem
for _, m := range res.List {
if m.SystemModel != nil && *m.SystemModel {
system = append(system, m)
}
}
return system, nil
}
// fillSubjectConfig 按 (subject_type, subject_id) 查配置,填充 hasConfig/enabled
//func (s *pricing) fillSubjectConfig(ctx context.Context, items []pricingDto.SubjectInfo) error {
// for i := range items {
// cfg, err := chargeDao.PricingConfig.Get(ctx, &pricingDto.GetConfigReq{
// SubjectType: items[i].SubjectType, SubjectID: items[i].SubjectID,
// })
// if err != nil {
// return err
// }
// if cfg != nil {
// items[i].HasConfig = true
// items[i].Enabled = cfg.Enabled
// }
// }
// return nil
//}
// modelTypeName 模型类型编码 → 名称(管理端分组展示)
func modelTypeName(code int) string {
switch code {
case pricingConsts.ModelTypeReasoning:
return "推理"
case pricingConsts.ModelTypeImage:
return "图片"
case pricingConsts.ModelTypeAudio:
return "音频"
case pricingConsts.ModelTypeVideo:
return "视频"
case pricingConsts.ModelTypeVector:
return "向量"
case pricingConsts.ModelTypeOmni:
return "多模态"
default:
return "其他"
}
}
// ListChargeModes 按 (subjectType, subjectId) 返回主体自身可选计费方式(spec 2026-09-01)。
// model 无需选择不入参(subjectType 校验层拦截);workflow/business 纯查常量,零外部依赖。
func (s *pricing) ListChargeModes(ctx context.Context, req *pricingDto.ChargeModeListReq) (*pricingDto.ChargeModeListRes, error) {
switch pricingConsts.SubjectType(req.SubjectType) {
case pricingConsts.SubjectTypeWorkflow:
if req.SubjectID != pricingConsts.WorkflowSubjectID {
return nil, fmt.Errorf("workflow 主体 subjectId 必须为 %q", pricingConsts.WorkflowSubjectID)
}
return &pricingDto.ChargeModeListRes{
SubjectType: req.SubjectType,
SubjectID: req.SubjectID,
ChargeModes: toChargeModeInfo(pricingConsts.WorkflowChargeModes),
}, nil
case pricingConsts.SubjectTypeBusiness:
for _, b := range pricingConsts.BusinessSubjects {
if b.SubjectID == req.SubjectID {
return &pricingDto.ChargeModeListRes{
SubjectType: req.SubjectType,
SubjectID: req.SubjectID,
ChargeModes: toChargeModeInfo(b.ChargeModes),
}, nil
}
}
return nil, fmt.Errorf("未知业务模块: %s", req.SubjectID)
default:
return nil, fmt.Errorf("不支持的计价对象类型: %s", req.SubjectType)
}
}
// toChargeModeInfo 计费方式 → {mode, name}name 未命中回退 mode,避免展示空串)
func toChargeModeInfo(modes []pricingConsts.ChargeMode) []pricingDto.ChargeModeInfo {
items := make([]pricingDto.ChargeModeInfo, 0, len(modes))
for _, m := range modes {
name, ok := pricingConsts.ChargeModeNames[m]
if !ok {
name = string(m)
}
items = append(items, pricingDto.ChargeModeInfo{Mode: string(m), Name: name})
}
return items
}
+128
View File
@@ -0,0 +1,128 @@
package service
import (
"context"
"errors"
"fmt"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/util/gconv"
"gitea.redpowerfuture.com/red-future/common/db/gfdb"
"gitea.redpowerfuture.com/red-future/common/utils"
walletConsts "shop-user-trade/consts/wallet"
walletDao "shop-user-trade/dao/wallet"
walletDto "shop-user-trade/model/dto/wallet"
walletEntity "shop-user-trade/model/entity/wallet"
)
// Account 钱包服务:管钱(钱包账务 + 流水)。
// 事务与 Redis 锁统一在本层编排:Recharge 持 common 锁 + 单事务原子完成;
// pricing 结算复用本层 WithUserLock 编排「钱包账务 + 计费单状态」。
// DAO 只提供事务透明的数据原语(Change),不加锁、不开事务、不互相调用。
var Account = new(account)
type account struct{}
const (
walletLockTTL = 30 // 钱包 Redis 锁 TTL(秒);锁串行化跨节点互斥,DB 事务 + 行锁兜底正确性
walletLockRetry = 20 // 抢锁最多尝试次数(每次间隔 500ms,≈10s),避免无限阻塞
)
var (
// ErrWalletNotFound 钱包不存在(service 层业务错误;DAO 不定义业务错误,见分层规范)
ErrWalletNotFound = errors.New("钱包不存在")
// ErrWalletDisabled 钱包状态不可用
ErrWalletDisabled = errors.New("钱包状态不可用")
)
// GetAccountByUserId 根据用户ID获取钱包信息
func (s *account) GetAccountByUserId(ctx context.Context, req *walletDto.GetAccountByUserIdReq) (*walletDto.AccountInfo, error) {
w, err := walletDao.Account.Get(ctx, req)
if err != nil {
g.Log().Errorf(ctx, "获取用户钱包失败, userId: %d, error: %v", req.UserId, err)
return nil, err
}
if w == nil {
return nil, ErrWalletNotFound
}
return &walletDto.AccountInfo{
ID: w.Id,
UserID: w.UserID,
Balance: w.Balance,
Currency: w.Currency,
Status: int(w.Status),
}, nil
}
// WithUserLock 持用户钱包 Redis 锁执行 fn(跨节点串行化单钱包互斥)。
// 锁用 common 统一实现 utils.WithLockuuid token + SET NX EX + WATCH/MULTI 归属释放 + 自动续期)。
// 事务由调用方自行开启(gfdb.DB(ctx).Transaction),账务原语在事务内自动继承,本方法不开事务。
func (s *account) WithUserLock(ctx context.Context, userID int64, fn func(ctx context.Context) error) error {
_, err := utils.WithLock(ctx, fmt.Sprintf("wallet:lock:%d", userID), walletLockTTL, fn, walletLockRetry)
return err
}
// Recharge 充值(钱包不存在则自动创建)。
// 事务在 service 层编排:持钱包锁 → 单事务内「建钱包 + income 流水 + 余额增加」原子完成。
func (s *account) Recharge(ctx context.Context, req *walletDto.RechargeReq) error {
if req.Amount <= 0 {
return errors.New("充值金额必须大于 0")
}
currency := req.Currency
if currency == "" {
currency = "CNY"
}
err := s.WithUserLock(ctx, req.UserId, func(ctx context.Context) error {
return gfdb.DB(ctx).Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
w, err := walletDao.Account.GetForUpdate(ctx, &walletDto.GetAccountByUserIdReq{UserId: req.UserId})
if err != nil {
return err
}
if w == nil {
if _, err = walletDao.Account.Create(ctx, &walletDto.CreateAccountReq{UserId: req.UserId, UserName: req.UserName, Status: 1, Currency: currency}); err != nil {
return err
}
}
return walletDao.Account.Change(ctx, &walletDto.ChangeAccountReq{
UserId: req.UserId,
DeltaBalance: req.Amount,
Type: walletConsts.WalletLogTypeIncome,
Amount: req.Amount,
OrderNo: req.OrderNo,
Description: req.Description,
})
})
})
if err != nil {
return err
}
return nil
}
// GetWalletLogs 分页获取钱包流水
func (s *account) GetWalletLogs(ctx context.Context, req *walletDto.GetAccountLogsReq) (*walletDto.AccountLogData, error) {
logs, total, err := walletDao.Account.ListLogs(ctx, req)
if err != nil {
return nil, err
}
var infos []walletDto.AccountLogInfo
for i := range logs {
infos = append(infos, s.logEntityToInfo(&logs[i]))
}
return &walletDto.AccountLogData{Logs: infos, Total: total}, nil
}
// logEntityToInfo 流水实体转Info
func (s *account) logEntityToInfo(e *walletEntity.AccountLog) walletDto.AccountLogInfo {
info := walletDto.AccountLogInfo{}
_ = gconv.Struct(e, &info)
info.ID = e.Id
info.Type = string(e.Type)
if e.CreatedAt != nil {
info.CreatedAt = e.CreatedAt.String()
}
return info
}
-153
View File
@@ -1,153 +0,0 @@
package service
import (
"context"
"errors"
"fmt"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/util/gconv"
walletDao "shop-user-trade/dao/wallet"
walletDto "shop-user-trade/model/dto/wallet"
walletEntity "shop-user-trade/model/entity/wallet"
)
type wallet struct{}
// Wallet 钱包服务
var Wallet = new(wallet)
// GetByUserId 根据用户ID获取钱包
func (s *wallet) GetByUserId(ctx context.Context, req *walletDto.GetWalletByUserIdReq) (*walletDto.GetWalletByUserIdResp, error) {
w, err := walletDao.Wallet.GetByUserID(ctx, req.UserId)
if err != nil {
g.Log().Errorf(ctx, "获取用户钱包失败, userId: %d, error: %v", req.UserId, err)
return nil, err
}
if w == nil {
return nil, errors.New("钱包不存在")
}
return &walletDto.GetWalletByUserIdResp{
Data: walletDto.WalletInfo{
ID: w.Id,
UserID: w.UserID,
Balance: w.Balance,
Currency: w.Currency,
Status: int(w.Status),
},
}, nil
}
// Create 创建钱包
func (s *wallet) Create(ctx context.Context, req *walletDto.CreateWalletReq) (*walletDto.CreateWalletResp, error) {
// 检查钱包是否已存在
existing, err := walletDao.Wallet.GetByUserID(ctx, req.UserId)
if err != nil {
g.Log().Errorf(ctx, "检查用户钱包失败, userId: %d, error: %v", req.UserId, err)
return nil, err
}
if existing != nil {
return nil, errors.New("钱包已存在")
}
id, err := walletDao.Wallet.Create(ctx, req)
if err != nil {
g.Log().Errorf(ctx, "创建钱包失败, userId: %d, error: %v", req.UserId, err)
return nil, err
}
return &walletDto.CreateWalletResp{
Data: walletDto.CreateWalletData{WalletID: id},
}, nil
}
// UpdateBalance 更新余额
func (s *wallet) UpdateBalance(ctx context.Context, req *walletDto.UpdateBalanceReq) (*walletDto.UpdateBalanceResp, error) {
w, err := walletDao.Wallet.GetByID(ctx, req.WalletID)
if err != nil {
g.Log().Errorf(ctx, "获取钱包失败, walletId: %d, error: %v", req.WalletID, err)
return nil, err
}
if w == nil {
return nil, errors.New("钱包不存在")
}
// 检查余额是否充足
if req.Type == "expense" && w.Balance < req.Amount {
return nil, errors.New("余额不足")
}
// 记录操作前余额
balanceBefore := w.Balance
var balanceAfter int64
switch req.Type {
case "income":
balanceAfter = balanceBefore + req.Amount
case "expense":
balanceAfter = balanceBefore - req.Amount
default:
balanceAfter = balanceBefore
}
// 更新余额(乐观锁)
success, err := walletDao.Wallet.UpdateBalance(ctx, req.WalletID, req.Amount, req.Type, w.Version)
if err != nil {
g.Log().Errorf(ctx, "更新余额失败, walletId: %d, error: %v", req.WalletID, err)
return nil, err
}
if !success {
return nil, errors.New("余额更新失败,请重试")
}
// 记录流水日志
logReq := &walletDto.CreateWalletLogReq{
UserID: w.UserID,
WalletID: w.Id,
OrderNo: req.OrderNo,
TransactionNo: req.TransactionNo,
Type: req.Type,
Amount: req.Amount,
BalanceBefore: balanceBefore,
BalanceAfter: balanceAfter,
Currency: w.Currency,
Description: req.Description,
ExtraData: req.ExtraData,
}
if _, err = walletDao.Wallet.CreateLog(ctx, logReq); err != nil {
g.Log().Warningf(ctx, "记录钱包日志失败, walletId: %d, error: %v", req.WalletID, err)
}
return &walletDto.UpdateBalanceResp{
Success: true,
Message: "更新成功",
}, nil
}
// GetWalletLogs 获取钱包日志
func (s *wallet) GetWalletLogs(ctx context.Context, req *walletDto.GetWalletLogsReq) (*walletDto.GetWalletLogsResp, error) {
logs, total, err := walletDao.Wallet.ListLogs(ctx, req.UserId, req.Page, req.PageSize)
if err != nil {
return nil, fmt.Errorf("获取钱包日志失败: %w", err)
}
var logInfos []walletDto.WalletLogInfo
for _, log := range logs {
logInfos = append(logInfos, s.logEntityToInfo(&log))
}
return &walletDto.GetWalletLogsResp{
Data: walletDto.WalletLogData{
Logs: logInfos,
Total: total,
},
}, nil
}
// logEntityToInfo 日志实体转换为Info
func (s *wallet) logEntityToInfo(e *walletEntity.WalletLog) walletDto.WalletLogInfo {
info := walletDto.WalletLogInfo{}
_ = gconv.Struct(e, &info)
info.ID = e.Id
info.Type = string(e.Type)
if e.CreatedAt != nil {
info.CreatedAt = e.CreatedAt.String()
}
return info
}