diff --git a/docs/docs.go b/docs/docs.go
index e4ae81d5..66cf12a4 100644
--- a/docs/docs.go
+++ b/docs/docs.go
@@ -1808,6 +1808,10 @@ const docTemplate = `{
"type": "string",
"maxLength": 100
},
+ "public_key": {
+ "type": "string",
+ "maxLength": 100
+ },
"redirect_uri": {
"type": "string",
"maxLength": 100
@@ -1836,6 +1840,10 @@ const docTemplate = `{
"type": "string",
"maxLength": 100
},
+ "public_key": {
+ "type": "string",
+ "maxLength": 100
+ },
"redirect_uri": {
"type": "string",
"maxLength": 100
diff --git a/docs/swagger.json b/docs/swagger.json
index 969d5ada..21d54bfd 100644
--- a/docs/swagger.json
+++ b/docs/swagger.json
@@ -1799,6 +1799,10 @@
"type": "string",
"maxLength": 100
},
+ "public_key": {
+ "type": "string",
+ "maxLength": 100
+ },
"redirect_uri": {
"type": "string",
"maxLength": 100
@@ -1827,6 +1831,10 @@
"type": "string",
"maxLength": 100
},
+ "public_key": {
+ "type": "string",
+ "maxLength": 100
+ },
"redirect_uri": {
"type": "string",
"maxLength": 100
diff --git a/docs/swagger.yaml b/docs/swagger.yaml
index ff85f3f9..e4d898dc 100644
--- a/docs/swagger.yaml
+++ b/docs/swagger.yaml
@@ -13,6 +13,9 @@ definitions:
notify_url:
maxLength: 100
type: string
+ public_key:
+ maxLength: 100
+ type: string
redirect_uri:
maxLength: 100
type: string
@@ -37,6 +40,9 @@ definitions:
notify_url:
maxLength: 100
type: string
+ public_key:
+ maxLength: 100
+ type: string
redirect_uri:
maxLength: 100
type: string
diff --git a/frontend/components/common/docs/api.tsx b/frontend/components/common/docs/api.tsx
index ac6fa9d6..b30f45da 100644
--- a/frontend/components/common/docs/api.tsx
+++ b/frontend/components/common/docs/api.tsx
@@ -19,14 +19,113 @@ export const DOCS_LAST_UPDATED = "2026-01-07"
export const apiSections: PolicySection[] = [
{
value: "official-service",
- title: "1. 官方服务接口",
+ title: "1. 官方 LDC 接口",
content: (
-
-
官方服务接口暂未上线,敬请期待
+
+
官方原生接口,使用 Ed25519 签名算法,安全性更高
+
+
+
1.1 概览
+
+ - 协议:官方 LDC 支付协议
+ - 服务类型:支持
type=ldcpay
+ - 网关基址:
https://credit.linux.do/epay
+ - 签名方式:Ed25519 非对称加密
+
+
+
1.2 对接流程
+
+ - 控制台创建应用,配置
client_id 并在应用设置中上传商户 Ed25519 公钥
+ - 根据“签名算法”及商户私钥生成
sign
+ - 调用
/pay/submit 发起积分流转请求
+ - 认证完成后,通过异步回调或轮询接口同步状态
+
+
+
1.3 鉴权与签名
+
1.3.1 签名算法
+
+
+ - 取除
sign 以外的所有非空请求参数
+ - 将参数按参数名 ASCII 码从到大排序(字典序)
+ - 使用
k1=v1&k2=v2... 格式拼接成字符串
+ - 将 应用密钥 (Client Secret) 直接追加到字符串末尾
+ - 使用商户私钥对最终字符串进行 Ed25519 签名
+ - 将签名结果转换成 Base64 编码作为
sign 参数
+
+
+
+
1.4 积分流转服务
+
+ - 方法:POST
/pay/submit.php
+ - 编码:
application/json 或 application/x-www-form-urlencoded
+
+
+
+
+
+ 参数
+ 必填
+ 说明
+
+
+
+
+ client_id
+ 是
+ 应用客户端 ID
+
+
+ type
+ 是
+ 固定 ldcpay
+
+
+ out_trade_no
+ 是
+ 业务单号
+
+
+ money
+ 是
+ 积分数量,必须保留两位小数(比如,10.00)
+
+
+ order_name
+ 是
+ 商品名称
+
+
+ sign
+ 是
+ 按“签名算法”生成的 Base64 签名串
+
+
+
+
+
1.5 其他接口
+
其他接口定义请参考 3. 其他接口。
+
),
+ children: [
+ { value: "1-1-overview", title: "1.1 概览" },
+ { value: "1-2-flow", title: "1.2 对接流程" },
+ { value: "1-3-auth-sign", title: "1.3 鉴权与签名" },
+ { value: "1-4-submit", title: "1.4 积分流转服务" },
+ { value: "1-5-others", title: "1.5 其他接口" },
+ ]
},
{
value: "epay-compatibility",
@@ -90,7 +189,7 @@ sign=$(echo -n "\${payload}\${SECRET}" | md5) # 输出小写`}
2.5 积分流转服务
- 方法:POST
/pay/submit.php
- - 编码:
application/x-www-form-urlencoded
+ - 编码:
application/json 或 application/x-www-form-urlencoded
- 成功:验签通过后,平台自动创建积分流转服务,并跳转到认证界面(Location=
https://credit.linux.do/paying?order_no=...)
- 失败:返回 JSON
{`{"error_msg":"...", "data":null}`}
@@ -171,7 +270,34 @@ sign=$(echo -n "\${payload}\${SECRET}" | md5) # 输出小写`}
language="bash"
/>
-
2.6 订单查询
+
2.6 其他接口
+
其他接口定义请参考 3. 其他接口。
+
+
+ ),
+ children: [
+ { value: "2-1-overview", title: "2.1 概览" },
+ { value: "2-2-common-errors", title: "2.2 常见错误" },
+ { value: "2-3-flow", title: "2.3 对接流程" },
+ { value: "2-4-auth-sign", title: "2.4 鉴权与签名" },
+ { value: "2-5-submit", title: "2.5 积分流转服务" },
+ { value: "2-6-others", title: "2.6 其他接口" },
+ ]
+ },
+ {
+ value: "common-services",
+ title: "3. 其他接口",
+ content: (
+
+
+
+
3.1 订单查询
- 方法:GET
/api.php
- 认证:
pid + key
@@ -229,7 +355,7 @@ sign=$(echo -n "\${payload}\${SECRET}" | md5) # 输出小写`}
/>
补充:status 1=成功,0=失败/处理中;不存在会返回 HTTP 404 且 {`{"code":-1,"msg":"服务不存在或已完成"}`}。
- 2.7 订单退款
+ 3.2 订单退款
应用需返回 HTTP 200 且响应体为 success(大小写不敏感),否则视为失败并继续重试。
+
+ 3.4 商户分发接口
+
+ - 方法:POST
/pay/distribute
+ - 编码:
application/json
+ - 认证:Basic Auth (使用
client_id:client_secret 进行 Base64 编码)
+
+
+
+
+
+ 参数
+ 必填
+ 说明
+
+
+
+
+ user_id
+ 是
+ 收款人用户 ID (数字)
+
+
+ username
+ 是
+ 收款人用户名 (用于二次校验)
+
+
+ amount
+ 是
+ 分发积分数量,最多 2 位小数
+
+
+ out_trade_no
+ 否
+ 商户自定义单号
+
+
+ remark
+ 否
+ 分发备注
+
+
+
+ 成功响应:{`{"code":1, "data":{"trade_no":"...", "out_trade_no":"..."}}`}
),
children: [
- { value: "2-1-overview", title: "2.1 概览" },
- { value: "2-2-common-errors", title: "2.2 常见错误" },
- { value: "2-3-flow", title: "2.3 对接流程" },
- { value: "2-4-auth-sign", title: "2.4 鉴权与签名" },
- { value: "2-5-submit", title: "2.5 积分流转服务" },
- { value: "2-6-order", title: "2.6 订单查询" },
- { value: "2-7-refund", title: "2.7 订单退款" },
- { value: "2-8-notify", title: "2.8 异步通知" },
+ { value: "3-1-order", title: "3.1 订单查询" },
+ { value: "3-2-refund", title: "3.2 订单退款" },
+ { value: "3-3-notify", title: "3.3 异步通知" },
+ { value: "3-4-distribute", title: "3.4 商户分发接口" },
]
},
]
diff --git a/frontend/components/common/merchant/merchant-dialog.tsx b/frontend/components/common/merchant/merchant-dialog.tsx
index 1e67410d..9d124ade 100644
--- a/frontend/components/common/merchant/merchant-dialog.tsx
+++ b/frontend/components/common/merchant/merchant-dialog.tsx
@@ -53,6 +53,7 @@ export function MerchantDialog({
app_description: '',
redirect_uri: '',
notify_url: '',
+ public_key: '',
test_mode: false,
})
@@ -71,6 +72,7 @@ export function MerchantDialog({
app_description: apiKey.app_description,
redirect_uri: apiKey.redirect_uri,
notify_url: apiKey.notify_url,
+ public_key: apiKey.public_key,
test_mode: apiKey.test_mode,
}
}
@@ -177,8 +179,8 @@ export function MerchantDialog({
setOpen(false)
} catch (error) {
- const errorMessage = (error as Error).message || `无法${ mode === 'create' ? '创建' : '更新' }应用`
- toast.error(`${ mode === 'create' ? '创建' : '更新' }失败`, {
+ const errorMessage = (error as Error).message || `无法${mode === 'create' ? '创建' : '更新'}应用`
+ toast.error(`${mode === 'create' ? '创建' : '更新'}失败`, {
description: errorMessage
})
throw error
@@ -329,6 +331,18 @@ export function MerchantDialog({
disabled={processing}
/>
+
+
+
+ setFormData({ ...formData, public_key: e.target.value })}
+ disabled={processing}
+ />
+
diff --git a/frontend/lib/services/merchant/types.ts b/frontend/lib/services/merchant/types.ts
index 322c37a4..cf395d2e 100644
--- a/frontend/lib/services/merchant/types.ts
+++ b/frontend/lib/services/merchant/types.ts
@@ -20,6 +20,8 @@ export interface MerchantAPIKey {
redirect_uri?: string;
/** 通知 URL */
notify_url: string;
+ /** 公钥 (Base64) */
+ public_key?: string;
/** 测试模式 */
test_mode: boolean;
/** 创建时间 */
@@ -44,6 +46,8 @@ export interface CreateAPIKeyRequest {
redirect_uri?: string;
/** 通知 URL(最大100字符,必须是有效的 URL) */
notify_url: string;
+ /** 公钥 (Base64 编码,32字节,可选) */
+ public_key?: string;
/** 测试模式(可选,默认为 false) */
test_mode?: boolean;
}
@@ -62,6 +66,8 @@ export interface UpdateAPIKeyRequest {
redirect_uri?: string;
/** 通知 URL(最大100字符,必须是有效的 URL,可选) */
notify_url?: string;
+ /** 公钥 (Base64 编码,32字节,可选) */
+ public_key?: string;
/** 测试模式(可选) */
test_mode?: boolean;
}
diff --git a/internal/apps/merchant/api_key/routers.go b/internal/apps/merchant/api_key/routers.go
index 314e0909..dc6cc64a 100644
--- a/internal/apps/merchant/api_key/routers.go
+++ b/internal/apps/merchant/api_key/routers.go
@@ -22,6 +22,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/linux-do/credit/internal/apps/merchant"
"github.com/linux-do/credit/internal/apps/oauth"
+ "github.com/linux-do/credit/internal/apps/payment"
"github.com/linux-do/credit/internal/db"
"github.com/linux-do/credit/internal/model"
"github.com/linux-do/credit/internal/util"
@@ -33,6 +34,7 @@ type CreateAPIKeyRequest struct {
AppDescription string `json:"app_description" binding:"max=100"`
RedirectURI string `json:"redirect_uri" binding:"omitempty,max=100,url"`
NotifyURL string `json:"notify_url" binding:"required,max=100,url"`
+ PublicKey string `json:"public_key" binding:"omitempty,max=100"`
TestMode bool `json:"test_mode"`
}
@@ -42,6 +44,7 @@ type UpdateAPIKeyRequest struct {
AppDescription string `json:"app_description" binding:"omitempty,max=100"`
RedirectURI string `json:"redirect_uri" binding:"omitempty,max=100,url"`
NotifyURL string `json:"notify_url" binding:"omitempty,max=100,url"`
+ PublicKey string `json:"public_key" binding:"omitempty,max=100"`
TestMode bool `json:"test_mode"`
}
@@ -78,6 +81,19 @@ func CreateAPIKey(c *gin.Context) {
TestMode: req.TestMode,
}
+ if len(req.PublicKey) > 0 {
+ publicKeyBytes, err := util.Base64Decode(req.PublicKey)
+ if err != nil {
+ c.JSON(http.StatusBadRequest, util.Err(payment.InvalidPublicKeyFormat))
+ return
+ }
+ if len(publicKeyBytes) != 32 {
+ c.JSON(http.StatusBadRequest, util.Err(payment.InvalidPublicKeyLength))
+ return
+ }
+ apiKey.PublicKey = publicKeyBytes
+ }
+
if err := db.DB(c.Request.Context()).Create(&apiKey).Error; err != nil {
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
return
@@ -143,6 +159,19 @@ func UpdateAPIKey(c *gin.Context) {
"test_mode": req.TestMode,
}
+ if len(req.PublicKey) > 0 {
+ publicKeyBytes, err := util.Base64Decode(req.PublicKey)
+ if err != nil {
+ c.JSON(http.StatusBadRequest, util.Err(payment.InvalidPublicKeyFormat))
+ return
+ }
+ if len(publicKeyBytes) != 32 {
+ c.JSON(http.StatusBadRequest, util.Err(payment.InvalidPublicKeyLength))
+ return
+ }
+ updates["public_key"] = publicKeyBytes
+ }
+
if err := db.DB(c.Request.Context()).
Model(&apiKey).
Updates(updates).Error; err != nil {
diff --git a/internal/apps/payment/errs.go b/internal/apps/payment/errs.go
index d55e0859..f082b238 100644
--- a/internal/apps/payment/errs.go
+++ b/internal/apps/payment/errs.go
@@ -17,12 +17,14 @@ limitations under the License.
package payment
const (
- OrderNotFound = "订单不存在或已完成"
- OrderStatusInvalid = "订单状态不允许支付"
- OrderExpired = "订单已过期"
- MerchantInfoNotFound = "商户信息不存在"
- RecipientNotFound = "收款人不存在"
- OrderNoFormatError = "订单号格式错误"
- CannotTransferToSelf = "不能转账给自己"
- PayConfigNotFound = "支付配置不存在"
+ OrderNotFound = "订单不存在或已完成"
+ OrderStatusInvalid = "订单状态不允许支付"
+ OrderExpired = "订单已过期"
+ MerchantInfoNotFound = "商户信息不存在"
+ RecipientNotFound = "收款人不存在"
+ OrderNoFormatError = "订单号格式错误"
+ CannotTransferToSelf = "不能转账给自己"
+ PayConfigNotFound = "支付配置不存在"
+ InvalidPublicKeyFormat = "公钥格式错误"
+ InvalidPublicKeyLength = "公钥长度必须为32字节"
)
diff --git a/internal/apps/payment/middlewares.go b/internal/apps/payment/middlewares.go
index ece7a35b..8dbfa227 100644
--- a/internal/apps/payment/middlewares.go
+++ b/internal/apps/payment/middlewares.go
@@ -52,13 +52,23 @@ type EPayRequest struct {
SignType string `form:"sign_type"`
}
-// ToCreateOrderRequest 转换为通用创建订单请求
-func (r *EPayRequest) ToCreateOrderRequest() *CreateOrderRequest {
+// LDCPayRequest LDC支付请求
+type LDCPayRequest struct {
+ ClientID string `form:"client_id" binding:"required"`
+ OrderName string `form:"order_name" binding:"required,max=64"`
+ MerchantOrderNo *string `form:"out_trade_no" binding:"required,min=1,max=64"`
+ Amount decimal.Decimal `form:"money" binding:"required"`
+ PayType string `form:"type" binding:"required"`
+ Sign string `form:"sign" binding:"required"`
+}
+
+// NewCreateOrderRequest 从支付请求创建通用订单请求
+func NewCreateOrderRequest(orderName string, merchantOrderNo *string, amount decimal.Decimal, payType string) *CreateOrderRequest {
return &CreateOrderRequest{
- OrderName: r.OrderName,
- MerchantOrderNo: r.MerchantOrderNo,
- Amount: r.Amount,
- PaymentType: r.PayType,
+ OrderName: orderName,
+ MerchantOrderNo: merchantOrderNo,
+ Amount: amount,
+ PaymentType: payType,
}
}
@@ -116,20 +126,28 @@ func RequireSignatureAuth() gin.HandlerFunc {
PayType := c.Request.FormValue("type")
var apiKey model.MerchantAPIKey
+ var createOrderReq *CreateOrderRequest
+ var err error
switch PayType {
+ case common.PayTypeLDCPay:
+ createOrderReq, err = VerifySignatureEd25519(c, &apiKey)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusUnauthorized, util.Err(err.Error()))
+ return
+ }
case common.PayTypeEPay:
- if createOrderReq, err := VerifySignature(c, &apiKey); err != nil {
+ createOrderReq, err = VerifySignatureMD5(c, &apiKey)
+ if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, util.Err(err.Error()))
return
- } else {
- util.SetToContext(c, CreateOrderRequestKey, createOrderReq)
}
default:
c.AbortWithStatusJSON(http.StatusBadRequest, util.Err("不支持的请求类型"))
return
}
+ util.SetToContext(c, CreateOrderRequestKey, createOrderReq)
util.SetToContext(c, APIKeyObjKey, &apiKey)
c.Next()
diff --git a/internal/apps/payment/tasks.go b/internal/apps/payment/tasks.go
index 5cbf5a0b..0a643e2b 100644
--- a/internal/apps/payment/tasks.go
+++ b/internal/apps/payment/tasks.go
@@ -74,10 +74,9 @@ func HandleMerchantPaymentNotify(ctx context.Context, t *asynq.Task) error {
"name": order.OrderName,
"money": order.Amount.Truncate(2).StringFixed(2),
"trade_status": "TRADE_SUCCESS",
- "sign_type": "MD5",
}
- callbackParams["sign"] = GenerateSignature(callbackParams, apiKey.ClientSecret)
+ callbackParams["sign"] = GenerateSignature(callbackParams, apiKey.ClientSecret, true)
if err := sendCallbackRequest(ctx, apiKey.NotifyURL, callbackParams); err != nil {
retried, _ := asynq.GetRetryCount(ctx)
diff --git a/internal/apps/payment/utils.go b/internal/apps/payment/utils.go
index 65aadaa5..1aab9d90 100644
--- a/internal/apps/payment/utils.go
+++ b/internal/apps/payment/utils.go
@@ -150,8 +150,8 @@ func ParseOrderNo(c *gin.Context, orderNo string) (*OrderContext, error) {
return ctx, nil
}
-// GenerateSignature 生成MD5签名
-func GenerateSignature(params map[string]string, secret string) string {
+// GenerateSignature 生成签名
+func GenerateSignature(params map[string]string, secret string, isMD5 bool) string {
// 按key排序
keys := make([]string, 0, len(params))
for k := range params {
@@ -180,13 +180,16 @@ func GenerateSignature(params map[string]string, secret string) string {
}
builder.WriteString(secret)
- // MD5加密
- hash := md5.Sum([]byte(builder.String()))
- return fmt.Sprintf("%x", hash)
+ if isMD5 {
+ // MD5加密
+ hash := md5.Sum([]byte(builder.String()))
+ return fmt.Sprintf("%x", hash)
+ }
+ return builder.String()
}
-// VerifySignature 验证MD5签名
-func VerifySignature(c *gin.Context, apiKey *model.MerchantAPIKey) (*CreateOrderRequest, error) {
+// VerifySignatureMD5 验证MD5签名
+func VerifySignatureMD5(c *gin.Context, apiKey *model.MerchantAPIKey) (*CreateOrderRequest, error) {
var req EPayRequest
if err := c.ShouldBind(&req); err != nil {
return nil, err
@@ -213,10 +216,10 @@ func VerifySignature(c *gin.Context, apiKey *model.MerchantAPIKey) (*CreateOrder
}
params["money"] = req.Amount.Truncate(2).StringFixed(2)
- expectedSignFixed := GenerateSignature(params, apiKey.ClientSecret)
+ expectedSignFixed := GenerateSignature(params, apiKey.ClientSecret, true)
params["money"] = req.Amount.Truncate(2).String()
- expectedSignTrimmed := GenerateSignature(params, apiKey.ClientSecret)
+ expectedSignTrimmed := GenerateSignature(params, apiKey.ClientSecret, true)
matchFixed := subtle.ConstantTimeCompare([]byte(strings.ToLower(expectedSignFixed)), []byte(strings.ToLower(req.Sign))) == 1
matchTrimmed := subtle.ConstantTimeCompare([]byte(strings.ToLower(expectedSignTrimmed)), []byte(strings.ToLower(req.Sign))) == 1
@@ -225,5 +228,49 @@ func VerifySignature(c *gin.Context, apiKey *model.MerchantAPIKey) (*CreateOrder
return nil, errors.New("签名验证失败")
}
- return req.ToCreateOrderRequest(), nil
+ return NewCreateOrderRequest(req.OrderName, req.MerchantOrderNo, req.Amount, req.PayType), nil
+}
+
+// VerifySignatureEd25519 验证 Ed25519 签名
+func VerifySignatureEd25519(c *gin.Context, apiKey *model.MerchantAPIKey) (*CreateOrderRequest, error) {
+ var req LDCPayRequest
+ if err := c.ShouldBind(&req); err != nil {
+ return nil, err
+ }
+
+ // 验证金额
+ if err := util.ValidateAmount(req.Amount); err != nil {
+ return nil, err
+ }
+
+ if err := apiKey.GetByClientID(db.DB(c.Request.Context()), req.ClientID); err != nil {
+ return nil, err
+ }
+
+ if len(apiKey.PublicKey) == 0 {
+ return nil, errors.New("商户未配置公钥")
+ }
+
+ signatureBytes, err := util.Base64Decode(req.Sign)
+ if err != nil {
+ return nil, errors.New("签名格式错误")
+ }
+
+ // 构建签名参数
+ params := map[string]string{
+ "client_id": req.ClientID,
+ "type": req.PayType,
+ "out_trade_no": util.DerefString(req.MerchantOrderNo),
+ "order_name": req.OrderName,
+ "money": req.Amount.Truncate(2).StringFixed(2),
+ }
+
+ signatureParam := GenerateSignature(params, apiKey.ClientSecret, false)
+ validTrimmed := util.Ed25519Verify(apiKey.PublicKey, []byte(signatureParam), signatureBytes)
+
+ if !validTrimmed {
+ return nil, errors.New("签名验证失败")
+ }
+
+ return NewCreateOrderRequest(req.OrderName, req.MerchantOrderNo, req.Amount, req.PayType), nil
}
diff --git a/internal/common/constants.go b/internal/common/constants.go
index 1456d6b9..118f7c5d 100644
--- a/internal/common/constants.go
+++ b/internal/common/constants.go
@@ -17,8 +17,8 @@ limitations under the License.
package common
const (
- // PayTypeLDPay Linux Do Credit 支付类型标识
- PayTypeLDPay = "ldpay"
+ // PayTypeLDCPay Linux Do Credit 支付类型标识
+ PayTypeLDCPay = "ldcpay"
// PayTypeEPay Epay 支付类型
PayTypeEPay = "epay"
)
diff --git a/internal/model/merchant_api_keys.go b/internal/model/merchant_api_keys.go
index dd5aaff3..f54406af 100644
--- a/internal/model/merchant_api_keys.go
+++ b/internal/model/merchant_api_keys.go
@@ -33,6 +33,7 @@ type MerchantAPIKey struct {
AppDescription string `json:"app_description" gorm:"size:100"`
RedirectURI string `json:"redirect_uri" gorm:"size:100"`
NotifyURL string `json:"notify_url" gorm:"size:100;not null"`
+ PublicKey []byte `json:"public_key" gorm:"type:bytea"`
TestMode bool `json:"test_mode" gorm:"default:false"`
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;index:idx_merchant_api_keys_user_created,priority:2"`
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
diff --git a/internal/model/orders.go b/internal/model/orders.go
index bda870d0..cf36d9ad 100644
--- a/internal/model/orders.go
+++ b/internal/model/orders.go
@@ -67,13 +67,13 @@ type Order struct {
PayerUsername string `json:"payer_username" gorm:"-:migration;->"`
PayeeUsername string `json:"payee_username" gorm:"-:migration;->"`
Amount decimal.Decimal `json:"amount" gorm:"type:numeric(20,2);not null;index"`
- Status OrderStatus `json:"status" gorm:"type:varchar(20);not null;index:idx_orders_payee_status_type_created,priority:2;index:idx_orders_payer_status_type_created,priority:2;index:idx_orders_client_status_created,priority:2;index:idx_orders_payer_status_type_trade,priority:2;index:idx_orders_payment_link_status,priority:2"`
+ Status OrderStatus `json:"status" gorm:"type:varchar(20);not null;index:idx_orders_payee_status_type_created,priority:2;index:idx_orders_payer_status_type_created,priority:2;index:idx_orders_client_status_created,priority:2;index:idx_orders_payer_status_type_trade,priority:2;index:idx_orders_payment_link_status,priority:2;index:idx_orders_status_expires,priority:1"`
Type OrderType `json:"type" gorm:"type:varchar(20);not null;index:idx_orders_payee_status_type_created,priority:3;index:idx_orders_payer_status_type_created,priority:3;index:idx_orders_payer_status_type_trade,priority:3"`
Remark string `json:"remark" gorm:"size:255"`
PaymentType string `json:"payment_type" gorm:"size:20"`
PaymentLinkID *uint64 `json:"payment_link_id,string" gorm:"index:idx_orders_payment_link_status,priority:1"`
TradeTime time.Time `json:"trade_time" gorm:"index:idx_orders_payer_status_type_trade,priority:4"`
- ExpiresAt time.Time `json:"expires_at" gorm:"not null"`
+ ExpiresAt time.Time `json:"expires_at" gorm:"not null;index:idx_orders_status_expires,priority:2"`
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;index:idx_orders_payee_status_type_created,priority:4;index:idx_orders_payer_status_type_created,priority:4;index:idx_orders_client_status_created,priority:3"`
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime;index"`
}
diff --git a/internal/util/crypto.go b/internal/util/crypto.go
index 822dca4c..5f055e84 100644
--- a/internal/util/crypto.go
+++ b/internal/util/crypto.go
@@ -19,6 +19,7 @@ package util
import (
"crypto/aes"
"crypto/cipher"
+ "crypto/ed25519"
"crypto/rand"
"encoding/base64"
"encoding/hex"
@@ -80,7 +81,7 @@ func encryptBytes(signKey string, plaintext []byte) (string, error) {
ciphertext := gcm.Seal(nonce, nonce, plaintext, nil)
// 返回 base64 编码的密文
- return base64.StdEncoding.EncodeToString(ciphertext), nil
+ return Base64Encode(ciphertext), nil
}
// decryptBytes 解密函数,处理字节数据
@@ -95,7 +96,7 @@ func decryptBytes(signKey string, ciphertext string) ([]byte, error) {
}
// 解码 base64 密文
- data, err := base64.StdEncoding.DecodeString(ciphertext)
+ data, err := Base64Decode(ciphertext)
if err != nil {
return nil, fmt.Errorf("failed to decode ciphertext: %w", err)
}
@@ -128,3 +129,30 @@ func decryptBytes(signKey string, ciphertext string) ([]byte, error) {
return plaintext, nil
}
+
+// Base64Encode Base64编码
+func Base64Encode(data []byte) string {
+ return base64.StdEncoding.EncodeToString(data)
+}
+
+// Base64Decode Base64解码
+func Base64Decode(encoded string) ([]byte, error) {
+ return base64.StdEncoding.DecodeString(encoded)
+}
+
+// Ed25519Verify 验证 Ed25519 签名
+// publicKey: 32 字节的公钥(已解码的二进制格式)
+// message: 待验证的原始消息
+// signature: 64 字节的签名(已解码的二进制格式)
+// return: 签名是否有效
+func Ed25519Verify(publicKey, message, signature []byte) bool {
+ if len(publicKey) != ed25519.PublicKeySize {
+ return false
+ }
+
+ if len(signature) != ed25519.SignatureSize {
+ return false
+ }
+
+ return ed25519.Verify(publicKey, message, signature)
+}