Files
epay-go/docs/plans/2026-02-07-epay-go-phase3-api-handlers.md
2026-03-28 14:55:39 +08:00

40 KiB

EPay Go 重构 - 阶段三:API Handler 层

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: 实现管理后台 API、商户端 API 和对外支付 API,完成后端所有接口开发。

Architecture: Handler 层负责参数校验、调用 Service、返回响应。使用 Gin 路由分组,JWT 中间件保护需要认证的接口。

Tech Stack: Go, Gin, JWT, Validator

前置条件: 阶段一、阶段二已完成。


Task 1: 创建路由注册入口

Files:

  • Create: epay-go/internal/router/router.go

Step 1: 创建路由注册文件

// internal/router/router.go
package router

import (
	"github.com/example/epay-go/internal/handler/admin"
	"github.com/example/epay-go/internal/handler/merchant"
	"github.com/example/epay-go/internal/handler/payment"
	"github.com/example/epay-go/internal/middleware"
	"github.com/example/epay-go/pkg/jwt"
	"github.com/gin-gonic/gin"
)

// Setup 设置所有路由
func Setup(r *gin.Engine) {
	// 对外支付 API(无需登录)
	payAPI := r.Group("/api/pay")
	{
		payAPI.POST("/create", payment.CreateOrder)
		payAPI.GET("/query", payment.QueryOrder)
		payAPI.POST("/notify/:channel", payment.HandleNotify)
		payAPI.GET("/return/:channel", payment.HandleReturn)
	}

	// 管理后台 API
	adminAPI := r.Group("/admin")
	{
		// 无需认证
		adminAPI.POST("/auth/login", admin.Login)

		// 需要认证
		adminAuth := adminAPI.Group("")
		adminAuth.Use(middleware.JWTAuth(jwt.TokenTypeAdmin))
		{
			adminAuth.POST("/auth/logout", admin.Logout)
			adminAuth.GET("/dashboard", admin.Dashboard)

			// 商户管理
			adminAuth.GET("/merchants", admin.ListMerchants)
			adminAuth.GET("/merchants/:id", admin.GetMerchant)
			adminAuth.PUT("/merchants/:id", admin.UpdateMerchant)
			adminAuth.PATCH("/merchants/:id/status", admin.UpdateMerchantStatus)

			// 订单管理
			adminAuth.GET("/orders", admin.ListOrders)
			adminAuth.GET("/orders/:trade_no", admin.GetOrder)
			adminAuth.POST("/orders/:trade_no/renotify", admin.RenotifyOrder)

			// 通道管理
			adminAuth.GET("/channels", admin.ListChannels)
			adminAuth.POST("/channels", admin.CreateChannel)
			adminAuth.GET("/channels/:id", admin.GetChannel)
			adminAuth.PUT("/channels/:id", admin.UpdateChannel)
			adminAuth.DELETE("/channels/:id", admin.DeleteChannel)

			// 结算管理
			adminAuth.GET("/settlements", admin.ListSettlements)
			adminAuth.PATCH("/settlements/:id/approve", admin.ApproveSettlement)
			adminAuth.PATCH("/settlements/:id/reject", admin.RejectSettlement)
		}
	}

	// 商户端 API
	merchantAPI := r.Group("/merchant")
	{
		// 无需认证
		merchantAPI.POST("/auth/login", merchant.Login)
		merchantAPI.POST("/auth/register", merchant.Register)

		// 需要认证
		merchantAuth := merchantAPI.Group("")
		merchantAuth.Use(middleware.JWTAuth(jwt.TokenTypeMerchant))
		{
			merchantAuth.POST("/auth/logout", merchant.Logout)
			merchantAuth.GET("/dashboard", merchant.Dashboard)

			// 个人信息
			merchantAuth.GET("/profile", merchant.GetProfile)
			merchantAuth.PUT("/profile", merchant.UpdateProfile)
			merchantAuth.PUT("/profile/password", merchant.UpdatePassword)
			merchantAuth.POST("/profile/reset-key", merchant.ResetAPIKey)

			// 订单
			merchantAuth.GET("/orders", merchant.ListOrders)
			merchantAuth.GET("/orders/:trade_no", merchant.GetOrder)

			// 结算
			merchantAuth.GET("/settlements", merchant.ListSettlements)
			merchantAuth.POST("/settlements", merchant.ApplySettlement)

			// 资金记录
			merchantAuth.GET("/records", merchant.ListRecords)
		}
	}
}

Step 2: 提交

cd d:/project/payment/epay-go
git add internal/router/router.go
git commit -m "feat: add router setup with all api routes"

Task 2: 创建管理员认证 Handler

Files:

  • Create: epay-go/internal/handler/admin/auth.go

Step 1: 创建管理员认证 Handler

// internal/handler/admin/auth.go
package admin

import (
	"github.com/example/epay-go/internal/service"
	"github.com/example/epay-go/pkg/jwt"
	"github.com/example/epay-go/pkg/response"
	"github.com/gin-gonic/gin"
)

var adminService = service.NewAdminService()

// LoginRequest 登录请求
type LoginRequest struct {
	Username string `json:"username" binding:"required"`
	Password string `json:"password" binding:"required"`
}

