From de9b4b7a013330a90caa27c66401b74297aca672 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=96=8C?= <259278618@qq.com> Date: Fri, 31 Jul 2026 13:28:59 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=9A=E5=91=98=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=EF=BC=88=E5=A5=97=E9=A4=90/=E7=8A=B6=E6=80=81/=E4=B8=8B?= =?UTF-8?q?=E5=8D=95/=E5=9B=9E=E8=B0=83=EF=BC=89+=20=E6=94=AF=E4=BB=98?= =?UTF-8?q?=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- common/auth_middleware.go | 1 + config.yml | 8 ++ main.go | 6 ++ styleagent/controller/member_controller.go | 100 +++++++++++++++++++++ styleagent/model/dto/dto.go | 40 +++++++++ 5 files changed, 155 insertions(+) create mode 100644 styleagent/controller/member_controller.go diff --git a/common/auth_middleware.go b/common/auth_middleware.go index 2a0c3cc..289c1dd 100644 --- a/common/auth_middleware.go +++ b/common/auth_middleware.go @@ -12,6 +12,7 @@ var publicPaths = []string{ "/user/register", "/hairstyle/list", "/api.json", + "/member/order/notify", } func Auth(r *ghttp.Request) { diff --git a/config.yml b/config.yml index 34c4570..ad278bc 100644 --- a/config.yml +++ b/config.yml @@ -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" diff --git a/main.go b/main.go index ce8c309..8a72ad0 100644 --- a/main.go +++ b/main.go @@ -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 文件服务(鉴权保护) ==================== diff --git a/styleagent/controller/member_controller.go b/styleagent/controller/member_controller.go new file mode 100644 index 0000000..95cef4a --- /dev/null +++ b/styleagent/controller/member_controller.go @@ -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() +} diff --git a/styleagent/model/dto/dto.go b/styleagent/model/dto/dto.go index 0b2689a..64dc5c1 100644 --- a/styleagent/model/dto/dto.go +++ b/styleagent/model/dto/dto.go @@ -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"` +}