Files
hyapi-server/internal/shared/fadada/callback.go
2026-07-21 15:53:29 +08:00

185 lines
5.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package fadada
import (
"encoding/json"
"fmt"
"net/http"
"strings"
)
// 常见回调事件名(以官方实际推送为准,解析时兼容多种字段)
const (
CallbackEventSignTaskSigned = "sign-task-signed"
CallbackEventSignTaskFinished = "sign-task-finished"
CallbackEventSignTaskCanceled = "sign-task-canceled"
CallbackEventCorpAuthorize = "corp-authorize"
CallbackEventCorpAuthSuccess = "corp-authorize-success"
)
// SuccessCallbackBody 回调应答,约定返回成功
var SuccessCallbackBody = []byte(`{"msg":"success"}`)
// VerifyCallback 校验法大大回调签名HMAC-SHA256与出站请求同一套算法
func (c *Client) VerifyCallback(headers map[string]string, bizContent string) error {
if c.config == nil || c.config.AppSecret == "" {
return fmt.Errorf("法大大 app_secret 未配置,无法验签")
}
normalized := normalizeHeaderMap(headers)
sign := firstHeader(normalized, "X-FASC-Sign", "X-Fasc-Sign")
if sign == "" {
return fmt.Errorf("缺少回调签名头 X-FASC-Sign")
}
timestamp := firstHeader(normalized, "X-FASC-Timestamp", "X-Fasc-Timestamp")
if timestamp == "" {
return fmt.Errorf("缺少回调时间戳头 X-FASC-Timestamp")
}
headMap := map[string]string{
"X-FASC-App-Id": firstHeader(normalized, "X-FASC-App-Id", "X-Fasc-App-Id"),
"X-FASC-Sign-Type": firstHeader(normalized, "X-FASC-Sign-Type", "X-Fasc-Sign-Type"),
"X-FASC-Timestamp": timestamp,
"X-FASC-Nonce": firstHeader(normalized, "X-FASC-Nonce", "X-Fasc-Nonce"),
"X-FASC-Api-SubVersion": firstHeader(normalized, "X-FASC-Api-SubVersion", "X-Fasc-Api-SubVersion"),
"bizContent": bizContent,
}
if event := firstHeader(normalized, "X-FASC-Event", "X-Fasc-Event"); event != "" {
headMap["X-FASC-Event"] = event
}
if token := firstHeader(normalized, "X-FASC-AccessToken", "X-Fasc-AccessToken"); token != "" {
headMap["X-FASC-AccessToken"] = token
}
expected := SignByMap(headMap, timestamp, c.config.AppSecret)
if !strings.EqualFold(expected, sign) {
return fmt.Errorf("法大大回调签名校验失败")
}
appID := firstHeader(normalized, "X-FASC-App-Id", "X-Fasc-App-Id")
if appID != "" && c.config.AppID != "" && appID != c.config.AppID {
return fmt.Errorf("法大大回调 AppId 不匹配")
}
return nil
}
// VerifyCallbackFromHTTP 从 http.Header 验签
func (c *Client) VerifyCallbackFromHTTP(h http.Header, bizContent string) error {
headers := make(map[string]string, len(h))
for k, vals := range h {
if len(vals) > 0 {
headers[k] = vals[0]
}
}
return c.VerifyCallback(headers, bizContent)
}
// ParseCallback 解析 bizContent 为统一 CallbackEvent
func (c *Client) ParseCallback(headers map[string]string, bizContent string) (*CallbackEvent, error) {
bizContent = strings.TrimSpace(bizContent)
if bizContent == "" {
return nil, fmt.Errorf("回调 bizContent 不能为空")
}
raw := make(map[string]interface{})
if err := json.Unmarshal([]byte(bizContent), &raw); err != nil {
return nil, fmt.Errorf("解析回调 bizContent 失败: %w", err)
}
normalized := normalizeHeaderMap(headers)
event := firstHeader(normalized, "X-FASC-Event", "X-Fasc-Event")
if event == "" {
event = stringValue(raw, "event", "eventType", "type")
}
ev := &CallbackEvent{
Event: event,
SignTaskID: stringValue(raw, "signTaskId", "sign_task_id"),
SignTaskStatus: stringValue(raw, "signTaskStatus", "sign_task_status"),
ClientCorpID: stringValue(raw, "clientCorpId", "client_corp_id"),
OpenCorpID: stringValue(raw, "openCorpId", "open_corp_id"),
AuthResult: stringValue(raw, "authResult", "auth_result", "result"),
EventTime: stringValue(raw, "eventTime", "event_time", "timestamp"),
Raw: raw,
}
if data, ok := raw["data"].(map[string]interface{}); ok {
if ev.SignTaskID == "" {
ev.SignTaskID = stringValue(data, "signTaskId", "sign_task_id")
}
if ev.SignTaskStatus == "" {
ev.SignTaskStatus = stringValue(data, "signTaskStatus", "sign_task_status")
}
if ev.ClientCorpID == "" {
ev.ClientCorpID = stringValue(data, "clientCorpId", "client_corp_id")
}
if ev.OpenCorpID == "" {
ev.OpenCorpID = stringValue(data, "openCorpId", "open_corp_id")
}
if ev.AuthResult == "" {
ev.AuthResult = stringValue(data, "authResult", "auth_result", "result")
}
}
return ev, nil
}
// IsSignCompletedCallback 是否签署完成类回调
func (e *CallbackEvent) IsSignCompletedCallback() bool {
if e == nil {
return false
}
if IsSignTaskCompletedStatus(e.SignTaskStatus) {
return true
}
switch e.Event {
case CallbackEventSignTaskFinished, CallbackEventSignTaskSigned:
return true
default:
return false
}
}
func normalizeHeaderMap(headers map[string]string) map[string]string {
out := make(map[string]string, len(headers))
for k, v := range headers {
out[http.CanonicalHeaderKey(k)] = v
out[k] = v
}
return out
}
func firstHeader(headers map[string]string, keys ...string) string {
for _, key := range keys {
if v := strings.TrimSpace(headers[key]); v != "" {
return v
}
if v := strings.TrimSpace(headers[http.CanonicalHeaderKey(key)]); v != "" {
return v
}
}
return ""
}
func stringValue(m map[string]interface{}, keys ...string) string {
for _, key := range keys {
if v, ok := m[key]; ok && v != nil {
switch t := v.(type) {
case string:
if strings.TrimSpace(t) != "" {
return strings.TrimSpace(t)
}
case float64:
return fmt.Sprintf("%.0f", t)
case json.Number:
return t.String()
default:
s := strings.TrimSpace(fmt.Sprint(t))
if s != "" && s != "<nil>" {
return s
}
}
}
}
return ""
}