// Login 管理员登录
func Login(c *gin.Context) {
	var req LoginRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		response.ParamError(c, "参数错误")
		return
	}

	admin, err := adminService.Login(&service.AdminLoginRequest{
		Username: req.Username,
		Password: req.Password,
	})
	if err != nil {
		response.Error(c, response.CodeUnauthorized, err.Error())
		return
	}

	// 生成 Token
	token, err := jwt.GenerateToken(admin.ID, admin.Username, jwt.TokenTypeAdmin)
	if err != nil {
		response.ServerError(c, "生成Token失败")
		return
	}

	response.Success(c, gin.H{
		"token": token,
		"admin": gin.H{
			"id":       admin.ID,
			"username": admin.Username,
			"role":     admin.Role,
		},
	})
}

// Logout 退出登录
func Logout(c *gin.Context) {
	// JWT 无状态,客户端删除 token 即可
	response.Success(c, nil)
}

Step 2: 提交

git add internal/handler/admin/auth.go
git commit -m "feat: add admin auth handler (login/logout)"

Task 3: 创建管理后台仪表盘和商户管理 Handler

Files:

  • Create: epay-go/internal/handler/admin/dashboard.go
  • Create: epay-go/internal/handler/admin/merchant.go

Step 1: 创建仪表盘 Handler

// internal/handler/admin/dashboard.go
package admin

import (
	"github.com/example/epay-go/internal/service"
	"github.com/example/epay-go/pkg/response"
	"github.com/gin-gonic/gin"
)

var orderService = service.NewOrderService()
var merchantService = service.NewMerchantService()

// Dashboard 仪表盘数据
func Dashboard(c *gin.Context) {
	// 今日统计
	todayCount, todayAmount, err := orderService.GetTodayStats(nil)
	if err != nil {
		response.ServerError(c, "获取统计数据失败")
		return
	}

	// 商户数量
	merchants, totalMerchants, _ := merchantService.List(1, 1, nil)
	_ = merchants

	response.Success(c, gin.H{
		"today_order_count":  todayCount,
		"today_order_amount": todayAmount,
		"total_merchants":    totalMerchants,
	})
}

Step 2: 创建商户管理 Handler

// internal/handler/admin/merchant.go
package admin

import (
	"strconv"

	"github.com/example/epay-go/pkg/response"
	"github.com/gin-gonic/gin"
)

// ListMerchants 商户列表
func ListMerchants(c *gin.Context) {
	page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
	pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))

	var status *int8
	if s := c.Query("status"); s != "" {
		v, _ := strconv.ParseInt(s, 10, 8)
		v8 := int8(v)
		status = &v8
	}

	merchants, total, err := merchantService.List(page, pageSize, status)
	if err != nil {
		response.ServerError(c, "获取商户列表失败")
		return
	}

	response.SuccessPage(c, merchants, total, page, pageSize)
}

// GetMerchant 获取商户详情
func GetMerchant(c *gin.Context) {
	id, err := strconv.ParseInt(c.Param("id"), 10, 64)
	if err != nil {
		response.ParamError(c, "无效的商户ID")
		return
	}

	merchant, err := merchantService.GetByID(id)
	if err != nil {
		response.NotFound(c, "商户不存在")
		return
	}

	response.Success(c, merchant)
}

// UpdateMerchantRequest 更新商户请求
type UpdateMerchantRequest struct {
	Email string `json:"email"`
	Phone string `json:"phone"`
}

// UpdateMerchant 更新商户信息
func UpdateMerchant(c *gin.Context) {
	id, err := strconv.ParseInt(c.Param("id"), 10, 64)
	if err != nil {
		response.ParamError(c, "无效的商户ID")
		return
	}

	var req UpdateMerchantRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		response.ParamError(c, "参数错误")
		return
	}

	merchant, err := merchantService.GetByID(id)
	if err != nil {
		response.NotFound(c, "商户不存在")
		return
	}

	merchant.Email = req.Email
	merchant.Phone = req.Phone

	// TODO: 调用 service 更新

	response.Success(c, merchant)
}

// UpdateMerchantStatusRequest 更新状态请求
type UpdateMerchantStatusRequest struct {
	Status int8 `json:"status" binding:"oneof=0 1"`
}

// UpdateMerchantStatus 更新商户状态
func UpdateMerchantStatus(c *gin.Context) {
	id, err := strconv.ParseInt(c.Param("id"), 10, 64)
	if err != nil {
		response.ParamError(c, "无效的商户ID")
		return
	}

	var req UpdateMerchantStatusRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		response.ParamError(c, "参数错误")
		return
	}

	if err := merchantService.UpdateStatus(id, req.Status); err != nil {
		response.ServerError(c, err.Error())
		return
	}

	response.Success(c, nil)
}

Step 3: 提交

git add internal/handler/admin/
git commit -m "feat: add admin dashboard and merchant management handlers"

Task 4: 创建管理后台订单和通道管理 Handler

Files:

  • Create: epay-go/internal/handler/admin/order.go
  • Create: epay-go/internal/handler/admin/channel.go

Step 1: 创建订单管理 Handler

// internal/handler/admin/order.go
package admin

import (
	"strconv"

	"github.com/example/epay-go/internal/service"
	"github.com/example/epay-go/pkg/response"
	"github.com/gin-gonic/gin"
)

var notifyService = service.NewNotifyService()

