249 lines
7.0 KiB
Go
249 lines
7.0 KiB
Go
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
|
||
}
|