package handler import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "net/http" "strconv" "github.com/gin-gonic/gin" "github.com/gouki/lunar-server/internal/config" "github.com/gouki/lunar-server/internal/model" "github.com/gouki/lunar-server/internal/service" ) // CreateOrder 创建订单 // 安全约束:金额与商品名一律以服务端商品表为准,不信任客户端传入值 func CreateOrder(c *gin.Context) { userID, exists := c.Get("userID") if !exists { c.JSON(http.StatusUnauthorized, gin.H{ "code": 401, "msg": "未授权", }) return } var req struct { Type string `json:"type" binding:"required"` // wish:许愿 vip:会员 ProductID uint `json:"productId" binding:"required"` // 商品ID Content string `json:"content"` // 许愿内容(许愿类型需要) } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "code": 400, "msg": "参数错误", }) return } // 服务端定价:按商品 ID 查库取价格,防止客户端篡改金额 wishService := service.NewWishService() product, err := wishService.GetWishProductByID(req.ProductID) if err != nil || product.Status != 1 { c.JSON(http.StatusBadRequest, gin.H{ "code": 400, "msg": "商品不存在或已下架", }) return } if req.Type == "wish" { if req.Content == "" { c.JSON(http.StatusBadRequest, gin.H{ "code": 400, "msg": "请输入许愿内容", }) return } if len([]rune(req.Content)) > 100 { c.JSON(http.StatusBadRequest, gin.H{ "code": 400, "msg": "许愿内容超过 100 字限制", }) return } } else { c.JSON(http.StatusBadRequest, gin.H{ "code": 400, "msg": "不支持的订单类型", }) return } orderService := service.NewOrderService() order := &model.Order{ UserID: userID.(uint), Type: req.Type, ProductID: product.ID, ProductName: product.Name, Amount: product.Price, // 金额以商品表为准 Remark: req.Content, // 许愿内容暂存订单,支付成功后才创建许愿 } if err := orderService.CreateOrder(order); err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "code": 500, "msg": "创建订单失败", }) return } // 创建微信支付订单 payParams, err := orderService.CreateWechatPayOrder(order, "") if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "code": 500, "msg": "创建支付订单失败", }) return } c.JSON(http.StatusOK, gin.H{ "code": 0, "msg": "success", "data": gin.H{ "orderId": order.ID, "orderNo": order.OrderNo, "payParams": payParams, }, }) } // paySignOf 计算支付回调签名:HMAC-SHA256(orderNo|transactionId, APIKEY) // 过渡方案:真实微信支付 V3 回调验签接入前的内部协议 func paySignOf(orderNo, transactionID, key string) string { mac := hmac.New(sha256.New, []byte(key)) mac.Write([]byte(orderNo + "|" + transactionID)) return hex.EncodeToString(mac.Sum(nil)) } // PayNotify 支付回调:必须携带 X-Pay-Sign 签名头,验签通过且幂等处理 func PayNotify(c *gin.Context) { cfg := config.Load() if cfg.Wechat.PayKey == "" { // 未配置支付密钥时拒绝一切回调,避免裸奔 c.JSON(http.StatusServiceUnavailable, gin.H{ "code": "FAIL", "msg": "支付服务未配置", }) return } var req struct { OrderNo string `json:"orderNo" binding:"required"` TransactionID string `json:"transactionId" binding:"required"` Status string `json:"status"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "code": "FAIL", "msg": "参数错误", }) return } // 验签:恒定时间比较,防时序攻击 sign := c.GetHeader("X-Pay-Sign") expected := paySignOf(req.OrderNo, req.TransactionID, cfg.Wechat.PayKey) if sign == "" || !hmac.Equal([]byte(sign), []byte(expected)) { c.JSON(http.StatusUnauthorized, gin.H{ "code": "FAIL", "msg": "签名验证失败", }) return } if req.Status != "SUCCESS" { c.JSON(http.StatusOK, gin.H{ "code": "SUCCESS", "msg": "OK", }) return } orderService := service.NewOrderService() if err := orderService.HandlePayNotify(req.OrderNo, req.TransactionID); err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "code": "FAIL", "msg": "处理失败", }) return } c.JSON(http.StatusOK, gin.H{ "code": "SUCCESS", "msg": "OK", }) } // GetPayStatus 获取支付状态(支持订单号或订单 ID) func GetPayStatus(c *gin.Context) { param := c.Param("orderId") orderService := service.NewOrderService() order, err := orderService.GetOrderByOrderNo(param) if err != nil { // 兼容传数字 ID 的调用方 if id, convErr := strconv.Atoi(param); convErr == nil { order, err = orderService.GetOrderByID(uint(id)) } } if err != nil { c.JSON(http.StatusNotFound, gin.H{ "code": 404, "msg": "订单不存在", }) return } c.JSON(http.StatusOK, gin.H{ "code": 0, "msg": "success", "data": gin.H{ "orderId": order.ID, "orderNo": order.OrderNo, "status": order.Status, "payTime": order.PayTime, }, }) }