feat: 会员接口(套餐/状态/下单/回调)+ 支付配置

This commit is contained in:
2026-07-31 13:28:59 +08:00
parent a57ab907fa
commit de9b4b7a01
5 changed files with 155 additions and 0 deletions
+1
View File
@@ -12,6 +12,7 @@ var publicPaths = []string{
"/user/register",
"/hairstyle/list",
"/api.json",
"/member/order/notify",
}
func Auth(r *ghttp.Request) {
+8
View File
@@ -38,3 +38,11 @@ llm:
model_name: "qwen-plus"
max_tokens: 4096
temperature: 0.8
# 支付(虎皮棋聚合支付,key 为空则支付功能降级关闭)
payment:
xunhu_appid: ""
xunhu_appsecret: ""
notify_url: "http://localhost:3007/member/order/notify" # 生产需公网可达
channel: "alipay,wechat"
api_base: "https://api.xunhupay.com"
+6
View File
@@ -32,6 +32,12 @@ func main() {
controller.Hairstyle,
controller.Outfit,
controller.PartnerStore,
controller.Member,
})
// 虎皮棋支付回调(裸文本 "success",不走统一 JSON 包装)
commonHttp.Httpserver.Group("/member/order", func(group *ghttp.RouterGroup) {
group.POST("/notify", controller.MemberNotify)
})
// ==================== Workspace 文件服务(鉴权保护) ====================
+100
View File
@@ -0,0 +1,100 @@
package controller
import (
"context"
"errors"
"fmt"
commonHttp "slogan-agent/common"
"slogan-agent/styleagent/dao"
"slogan-agent/styleagent/model/dto"
"slogan-agent/styleagent/model/entity"
"slogan-agent/styleagent/payment"
"slogan-agent/styleagent/service"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
)
type member struct{}
var Member = new(member)
// PlanList 会员套餐列表
func (c *member) PlanList(ctx context.Context, req *dto.MemberPlanListReq) (res *dto.MemberPlanListRes, err error) {
list, err := service.MemberService.PlanList(ctx)
if err != nil {
return nil, err
}
return &dto.MemberPlanListRes{List: list}, nil
}
// Status 我的会员状态
func (c *member) Status(ctx context.Context, req *dto.MemberStatusReq) (res *dto.MemberStatusRes, err error) {
st, err := service.MemberService.Status(ctx, commonHttp.GetUserId(g.RequestFromCtx(ctx)))
if err != nil {
return nil, err
}
return &dto.MemberStatusRes{
IsVip: st.IsVip, ExpireAt: st.ExpireAt, PlanName: st.PlanName, Benefits: st.Benefits,
}, nil
}
// OrderCreate 下单 → 返回支付 URL
func (c *member) OrderCreate(ctx context.Context, req *dto.MemberOrderCreateReq) (res *dto.MemberOrderCreateRes, err error) {
order, payURL, err := service.MemberService.CreateOrder(ctx, commonHttp.GetUserId(g.RequestFromCtx(ctx)), req.PlanId)
if err != nil {
return nil, err
}
return &dto.MemberOrderCreateRes{OrderNo: order.OrderNo, PayUrl: payURL}, nil
}
// OrderStatus 订单状态(App 轮询)
func (c *member) OrderStatus(ctx context.Context, req *dto.MemberOrderStatusReq) (res *dto.MemberOrderStatusRes, err error) {
order, err := service.MemberService.OrderStatus(ctx, req.OrderNo)
if err != nil || order == nil {
return nil, errors.New("订单不存在")
}
paidAt := ""
if order.PaidAt != nil {
paidAt = order.PaidAt.Format("2006-01-02 15:04:05")
}
return &dto.MemberOrderStatusRes{Status: order.Status, TradeNo: order.TradeNo, PaidAt: paidAt}, nil
}
// MemberNotify 虎皮棋支付回调:验签 → 幂等开通 → 返回裸文本 "success"
// 虎皮棋要求回调响应体为字面 "success",故不走统一 JSON 包装
func MemberNotify(r *ghttp.Request) {
ctx := r.Context()
body := r.GetBodyString()
hash := r.Get("hash").String()
orderNo := r.Get("trade_order_id").String()
remoteIP := r.GetClientIp()
params := make(map[string]string)
for k, v := range r.GetRequestMap() {
params[k] = fmt.Sprint(v)
}
ok := payment.VerifyNotify(params, hash, g.Cfg().MustGet(ctx, "payment.xunhu_appsecret", "").String())
if !ok {
_ = dao.PayNotifyLog.Insert(ctx, &entity.PayNotifyLog{
OrderNo: orderNo, Body: body, Sign: hash, RemoteIp: remoteIP, Status: "bad_sign",
})
r.Response.Write("fail")
r.ExitAll()
return
}
state, err := service.MemberService.HandlePaidNotify(ctx, orderNo, r.Get("transaction_id").String(), body)
_ = dao.PayNotifyLog.Insert(ctx, &entity.PayNotifyLog{
OrderNo: orderNo, Body: body, Sign: hash, RemoteIp: remoteIP, Status: state,
})
if err != nil || state != "ok" {
r.Response.Write("fail")
r.ExitAll()
return
}
r.Response.Write("success")
r.ExitAll()
}
+40
View File
@@ -199,3 +199,43 @@ type StoreListReq struct {
type StoreListRes struct {
List []*entity.PartnerStore `json:"list"`
}
type MemberPlanListReq struct {
g.Meta `path:"/plan/list" method:"get" tags:"会员" summary:"会员套餐列表"`
}
type MemberPlanListRes struct {
List []*entity.MemberPlan `json:"list"`
}
type MemberStatusReq struct {
g.Meta `path:"/status" method:"get" tags:"会员" summary:"我的会员状态"`
}
type MemberStatusRes struct {
IsVip bool `json:"is_vip"`
ExpireAt string `json:"expire_at"`
PlanName string `json:"plan_name"`
Benefits []string `json:"benefits"`
}
type MemberOrderCreateReq struct {
g.Meta `path:"/order/create" method:"post" tags:"会员" summary:"创建支付订单"`
PlanId int64 `v:"required" json:"plan_id"`
}
type MemberOrderCreateRes struct {
OrderNo string `json:"order_no"`
PayUrl string `json:"pay_url"`
}
type MemberOrderStatusReq struct {
g.Meta `path:"/order/status" method:"get" tags:"会员" summary:"订单状态"`
OrderNo string `v:"required" json:"order_no"`
}
type MemberOrderStatusRes struct {
Status string `json:"status"`
TradeNo string `json:"trade_no"`
PaidAt string `json:"paid_at"`
}