初始提交:岩美 pay 收款服务(支付宝当面付 + 多商户多渠道架构)

- Go/Gin/GORM + 纯 Go SQLite(无 cgo)
- Channel 多渠道接口:支付宝当面付(precreate)/电脑网站支付(page.pay) 已实现,微信占位
- 多商户 merchants 表,回调验签+金额核对+幂等+查单兜底
- 收款页/结果页/二维码端点;docs/ 设计文档与部署 Runbook
- 密钥走环境变量/Bitwarden,不入库

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-25 07:21:48 +08:00
commit 6da6964451
29 changed files with 2205 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
package util
import (
"fmt"
"strings"
"time"
"github.com/google/uuid"
)
// NewOutTradeNo 生成全局唯一的商户订单号:<code>-<时间>-<随机>,控制在 64 字符内。
// 例:yanmei-20260624153012-a1b2c3d4
func NewOutTradeNo(merchantCode string) string {
code := merchantCode
if len(code) > 16 {
code = code[:16]
}
ts := time.Now().Format("20060102150405")
suffix := strings.ReplaceAll(uuid.NewString(), "-", "")[:8]
return fmt.Sprintf("%s-%s-%s", code, ts, suffix)
}
+31
View File
@@ -0,0 +1,31 @@
package util
import (
"fmt"
"math"
"strconv"
"strings"
)
// AmountToCents 把 "0.01" / "12.30" 元金额转为分(int64),便于精确比较。
func AmountToCents(s string) (int64, error) {
s = strings.TrimSpace(s)
if s == "" {
return 0, fmt.Errorf("空金额")
}
f, err := strconv.ParseFloat(s, 64)
if err != nil {
return 0, fmt.Errorf("金额格式错误 %q: %w", s, err)
}
if f < 0 {
return 0, fmt.Errorf("金额不能为负: %q", s)
}
return int64(math.Round(f * 100)), nil
}
// AmountEqual 判断两个元金额字符串是否等值(按分比较,避免浮点/格式差异)。
func AmountEqual(a, b string) bool {
ca, err1 := AmountToCents(a)
cb, err2 := AmountToCents(b)
return err1 == nil && err2 == nil && ca == cb
}
+17
View File
@@ -0,0 +1,17 @@
package util
import (
"net/http"
"github.com/gin-gonic/gin"
)
// RespondError 结构化错误:{"code":..,"message":..}
func RespondError(c *gin.Context, status int, code, msg string) {
c.JSON(status, gin.H{"code": code, "message": msg})
}
// RespondSuccess 成功:{"data":..}
func RespondSuccess(c *gin.Context, data interface{}) {
c.JSON(http.StatusOK, gin.H{"data": data})
}