package zxchainsdk import ( "bytes" "context" "encoding/json" "errors" "fmt" "net/http" "net/url" "path" "strings" "time" "git.yuanzoo.cn/zhangzhengqiang/popi-zxl/third_party/official/gosdk_demo/zxsdk/wallet_sdk" "github.com/tencentyun/cos-go-sdk-v5" ) var ( ErrMissingCertificateStorage = errors.New("missing certificate storage") ErrMissingCOSSecretID = errors.New("missing cos secret id") ErrMissingCOSSecretKey = errors.New("missing cos secret key") ) // downloadAndHashFromURL 从指定的 URL 下载内容并计算 SM3 哈希。 func downloadAndHashFromURL(ctx context.Context, url string) (string, error) { req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return "", fmt.Errorf("failed to create request: %w", err) } resp, err := http.DefaultClient.Do(req) if err != nil { return "", fmt.Errorf("failed to download from URL: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("failed to download from URL, status: %d", resp.StatusCode) } var buf bytes.Buffer _, err = buf.ReadFrom(resp.Body) if err != nil { return "", fmt.Errorf("failed to read response body: %w", err) } hash, err := wallet_sdk.SM3Hash(buf.Bytes()) if err != nil { return "", fmt.Errorf("failed to calculate SM3 hash: %w", err) } return hash, nil } // SimpleHashAttestationCertificateRequest 是简化的一站式证书请求,只需要最少的业务字段。 type SimpleHashAttestationCertificateRequest struct { UserName string `json:"user_name"` // UserName 表示申请用户名称 UserID string `json:"user_id"` // UserID 表示申请用户 ID WorkName string `json:"work_name"` // WorkName 表示作品名称 TaskID string `json:"task_id"` // TaskID 表示生成任务 ID CompletedAt string `json:"completed_at"` // CompletedAt 表示作品完成时间 WorkPreviewURL string `json:"work_preview_url"` // WorkPreviewURL 表示作品预览图 URL EvidenceHash string `json:"evidence_hash,omitempty"` // EvidenceHash 表示作品文件哈希,可选。如果不传,SDK 将从 WorkPreviewURL 下载并计算哈希 // 可选:如果不传,将使用内置默认值 CertificateStorage *COSCertificateStorage `json:"certificate_storage,omitempty"` // CertificateStorage 表示 COS 上传配置 } // HashAttestationCertificateRequest 是业务层发起 Hash 存证并生成证书的一站式请求。 type HashAttestationCertificateRequest struct { HashAttestationRequest UserName string `json:"user_name"` // UserName 表示申请用户名称 UserID string `json:"user_id"` // UserID 表示申请用户 ID WorkName string `json:"work_name"` // WorkName 表示作品名称 TaskID string `json:"task_id"` // TaskID 表示生成任务 ID CompletedAt string `json:"completed_at"` // CompletedAt 表示作品完成时间 WorkPreviewURL string `json:"work_preview_url"` // WorkPreviewURL 表示作品预览图 URL WorkPreviewBytes []byte `json:"-"` // WorkPreviewBytes 表示作品预览图二进制数据 EvidenceHash string `json:"evidence_hash"` // EvidenceHash 表示作品文件哈希 BackgroundURL string `json:"background_url"` // BackgroundURL 表示证书背景图 URL LogoURL string `json:"logo_url"` // LogoURL 表示证书 logo URL FontPath string `json:"font_path"` // FontPath 表示用于绘制证书的字体路径 HTTPClient *http.Client `json:"-"` // HTTPClient 表示证书生成时使用的 HTTP 客户端 CertBucketBase string `json:"cert_bucket_base"` // CertBucketBase 表示证书对象 URL 的基础 Bucket URL CertificateStorage *COSCertificateStorage `json:"certificate_storage"` // CertificateStorage 表示 COS 上传配置 } // COSCertificateStorage 定义 SDK 上传 Popi 证书到 COS 所需的配置。 type COSCertificateStorage struct { BucketURL string `json:"bucket_url"` // BucketURL 表示 COS 存储桶地址 PublicBaseURL string `json:"public_base_url"` // PublicBaseURL 表示公开访问的 Bucket 基础 URL SecretID string `json:"secret_id"` // SecretID 表示 COS 访问密钥 ID SecretKey string `json:"secret_key"` // SecretKey 表示 COS 访问密钥 SessionToken string `json:"session_token"` // SessionToken 表示 COS 临时会话令牌 ObjectKey string `json:"object_key"` // ObjectKey 表示证书对象完整 Key ObjectKeyPrefix string `json:"object_key_prefix"` // ObjectKeyPrefix 表示证书对象 Key 前缀 } // HashAttestationCertificateResult 是业务接口可直接返回或落库的证书结果快照。 type HashAttestationCertificateResult struct { UserName string `json:"user_name"` // UserName 表示申请用户名称 UserID string `json:"user_id"` // UserID 表示申请用户 ID WorkName string `json:"work_name"` // WorkName 表示作品名称 TaskID string `json:"task_id"` // TaskID 表示生成任务 ID CompletedAt string `json:"completed_at"` // CompletedAt 表示作品完成时间 WorkPreviewURL string `json:"work_preview_url,omitempty"` // WorkPreviewURL 表示作品预览图 URL EvidenceHash string `json:"evidence_hash,omitempty"` // EvidenceHash 表示作品哈希 ReceiptID string `json:"receipt_id,omitempty"` // ReceiptID 表示官方 EvSave 返回的 evId RequestID string `json:"request_id,omitempty"` // RequestID 表示官方 EvSave 返回的 txId BlockHeight int64 `json:"block_height,omitempty"` // BlockHeight 表示上链区块高度 TxTime string `json:"tx_time,omitempty"` // TxTime 表示上链时间 SM3Hash string `json:"sm3_hash,omitempty"` // SM3Hash 表示本次上链内容的 SM3 哈希 PopiCertificateCertNo string `json:"popi_certificate_cert_no,omitempty"` // PopiCertificateCertNo 表示 popiart 证书编号 PopiCertificateVerifyURL string `json:"popi_certificate_verify_url,omitempty"` // PopiCertificateVerifyURL 表示 popiart 证书二维码映射地址 PopiCertificateCertificateImageURL string `json:"popi_certificate_certificate_image_url,omitempty"` // PopiCertificateCertificateImageURL 表示 popiart 证书地址 ZXChainCertURL string `json:"zxchain_cert_url"` // ZXChainCertURL 表示至信链官方证书 URL Attestation *AttestationResult `json:"attestation,omitempty"` // Attestation 表示链上存证返回结果 PopiCertificate PopiCertificateFields `json:"popi_certificate,omitempty"` // PopiCertificate 表示 Popi 证书字段 } // PopiCertificateFields 定义 Popi 自定义证书字段。 type PopiCertificateFields struct { CertNo string `json:"cert_no"` // CertNo 表示证书编号 VerifyURL string `json:"verify_url"` // VerifyURL 表示证书核验 URL CertificateImageKey string `json:"certificate_image_key"` // CertificateImageKey 表示证书图片对象 Key CertificateImageURL string `json:"certificate_image_url"` // CertificateImageURL 表示证书图片公开 URL } type evidencePackage struct { UserName string `json:"user_name"` // UserName 表示申请用户名称 UserID string `json:"user_id"` // UserID 表示申请用户 ID WorkName string `json:"work_name"` // WorkName 表示作品名称 TaskID string `json:"task_id"` // TaskID 表示生成任务 ID CompletedAt string `json:"completed_at"` // CompletedAt 表示作品完成时间 WorkPreviewURL string `json:"work_preview_url"` // WorkPreviewURL 表示作品预览图 URL WorkFileHash string `json:"work_file_hash"` // WorkFileHash 表示作品文件哈希 } func buildEvidencePackage(req HashAttestationCertificateRequest) ([]byte, error) { workFileHash := firstNonEmpty(req.EvidenceHash, req.HashAttestationRequest.Hash) if workFileHash == "" { data := req.HashAttestationRequest.ContentBytes if len(data) == 0 && req.HashAttestationRequest.Content != "" { data = []byte(req.HashAttestationRequest.Content) } if len(data) > 0 { hash, err := wallet_sdk.SM3Hash(data) if err != nil { return nil, err } workFileHash = hash } } if workFileHash == "" { return nil, errors.New("missing work file hash for evidence package") } pkg := evidencePackage{ UserName: req.UserName, UserID: req.UserID, WorkName: req.WorkName, TaskID: req.TaskID, CompletedAt: req.CompletedAt, WorkPreviewURL: req.WorkPreviewURL, WorkFileHash: workFileHash, } return json.Marshal(pkg) } // CreateSimpleHashAttestationCertificate 简化的一站式证书生成方法,只需要最少的业务字段。 func (c *SDKClient) CreateSimpleHashAttestationCertificate(ctx context.Context, req SimpleHashAttestationCertificateRequest) (*HashAttestationCertificateResult, error) { // 如果没有提供 EvidenceHash,从 WorkPreviewURL 下载并计算哈希 evidenceHash := req.EvidenceHash if evidenceHash == "" { if req.WorkPreviewURL == "" { return nil, errors.New("either EvidenceHash or WorkPreviewURL must be provided") } hash, err := downloadAndHashFromURL(ctx, req.WorkPreviewURL) if err != nil { return nil, fmt.Errorf("failed to download and hash from WorkPreviewURL: %w", err) } evidenceHash = hash } // 转换为完整的请求结构体,使用默认值 fullReq := HashAttestationCertificateRequest{ HashAttestationRequest: HashAttestationRequest{ BizID: req.TaskID, // 使用 TaskID 作为 BizID Scene: "simple-popi-work-hash", }, UserName: req.UserName, UserID: req.UserID, WorkName: req.WorkName, TaskID: req.TaskID, CompletedAt: req.CompletedAt, WorkPreviewURL: req.WorkPreviewURL, EvidenceHash: evidenceHash, CertificateStorage: req.CertificateStorage, } // 如果没有传 CertificateStorage,使用默认的 if fullReq.CertificateStorage == nil { fullReq.CertificateStorage = &COSCertificateStorage{ BucketURL: defaultCertificateBucketBaseURL, PublicBaseURL: defaultCertificateBucketBaseURL, SecretID: "", // 需要业务侧传入 SecretKey: "", // 需要业务侧传入 } } return c.CreateHashAttestationCertificate(ctx, fullReq) } // CreateHashAttestationCertificate 完成 Hash 存证、至信链证书查询、Popi 证书生成和上传。 func (c *SDKClient) CreateHashAttestationCertificate(ctx context.Context, req HashAttestationCertificateRequest) (*HashAttestationCertificateResult, error) { evidenceBytes, err := buildEvidencePackage(req) if err != nil { return nil, err } req.HashAttestationRequest.ContentBytes = evidenceBytes req.HashAttestationRequest.Content = "" attestation, err := c.CreateHashAttestation(ctx, req.HashAttestationRequest) if err != nil { return nil, err } zxCert, err := c.GetHashCert(ctx, attestation.ReceiptID) if err != nil { return nil, err } certNo := NewHashCertificateNo(time.Now(), req.UserID) storage := req.CertificateStorage objectKey := certificateImageObjectKey(attestation.ReceiptID, certNo) if storage != nil && storage.ObjectKeyPrefix != "" { objectKey = certificateObjectKeyWithPrefix(storage.ObjectKeyPrefix, attestation.ReceiptID, certNo) } if storage != nil && storage.ObjectKey != "" { objectKey = strings.TrimLeft(storage.ObjectKey, "/") } certificateImageURL := joinBucketURL(firstNonEmpty(req.CertBucketBase, storagePublicBaseURL(storage), defaultCertificateBucketBaseURL), objectKey) verifyURL := certificateImageURL cert, err := GenerateHashCertificate(ctx, HashCertificateRequest{ Title: "数字作品区块链存证证明", CertNo: certNo, UserName: req.UserName, UserID: req.UserID, WorkName: req.WorkName, TaskID: req.TaskID, CompletedAt: req.CompletedAt, EvidenceID: attestation.ReceiptID, TxID: attestation.RequestID, BlockHeight: fmt.Sprint(attestation.BlockHeight), FinalHash: attestation.SM3Hash, EvidenceHash: req.EvidenceHash, TrustedAt: attestation.TxTime, VerifyURL: verifyURL, WorkPreviewURL: req.WorkPreviewURL, WorkPreviewBytes: req.WorkPreviewBytes, BackgroundURL: req.BackgroundURL, LogoURL: req.LogoURL, FontPath: req.FontPath, HTTPClient: req.HTTPClient, BucketBaseURL: firstNonEmpty(req.CertBucketBase, storagePublicBaseURL(storage)), }) if err != nil { return nil, err } if storage != nil && storage.ObjectKey == "" { storageCopy := *storage storageCopy.ObjectKey = objectKey storage = &storageCopy } certURL, certKey, err := UploadCertificatePNGToCOS(ctx, cert.PNG, cert.CertificateImageKey, storage) if err != nil { return nil, err } if certURL != "" { cert.CertificateImageURL = certURL } if certKey != "" { cert.CertificateImageKey = certKey } return &HashAttestationCertificateResult{ UserName: req.UserName, UserID: req.UserID, WorkName: req.WorkName, TaskID: req.TaskID, CompletedAt: req.CompletedAt, WorkPreviewURL: req.WorkPreviewURL, EvidenceHash: req.EvidenceHash, ReceiptID: attestation.ReceiptID, RequestID: attestation.RequestID, BlockHeight: attestation.BlockHeight, TxTime: attestation.TxTime, SM3Hash: attestation.SM3Hash, PopiCertificateCertNo: cert.CertNo, PopiCertificateVerifyURL: cert.VerifyURL, PopiCertificateCertificateImageURL: cert.CertificateImageURL, ZXChainCertURL: zxCert.CertURL, Attestation: attestation, PopiCertificate: PopiCertificateFields{ CertNo: cert.CertNo, VerifyURL: cert.VerifyURL, CertificateImageKey: cert.CertificateImageKey, CertificateImageURL: cert.CertificateImageURL, }, }, nil } // BuildCertificateVerifyURL 生成二维码里使用的 Popi 在线核验 URL。 func BuildCertificateVerifyURL(baseURL, evidenceID string) string { if baseURL == "" { baseURL = "https://popi.art/cert/verify" } return strings.TrimRight(baseURL, "/") + "/" + url.PathEscape(evidenceID) } // NewHashCertificateNo 生成证书编号,格式为 yyyyMMddHHmmss + 用户ID。 func NewHashCertificateNo(t time.Time, userID string) string { return t.Format("20060102150405") + userID } // UploadCertificatePNGToCOS 上传 Popi 证书 PNG 到 COS,并返回公开 URL 和对象 Key。 func UploadCertificatePNGToCOS(ctx context.Context, pngBytes []byte, defaultObjectKey string, storage *COSCertificateStorage) (string, string, error) { if storage == nil { return "", "", ErrMissingCertificateStorage } if storage.SecretID == "" { return "", "", ErrMissingCOSSecretID } if storage.SecretKey == "" { return "", "", ErrMissingCOSSecretKey } bucketURL := firstNonEmpty(storage.BucketURL, defaultCertificateBucketBaseURL) parsed, err := url.Parse(bucketURL) if err != nil { return "", "", err } objectKey := strings.TrimLeft(firstNonEmpty(storage.ObjectKey, defaultObjectKey), "/") if objectKey == "" { return "", "", errors.New("missing certificate object key") } client := cos.NewClient(&cos.BaseURL{BucketURL: parsed}, &http.Client{ Transport: &cos.AuthorizationTransport{ SecretID: storage.SecretID, SecretKey: storage.SecretKey, SessionToken: storage.SessionToken, }, }) _, err = client.Object.Put(ctx, objectKey, bytes.NewReader(pngBytes), &cos.ObjectPutOptions{ ObjectPutHeaderOptions: &cos.ObjectPutHeaderOptions{ ContentType: "image/png", ContentLength: len(pngBytes), CacheControl: "public, max-age=31536000, immutable", }, }) if err != nil { return "", "", err } publicBaseURL := firstNonEmpty(storage.PublicBaseURL, bucketURL) return joinBucketURL(publicBaseURL, objectKey), objectKey, nil } func storagePublicBaseURL(storage *COSCertificateStorage) string { if storage == nil { return "" } return firstNonEmpty(storage.PublicBaseURL, storage.BucketURL) } func firstNonEmpty(values ...string) string { for _, value := range values { if value != "" { return value } } return "" } func certificateObjectKeyWithPrefix(prefix, evidenceID, certNo string) string { dirName := evidenceID if dirName == "" { dirName = certNo } if prefix == "" { prefix = "certificates" } return path.Join(prefix, strings.Trim(dirName, "/"), "certificate.png") }