Files
epay-go/docs/plans/2026-02-08-complete-payment-system-features.md
2026-03-28 14:55:39 +08:00

865 lines
21 KiB
Markdown

# 完整支付系统功能细化实现计划
> **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 <noreply@anthropic.com>"
```
---
## 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 周可完成全部功能细化。