# 完整支付系统功能细化实现计划 > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **目标:** 参考 epay 项目,为 epay-go 补全企业级聚合支付系统的完整功能,包括管理员管理、订单退款、统计报表、系统配置、安全增强等核心模块。 **架构:** 采用三层架构(Handler-Service-Repository),前端使用 Vue 3 + TypeScript + Arco Design,后端使用 Go + Gin + GORM + PostgreSQL。 **技术栈:** Go 1.21+, Gin, GORM, PostgreSQL, Redis, Vue 3, TypeScript, Arco Design, Vite --- ## 功能优先级划分 ### 🔴 P0 - 核心业务功能(必须实现) 1. 订单退款功能 2. 管理员管理模块 3. 商户余额调整 4. 系统配置管理 ### 🟡 P1 - 重要运营功能(优先实现) 5. 操作日志记录 6. 统计报表与图表 7. 数据导出功能 8. 订单补单功能 ### 🟢 P2 - 增强功能(后续实现) 9. 登录日志与安全增强 10. 消息通知系统 11. API 文档生成 --- ## Task 1: 订单退款功能 **文件:** - Modify: `internal/model/order.go` - Create: `internal/model/refund.go` - Create: `internal/repository/refund.go` - Create: `internal/service/refund.go` - Create: `internal/handler/admin/refund.go` - Create: `internal/handler/merchant/refund.go` - Modify: `internal/router/router.go` - Create: `web/src/views/admin/Refunds.vue` - Create: `web/src/views/merchant/Refunds.vue` - Modify: `web/src/api/admin.ts` - Modify: `web/src/api/merchant.ts` - Modify: `web/src/api/types.ts` ### 步骤 1: 创建退款数据模型 在 `internal/model/refund.go` 创建退款模型: ```go // internal/model/refund.go package model import ( "time" "gorm.io/gorm" ) // Refund 退款订单 type Refund struct { ID int64 `gorm:"primaryKey" json:"id"` RefundNo string `gorm:"size:32;uniqueIndex;not null" json:"refund_no"` // 退款单号 TradeNo string `gorm:"size:32;index;not null" json:"trade_no"` // 原订单号 MerchantID int64 `gorm:"index;not null" json:"merchant_id"` // 商户ID OrderID int64 `gorm:"index;not null" json:"order_id"` // 原订单ID ChannelID int64 `gorm:"not null" json:"channel_id"` // 支付通道ID Amount string `gorm:"type:decimal(10,2);not null" json:"amount"` // 退款金额 RefundFee string `gorm:"type:decimal(10,2);default:0.00" json:"refund_fee"` // 退款手续费 Reason string `gorm:"size:200" json:"reason"` // 退款原因 Status int8 `gorm:"default:0;index" json:"status"` // 状态:0待处理 1成功 2失败 ApiRefundNo string `gorm:"size:64" json:"api_refund_no"` // 第三方退款单号 FailReason string `gorm:"size:200" json:"fail_reason"` // 失败原因 NotifyURL string `gorm:"size:255" json:"notify_url"` // 异步通知地址 NotifyStatus int8 `gorm:"default:0" json:"notify_status"` // 通知状态:0未通知 1已通知 NotifyCount int `gorm:"default:0" json:"notify_count"` // 通知次数 NextNotifyTime *time.Time `json:"next_notify_time"` // 下次通知时间 ProcessedAt *time.Time `json:"processed_at"` // 处理完成时间 CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` } // 退款状态常量 const ( RefundStatusPending = 0 // 待处理 RefundStatusSuccess = 1 // 成功 RefundStatusFailed = 2 // 失败 ) // TableName 指定表名 func (Refund) TableName() string { return "refunds" } ``` ### 步骤 2: 添加退款表迁移 在 `internal/database/migrate.go` 的 `Migrate()` 函数中添加: ```go &model.Refund{}, ``` ### 步骤 3: 创建退款 Repository 在 `internal/repository/refund.go` 创建: ```go // internal/repository/refund.go package repository import ( "github.com/example/epay-go/internal/database" "github.com/example/epay-go/internal/model" ) type RefundRepository struct { db *gorm.DB } func NewRefundRepository() *RefundRepository { return &RefundRepository{db: database.Get()} } // Create 创建退款单 func (r *RefundRepository) Create(refund *model.Refund) error { return r.db.Create(refund).Error } // GetByRefundNo 根据退款单号查询 func (r *RefundRepository) GetByRefundNo(refundNo string) (*model.Refund, error) { var refund model.Refund err := r.db.Where("refund_no = ?", refundNo).First(&refund).Error return &refund, err } // GetByTradeNo 根据订单号查询退款记录 func (r *RefundRepository) GetByTradeNo(tradeNo string) ([]*model.Refund, error) { var refunds []*model.Refund err := r.db.Where("trade_no = ?", tradeNo).Find(&refunds).Error return refunds, err } // List 分页查询退款列表 func (r *RefundRepository) List(page, pageSize int, merchantID *int64, status *int8) ([]*model.Refund, int64, error) { var refunds []*model.Refund var total int64 query := r.db.Model(&model.Refund{}) if merchantID != nil { query = query.Where("merchant_id = ?", *merchantID) } if status != nil { query = query.Where("status = ?", *status) } err := query.Count(&total).Error if err != nil { return nil, 0, err } offset := (page - 1) * pageSize err = query.Order("id DESC").Limit(pageSize).Offset(offset).Find(&refunds).Error return refunds, total, err } // Update 更新退款单 func (r *RefundRepository) Update(refund *model.Refund) error { return r.db.Save(refund).Error } ``` ### 步骤 4: 创建退款 Service 在 `internal/service/refund.go` 创建: ```go // internal/service/refund.go package service import ( "errors" "time" "github.com/example/epay-go/internal/model" "github.com/example/epay-go/internal/repository" "github.com/example/epay-go/pkg/utils" "github.com/shopspring/decimal" "gorm.io/gorm" ) type RefundService struct { refundRepo *repository.RefundRepository orderRepo *repository.OrderRepository merchantRepo *repository.MerchantRepository recordRepo *repository.BalanceRecordRepository } func NewRefundService() *RefundService { return &RefundService{ refundRepo: repository.NewRefundRepository(), orderRepo: repository.NewOrderRepository(), merchantRepo: repository.NewMerchantRepository(), recordRepo: repository.NewBalanceRecordRepository(), } } // CreateRefundRequest 创建退款请求 type CreateRefundRequest struct { TradeNo string `json:"trade_no" binding:"required"` Amount string `json:"amount" binding:"required"` Reason string `json:"reason"` NotifyURL string `json:"notify_url"` } // CreateRefund 创建退款单 func (s *RefundService) CreateRefund(merchantID int64, req *CreateRefundRequest) (*model.Refund, error) { // 查询原订单 order, err := s.orderRepo.GetByTradeNo(req.TradeNo) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, errors.New("订单不存在") } return nil, err } // 验证商户权限 if order.MerchantID != merchantID { return nil, errors.New("无权操作此订单") } // 验证订单状态 if order.Status != model.OrderStatusPaid { return nil, errors.New("订单未支付,无法退款") } // 验证退款金额 refundAmount, err := decimal.NewFromString(req.Amount) if err != nil { return nil, errors.New("退款金额格式错误") } orderAmount, _ := decimal.NewFromString(order.Amount) if refundAmount.GreaterThan(orderAmount) { return nil, errors.New("退款金额不能大于订单金额") } // 检查是否已有退款记录 existingRefunds, err := s.refundRepo.GetByTradeNo(req.TradeNo) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return nil, err } // 计算已退款总额 totalRefunded := decimal.Zero for _, r := range existingRefunds { if r.Status == model.RefundStatusSuccess { amt, _ := decimal.NewFromString(r.Amount) totalRefunded = totalRefunded.Add(amt) } } if totalRefunded.Add(refundAmount).GreaterThan(orderAmount) { return nil, errors.New("退款总额超过订单金额") } // 创建退款单 refund := &model.Refund{ RefundNo: utils.GenerateOrderNo("R"), TradeNo: order.TradeNo, MerchantID: order.MerchantID, OrderID: order.ID, ChannelID: order.ChannelID, Amount: req.Amount, RefundFee: "0.00", Reason: req.Reason, Status: model.RefundStatusPending, NotifyURL: req.NotifyURL, } if err := s.refundRepo.Create(refund); err != nil { return nil, err } return refund, nil } // ProcessRefund 处理退款(管理员审核) func (s *RefundService) ProcessRefund(refundNo string, success bool, failReason string) error { refund, err := s.refundRepo.GetByRefundNo(refundNo) if err != nil { return err } if refund.Status != model.RefundStatusPending { return errors.New("退款单已处理") } now := time.Now() if success { refund.Status = model.RefundStatusSuccess refund.ProcessedAt = &now // 退款到商户余额 amount, _ := decimal.NewFromString(refund.Amount) merchant, err := s.merchantRepo.GetByID(refund.MerchantID) if err != nil { return err } beforeBalance, _ := decimal.NewFromString(merchant.Balance) afterBalance := beforeBalance.Add(amount) if err := s.merchantRepo.UpdateBalance(refund.MerchantID, afterBalance.String()); err != nil { return err } // 记录资金变动 record := &model.BalanceRecord{ MerchantID: refund.MerchantID, Action: 1, // 增加 Amount: refund.Amount, BeforeBalance: beforeBalance.String(), AfterBalance: afterBalance.String(), Type: "refund", TradeNo: refund.RefundNo, } s.recordRepo.Create(record) } else { refund.Status = model.RefundStatusFailed refund.FailReason = failReason refund.ProcessedAt = &now } return s.refundRepo.Update(refund) } // GetRefundByNo 根据退款单号查询 func (s *RefundService) GetRefundByNo(refundNo string) (*model.Refund, error) { return s.refundRepo.GetByRefundNo(refundNo) } // List 退款列表 func (s *RefundService) List(page, pageSize int, merchantID *int64, status *int8) ([]*model.Refund, int64, error) { return s.refundRepo.List(page, pageSize, merchantID, status) } ``` ### 步骤 5: 创建管理后台退款 Handler 在 `internal/handler/admin/refund.go` 创建: ```go // internal/handler/admin/refund.go package admin import ( "strconv" "github.com/example/epay-go/internal/service" "github.com/example/epay-go/pkg/response" "github.com/gin-gonic/gin" ) // ListRefunds 退款列表 func ListRefunds(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 } refundService := service.NewRefundService() refunds, total, err := refundService.List(page, pageSize, merchantID, status) if err != nil { response.ServerError(c, "获取退款列表失败") return } response.SuccessPage(c, refunds, total, page, pageSize) } // ProcessRefundRequest 处理退款请求 type ProcessRefundRequest struct { Success bool `json:"success" binding:"required"` FailReason string `json:"fail_reason"` } // ProcessRefund 处理退款 func ProcessRefund(c *gin.Context) { refundNo := c.Param("refund_no") var req ProcessRefundRequest if err := c.ShouldBindJSON(&req); err != nil { response.ParamError(c, "参数错误") return } refundService := service.NewRefundService() if err := refundService.ProcessRefund(refundNo, req.Success, req.FailReason); err != nil { response.Error(c, response.CodeServerError, err.Error()) return } response.Success(c, nil) } ``` ### 步骤 6: 创建商户端退款 Handler 在 `internal/handler/merchant/refund.go` 创建: ```go // internal/handler/merchant/refund.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" ) // CreateRefund 创建退款申请 func CreateRefund(c *gin.Context) { merchantID := middleware.GetUserID(c) var req service.CreateRefundRequest if err := c.ShouldBindJSON(&req); err != nil { response.ParamError(c, "参数错误: "+err.Error()) return } refundService := service.NewRefundService() refund, err := refundService.CreateRefund(merchantID, &req) if err != nil { response.Error(c, response.CodeServerError, err.Error()) return } response.Success(c, refund) } // ListRefunds 退款列表 func ListRefunds(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 } refundService := service.NewRefundService() refunds, total, err := refundService.List(page, pageSize, &merchantID, status) if err != nil { response.ServerError(c, "获取退款列表失败") return } response.SuccessPage(c, refunds, total, page, pageSize) } ``` ### 步骤 7: 注册路由 在 `internal/router/router.go` 添加: ```go // 管理后台 - 退款管理 adminGroup.GET("/refunds", admin.ListRefunds) adminGroup.POST("/refunds/:refund_no/process", admin.ProcessRefund) // 商户中心 - 退款管理 merchantGroup.POST("/refunds", merchant.CreateRefund) merchantGroup.GET("/refunds", merchant.ListRefunds) ``` ### 步骤 8: 添加前端类型定义 在 `web/src/api/types.ts` 添加: ```typescript export interface Refund { id: number refund_no: string trade_no: string merchant_id: number order_id: number channel_id: number amount: string refund_fee: string reason: string status: number // 0待处理 1成功 2失败 api_refund_no: string fail_reason: string notify_url: string notify_status: number notify_count: number next_notify_time: string | null processed_at: string | null created_at: string } ``` ### 步骤 9: 添加管理后台 API 在 `web/src/api/admin.ts` 添加: ```typescript // 退款管理 export const getRefunds = (params: any) => request.get<{ list: Refund[]; total: number }>('/admin/refunds', { params }) export const processRefund = (refundNo: string, data: { success: boolean; fail_reason?: string }) => request.post(`/admin/refunds/${refundNo}/process`, data) ``` ### 步骤 10: 添加商户端 API 在 `web/src/api/merchant.ts` 添加: ```typescript // 退款管理 export const createRefund = (data: { trade_no: string amount: string reason?: string notify_url?: string }) => request.post('/merchant/refunds', data) export const getRefunds = (params: any) => request.get<{ list: Refund[]; total: number }>('/merchant/refunds', { params }) ``` ### 步骤 11: 创建管理后台退款页面 创建 `web/src/views/admin/Refunds.vue` ### 步骤 12: 创建商户端退款页面 创建 `web/src/views/merchant/Refunds.vue` ### 步骤 13: 测试退款功能 ```bash # 重新构建后端 docker-compose build backend docker-compose up -d backend # 重启前端 cd web && npm run dev ``` 测试步骤: 1. 商户端创建退款申请 2. 管理后台审核退款 3. 验证商户余额变动 4. 验证资金记录 ### 步骤 14: 提交代码 ```bash git add . git commit -m "feat: add refund functionality - Add Refund model and database migration - Implement refund repository, service, and handlers - Add admin refund approval workflow - Add merchant refund application - Refund amount validation and balance refund - Add frontend refund management pages Co-Authored-By: Claude Sonnet 4.5 " ``` --- ## Task 2: 管理员管理模块 **文件:** - Modify: `internal/model/admin.go` - Create: `internal/service/admin_manage.go` - Create: `internal/handler/admin/admin_manage.go` - Modify: `internal/router/router.go` - Create: `web/src/views/admin/Admins.vue` ### 步骤 1: 扩展管理员模型 在 `internal/model/admin.go` 添加字段: ```go Email string `gorm:"size:100" json:"email"` Status int8 `gorm:"default:1" json:"status"` // 状态:0禁用 1启用 ``` ### 步骤 2: 创建管理员管理 Service 在 `internal/service/admin_manage.go` 实现管理员的增删改查功能。 ### 步骤 3: 创建管理员管理 Handler ### 步骤 4: 实现前端页面 ### 步骤 5: 测试并提交 --- ## Task 3: 商户余额调整 **文件:** - Create: `internal/handler/admin/balance.go` - Modify: `web/src/views/admin/Merchants.vue` ### 步骤 1: 创建余额调整 Handler 实现后台手动增加/减少商户余额的功能。 ### 步骤 2: 添加资金变动记录 记录所有余额变动操作。 ### 步骤 3: 实现前端界面 在商户管理页面添加余额调整按钮和对话框。 ### 步骤 4: 测试并提交 --- ## Task 4: 系统配置管理 **文件:** - Create: `internal/service/config.go` - Create: `internal/handler/admin/config.go` - Create: `web/src/views/admin/Settings.vue` ### 步骤 1: 实现配置 Service 支持系统参数的读取和更新。 ### 步骤 2: 定义配置项 - 站点名称 - 结算手续费率 - 异步通知重试次数 - 异步通知间隔时间 - 商户注册开关 - 测试模式开关 ### 步骤 3: 实现前端配置页面 ### 步骤 4: 测试并提交 --- ## Task 5: 操作日志记录 **文件:** - Create: `internal/model/log.go` - Create: `internal/repository/log.go` - Create: `internal/service/log.go` - Create: `internal/middleware/logger.go` - Create: `internal/handler/admin/log.go` - Create: `web/src/views/admin/Logs.vue` ### 步骤 1: 创建日志模型 ### 步骤 2: 实现日志中间件 自动记录管理员和商户的重要操作。 ### 步骤 3: 实现日志查询界面 ### 步骤 4: 测试并提交 --- ## Task 6: 统计报表与图表 **文件:** - Create: `internal/service/statistics.go` - Create: `internal/handler/admin/statistics.go` - Modify: `web/src/views/admin/Dashboard.vue` - Modify: `web/src/views/merchant/Dashboard.vue` ### 步骤 1: 实现统计 Service - 按日期统计订单 - 按通道统计订单 - 按商户统计订单 - 收入趋势统计 ### 步骤 2: 安装图表库 ```bash cd web npm install @arco-design/web-vue echarts ``` ### 步骤 3: 实现仪表盘图表 - 订单趋势折线图 - 收入统计柱状图 - 通道占比饼图 - 商户排行榜 ### 步骤 4: 测试并提交 --- ## Task 7: 数据导出功能 **文件:** - Create: `internal/handler/admin/export.go` - Modify: `web/src/views/admin/Orders.vue` ### 步骤 1: 安装 Excel 库 ```bash go get github.com/xuri/excelize/v2 ``` ### 步骤 2: 实现订单导出 支持导出 Excel 格式的订单数据。 ### 步骤 3: 添加导出按钮 ### 步骤 4: 测试并提交 --- ## Task 8: 订单补单功能 **文件:** - Create: `internal/handler/admin/order_repair.go` - Modify: `web/src/views/admin/Orders.vue` ### 步骤 1: 实现补单逻辑 手动标记订单为已支付,并触发异步通知。 ### 步骤 2: 添加权限验证 只有超级管理员可以补单。 ### 步骤 3: 实现前端界面 ### 步骤 4: 测试并提交 --- ## Task 9: 登录日志与安全增强 **文件:** - Create: `internal/model/login_log.go` - Create: `internal/middleware/rate_limit.go` - Create: `internal/middleware/ip_whitelist.go` ### 步骤 1: 记录登录日志 记录登录时间、IP、设备信息、登录结果。 ### 步骤 2: 实现 IP 白名单 ### 步骤 3: 实现频率限制 防止暴力破解。 ### 步骤 4: 测试并提交 --- ## Task 10: 消息通知系统 **文件:** - Create: `internal/service/notification.go` - Create: `pkg/mail/sender.go` - Create: `pkg/sms/sender.go` ### 步骤 1: 实现邮件通知 使用 SMTP 发送邮件。 ### 步骤 2: 实现短信通知 集成短信服务商 API。 ### 步骤 3: 定义通知场景 - 订单支付成功 - 结算审核通过 - 余额不足提醒 ### 步骤 4: 测试并提交 --- ## Task 11: API 文档生成 **文件:** - 安装 Swagger - 添加 API 注释 ### 步骤 1: 安装 Swagger ```bash go install github.com/swaggo/swag/cmd/swag@latest ``` ### 步骤 2: 添加 Swagger 注释 为所有 Handler 添加注释。 ### 步骤 3: 生成文档 ```bash swag init -g cmd/server/main.go ``` ### 步骤 4: 访问文档 http://localhost:8099/swagger/index.html ### 步骤 5: 提交代码 --- ## 总结 本计划涵盖了 11 个核心功能模块,完整参考了 epay 项目的功能设计,将使 epay-go 成为一个功能完善的企业级聚合支付系统。 **实施建议:** 1. 按优先级顺序实施(P0 → P1 → P2) 2. 每个 Task 完成后进行充分测试 3. 保持代码质量和架构一致性 4. 及时更新文档 **预计工作量:** - P0 功能:2-3 周 - P1 功能:2 周 - P2 功能:1-2 周 总计:5-7 周可完成全部功能细化。