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=
+378 -119
View File
@@ -1,164 +1,423 @@
-- 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 (
id BIGINT PRIMARY KEY, -- 基础字段(继承 SQLBaseCol 通用字段,与 SQLBaseDO 对齐)
tenant_id BIGINT NOT NULL DEFAULT 0, id BIGINT PRIMARY KEY,
creator VARCHAR(64) NOT NULL DEFAULT '', tenant_id BIGINT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), creator VARCHAR(64) NOT NULL,
updater VARCHAR(64) NOT NULL DEFAULT '', created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updater VARCHAR(64) NOT NULL,
deleted_at TIMESTAMPTZ, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
is_deleted BOOLEAN NOT NULL DEFAULT FALSE, deleted_at timestamp(6),
user_id BIGINT NOT NULL, -- 背包核心字段
asset_id BIGINT NOT NULL, user_id BIGINT NOT NULL,
asset_name VARCHAR(255) NOT NULL DEFAULT '', asset_id BIGINT NOT NULL,
sku_id BIGINT NOT NULL DEFAULT 0, asset_name VARCHAR(255) NOT NULL DEFAULT '',
sku_name VARCHAR(255) NOT NULL DEFAULT '', sku_id BIGINT NOT NULL DEFAULT 0,
spec_values JSONB, sku_name VARCHAR(255) NOT NULL DEFAULT '',
image_url VARCHAR(512) NOT NULL DEFAULT '', spec_values JSONB,
type VARCHAR(32) NOT NULL, image_url VARCHAR(512) NOT NULL DEFAULT '',
stock_detail_id BIGINT NOT NULL DEFAULT 0, type VARCHAR(32) NOT NULL,
batch_id BIGINT NOT NULL DEFAULT 0, stock_detail_id BIGINT NOT NULL DEFAULT 0,
batch_no VARCHAR(64) NOT NULL DEFAULT '', batch_id BIGINT NOT NULL DEFAULT 0,
stock_mode SMALLINT NOT NULL DEFAULT 1, batch_no VARCHAR(64) NOT NULL DEFAULT '',
status SMALLINT NOT NULL DEFAULT 1, stock_mode SMALLINT NOT NULL DEFAULT 1,
used_at BIGINT, status SMALLINT NOT NULL DEFAULT 1,
expire_at BIGINT, used_at BIGINT,
expire_at BIGINT,
physical_asset_config JSONB, physical_asset_config JSONB,
service_asset_config JSONB, service_asset_config JSONB,
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 (
id BIGINT PRIMARY KEY, -- 基础字段(继承 SQLBaseCol 通用字段,与 SQLBaseDO 对齐)
tenant_id BIGINT NOT NULL DEFAULT 0, id BIGINT PRIMARY KEY,
creator VARCHAR(64) NOT NULL DEFAULT '', tenant_id BIGINT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), creator VARCHAR(64) NOT NULL,
updater VARCHAR(64) NOT NULL DEFAULT '', created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updater VARCHAR(64) NOT NULL,
deleted_at TIMESTAMPTZ, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
is_deleted BOOLEAN NOT NULL DEFAULT FALSE, deleted_at timestamp(6),
knapsack_id BIGINT NOT NULL, -- 日志核心字段
user_id BIGINT NOT NULL, knapsack_id BIGINT NOT NULL,
action VARCHAR(32) NOT NULL, user_id BIGINT NOT NULL,
operator_id BIGINT NOT NULL DEFAULT 0, action VARCHAR(32) NOT NULL,
operator_name VARCHAR(64) NOT NULL DEFAULT '', operator_id BIGINT NOT NULL DEFAULT 0,
description TEXT NOT NULL DEFAULT '' operator_name VARCHAR(64) 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 (
id BIGINT PRIMARY KEY, -- 基础字段(继承 SQLBaseCol 通用字段,与 SQLBaseDO 对齐)
tenant_id BIGINT NOT NULL DEFAULT 0, id BIGINT PRIMARY KEY,
creator VARCHAR(64) NOT NULL DEFAULT '', tenant_id BIGINT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), creator VARCHAR(64) NOT NULL,
updater VARCHAR(64) NOT NULL DEFAULT '', created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updater VARCHAR(64) NOT NULL,
deleted_at TIMESTAMPTZ, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
is_deleted BOOLEAN NOT NULL DEFAULT FALSE, deleted_at timestamp(6),
knapsack_id BIGINT NOT NULL, -- 市场核心字段
user_id BIGINT NOT NULL, knapsack_id BIGINT NOT NULL,
asset_id BIGINT NOT NULL, user_id BIGINT NOT NULL,
asset_name VARCHAR(255) NOT NULL DEFAULT '', asset_id BIGINT NOT NULL,
sku_id BIGINT NOT NULL DEFAULT 0, asset_name VARCHAR(255) NOT NULL DEFAULT '',
sku_name VARCHAR(255) NOT NULL DEFAULT '', sku_id BIGINT NOT NULL DEFAULT 0,
image_url VARCHAR(512) NOT NULL DEFAULT '', sku_name VARCHAR(255) NOT NULL DEFAULT '',
type VARCHAR(32) NOT NULL, image_url VARCHAR(512) NOT NULL DEFAULT '',
stock_detail_id BIGINT NOT NULL DEFAULT 0, type VARCHAR(32) NOT NULL,
batch_id BIGINT NOT NULL DEFAULT 0, stock_detail_id BIGINT NOT NULL DEFAULT 0,
batch_no VARCHAR(64) NOT NULL DEFAULT '', batch_id BIGINT NOT NULL DEFAULT 0,
stock_mode SMALLINT NOT NULL DEFAULT 1, batch_no VARCHAR(64) NOT NULL DEFAULT '',
price BIGINT NOT NULL DEFAULT 0, stock_mode SMALLINT NOT NULL DEFAULT 1,
original_price BIGINT NOT NULL DEFAULT 0, price BIGINT NOT NULL DEFAULT 0,
description TEXT NOT NULL DEFAULT '', original_price BIGINT NOT NULL DEFAULT 0,
status SMALLINT NOT NULL DEFAULT 1, description TEXT NOT NULL DEFAULT '',
status SMALLINT NOT NULL DEFAULT 1,
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 (
id BIGINT PRIMARY KEY, -- 基础字段(继承 SQLBaseCol 通用字段,与 SQLBaseDO 对齐)
tenant_id BIGINT NOT NULL DEFAULT 0, id BIGINT PRIMARY KEY,
creator VARCHAR(64) NOT NULL DEFAULT '', tenant_id BIGINT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), creator VARCHAR(64) NOT NULL,
updater VARCHAR(64) NOT NULL DEFAULT '', created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updater VARCHAR(64) NOT NULL,
deleted_at TIMESTAMPTZ, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
is_deleted BOOLEAN NOT NULL DEFAULT FALSE, deleted_at timestamp(6),
market_id BIGINT NOT NULL, -- 日志核心字段
knapsack_id BIGINT NOT NULL, market_id BIGINT NOT NULL,
user_id BIGINT NOT NULL, knapsack_id BIGINT NOT NULL,
action VARCHAR(32) NOT NULL, user_id BIGINT NOT NULL,
operator_id BIGINT NOT NULL DEFAULT 0, action VARCHAR(32) NOT NULL,
operator_name VARCHAR(64) NOT NULL DEFAULT '', operator_id BIGINT NOT NULL DEFAULT 0,
description TEXT NOT NULL DEFAULT '' operator_name VARCHAR(64) 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);
-- 钱包 -- 表和字段注释
CREATE TABLE IF NOT EXISTS wallet ( COMMENT ON TABLE market_log IS '市场日志表';
id BIGINT PRIMARY KEY, COMMENT ON COLUMN market_log.id IS '主键ID(非自增)';
tenant_id BIGINT NOT NULL DEFAULT 0, COMMENT ON COLUMN market_log.tenant_id IS '租户ID';
creator VARCHAR(64) NOT NULL DEFAULT '', COMMENT ON COLUMN market_log.creator IS '创建人';
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), COMMENT ON COLUMN market_log.created_at IS '创建时间';
updater VARCHAR(64) NOT NULL DEFAULT '', COMMENT ON COLUMN market_log.updater IS '更新人';
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), COMMENT ON COLUMN market_log.updated_at IS '更新时间';
deleted_at TIMESTAMPTZ, COMMENT ON COLUMN market_log.deleted_at IS '删除时间(软删)';
is_deleted BOOLEAN NOT NULL DEFAULT FALSE, 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 '操作描述';
user_id BIGINT NOT NULL UNIQUE, --------------------pgsql创建market_log表语句---------------------------
balance BIGINT NOT NULL DEFAULT 0,
currency VARCHAR(16) NOT NULL DEFAULT 'CNY', --------------------pgsql创建wallet_account表语句---------------------------
status SMALLINT NOT NULL DEFAULT 1,
version BIGINT NOT NULL DEFAULT 1 -- 钱包表
CREATE TABLE IF NOT EXISTS wallet_account (
-- 基础字段(继承 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),
-- 钱包核心字段
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',
status SMALLINT 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 (
id BIGINT PRIMARY KEY, -- 基础字段(继承 SQLBaseCol 通用字段,与 SQLBaseDO 对齐)
tenant_id BIGINT NOT NULL DEFAULT 0, id BIGINT PRIMARY KEY,
creator VARCHAR(64) NOT NULL DEFAULT '', tenant_id BIGINT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), creator VARCHAR(64) NOT NULL,
updater VARCHAR(64) NOT NULL DEFAULT '', created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updater VARCHAR(64) NOT NULL,
deleted_at TIMESTAMPTZ, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
is_deleted BOOLEAN NOT NULL DEFAULT FALSE, deleted_at timestamp(6),
user_id BIGINT NOT NULL, -- 流水核心字段
wallet_id BIGINT NOT NULL, user_id BIGINT NOT NULL,
order_no VARCHAR(64) NOT NULL DEFAULT '', wallet_id BIGINT NOT NULL,
transaction_no VARCHAR(64) NOT NULL DEFAULT '', order_no VARCHAR(64) NOT NULL DEFAULT '',
type VARCHAR(16) NOT NULL, transaction_no VARCHAR(64) NOT NULL DEFAULT '',
amount BIGINT NOT NULL DEFAULT 0, type VARCHAR(16) NOT NULL,
balance_before BIGINT NOT NULL DEFAULT 0, amount NUMERIC(15,2) NOT NULL DEFAULT 0,
balance_after BIGINT NOT NULL DEFAULT 0, balance_before NUMERIC(15,2) NOT NULL DEFAULT 0,
currency VARCHAR(16) NOT NULL DEFAULT 'CNY', balance_after NUMERIC(15,2) NOT NULL DEFAULT 0,
description TEXT NOT NULL DEFAULT '', currency VARCHAR(16) NOT NULL DEFAULT 'CNY',
extra_data JSONB description TEXT NOT NULL DEFAULT '',
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
}