// ListOrders 订单列表
func ListOrders(c *gin.Context) {
	page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
	pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))

	var merchantID *int64
	if mid := c.Query("merchant_id"); mid != "" {
		v, _ := strconv.ParseInt(mid, 10, 64)
		merchantID = &v
	}

	var status *int8
	if s := c.Query("status"); s != "" {
		v, _ := strconv.ParseInt(s, 10, 8)
		v8 := int8(v)
		status = &v8
	}

	orders, total, err := orderService.List(page, pageSize, merchantID, status)
	if err != nil {
		response.ServerError(c, "获取订单列表失败")
		return
	}

	response.SuccessPage(c, orders, total, page, pageSize)
}

// GetOrder 获取订单详情
func GetOrder(c *gin.Context) {
	tradeNo := c.Param("trade_no")

	order, err := orderService.GetByTradeNo(tradeNo)
	if err != nil {
		response.NotFound(c, "订单不存在")
		return
	}

	response.Success(c, order)
}

// RenotifyOrder 重新发送通知
func RenotifyOrder(c *gin.Context) {
	tradeNo := c.Param("trade_no")

	order, err := orderService.GetByTradeNo(tradeNo)
	if err != nil {
		response.NotFound(c, "订单不存在")
		return
	}

	if order.Status != 1 {
		response.Error(c, response.CodeParamError, "订单未支付,无法发送通知")
		return
	}

	if err := notifyService.SendNotify(order); err != nil {
		response.ServerError(c, "发送通知失败: "+err.Error())
		return
	}

	response.Success(c, nil)
}

Step 2: 创建通道管理 Handler

// internal/handler/admin/channel.go
package admin

import (
	"strconv"

	"github.com/example/epay-go/internal/service"
	"github.com/example/epay-go/pkg/response"
	"github.com/gin-gonic/gin"
)

var channelService = service.NewChannelService()

// ListChannels 通道列表
func ListChannels(c *gin.Context) {
	page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
	pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))

	channels, total, err := channelService.List(page, pageSize)
	if err != nil {
		response.ServerError(c, "获取通道列表失败")
		return
	}

	response.SuccessPage(c, channels, total, page, pageSize)
}

// GetChannel 获取通道详情
func GetChannel(c *gin.Context) {
	id, err := strconv.ParseInt(c.Param("id"), 10, 64)
	if err != nil {
		response.ParamError(c, "无效的通道ID")
		return
	}

	channel, err := channelService.GetByID(id)
	if err != nil {
		response.NotFound(c, "通道不存在")
		return
	}

	response.Success(c, channel)
}

// CreateChannel 创建通道
func CreateChannel(c *gin.Context) {
	var req service.CreateChannelRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		response.ParamError(c, "参数错误: "+err.Error())
		return
	}

	channel, err := channelService.Create(&req)
	if err != nil {
		response.ServerError(c, "创建通道失败: "+err.Error())
		return
	}

	response.Success(c, channel)
}

// UpdateChannel 更新通道
func UpdateChannel(c *gin.Context) {
	id, err := strconv.ParseInt(c.Param("id"), 10, 64)
	if err != nil {
		response.ParamError(c, "无效的通道ID")
		return
	}

	var req service.UpdateChannelRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		response.ParamError(c, "参数错误: "+err.Error())
		return
	}

	if err := channelService.Update(id, &req); err != nil {
		response.ServerError(c, "更新通道失败: "+err.Error())
		return
	}

	response.Success(c, nil)
}

// DeleteChannel 删除通道
func DeleteChannel(c *gin.Context) {
	id, err := strconv.ParseInt(c.Param("id"), 10, 64)
	if err != nil {
		response.ParamError(c, "无效的通道ID")
		return
	}

	if err := channelService.Delete(id); err != nil {
		response.ServerError(c, "删除通道失败: "+err.Error())
		return
	}

	response.Success(c, nil)
}

Step 3: 提交

git add internal/handler/admin/
git commit -m "feat: add admin order and channel management handlers"

Task 5: 创建管理后台结算管理 Handler

Files:

  • Create: epay-go/internal/handler/admin/settlement.go

Step 1: 创建结算管理 Handler

// internal/handler/admin/settlement.go
package admin

import (
	"strconv"

	"github.com/example/epay-go/internal/service"
	"github.com/example/epay-go/pkg/response"
	"github.com/gin-gonic/gin"
)

var settlementService = service.NewSettlementService()

// ListSettlements 结算列表
func ListSettlements(c *gin.Context) {
	page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
	pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))

	var merchantID *int64
	if mid := c.Query("merchant_id"); mid != "" {
		v, _ := strconv.ParseInt(mid, 10, 64)
		merchantID = &v
	}

	var status *int8
	if s := c.Query("status"); s != "" {
		v, _ := strconv.ParseInt(s, 10, 8)
		v8 := int8(v)
		status = &v8
	}

	settlements, total, err := settlementService.List(page, pageSize, merchantID, status)
	if err != nil {
		response.ServerError(c, "获取结算列表失败")
		return
	}

	response.SuccessPage(c, settlements, total, page, pageSize)
}

// ApproveSettlement 审核通过
func ApproveSettlement(c *gin.Context) {
	id, err := strconv.ParseInt(c.Param("id"), 10, 64)
	if err != nil {
		response.ParamError(c, "无效的结算ID")
		return
	}

	if err := settlementService.Approve(id); err != nil {
		response.ServerError(c, err.Error())
		return
	}

	response.Success(c, nil)
}

