- 新增 accounts 表与接口:POST /api/v1/accounts(用只读接口试拉流水校验,失败 ERR_BINANCE)、
GET /api/v1/accounts/{id}、POST .../verify、.../disable、.../enable;Secret 以 AES-256-GCM 加密存储,密钥由 API_AUTH_KEY 派生
- 订单增加 account_id(旧库自动补列,默认 default);唯一金额按账号分配;Matcher 按账号各自轮询、缓存与匹配,
无待核销订单的账号不调币安接口;回填(claim)只在订单所属账号的流水里找
- 收银页按订单所属账号展示 UID / 收款链接 / 二维码;订单与回调体新增 account_id,订单响应新增 receive_link / receive_email
- Go / Python SDK 同步(AccountID、账号方法);协议文档 §2.6;e2e 覆盖多账号隔离、回填、停用与默认账号保护
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
121 lines
2.4 KiB
Go
121 lines
2.4 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const scale = int64(100000000) // 1e8,金额内部一律用 1e-8 定点整数,避免浮点误差
|
|
|
|
func nowMs() int64 { return time.Now().UnixMilli() }
|
|
|
|
// parseAmount 把十进制字符串解析为 1e-8 定点整数。拒绝科学计数法、超 8 位小数、非法字符。
|
|
func parseAmount(s string) (int64, error) {
|
|
s = strings.TrimSpace(s)
|
|
neg := false
|
|
if strings.HasPrefix(s, "-") {
|
|
neg = true
|
|
s = s[1:]
|
|
}
|
|
if s == "" {
|
|
return 0, errors.New("empty amount")
|
|
}
|
|
intPart, frac := s, ""
|
|
if i := strings.IndexByte(s, '.'); i >= 0 {
|
|
intPart, frac = s[:i], s[i+1:]
|
|
}
|
|
if intPart == "" {
|
|
intPart = "0"
|
|
}
|
|
if len(frac) > 8 {
|
|
return 0, errors.New("more than 8 decimals")
|
|
}
|
|
for _, c := range intPart + frac {
|
|
if c < '0' || c > '9' {
|
|
return 0, errors.New("invalid amount")
|
|
}
|
|
}
|
|
var v int64
|
|
for _, c := range intPart {
|
|
v = v*10 + int64(c-'0')
|
|
if v > (int64(1)<<62)/scale {
|
|
return 0, errors.New("amount too large")
|
|
}
|
|
}
|
|
v *= scale
|
|
f := int64(0)
|
|
for _, c := range frac {
|
|
f = f*10 + int64(c-'0')
|
|
}
|
|
for i := len(frac); i < 8; i++ {
|
|
f *= 10
|
|
}
|
|
v += f
|
|
if neg {
|
|
v = -v
|
|
}
|
|
return v, nil
|
|
}
|
|
|
|
// fmtAmount 定点整数转字符串,去掉小数尾零。
|
|
func fmtAmount(v int64) string {
|
|
neg := v < 0
|
|
if neg {
|
|
v = -v
|
|
}
|
|
ip := v / scale
|
|
fp := v % scale
|
|
s := fmt.Sprintf("%d", ip)
|
|
if fp > 0 {
|
|
fs := strings.TrimRight(fmt.Sprintf("%08d", fp), "0")
|
|
s = s + "." + fs
|
|
}
|
|
if neg {
|
|
s = "-" + s
|
|
}
|
|
return s
|
|
}
|
|
|
|
// decimalsOf 返回该定点金额实际用到的小数位数。
|
|
func decimalsOf(v int64) int {
|
|
if v < 0 {
|
|
v = -v
|
|
}
|
|
fp := v % scale
|
|
if fp == 0 {
|
|
return 0
|
|
}
|
|
d := 8
|
|
for fp%10 == 0 {
|
|
fp /= 10
|
|
d--
|
|
}
|
|
return d
|
|
}
|
|
|
|
const idAlphabet = "0123456789abcdefghjkmnpqrstvwxyz"
|
|
|
|
// codeAlphabet 去掉了易混淆的 0O1IL
|
|
const codeAlphabet = "ABCDEFGHJKMNPQRSTUVWXYZ23456789"
|
|
|
|
func randString(alphabet string, n int) string {
|
|
b := make([]byte, n)
|
|
if _, err := rand.Read(b); err != nil {
|
|
panic(err)
|
|
}
|
|
out := make([]byte, n)
|
|
for i, c := range b {
|
|
out[i] = alphabet[int(c)%len(alphabet)]
|
|
}
|
|
return string(out)
|
|
}
|
|
|
|
func newOrderID() string { return "o" + randString(idAlphabet, 25) }
|
|
func newAccountID() string { return "a" + randString(idAlphabet, 20) }
|
|
func newToken() string { return randString(idAlphabet, 26) }
|
|
func newNoteCode() string { return randString(codeAlphabet, 6) }
|
|
func newNonce() string { return randString(idAlphabet, 24) }
|