feat: 会员服务(套餐/状态/下单/回调幂等续期)
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"slogan-agent/styleagent/consts"
|
||||
"slogan-agent/styleagent/dao"
|
||||
"slogan-agent/styleagent/model/entity"
|
||||
"slogan-agent/styleagent/payment"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
type memberService struct{}
|
||||
|
||||
var MemberService = new(memberService)
|
||||
|
||||
func (s *memberService) PlanList(ctx context.Context) ([]*entity.MemberPlan, error) {
|
||||
return dao.MemberPlan.ListEnabled(ctx)
|
||||
}
|
||||
|
||||
type MemberStatus struct {
|
||||
IsVip bool `json:"is_vip"`
|
||||
ExpireAt string `json:"expire_at"`
|
||||
PlanName string `json:"plan_name"`
|
||||
Benefits []string `json:"benefits"`
|
||||
}
|
||||
|
||||
func (s *memberService) Status(ctx context.Context, userId int64) (*MemberStatus, error) {
|
||||
st := &MemberStatus{Benefits: make([]string, 0)}
|
||||
um, err := dao.UserMember.GetByUser(ctx, userId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if um == nil || um.ExpireAt == nil || um.ExpireAt.Time.Before(time.Now()) {
|
||||
return st, nil
|
||||
}
|
||||
st.IsVip = true
|
||||
st.ExpireAt = um.ExpireAt.Format("2006-01-02 15:04:05")
|
||||
if plan, _ := dao.MemberPlan.GetOne(ctx, um.PlanId); plan != nil {
|
||||
st.PlanName = plan.Name
|
||||
st.Benefits = parseBenefits(plan.Features)
|
||||
}
|
||||
return st, nil
|
||||
}
|
||||
|
||||
// CreateOrder 下单:生成业务订单号 → 虎皮棋下单 → 返回支付 URL
|
||||
func (s *memberService) CreateOrder(ctx context.Context, userId, planId int64) (*entity.PaymentOrder, string, error) {
|
||||
plan, err := dao.MemberPlan.GetOne(ctx, planId)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if plan == nil {
|
||||
return nil, "", errors.New("套餐不存在")
|
||||
}
|
||||
order := &entity.PaymentOrder{
|
||||
OrderNo: fmt.Sprintf("M%d%d", time.Now().UnixNano()/1e6, userId%1000),
|
||||
UserId: userId,
|
||||
PlanId: planId,
|
||||
AmountFen: plan.PriceFen,
|
||||
Channel: "alipay",
|
||||
Status: consts.PayStatusPending,
|
||||
}
|
||||
if _, err := dao.PaymentOrder.Insert(ctx, order); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
payURL, err := payment.CreateOrder(ctx, order.OrderNo, plan.PriceFen)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return order, payURL, nil
|
||||
}
|
||||
|
||||
func (s *memberService) OrderStatus(ctx context.Context, orderNo string) (*entity.PaymentOrder, error) {
|
||||
return dao.PaymentOrder.GetByOrderNo(ctx, orderNo)
|
||||
}
|
||||
|
||||
// HandlePaidNotify 验签已在 handler 完成;状态机 pending→paid 幂等,成功开通/续期
|
||||
func (s *memberService) HandlePaidNotify(ctx context.Context, orderNo, tradeNo, notifyRaw string) (string, error) {
|
||||
order, err := dao.PaymentOrder.GetByOrderNo(ctx, orderNo)
|
||||
if err != nil {
|
||||
return "no_order", err
|
||||
}
|
||||
if order == nil {
|
||||
return "no_order", nil
|
||||
}
|
||||
ok, err := dao.PaymentOrder.MarkPaid(ctx, orderNo, tradeNo, notifyRaw)
|
||||
if err != nil {
|
||||
return "no_order", err
|
||||
}
|
||||
if !ok {
|
||||
return "duplicate", nil // 已是 paid 或已关闭
|
||||
}
|
||||
days := 30
|
||||
if plan, _ := dao.MemberPlan.GetOne(ctx, order.PlanId); plan != nil {
|
||||
days = plan.DurationDays
|
||||
}
|
||||
um, _ := dao.UserMember.GetByUser(ctx, order.UserId)
|
||||
expireAt := NextExpire(um.ExpireAt, days)
|
||||
if err := dao.UserMember.Upsert(ctx, order.UserId, order.PlanId, expireAt, consts.MemberSourceVipPay); err != nil {
|
||||
return "no_order", err
|
||||
}
|
||||
g.Log().Infof(ctx, "会员开通成功 user=%d order=%s expire=%s", order.UserId, orderNo, expireAt)
|
||||
return "ok", nil
|
||||
}
|
||||
|
||||
// NextExpire 续期计算:未过期在原有效期上叠加,过期/无记录从现在起算
|
||||
func NextExpire(old *gtime.Time, days int) string {
|
||||
base := time.Now()
|
||||
if old != nil && old.Time.After(base) {
|
||||
base = old.Time
|
||||
}
|
||||
return base.Add(time.Duration(days) * 24 * time.Hour).Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
func parseBenefits(features string) []string {
|
||||
var list []string
|
||||
_ = json.Unmarshal([]byte(features), &list)
|
||||
if list == nil {
|
||||
list = make([]string, 0)
|
||||
}
|
||||
return list
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
func TestNextExpireFromNow(t *testing.T) {
|
||||
got := NextExpire(nil, 30)
|
||||
want := time.Now().Add(30 * 24 * time.Hour).Format("2006-01-02 15:04:05")
|
||||
gotT, _ := time.Parse("2006-01-02 15:04:05", got)
|
||||
wantT, _ := time.Parse("2006-01-02 15:04:05", want)
|
||||
if !gotT.Equal(wantT) {
|
||||
t.Fatalf("过期会员应从现在起算: got=%s want~%s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNextExpireStackOnFuture(t *testing.T) {
|
||||
base := gtime.NewFromTime(time.Now().Add(10 * 24 * time.Hour))
|
||||
got := NextExpire(base, 30)
|
||||
gotT, _ := time.Parse("2006-01-02 15:04:05", got)
|
||||
if gotT.Before(base.Time) {
|
||||
t.Fatalf("未过期会员应叠加: got=%s base=%s", got, base.Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user