// RejectSettlementRequest 驳回请求
type RejectSettlementRequest struct {
	Remark string `json:"remark" binding:"required"`
}

// RejectSettlement 驳回结算
func RejectSettlement(c *gin.Context) {
	id, err := strconv.ParseInt(c.Param("id"), 10, 64)
	if err != nil {
		response.ParamError(c, "无效的结算ID")
		return
	}

	var req RejectSettlementRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		response.ParamError(c, "请填写驳回原因")
		return
	}

	if err := settlementService.Reject(id, req.Remark); err != nil {
		response.ServerError(c, err.Error())
		return
	}

	response.Success(c, nil)
}

Step 2: 提交

git add internal/handler/admin/settlement.go
git commit -m "feat: add admin settlement management handler"

Task 6: 创建商户端认证 Handler

Files:

  • Create: epay-go/internal/handler/merchant/auth.go

Step 1: 创建商户认证 Handler

// internal/handler/merchant/auth.go
package merchant

import (
	"github.com/example/epay-go/internal/service"
	"github.com/example/epay-go/pkg/jwt"
	"github.com/example/epay-go/pkg/response"
	"github.com/gin-gonic/gin"
)

var merchantService = service.NewMerchantService()

// RegisterRequest 注册请求
type RegisterRequest struct {
	Username string `json:"username" binding:"required,min=4,max=32"`
	Password string `json:"password" binding:"required,min=6,max=32"`
	Email    string `json:"email" binding:"omitempty,email"`
	Phone    string `json:"phone" binding:"omitempty"`
}

// Register 商户注册
func Register(c *gin.Context) {
	var req RegisterRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		response.ParamError(c, "参数错误: "+err.Error())
		return
	}

	merchant, err := merchantService.Register(&service.RegisterRequest{
		Username: req.Username,
		Password: req.Password,
		Email:    req.Email,
		Phone:    req.Phone,
	})
	if err != nil {
		response.Error(c, response.CodeParamError, err.Error())
		return
	}

	// 生成 Token
	token, err := jwt.GenerateToken(merchant.ID, merchant.Username, jwt.TokenTypeMerchant)
	if err != nil {
		response.ServerError(c, "生成Token失败")
		return
	}

	response.Success(c, gin.H{
		"token": token,
		"merchant": gin.H{
			"id":       merchant.ID,
			"username": merchant.Username,
			"api_key":  merchant.ApiKey,
		},
	})
}

// LoginRequest 登录请求
type LoginRequest struct {
	Username string `json:"username" binding:"required"`
	Password string `json:"password" binding:"required"`
}

// Login 商户登录
func Login(c *gin.Context) {
	var req LoginRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		response.ParamError(c, "参数错误")
		return
	}

	merchant, err := merchantService.Login(&service.LoginRequest{
		Username: req.Username,
		Password: req.Password,
	})
	if err != nil {
		response.Error(c, response.CodeUnauthorized, err.Error())
		return
	}

	// 生成 Token
	token, err := jwt.GenerateToken(merchant.ID, merchant.Username, jwt.TokenTypeMerchant)
	if err != nil {
		response.ServerError(c, "生成Token失败")
		return
	}

	response.Success(c, gin.H{
		"token": token,
		"merchant": gin.H{
			"id":       merchant.ID,
			"username": merchant.Username,
			"balance":  merchant.Balance,
		},
	})
}

// Logout 退出登录
func Logout(c *gin.Context) {
	response.Success(c, nil)
}

Step 2: 提交

git add internal/handler/merchant/auth.go
git commit -m "feat: add merchant auth handler (register/login/logout)"

Task 7: 创建商户端个人信息和仪表盘 Handler

Files:

  • Create: epay-go/internal/handler/merchant/profile.go
  • Create: epay-go/internal/handler/merchant/dashboard.go

Step 1: 创建个人信息 Handler

// internal/handler/merchant/profile.go
package merchant

import (
	"github.com/example/epay-go/internal/middleware"
	"github.com/example/epay-go/pkg/response"
	"github.com/gin-gonic/gin"
)

// GetProfile 获取个人信息
func GetProfile(c *gin.Context) {
	merchantID := middleware.GetUserID(c)

	merchant, err := merchantService.GetByID(merchantID)
	if err != nil {
		response.NotFound(c, "商户不存在")
		return
	}

	response.Success(c, gin.H{
		"id":             merchant.ID,
		"username":       merchant.Username,
		"email":          merchant.Email,
		"phone":          merchant.Phone,
		"api_key":        merchant.ApiKey,
		"balance":        merchant.Balance,
		"frozen_balance": merchant.FrozenBalance,
		"status":         merchant.Status,
		"created_at":     merchant.CreatedAt,
	})
}

// UpdateProfileRequest 更新信息请求
type UpdateProfileRequest struct {
	Email string `json:"email" binding:"omitempty,email"`
	Phone string `json:"phone" binding:"omitempty"`
}

// UpdateProfile 更新个人信息
func UpdateProfile(c *gin.Context) {
	merchantID := middleware.GetUserID(c)

	var req UpdateProfileRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		response.ParamError(c, "参数错误: "+err.Error())
		return
	}

	merchant, err := merchantService.GetByID(merchantID)
	if err != nil {
		response.NotFound(c, "商户不存在")
		return
	}

	merchant.Email = req.Email
	merchant.Phone = req.Phone

	// TODO: 保存更新

	response.Success(c, nil)
}

