至信SDK
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

412 lines
16 KiB

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: int64(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")
}