135 lines
5.8 KiB
Go
Executable File
135 lines
5.8 KiB
Go
Executable File
// Package model 领域模型:订单、状态常量、金额类型
|
|
package model
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// 订单状态(状态机)
|
|
const (
|
|
StatusCreated = "CREATED" // 已创建
|
|
StatusWaitPay = "WAIT_BUYER_PAY" // 等待支付(与易支付协议保持一致)
|
|
StatusConfirming = "CONFIRMING" // 链上确认中(未来 crypto 用)
|
|
StatusSuccess = "TRADE_SUCCESS" // 支付成功(与易支付协议保持一致)
|
|
StatusClosed = "TRADE_CLOSED" // 已关闭/超时(与易支付协议保持一致)
|
|
StatusFailed = "FAILED" // 支付失败(链上重组等)
|
|
)
|
|
|
|
// 订单状态允许的流转(防止状态回跳/非法跳转)
|
|
var allowedTransitions = map[string][]string{
|
|
StatusCreated: {StatusWaitPay, StatusClosed},
|
|
StatusWaitPay: {StatusSuccess, StatusClosed, StatusConfirming},
|
|
StatusConfirming: {StatusSuccess, StatusFailed, StatusClosed},
|
|
StatusSuccess: {}, // 终态,不可再变
|
|
StatusClosed: {}, // 终态
|
|
StatusFailed: {}, // 终态
|
|
}
|
|
|
|
// CanTransition 判断状态流转是否合法
|
|
func CanTransition(from, to string) bool {
|
|
if from == to {
|
|
return true // 幂等
|
|
}
|
|
for _, next := range allowedTransitions[from] {
|
|
if next == to {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Order 订单领域模型
|
|
type Order struct {
|
|
ID int64 `json:"id"`
|
|
OutTradeNo string `json:"out_trade_no"` // 接入方订单号(幂等依据)
|
|
GatewayNo string `json:"gateway_no"` // 网关订单号
|
|
Channel string `json:"channel"` // alipay / wechat / paypal / usdt
|
|
ChannelTxID string `json:"channel_tx_id"` // 通道交易号(回调去重)
|
|
AccountID int64 `json:"account_id"` // 处理这笔订单的账户ID(账户池)
|
|
Pid string `json:"pid"` // 上游易支付商户ID(多商户路由,空=default)
|
|
UpstreamName string `json:"upstream_name,omitempty"` // 上游名称(来源标识)
|
|
Attach string `json:"attach,omitempty"` // 上游请求ID(易支付 attach 字段,原样透传,用于对账)
|
|
AccountName string `json:"account_name,omitempty"` // 账户名称(展示用,查询时附加)
|
|
AmountMinor int64 `json:"amount_minor"` // 金额(最小单位:分)
|
|
Currency string `json:"currency"` // CNY / USD / USDT
|
|
Subject string `json:"subject"` // 商品名称
|
|
Status string `json:"status"` // 状态机
|
|
NotifyURL string `json:"notify_url"` // 回调地址(接入方)
|
|
ReturnURL string `json:"return_url"` // 跳转地址
|
|
QrCode string `json:"qr_code"` // 二维码内容
|
|
PayURL string `json:"pay_url"` // 支付链接/凭据
|
|
Payer string `json:"payer"` // 付款方标识(可选)
|
|
NoteCode string `json:"note_code,omitempty"` // 个人免签:备注码
|
|
QrToken string `json:"qr_token,omitempty"` // 个人免签:套码 token
|
|
DeviceID string `json:"device_id,omitempty"` // 个人免签:承接的监听端设备
|
|
BuyerKey string `json:"buyer_key,omitempty"` // 下单环境唯一标识(IP+UA+设备指纹 hash,买家级串行/防刷单用)
|
|
FPKey string `json:"fp_key,omitempty"` // 浏览器指纹标识(canvas/时区/屏幕 hash,支付页 JS 上报,抗换 IP)
|
|
Notified bool `json:"notified"` // 是否已通知接入方
|
|
CreatedAt time.Time `json:"created_at"`
|
|
SessionExpiresAt time.Time `json:"session_expires_at,omitempty"`
|
|
PaidAt *time.Time `json:"paid_at,omitempty"`
|
|
NotifiedAt *time.Time `json:"notified_at,omitempty"`
|
|
NotifyCount int `json:"notify_count"` // 通知次数(outbox 重试用)
|
|
|
|
// 人工异常处理标记(人工池处理后永久留痕)
|
|
ExceptionID int64 `json:"exception_id"`
|
|
HandledBy string `json:"handled_by"`
|
|
HandledAction string `json:"handled_action"` // manual_settle / manual_close / noop
|
|
HandledAt string `json:"handled_at"`
|
|
HandledNote string `json:"handled_note"`
|
|
}
|
|
|
|
// 兼容易支付协议:金额为字符串"元",需要与最小单位互转
|
|
|
|
// MoneyYuan 返回金额(元,字符串,易支付协议格式)
|
|
func (o *Order) MoneyYuan() string {
|
|
if o.AmountMinor <= 0 {
|
|
return "0.00"
|
|
}
|
|
return fmt.Sprintf("%d.%02d", o.AmountMinor/100, o.AmountMinor%100)
|
|
}
|
|
|
|
// ParseMoneyToMinor 将金额字符串(元)解析为最小单位(分)
|
|
// 支持 "0.01"、"10"、"10.5"、"10.50" 等格式;非法返回 error
|
|
func ParseMoneyToMinor(money string) (int64, error) {
|
|
money = strings.TrimSpace(money)
|
|
if money == "" {
|
|
return 0, fmt.Errorf("金额为空")
|
|
}
|
|
// 拆分整数和小数部分
|
|
parts := strings.Split(money, ".")
|
|
intPart := parts[0]
|
|
fracPart := ""
|
|
if len(parts) > 1 {
|
|
fracPart = parts[1]
|
|
if len(parts) > 2 {
|
|
return 0, fmt.Errorf("金额格式非法: %s", money)
|
|
}
|
|
}
|
|
// 整数部分
|
|
whole, err := strconv.ParseInt(intPart, 10, 64)
|
|
if err != nil || whole < 0 {
|
|
return 0, fmt.Errorf("金额整数部分非法: %s", money)
|
|
}
|
|
// 小数部分:最多 2 位,补零到 2 位
|
|
if len(fracPart) > 2 {
|
|
return 0, fmt.Errorf("金额最多两位小数: %s", money)
|
|
}
|
|
frac := fracPart + strings.Repeat("0", 2-len(fracPart))
|
|
fracInt := int64(0)
|
|
if frac != "" {
|
|
fracInt, err = strconv.ParseInt(frac, 10, 64)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("金额小数部分非法: %s", money)
|
|
}
|
|
}
|
|
minor := whole*100 + fracInt
|
|
if minor <= 0 {
|
|
return 0, fmt.Errorf("金额必须大于 0: %s", money)
|
|
}
|
|
return minor, nil
|
|
}
|