// UpdatePasswordRequest 修改密码请求
type UpdatePasswordRequest struct {
	OldPassword string `json:"old_password" binding:"required"`
	NewPassword string `json:"new_password" binding:"required,min=6,max=32"`
}

// UpdatePassword 修改密码
func UpdatePassword(c *gin.Context) {
	merchantID := middleware.GetUserID(c)

	var req UpdatePasswordRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		response.ParamError(c, "参数错误: "+err.Error())
		return
	}

	if err := merchantService.UpdatePassword(merchantID, req.OldPassword, req.NewPassword); err != nil {
		response.Error(c, response.CodeParamError, err.Error())
		return
	}

	response.Success(c, nil)
}

// ResetAPIKey 重置API密钥
func ResetAPIKey(c *gin.Context) {
	merchantID := middleware.GetUserID(c)

	newKey, err := merchantService.ResetAPIKey(merchantID)
	if err != nil {
		response.ServerError(c, "重置密钥失败")
		return
	}

	response.Success(c, gin.H{
		"api_key": newKey,
	})
}

Step 2: 创建仪表盘 Handler

// internal/handler/merchant/dashboard.go
package merchant

import (
	"github.com/example/epay-go/internal/middleware"
	"github.com/example/epay-go/internal/service"
	"github.com/example/epay-go/pkg/response"
	"github.com/gin-gonic/gin"
)

var orderService = service.NewOrderService()

// Dashboard 商户仪表盘
func Dashboard(c *gin.Context) {
	merchantID := middleware.GetUserID(c)

	// 获取商户信息
	merchant, err := merchantService.GetByID(merchantID)
	if err != nil {
		response.NotFound(c, "商户不存在")
		return
	}

	// 今日统计
	todayCount, todayAmount, err := orderService.GetTodayStats(&merchantID)
	if err != nil {
		response.ServerError(c, "获取统计数据失败")
		return
	}

	response.Success(c, gin.H{
		"balance":            merchant.Balance,
		"frozen_balance":     merchant.FrozenBalance,
		"today_order_count":  todayCount,
		"today_order_amount": todayAmount,
	})
}

Step 3: 提交

git add internal/handler/merchant/
git commit -m "feat: add merchant profile and dashboard handlers"

Task 8: 创建商户端订单、结算、资金记录 Handler

Files:

  • Create: epay-go/internal/handler/merchant/order.go
  • Create: epay-go/internal/handler/merchant/settlement.go
  • Create: epay-go/internal/handler/merchant/record.go

Step 1: 创建订单 Handler

// internal/handler/merchant/order.go
package merchant

import (
	"strconv"

	"github.com/example/epay-go/internal/middleware"
	"github.com/example/epay-go/pkg/response"
	"github.com/gin-gonic/gin"
)

// ListOrders 订单列表
func ListOrders(c *gin.Context) {
	merchantID := middleware.GetUserID(c)
	page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
	pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))

	var status *int8
	if s := c.Query("status"); s != "" {
		v, _ := strconv.ParseInt(s, 10, 8)
		v8 := int8(v)
		status = &v8
	}

	orders, total, err := orderService.List(page, pageSize, &merchantID, status)
	if err != nil {
		response.ServerError(c, "获取订单列表失败")
		return
	}

	response.SuccessPage(c, orders, total, page, pageSize)
}

// GetOrder 获取订单详情
func GetOrder(c *gin.Context) {
	merchantID := middleware.GetUserID(c)
	tradeNo := c.Param("trade_no")

	order, err := orderService.GetByTradeNo(tradeNo)
	if err != nil {
		response.NotFound(c, "订单不存在")
		return
	}

	// 验证订单归属
	if order.MerchantID != merchantID {
		response.Forbidden(c, "无权查看此订单")
		return
	}

	response.Success(c, order)
}

Step 2: 创建结算 Handler

// internal/handler/merchant/settlement.go
package merchant

import (
	"strconv"

	"github.com/example/epay-go/internal/middleware"
	"github.com/example/epay-go/internal/service"
	"github.com/example/epay-go/pkg/response"
	"github.com/gin-gonic/gin"
	"github.com/shopspring/decimal"
)

var settlementService = service.NewSettlementService()

// ListSettlements 结算列表
func ListSettlements(c *gin.Context) {
	merchantID := middleware.GetUserID(c)
	page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
	pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))

	var status *int8
	if s := c.Query("status"); s != "" {
		v, _ := strconv.ParseInt(s, 10, 8)
		v8 := int8(v)
		status = &v8
	}

	settlements, total, err := settlementService.List(page, pageSize, &merchantID, status)
	if err != nil {
		response.ServerError(c, "获取结算列表失败")
		return
	}

	response.SuccessPage(c, settlements, total, page, pageSize)
}

// ApplySettlementRequest 申请结算请求
type ApplySettlementRequest struct {
	Amount      string `json:"amount" binding:"required"`
	AccountType string `json:"account_type" binding:"required,oneof=alipay bank"`
	AccountNo   string `json:"account_no" binding:"required"`
	AccountName string `json:"account_name" binding:"required"`
}

