fix(server): 安全加固与版本链路修复
- 微信登录改为真实 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 白名单配置
This commit is contained in:
@@ -3,7 +3,9 @@ package service
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
@@ -98,9 +100,18 @@ func (s *OrderService) UpdateOrderStatus(orderNo, status string) error {
|
||||
return s.db.Model(&model.Order{}).Where("order_no = ?", orderNo).Updates(updates).Error
|
||||
}
|
||||
|
||||
// CancelOrder 取消订单
|
||||
func (s *OrderService) CancelOrder(id uint) error {
|
||||
return s.db.Model(&model.Order{}).Where("id = ?", id).Update("status", "cancelled").Error
|
||||
// CancelOrder 取消订单(仅限本人且仅待支付状态可取消)
|
||||
func (s *OrderService) CancelOrder(userID, id uint) error {
|
||||
result := s.db.Model(&model.Order{}).
|
||||
Where("id = ? AND user_id = ? AND status = ?", id, userID, "pending").
|
||||
Update("status", "cancelled")
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return errors.New("订单不存在或当前状态不可取消")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckOrderExpired 检查订单是否过期
|
||||
@@ -138,14 +149,53 @@ func (s *OrderService) generatePaySign(orderNo string) string {
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
// HandlePayNotify 处理支付回调
|
||||
// HandlePayNotify 处理支付回调(幂等:已支付订单不重复处理)
|
||||
func (s *OrderService) HandlePayNotify(orderNo, transactionID string) error {
|
||||
// 更新订单状态
|
||||
if err := s.UpdateOrderStatus(orderNo, "paid"); err != nil {
|
||||
return err
|
||||
order, err := s.GetOrderByOrderNo(orderNo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("order not found: %w", err)
|
||||
}
|
||||
|
||||
// TODO: 处理业务逻辑(如创建许愿、开通会员等)
|
||||
// 幂等:已支付直接返回成功
|
||||
if order.Status == "paid" {
|
||||
return nil
|
||||
}
|
||||
if order.Status != "pending" {
|
||||
return fmt.Errorf("order %s in unexpected status %s", orderNo, order.Status)
|
||||
}
|
||||
|
||||
// 条件更新防止并发重复入账
|
||||
updates := map[string]interface{}{
|
||||
"status": "paid",
|
||||
"pay_time": time.Now(),
|
||||
"remark": order.Remark + " [tx:" + transactionID + "]",
|
||||
}
|
||||
result := s.db.Model(&model.Order{}).
|
||||
Where("order_no = ? AND status = ?", orderNo, "pending").
|
||||
Updates(updates)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
// 并发下已被其他回调处理,视为幂等成功
|
||||
return nil
|
||||
}
|
||||
|
||||
// 支付成功后才创建付费许愿
|
||||
if order.Type == "wish" && order.Remark != "" {
|
||||
wishService := NewWishService()
|
||||
wish := &model.Wish{
|
||||
UserID: order.UserID,
|
||||
TreeID: 1, // 默认祈福树
|
||||
Content: order.Remark,
|
||||
Type: "paid",
|
||||
Status: 1,
|
||||
}
|
||||
if err := wishService.CreateWish(wish); err != nil {
|
||||
// 许愿创建失败不回滚支付状态,由后台补处理
|
||||
log.Printf("create paid wish for order %s failed: %v", orderNo, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -93,9 +93,48 @@ func (s *UserService) ParseToken(tokenString, secret string) (uint, error) {
|
||||
}
|
||||
|
||||
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
|
||||
userID := uint(claims["user_id"].(float64))
|
||||
return userID, nil
|
||||
// 管理员 token 不绑定具体用户,不能当普通用户 token 使用
|
||||
if role, _ := claims["role"].(string); role == "admin" {
|
||||
return 0, errors.New("admin token")
|
||||
}
|
||||
userIDFloat, ok := claims["user_id"].(float64)
|
||||
if !ok {
|
||||
return 0, errors.New("invalid token claims")
|
||||
}
|
||||
return uint(userIDFloat), nil
|
||||
}
|
||||
|
||||
return 0, errors.New("invalid token")
|
||||
}
|
||||
|
||||
// AdminTokenCookie 管理员会话 Cookie 名
|
||||
const AdminTokenCookie = "lunar_admin_token"
|
||||
|
||||
// GenerateAdminToken 签发管理员 JWT(12 小时过期)
|
||||
func (s *UserService) GenerateAdminToken(secret string) (string, error) {
|
||||
claims := jwt.MapClaims{
|
||||
"role": "admin",
|
||||
"exp": time.Now().Add(time.Hour * 12).Unix(),
|
||||
"iat": time.Now().Unix(),
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString([]byte(secret))
|
||||
}
|
||||
|
||||
// IsAdminToken 校验是否为有效的管理员 JWT
|
||||
func (s *UserService) IsAdminToken(tokenString, secret string) bool {
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(secret), nil
|
||||
})
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok || !token.Valid {
|
||||
return false
|
||||
}
|
||||
role, _ := claims["role"].(string)
|
||||
return role == "admin"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user