feat: 会员服务(套餐/状态/下单/回调幂等续期)

This commit is contained in:
2026-07-31 13:25:43 +08:00
parent 34652019bb
commit a57ab907fa
2 changed files with 156 additions and 0 deletions
+128
View File
@@ -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
}
+28
View File
@@ -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"))
}
}