// ApplySettlement 申请结算
func ApplySettlement(c *gin.Context) {
	merchantID := middleware.GetUserID(c)

	var req ApplySettlementRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		response.ParamError(c, "参数错误: "+err.Error())
		return
	}

	amount, err := decimal.NewFromString(req.Amount)
	if err != nil || amount.LessThanOrEqual(decimal.Zero) {
		response.ParamError(c, "金额格式错误")
		return
	}

	settlement, err := settlementService.Apply(&service.ApplyRequest{
		MerchantID:  merchantID,
		Amount:      amount,
		AccountType: req.AccountType,
		AccountNo:   req.AccountNo,
		AccountName: req.AccountName,
	})
	if err != nil {
		response.Error(c, response.CodeParamError, err.Error())
		return
	}

	response.Success(c, settlement)
}

Step 3: 创建资金记录 Handler

// internal/handler/merchant/record.go
package merchant

import (
	"strconv"

	"github.com/example/epay-go/internal/middleware"
	"github.com/example/epay-go/internal/repository"
	"github.com/example/epay-go/pkg/response"
	"github.com/gin-gonic/gin"
)

var recordRepo = repository.NewBalanceRecordRepository()

// ListRecords 资金记录列表
func ListRecords(c *gin.Context) {
	merchantID := middleware.GetUserID(c)
	page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
	pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))

	records, total, err := recordRepo.List(page, pageSize, merchantID)
	if err != nil {
		response.ServerError(c, "获取资金记录失败")
		return
	}

	response.SuccessPage(c, records, total, page, pageSize)
}

Step 4: 提交

git add internal/handler/merchant/
git commit -m "feat: add merchant order, settlement, record handlers"

Task 9: 创建对外支付 API Handler

Files:

  • Create: epay-go/internal/handler/payment/create.go
  • Create: epay-go/internal/handler/payment/query.go
  • Create: epay-go/internal/handler/payment/notify.go

Step 1: 创建签名验证工具

// pkg/sign/sign.go
package sign

import (
	"crypto/md5"
	"encoding/hex"
	"net/url"
	"sort"
	"strings"
)

// VerifyMD5Sign 验证MD5签名(与原epay兼容)
func VerifyMD5Sign(params url.Values, key, sign string) bool {
	// 按key排序
	var keys []string
	for k := range params {
		if k != "sign" && k != "sign_type" && params.Get(k) != "" {
			keys = append(keys, k)
		}
	}
	sort.Strings(keys)

	// 拼接字符串
	var buf strings.Builder
	for i, k := range keys {
		if i > 0 {
			buf.WriteString("&")
		}
		buf.WriteString(k)
		buf.WriteString("=")
		buf.WriteString(params.Get(k))
	}
	buf.WriteString(key)

	// MD5
	hash := md5.Sum([]byte(buf.String()))
	expected := hex.EncodeToString(hash[:])

	return strings.EqualFold(expected, sign)
}

// GenerateMD5Sign 生成MD5签名
func GenerateMD5Sign(params url.Values, key string) string {
	var keys []string
	for k := range params {
		if k != "sign" && k != "sign_type" && params.Get(k) != "" {
			keys = append(keys, k)
		}
	}
	sort.Strings(keys)

	var buf strings.Builder
	for i, k := range keys {
		if i > 0 {
			buf.WriteString("&")
		}
		buf.WriteString(k)
		buf.WriteString("=")
		buf.WriteString(params.Get(k))
	}
	buf.WriteString(key)

	hash := md5.Sum([]byte(buf.String()))
	return hex.EncodeToString(hash[:])
}

Step 2: 创建订单创建 Handler

// internal/handler/payment/create.go
package payment

import (
	"context"
	"net/url"

	"github.com/example/epay-go/internal/service"
	"github.com/example/epay-go/pkg/response"
	"github.com/example/epay-go/pkg/sign"
	"github.com/example/epay-go/pkg/utils"
	"github.com/gin-gonic/gin"
	"github.com/shopspring/decimal"
)

var merchantService = service.NewMerchantService()
var orderService = service.NewOrderService()

// CreateOrderRequest 创建订单请求(兼容原epay)
type CreateOrderRequest struct {
	Pid        string `form:"pid" binding:"required"`         // 商户ID
	Type       string `form:"type" binding:"required"`        // 支付类型
	OutTradeNo string `form:"out_trade_no" binding:"required"`// 商户订单号
	NotifyURL  string `form:"notify_url" binding:"required"`  // 异步通知地址
	ReturnURL  string `form:"return_url"`                     // 同步跳转地址
	Name       string `form:"name" binding:"required"`        // 商品名称
	Money      string `form:"money" binding:"required"`       // 金额
	Sign       string `form:"sign" binding:"required"`        // 签名
	SignType   string `form:"sign_type"`                      // 签名类型
}

