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.
411 lines
16 KiB
411 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",
|
|
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")
|
|
}
|
|
|