package payment import ( "backend/internal/dto" "crypto/aes" "crypto/cipher" "encoding/base64" "encoding/json" "errors" "sort" "strings" "time" ) // 微信支付平台证书自动获取(APIv3 GET /v3/certificates)。 // 平台证书用于回调/应答验签,属于公开证书,但仍落在受保护目录。 // Logan.Li // wechatCertResponse /v3/certificates 响应结构(仅取所需字段) // Logan.Li type wechatCertResponse struct { Data []struct { SerialNo string `json:"serial_no"` EffectiveTime string `json:"effective_time"` ExpireTime string `json:"expire_time"` EncryptCertificate struct { Algorithm string `json:"algorithm"` Ciphertext string `json:"ciphertext"` AssociatedData string `json:"associated_data"` Nonce string `json:"nonce"` } `json:"encrypt_certificate"` } `json:"data"` } // FetchWechatPlatformCerts 拉取并保存平台证书,返回证书清单 // Logan.Li func (s *ConfigService) FetchWechatPlatformCerts() ([]dto.WechatPlatformCertItem, string, error) { row, err := s.loadRow() if err != nil { return nil, "", err } var missing []string if strings.TrimSpace(row.WechatMchID) == "" { missing = append(missing, "商户号 mchid") } if strings.TrimSpace(row.WechatSerialNo) == "" { missing = append(missing, "证书序列号 serial_no") } if strings.TrimSpace(row.WechatKeyPath) == "" { missing = append(missing, "apiclient_key.pem") } if strings.TrimSpace(row.WechatAPIv3KeyEnc) == "" { missing = append(missing, "APIv3 密钥") } if len(missing) > 0 { return nil, "未配置:" + strings.Join(missing, "、") + "(跳过自动获取)", nil } keyBytes, err := ReadSecretFile(row.WechatKeyPath) if err != nil { return nil, "", errors.New("商户 API 私钥不可用:" + err.Error()) } priv, err := ParseRSAPrivateKey(keyBytes) if err != nil { return nil, "", errors.New("商户 API 私钥解析失败:" + err.Error()) } apiV3Key, err := DecryptSecret(row.WechatAPIv3KeyEnc) if err != nil { return nil, "", err } status, body, _, err := wechatAPIGet("/v3/certificates", row.WechatMchID, row.WechatSerialNo, priv) if err != nil { return nil, "", errors.New("微信支付接口不可达:" + err.Error()) } if status != 200 { var eb struct { Code string `json:"code"` Message string `json:"message"` } _ = json.Unmarshal(body, &eb) return nil, "获取平台证书失败:HTTP " + itoa(status) + " " + firstNonEmptyStr(eb.Code, "") + " " + truncateText(eb.Message, 120), nil } var resp wechatCertResponse if err := json.Unmarshal(body, &resp); err != nil { return nil, "平台证书响应解析失败", nil } if len(resp.Data) == 0 { return nil, "商户平台未返回任何平台证书", nil } items := make([]dto.WechatPlatformCertItem, 0, len(resp.Data)) type candidate struct { item dto.WechatPlatformCertItem pem []byte exp string } cands := make([]candidate, 0, len(resp.Data)) for _, d := range resp.Data { pemBytes, derr := DecryptWechatResource(apiV3Key, d.EncryptCertificate.AssociatedData, d.EncryptCertificate.Nonce, d.EncryptCertificate.Ciphertext) if derr != nil { items = append(items, dto.WechatPlatformCertItem{SerialNo: d.SerialNo, Subject: "解密失败:" + derr.Error()}) continue } item := dto.WechatPlatformCertItem{ SerialNo: d.SerialNo, NotBefore: formatWechatTime(d.EffectiveTime), NotAfter: formatWechatTime(d.ExpireTime), } if cert, perr := ParseCertificatePEM(pemBytes); perr == nil { item.Subject = cert.Subject.CommonName } cands = append(cands, candidate{item: item, pem: pemBytes, exp: d.ExpireTime}) items = append(items, item) } if len(cands) == 0 { return items, "平台证书全部解密失败,请核对 APIv3 密钥是否正确", nil } // 取有效期最晚的一张落地(微信通常只返回一张,未来可能轮换为多张) // Logan.Li sort.Slice(cands, func(i, j int) bool { return cands[i].exp > cands[j].exp }) saved, err := WriteSecretFile(UploadKindWechatPlatformCert, cands[0].pem) if err != nil { return items, "", err } row.WechatPlatformCertPath = saved if err := s.db.Save(row).Error; err != nil { return items, "", errors.New("保存平台证书记录失败") } for i := range items { if items[i].SerialNo == cands[0].item.SerialNo { items[i].IsSaved = true } } return items, "已获取并保存平台证书(序列号 " + cands[0].item.SerialNo + ")", nil } // DecryptWechatResource 用 APIv3 密钥解密微信回调/证书资源(AES-256-GCM) // Logan.Li func DecryptWechatResource(apiV3Key, associatedData, nonce, ciphertext string) ([]byte, error) { keyBytes := []byte(apiV3Key) if len(keyBytes) != 32 { return nil, errors.New("APIv3 密钥必须为 32 字节") } data, err := base64.StdEncoding.DecodeString(ciphertext) if err != nil { return nil, errors.New("密文解码失败") } block, err := aes.NewCipher(keyBytes) if err != nil { return nil, err } gcm, err := cipher.NewGCM(block) if err != nil { return nil, err } plain, err := gcm.Open(nil, []byte(nonce), data, []byte(associatedData)) if err != nil { return nil, errors.New("解密失败(APIv3 密钥不匹配)") } return plain, nil } // formatWechatTime 微信 RFC3339 时间转本地展示格式 // Logan.Li func formatWechatTime(raw string) string { if strings.TrimSpace(raw) == "" { return "" } if t, err := time.Parse(time.RFC3339, raw); err == nil { return t.Local().Format("2006-01-02 15:04:05") } return raw }