// CreateOrder 创建支付订单
func CreateOrder(c *gin.Context) {
	var req CreateOrderRequest
	if err := c.ShouldBind(&req); err != nil {
		response.ParamError(c, "参数错误: "+err.Error())
		return
	}

	// 获取商户信息
	merchant, err := merchantService.GetByAPIKey(req.Pid)
	if err != nil {
		response.Error(c, response.CodeParamError, "商户不存在")
		return
	}

	if merchant.Status != 1 {
		response.Error(c, response.CodeForbidden, "商户已被禁用")
		return
	}

	// 验证签名
	params := url.Values{}
	params.Set("pid", req.Pid)
	params.Set("type", req.Type)
	params.Set("out_trade_no", req.OutTradeNo)
	params.Set("notify_url", req.NotifyURL)
	params.Set("name", req.Name)
	params.Set("money", req.Money)
	if req.ReturnURL != "" {
		params.Set("return_url", req.ReturnURL)
	}

	if !sign.VerifyMD5Sign(params, merchant.ApiKey, req.Sign) {
		response.Error(c, response.CodeParamError, "签名验证失败")
		return
	}

	// 解析金额
	amount, err := decimal.NewFromString(req.Money)
	if err != nil || amount.LessThanOrEqual(decimal.Zero) {
		response.ParamError(c, "金额格式错误")
		return
	}

	// 创建订单
	orderReq := &service.CreateOrderRequest{
		MerchantID: merchant.ID,
		OutTradeNo: req.OutTradeNo,
		Amount:     amount,
		Name:       req.Name,
		PayType:    req.Type,
		NotifyURL:  req.NotifyURL,
		ReturnURL:  req.ReturnURL,
		ClientIP:   utils.GetClientIP(c),
		PayMethod:  c.DefaultQuery("pay_method", "scan"),
	}

	orderResp, err := orderService.Create(context.Background(), orderReq)
	if err != nil {
		response.Error(c, response.CodeServerError, err.Error())
		return
	}

	response.Success(c, orderResp)
}

Step 3: 创建订单查询 Handler

// internal/handler/payment/query.go
package payment

import (
	"net/url"

	"github.com/example/epay-go/pkg/response"
	"github.com/example/epay-go/pkg/sign"
	"github.com/gin-gonic/gin"
)

// QueryOrderRequest 查询订单请求
type QueryOrderRequest struct {
	Pid        string `form:"pid" binding:"required"`
	TradeNo    string `form:"trade_no"`
	OutTradeNo string `form:"out_trade_no"`
	Sign       string `form:"sign" binding:"required"`
}

// QueryOrder 查询订单
func QueryOrder(c *gin.Context) {
	var req QueryOrderRequest
	if err := c.ShouldBind(&req); err != nil {
		response.ParamError(c, "参数错误")
		return
	}

	if req.TradeNo == "" && req.OutTradeNo == "" {
		response.ParamError(c, "trade_no 和 out_trade_no 不能同时为空")
		return
	}

	// 获取商户
	merchant, err := merchantService.GetByAPIKey(req.Pid)
	if err != nil {
		response.Error(c, response.CodeParamError, "商户不存在")
		return
	}

	// 验证签名
	params := url.Values{}
	params.Set("pid", req.Pid)
	if req.TradeNo != "" {
		params.Set("trade_no", req.TradeNo)
	}
	if req.OutTradeNo != "" {
		params.Set("out_trade_no", req.OutTradeNo)
	}

	if !sign.VerifyMD5Sign(params, merchant.ApiKey, req.Sign) {
		response.Error(c, response.CodeParamError, "签名验证失败")
		return
	}

	// 查询订单
	var order interface{}
	if req.TradeNo != "" {
		order, err = orderService.GetByTradeNo(req.TradeNo)
	} else {
		// TODO: 按商户订单号查询
		order, err = orderService.GetByTradeNo(req.OutTradeNo)
	}

	if err != nil {
		response.NotFound(c, "订单不存在")
		return
	}

	response.Success(c, order)
}

Step 4: 创建回调处理 Handler

// internal/handler/payment/notify.go
package payment

import (
	"io"
	"log"
	"net/http"

	"github.com/example/epay-go/internal/model"
	intPayment "github.com/example/epay-go/internal/payment"
	"github.com/example/epay-go/internal/repository"
	"github.com/example/epay-go/internal/service"
	"github.com/gin-gonic/gin"
)

var channelRepo = repository.NewChannelRepository()
var notifyService = service.NewNotifyService()

// HandleNotify 处理支付回调
func HandleNotify(c *gin.Context) {
	channelPlugin := c.Param("channel")

	// 获取通道配置
	channel, err := channelRepo.GetByPluginAndPayType(channelPlugin, "")
	if err != nil {
		log.Printf("Channel not found: %s", channelPlugin)
		c.String(http.StatusOK, "fail")
		return
	}

	// 创建适配器
	adapter, err := intPayment.NewAdapter(channel.Plugin, channel.Config)
	if err != nil {
		log.Printf("Create adapter failed: %v", err)
		c.String(http.StatusOK, "fail")
		return
	}

	// 解析回调
	result, err := adapter.ParseNotify(c.Request.Context(), c.Request)
	if err != nil {
		log.Printf("Parse notify failed: %v", err)
		c.String(http.StatusOK, "fail")
		return
	}

	// 处理支付结果
	if result.Status == "success" {
		if err := orderService.ProcessPayNotify(result.TradeNo, result.ApiTradeNo, result.Buyer, result.Amount); err != nil {
			log.Printf("Process notify failed: %v", err)
			c.String(http.StatusOK, "fail")
			return
		}

		// 发送商户通知
		order, _ := orderService.GetByTradeNo(result.TradeNo)
		if order != nil && order.Status == model.OrderStatusPaid {
			go notifyService.SendNotify(order)
		}
	}

	// 返回成功响应
	c.String(http.StatusOK, adapter.NotifySuccess())
}

