Files
qitongxue-api/internal/logic/quota_api.go
2026-09-17 17:38:45 +08:00

249 lines
7.0 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package logic
import (
"context"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
v1 "tool-api/api/user/v1"
"tool-api/internal/consts"
"tool-api/internal/model/entity"
)
// ============================================================================
// 额度相关的对外接口实现
// ============================================================================
// QuotaMy 我的额度总览:周期、会员状态、每个工具的额度明细、通用次数包档位
func QuotaMy(ctx context.Context) (*v1.QuotaMyRes, error) {
user, err := getUserById(ctx, CtxUserId(ctx))
if err != nil {
return nil, err
}
tools, err := quotaListForUser(ctx, user)
if err != nil {
return nil, err
}
packs, err := listQuotaPacks(ctx, "")
if err != nil {
return nil, err
}
level, err := EffectiveLevelInfo(ctx, user)
if err != nil {
return nil, err
}
totalLeft := 0
for i := range tools {
totalLeft += tools[i].TotalLeft
}
res := &v1.QuotaMyRes{
PeriodDays: EffectiveQuotaPeriodDays(ctx, user),
IsMember: EffectiveLevelKey(user) != consts.DefaultLevelKey,
LevelKey: EffectiveLevelKey(user),
Tools: tools,
TotalLeft: totalLeft,
Packs: packs,
}
if level != nil {
res.LevelName = level.Name
}
if user.LevelExpireAt != nil && !user.LevelExpireAt.IsZero() {
res.LevelExpireAt = user.LevelExpireAt.Format("Y-m-d")
}
return res, nil
}
// QuotaToolOf 单个工具的额度状态(进入工具页时调用)
func QuotaToolOf(ctx context.Context, toolKey string) (*v1.QuotaToolRes, error) {
user, err := getUserById(ctx, CtxUserId(ctx))
if err != nil {
return nil, err
}
tool, err := toolRow(ctx, toolKey)
if err != nil {
return nil, err
}
status, err := ToolQuotaStatus(ctx, user, tool)
if err != nil {
return nil, err
}
return &v1.QuotaToolRes{Tool: status}, nil
}
// QuotaConsume 扣减一次额度(产出结果时调用)
func QuotaConsume(ctx context.Context, toolKey string) (*v1.QuotaConsumeRes, error) {
status, err := ConsumeToolQuota(ctx, CtxUserId(ctx), toolKey)
if err != nil {
return nil, err
}
return &v1.QuotaConsumeRes{Tool: status}, nil
}
// QuotaPacksFor 某工具可购买的次数包(通用档位 + 该工具专属档位)
func QuotaPacksFor(ctx context.Context, toolKey string) (*v1.QuotaPacksRes, error) {
list, err := listQuotaPacks(ctx, toolKey)
if err != nil {
return nil, err
}
return &v1.QuotaPacksRes{List: list}, nil
}
// QuotaOrderCreate 创建次数包订单;与会员订单共用下单/发货链路
func QuotaOrderCreate(ctx context.Context, packKey, toolKey, code string) (*v1.QuotaOrderCreateRes, error) {
userId := CtxUserId(ctx)
if !xpayConfig(ctx).Configured() {
return nil, gerror.NewCode(gcodePayFail(),
"虚拟支付尚未配置,请在 MP 后台【虚拟支付 → 基本配置】获取 OfferID 与现网 AppKey 后填入 config.yaml")
}
pack, err := quotaPackByKey(ctx, packKey)
if err != nil {
return nil, err
}
if pack == nil {
return nil, gerror.NewCode(gcodePayFail(), "次数包不存在或已下架")
}
// 专属档位只能卖给它绑定的工具,避免把 A 工具的专属价套到 B 工具上
if pack.ToolKey != "" && pack.ToolKey != toolKey {
return nil, gerror.NewCode(gcodePayFail(), "该次数包仅适用于指定工具")
}
if pack.ProductId == "" || pack.PriceCents <= 0 {
return nil, gerror.NewCode(gcodePayFail(), "次数包未配置微信道具 ID 或价格,请先在后台核对")
}
tool, err := toolRow(ctx, toolKey)
if err != nil {
return nil, err
}
if tool.IsEnabled != 1 {
return nil, gerror.NewCode(gcodePayFail(), "工具不存在或已下架")
}
if pack.Times <= 0 {
return nil, gerror.NewCode(gcodePayFail(), "次数包次数配置异常")
}
validDays := pack.ValidDays
if validDays <= 0 {
validDays = 365
}
pay, err := prepareXpayOrder(ctx, userId, code, pack.ProductId, pack.PriceCents, g.Map{
"order_type": consts.OrderTypeQuota,
"pack_key": pack.PackKey,
"tool_key": tool.ToolKey,
"times": pack.Times,
"duration_days": validDays, // 次数包复用 duration_days 承载「有效期天数」
})
if err != nil {
return nil, err
}
return &v1.QuotaOrderCreateRes{
OutTradeNo: pay.OutTradeNo,
Mode: consts.XPayMode,
SignData: pay.SignData,
PaySig: pay.PaySig,
Signature: pay.Signature,
Env: pay.Env,
PackKey: pack.PackKey,
ToolKey: tool.ToolKey,
Times: pack.Times,
ValidDays: validDays,
}, nil
}
// ===== 内部辅助 =====
// quotaListForUser 用户可见(已启用)的全部工具的额度状态
func quotaListForUser(ctx context.Context, user *entity.Users) ([]v1.QuotaToolOut, error) {
records, err := g.Model(consts.TableTools).Where("is_enabled", 1).Order("sort asc").All()
if err != nil {
return nil, err
}
tools := make([]*entity.Tools, 0, len(records))
for _, r := range records {
tool := &entity.Tools{}
if err = r.Struct(tool); err != nil {
continue
}
tools = append(tools, tool)
}
snapshot, err := QuotaSnapshotForTools(ctx, user, tools)
if err != nil {
return nil, err
}
list := make([]v1.QuotaToolOut, 0, len(tools))
for _, tool := range tools {
if out, ok := snapshot[tool.ToolKey]; ok {
list = append(list, *out)
}
}
return list, nil
}
func quotaPackByKey(ctx context.Context, packKey string) (*entity.QuotaPacks, error) {
record, err := g.Model(consts.TableQuotaPacks).
Where("pack_key", packKey).Where("is_enabled", 1).One()
if err != nil {
return nil, err
}
if record.IsEmpty() {
return nil, nil
}
pack := &entity.QuotaPacks{}
if err = record.Struct(pack); err != nil {
return nil, err
}
return pack, nil
}
// listQuotaPacks 可用次数包档位;toolKey 非空时返回「通用档位 + 该工具专属档位」
func listQuotaPacks(ctx context.Context, toolKey string) ([]v1.QuotaPackOut, error) {
m := g.Model(consts.TableQuotaPacks).Where("is_enabled", 1)
if toolKey == "" {
m = m.Where("tool_key", "")
} else {
m = m.Where("tool_key = ? OR tool_key = ?", "", toolKey)
}
records, err := m.OrderAsc("times").All()
if err != nil {
return nil, err
}
list := make([]v1.QuotaPackOut, 0, len(records))
for _, r := range records {
times := r["times"].Int()
price := r["price_cents"].Int64()
unit := int64(0)
if times > 0 {
unit = price * 100 / int64(times) // 每 100 次均价(分),服务端算好避免前端浮点误差
}
validDays := r["valid_days"].Int()
if validDays <= 0 {
validDays = 365
}
list = append(list, v1.QuotaPackOut{
PackKey: r["pack_key"].String(),
Name: r["name"].String(),
Times: times,
PriceCents: price,
ValidDays: validDays,
Badge: r["badge"].String(),
ToolKey: r["tool_key"].String(),
UnitPriceCents: unit,
})
}
return list, nil
}
func quotaPackNameMap(ctx context.Context) (map[string]string, error) {
records, err := g.Model(consts.TableQuotaPacks).All()
if err != nil {
return nil, err
}
m := map[string]string{}
for _, r := range records {
m[r["pack_key"].String()] = r["name"].String()
}
return m, nil
}