Initial commit
This commit is contained in:
@@ -0,0 +1,412 @@
|
||||
package zxchainsdk
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tencentyun/cos-go-sdk-v5"
|
||||
"wallet_sdk"
|
||||
)
|
||||
|
||||
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")
|
||||
}
|
||||
Reference in New Issue
Block a user