// HandleReturn 处理同步跳转
func HandleReturn(c *gin.Context) {
	// 从参数获取订单号
	tradeNo := c.Query("out_trade_no")
	if tradeNo == "" {
		// 尝试从 body 读取
		body, _ := io.ReadAll(c.Request.Body)
		log.Printf("Return body: %s", string(body))
	}

	order, err := orderService.GetByTradeNo(tradeNo)
	if err != nil {
		c.Redirect(http.StatusFound, "/")
		return
	}

	// 跳转到商户 return_url
	if order.ReturnURL != "" {
		c.Redirect(http.StatusFound, order.ReturnURL)
		return
	}

	c.String(http.StatusOK, "支付完成")
}

Step 5: 提交

git add pkg/sign/sign.go internal/handler/payment/
git commit -m "feat: add payment api handlers (create/query/notify)"

Task 10: 更新主程序入口,注册路由

Files:

  • Modify: epay-go/cmd/server/main.go

Step 1: 更新主程序

// cmd/server/main.go
package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"os/signal"
	"syscall"

	"github.com/example/epay-go/internal/cache"
	"github.com/example/epay-go/internal/config"
	"github.com/example/epay-go/internal/database"
	"github.com/example/epay-go/internal/middleware"
	"github.com/example/epay-go/internal/router"
	"github.com/example/epay-go/internal/service"
	"github.com/gin-gonic/gin"

	// 注册支付适配器
	_ "github.com/example/epay-go/internal/payment"
)

func main() {
	// 加载配置
	if err := config.Load("config.yaml"); err != nil {
		log.Fatalf("Failed to load config: %v", err)
	}
	cfg := config.Get()

	// 初始化数据库
	if err := database.Init(); err != nil {
		log.Fatalf("Failed to init database: %v", err)
	}
	defer database.Close()

	// 数据库迁移
	if err := database.Migrate(); err != nil {
		log.Fatalf("Failed to migrate database: %v", err)
	}

	// 初始化 Redis
	if err := cache.Init(); err != nil {
		log.Fatalf("Failed to init redis: %v", err)
	}
	defer cache.Close()

	// 初始化默认管理员
	adminService := service.NewAdminService()
	if err := adminService.InitDefaultAdmin(); err != nil {
		log.Printf("Failed to init default admin: %v", err)
	}

	// 设置 Gin 模式
	gin.SetMode(cfg.Server.Mode)

	// 创建 Gin 引擎
	r := gin.New()

	// 全局中间件
	r.Use(middleware.Recovery())
	r.Use(middleware.Logger())
	r.Use(middleware.Cors())

	// 健康检查
	r.GET("/health", func(c *gin.Context) {
		c.JSON(200, gin.H{"status": "ok"})
	})

	// 注册所有路由
	router.Setup(r)

	// 启动异步通知工作协程
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	notifyService := service.NewNotifyService()
	go notifyService.StartNotifyWorker(ctx)

	// 优雅关闭
	go func() {
		quit := make(chan os.Signal, 1)
		signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
		<-quit
		log.Println("Shutting down server...")
		cancel()
	}()

	// 启动服务
	addr := fmt.Sprintf(":%d", cfg.Server.Port)
	log.Printf("Server starting on %s", addr)
	if err := r.Run(addr); err != nil {
		log.Fatalf("Failed to start server: %v", err)
	}
}

Step 2: 验证编译

Run: cd d:/project/payment/epay-go && go build -o epay-server ./cmd/server Expected: 编译成功

Step 3: 提交

git add cmd/server/main.go
git commit -m "feat: register all routes in main entry"

阶段三完成检查清单

  • 路由注册入口 (internal/router/router.go)
  • 管理员认证 Handler (login/logout)
  • 管理后台仪表盘和商户管理 Handler
  • 管理后台订单和通道管理 Handler
  • 管理后台结算管理 Handler
  • 商户端认证 Handler (register/login/logout)
  • 商户端个人信息和仪表盘 Handler
  • 商户端订单、结算、资金记录 Handler
  • 对外支付 API Handler (create/query/notify)
  • 签名验证工具 (pkg/sign/sign.go)
  • 主程序集成路由

API 接口汇总

对外支付 API

方法 路径 说明
POST /api/pay/create 创建支付订单
GET /api/pay/query 查询订单状态
POST /api/pay/notify/:channel 支付回调
GET /api/pay/return/:channel 同步跳转

管理后台 API

方法 路径 说明
POST /admin/auth/login 管理员登录
GET /admin/dashboard 仪表盘
GET /admin/merchants 商户列表
GET/PUT /admin/merchants/:id 商户详情/更新
GET /admin/orders 订单列表
GET/POST /admin/channels 通道列表/创建
GET /admin/settlements 结算列表
PATCH /admin/settlements/:id/approve 审核通过

商户端 API

方法 路径 说明
POST /merchant/auth/register 商户注册
POST /merchant/auth/login 商户登录
GET /merchant/dashboard 仪表盘
GET/PUT /merchant/profile 个人信息
GET /merchant/orders 订单列表
GET/POST /merchant/settlements 结算列表/申请
GET /merchant/records 资金记录

下一阶段: 阶段四将实现 Vue3 前端(管理后台、商户中心、收银台)。