- 微信登录改为真实 jscode2session,session_key 不再下发客户端 - 支付回调增加 HMAC 验签(X-Pay-Sign)与幂等处理,未配置密钥时拒绝回调 - 订单金额一律以服务端商品表定价,禁止客户端传入金额 - 付费许愿改为支付成功后创建,不再先许愿后付款 - 管理后台增加登录认证(ADMIN_PASSWORD + role=admin JWT + HttpOnly Cookie) - 订单详情/取消增加本人归属校验,修复越权访问 - 版本信息改为 ldflags 注入单一链路,GoVersion 用 runtime.Version() - 恢复 gin 默认访问日志(原 Logger 中间件输出为空) - 加载 HTML 模板修复后台页面 500;godotenv 加载 .env.local - CORS 支持 CORS_ORIGINS 白名单配置
214 lines
5.1 KiB
Go
214 lines
5.1 KiB
Go
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,
|
|
},
|
|
})
|
|
}
|