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 创建次数包订单(**只建单**,N1:与会员套餐两步式同款,共用 createOrder)。 func QuotaOrderCreate(ctx context.Context, packKey, toolKey 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 } res, err := createOrder(ctx, userId, consts.OrderTypeQuota, pack.ProductId, pack.PriceCents, g.Map{"pack_key": pack.PackKey, "tool_key": tool.ToolKey}, 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: res.OutTradeNo, Reused: res.Reused, OriginPriceCents: res.OriginPriceCents, PaidPriceCents: res.PaidPriceCents, PackKey: pack.PackKey, ToolKey: tool.ToolKey, Times: pack.Times, ValidDays: validDays, Status: res.Status, }, nil } // ===== 内部辅助 ===== // quotaListForUser 用户可见(已启用)的全部工具的额度状态 func quotaListForUser(ctx context.Context, user *entity.Users) ([]v1.QuotaToolOut, error) { records, err := g.Model(consts.TableTools).Where("is_enabled", 1).WhereNull("deleted_at").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 }