Initial commit
This commit is contained in:
@@ -0,0 +1,421 @@
|
||||
package zxchainsdk
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fogleman/gg"
|
||||
"github.com/skip2/go-qrcode"
|
||||
xdraw "golang.org/x/image/draw"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultCertificateBackgroundURL = "https://popiart-public-1313913486.cos.ap-guangzhou.myqcloud.com/media/2026/0511/83034.png"
|
||||
defaultCertificateLogoURL = "https://popiart-public-1313913486.cos.ap-guangzhou.myqcloud.com/media/2026/0511/83035.png"
|
||||
defaultCertificateBucketBaseURL = "https://popi-certificate-1386008232.cos.ap-guangzhou.myqcloud.com"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrMissingCertificateNo = errors.New("missing certificate no")
|
||||
ErrMissingVerifyURL = errors.New("missing verify url")
|
||||
)
|
||||
|
||||
// HashCertificateRequest 定义 Popi Hash 存证证书生成请求。
|
||||
type HashCertificateRequest struct {
|
||||
Title string
|
||||
CertNo string
|
||||
UserName string
|
||||
UserID string
|
||||
WorkName string
|
||||
TaskID string
|
||||
CompletedAt string
|
||||
EvidenceID string
|
||||
TxID string
|
||||
BlockHeight string
|
||||
FinalHash string
|
||||
EvidenceHash string
|
||||
TrustedAt string
|
||||
VerifyURL string
|
||||
|
||||
WorkPreviewURL string
|
||||
WorkPreviewBytes []byte
|
||||
|
||||
BackgroundURL string
|
||||
BackgroundBytes []byte
|
||||
LogoURL string
|
||||
LogoBytes []byte
|
||||
FontPath string
|
||||
HTTPClient *http.Client
|
||||
BucketBaseURL string
|
||||
}
|
||||
|
||||
// HashCertificateResult 保存生成后的证书 PNG 和建议上传路径。
|
||||
type HashCertificateResult struct {
|
||||
CertNo string
|
||||
VerifyURL string
|
||||
PNG []byte
|
||||
CertificateImageKey string
|
||||
CertificateImageURL string
|
||||
}
|
||||
|
||||
// GenerateHashCertificate 按 Figma 模板合成 Popi Hash 存证证书 PNG。
|
||||
func GenerateHashCertificate(ctx context.Context, req HashCertificateRequest) (*HashCertificateResult, error) {
|
||||
if req.CertNo == "" {
|
||||
return nil, ErrMissingCertificateNo
|
||||
}
|
||||
if req.VerifyURL == "" {
|
||||
return nil, ErrMissingVerifyURL
|
||||
}
|
||||
|
||||
if req.Title == "" {
|
||||
req.Title = "数字作品区块链存证证明"
|
||||
}
|
||||
if req.BackgroundURL == "" && len(req.BackgroundBytes) == 0 {
|
||||
req.BackgroundURL = defaultCertificateBackgroundURL
|
||||
}
|
||||
if req.LogoURL == "" && len(req.LogoBytes) == 0 {
|
||||
req.LogoURL = defaultCertificateLogoURL
|
||||
}
|
||||
if req.BucketBaseURL == "" {
|
||||
req.BucketBaseURL = defaultCertificateBucketBaseURL
|
||||
}
|
||||
|
||||
background, err := loadCertificateImage(ctx, req.HTTPClient, req.BackgroundURL, req.BackgroundBytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load certificate background: %w", err)
|
||||
}
|
||||
logo, err := loadCertificateImage(ctx, req.HTTPClient, req.LogoURL, req.LogoBytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load certificate logo: %w", err)
|
||||
}
|
||||
|
||||
var workPreview image.Image
|
||||
if req.WorkPreviewURL != "" || len(req.WorkPreviewBytes) > 0 {
|
||||
workPreview, err = loadCertificateImage(ctx, req.HTTPClient, req.WorkPreviewURL, req.WorkPreviewBytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load work preview: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
qr, err := newQRCodeImage(req.VerifyURL, 184)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("generate qr code: %w", err)
|
||||
}
|
||||
|
||||
fontPath, err := resolveCertificateFont(req.FontPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dc := gg.NewContext(1369, 1920)
|
||||
dc.DrawImage(resizeImage(background, 1369, 1920), 0, 0)
|
||||
dc.DrawImage(resizeImage(logo, 304, 60), 533, 470)
|
||||
|
||||
drawTitle(dc, fontPath, req.Title)
|
||||
drawCertNo(dc, fontPath, req.CertNo)
|
||||
drawCertificateText(dc, fontPath, req)
|
||||
drawWorkPreview(dc, workPreview)
|
||||
dc.DrawImage(resizeImage(qr, 184, 184), 593, 1493)
|
||||
drawQRCodeText(dc, fontPath)
|
||||
drawDisclaimer(dc, fontPath)
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := png.Encode(&buf, dc.Image()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
imageKey := certificateImageObjectKey(req.EvidenceID, req.CertNo)
|
||||
return &HashCertificateResult{
|
||||
CertNo: req.CertNo,
|
||||
VerifyURL: req.VerifyURL,
|
||||
PNG: buf.Bytes(),
|
||||
CertificateImageKey: imageKey,
|
||||
CertificateImageURL: joinBucketURL(req.BucketBaseURL, imageKey),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func drawTitle(dc *gg.Context, fontPath, title string) {
|
||||
_ = dc.LoadFontFace(fontPath, 85)
|
||||
dc.SetHexColor("#000000")
|
||||
dc.DrawStringAnchored(title, 684.5, 550, 0.5, 1)
|
||||
}
|
||||
|
||||
func drawCertNo(dc *gg.Context, fontPath, certNo string) {
|
||||
gradient := gg.NewLinearGradient(331.5, 670, 1037.5, 670)
|
||||
gradient.AddColorStop(0, color.White)
|
||||
gradient.AddColorStop(0.15, mustHexColor("#eed595"))
|
||||
gradient.AddColorStop(0.50142, mustHexColor("#9c6b3c"))
|
||||
gradient.AddColorStop(0.85, mustHexColor("#eed595"))
|
||||
gradient.AddColorStop(1, color.White)
|
||||
|
||||
dc.SetFillStyle(gradient)
|
||||
dc.DrawRectangle(331.5, 670, 706, 60)
|
||||
dc.Fill()
|
||||
|
||||
_ = dc.LoadFontFace(fontPath, 25)
|
||||
dc.SetHexColor("#ffffff")
|
||||
dc.DrawStringAnchored("编号:"+certNo, 684.5, 700, 0.5, 0.5)
|
||||
}
|
||||
|
||||
func drawCertificateText(dc *gg.Context, fontPath string, req HashCertificateRequest) {
|
||||
subjectLines := []certificateLine{
|
||||
{Text: "一、存证主体", Medium: true},
|
||||
{Text: "申请用户名称:" + req.UserName},
|
||||
{Text: "用户ID:" + req.UserID},
|
||||
}
|
||||
drawLines(dc, fontPath, subjectLines, 143, 796, 1083, 49)
|
||||
|
||||
summaryLines := []certificateLine{
|
||||
{Text: "二、存证信息摘要", Medium: true},
|
||||
{Text: "作品名称:" + req.WorkName},
|
||||
{Text: "生成任务ID:" + req.TaskID},
|
||||
{Text: "作品完成时间:" + req.CompletedAt},
|
||||
{Text: "存证ID:" + req.EvidenceID},
|
||||
{Text: `区块链交易ID:"` + req.TxID + `"`},
|
||||
{Text: "区块高度:" + req.BlockHeight},
|
||||
{Text: `最终产物 Hash:"` + req.FinalHash + `"`},
|
||||
{Text: `证据包 Hash:"` + req.EvidenceHash + `"`},
|
||||
{Text: "可信存证时间:" + req.TrustedAt},
|
||||
}
|
||||
drawLines(dc, fontPath, summaryLines, 143, 963, 1083, 49)
|
||||
}
|
||||
|
||||
type certificateLine struct {
|
||||
Text string
|
||||
Medium bool
|
||||
}
|
||||
|
||||
func drawLines(dc *gg.Context, fontPath string, lines []certificateLine, x, y, maxWidth, lineStep float64) {
|
||||
for i, line := range lines {
|
||||
size := fitFontSize(dc, fontPath, line.Text, 25, 18, maxWidth)
|
||||
_ = dc.LoadFontFace(fontPath, size)
|
||||
dc.SetHexColor("#000000")
|
||||
dc.DrawStringAnchored(line.Text, x, y+float64(i)*lineStep, 0, 1)
|
||||
}
|
||||
}
|
||||
|
||||
func drawWorkPreview(dc *gg.Context, preview image.Image) {
|
||||
dc.Push()
|
||||
dc.RotateAbout(gg.Radians(4.33), 959, 950)
|
||||
gradient := gg.NewLinearGradient(727, 823, 1190, 1078)
|
||||
gradient.AddColorStop(0.10985, mustHexColor("#eed594"))
|
||||
gradient.AddColorStop(0.89015, mustHexColor("#b78e5a"))
|
||||
dc.SetFillStyle(gradient)
|
||||
dc.DrawRectangle(735, 825, 463, 255)
|
||||
dc.Fill()
|
||||
dc.Pop()
|
||||
|
||||
dc.SetHexColor("#ffffff")
|
||||
dc.DrawRectangle(717, 813, 488, 273)
|
||||
dc.Fill()
|
||||
if preview != nil {
|
||||
fitted := containImage(preview, 488, 273)
|
||||
dc.DrawImageAnchored(fitted, 961, 950, 0.5, 0.5)
|
||||
}
|
||||
dc.SetHexColor("#eed594")
|
||||
dc.SetLineWidth(2)
|
||||
dc.DrawRectangle(717, 813, 488, 273)
|
||||
dc.Stroke()
|
||||
}
|
||||
|
||||
func drawQRCodeText(dc *gg.Context, fontPath string) {
|
||||
_ = dc.LoadFontFace(fontPath, 25)
|
||||
dc.SetHexColor("#000000")
|
||||
dc.DrawStringAnchored("扫码查看在线证书", 684.5, 1715, 0.5, 1)
|
||||
}
|
||||
|
||||
func drawDisclaimer(dc *gg.Context, fontPath string) {
|
||||
_ = dc.LoadFontFace(fontPath, 18)
|
||||
dc.SetHexColor("#999999")
|
||||
text := "*本凭证为电子数据存证凭证,不等同于官方著作权登记证书。作品原创性、素材合法性及第三方授权责任由申请用户承担。"
|
||||
dc.DrawStringAnchored(text, 194, 1853.5, 0, 0.5)
|
||||
}
|
||||
|
||||
func fitFontSize(dc *gg.Context, fontPath, text string, preferred, minimum, maxWidth float64) float64 {
|
||||
for size := preferred; size >= minimum; size-- {
|
||||
if err := dc.LoadFontFace(fontPath, size); err != nil {
|
||||
return preferred
|
||||
}
|
||||
width, _ := dc.MeasureString(text)
|
||||
if width <= maxWidth {
|
||||
return size
|
||||
}
|
||||
}
|
||||
return minimum
|
||||
}
|
||||
|
||||
func newQRCodeImage(content string, size int) (image.Image, error) {
|
||||
qr, err := qrcode.New(content, qrcode.Medium)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
qr.DisableBorder = true
|
||||
return qr.Image(size), nil
|
||||
}
|
||||
|
||||
func loadCertificateImage(ctx context.Context, client *http.Client, imageURL string, inline []byte) (image.Image, error) {
|
||||
var data []byte
|
||||
var err error
|
||||
if len(inline) > 0 {
|
||||
data = inline
|
||||
} else {
|
||||
if imageURL == "" {
|
||||
return nil, errors.New("missing image url")
|
||||
}
|
||||
data, err = fetchBytes(ctx, client, imageURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
img, format, err := image.Decode(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if format == "jpeg" {
|
||||
return img, nil
|
||||
}
|
||||
return img, nil
|
||||
}
|
||||
|
||||
func fetchBytes(ctx context.Context, client *http.Client, rawURL string) ([]byte, error) {
|
||||
if client == nil {
|
||||
client = &http.Client{Timeout: 20 * time.Second}
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, rawURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return nil, fmt.Errorf("unexpected status %d", resp.StatusCode)
|
||||
}
|
||||
return io.ReadAll(resp.Body)
|
||||
}
|
||||
|
||||
func resizeImage(src image.Image, width, height int) image.Image {
|
||||
dst := image.NewRGBA(image.Rect(0, 0, width, height))
|
||||
xdraw.CatmullRom.Scale(dst, dst.Bounds(), src, src.Bounds(), xdraw.Over, nil)
|
||||
return dst
|
||||
}
|
||||
|
||||
func containImage(src image.Image, boxWidth, boxHeight int) image.Image {
|
||||
bounds := src.Bounds()
|
||||
sourceWidth := bounds.Dx()
|
||||
sourceHeight := bounds.Dy()
|
||||
if sourceWidth == 0 || sourceHeight == 0 {
|
||||
return image.NewRGBA(image.Rect(0, 0, boxWidth, boxHeight))
|
||||
}
|
||||
|
||||
scale := min(float64(boxWidth)/float64(sourceWidth), float64(boxHeight)/float64(sourceHeight))
|
||||
width := max(1, int(float64(sourceWidth)*scale))
|
||||
height := max(1, int(float64(sourceHeight)*scale))
|
||||
|
||||
dst := image.NewRGBA(image.Rect(0, 0, boxWidth, boxHeight))
|
||||
x := (boxWidth - width) / 2
|
||||
y := (boxHeight - height) / 2
|
||||
target := image.Rect(x, y, x+width, y+height)
|
||||
xdraw.CatmullRom.Scale(dst, target, src, bounds, xdraw.Over, nil)
|
||||
return dst
|
||||
}
|
||||
|
||||
func resolveCertificateFont(explicitPath string) (string, error) {
|
||||
if explicitPath != "" {
|
||||
if _, err := os.Stat(explicitPath); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, err := gg.LoadFontFace(explicitPath, 25); err != nil {
|
||||
return "", fmt.Errorf("load certificate font %s: %w", explicitPath, err)
|
||||
}
|
||||
return explicitPath, nil
|
||||
}
|
||||
|
||||
candidates := []string{
|
||||
"/System/Library/Fonts/Supplemental/Arial Unicode.ttf",
|
||||
"/System/Library/Fonts/Supplemental/Arial.ttf",
|
||||
"/System/Library/Fonts/Supplemental/Microsoft Sans Serif.ttf",
|
||||
"/System/Library/Fonts/STHeiti Medium.ttc",
|
||||
"/System/Library/Fonts/Hiragino Sans GB.ttc",
|
||||
"/Library/Fonts/Arial Unicode.ttf",
|
||||
"/usr/share/fonts/opentype/source-han-sans/SourceHanSansCN-Regular.otf",
|
||||
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.otf",
|
||||
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
|
||||
"/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc",
|
||||
}
|
||||
for _, candidate := range candidates {
|
||||
if _, err := os.Stat(candidate); err != nil {
|
||||
continue
|
||||
}
|
||||
if _, err := gg.LoadFontFace(candidate, 25); err == nil {
|
||||
return candidate, nil
|
||||
}
|
||||
}
|
||||
return "", errors.New("missing certificate font, set HashCertificateRequest.FontPath to a Chinese TTF/TTC font")
|
||||
}
|
||||
|
||||
func certificateImageObjectKey(evidenceID, certNo string) string {
|
||||
dirName := evidenceID
|
||||
if dirName == "" {
|
||||
dirName = certNo
|
||||
}
|
||||
dirName = strings.Trim(dirName, "/")
|
||||
if dirName == "" {
|
||||
dirName = "unknown"
|
||||
}
|
||||
return path.Join("certificates", dirName, "certificate.png")
|
||||
}
|
||||
|
||||
func joinBucketURL(bucketBaseURL, objectKey string) string {
|
||||
if bucketBaseURL == "" || objectKey == "" {
|
||||
return ""
|
||||
}
|
||||
escaped := strings.TrimLeft(objectKey, "/")
|
||||
u, err := url.Parse(strings.TrimRight(bucketBaseURL, "/"))
|
||||
if err != nil {
|
||||
return strings.TrimRight(bucketBaseURL, "/") + "/" + escaped
|
||||
}
|
||||
u.Path = path.Join(u.Path, escaped)
|
||||
return u.String()
|
||||
}
|
||||
|
||||
func mustHexColor(hex string) color.Color {
|
||||
c, err := parseHexColor(hex)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func parseHexColor(hex string) (color.Color, error) {
|
||||
hex = strings.TrimPrefix(hex, "#")
|
||||
if len(hex) != 6 {
|
||||
return nil, fmt.Errorf("invalid hex color %q", hex)
|
||||
}
|
||||
var r, g, b uint8
|
||||
if _, err := fmt.Sscanf(hex, "%02x%02x%02x", &r, &g, &b); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return color.RGBA{R: r, G: g, B: b, A: 255}, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
image.RegisterFormat("jpeg", "\xff\xd8", jpeg.Decode, jpeg.DecodeConfig)
|
||||
image.RegisterFormat("png", "\x89PNG\r\n\x1a\n", png.Decode, png.DecodeConfig)
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package zxchainsdk
|
||||
|
||||
import "context"
|
||||
|
||||
// Client 定义可复用的至信链存证 SDK 能力。
|
||||
type Client interface {
|
||||
// CreateHashAttestation 发起 Hash 存证。
|
||||
CreateHashAttestation(ctx context.Context, req HashAttestationRequest) (*AttestationResult, error)
|
||||
|
||||
// QueryHashAttestation 查询 Hash 存证链上记录。
|
||||
QueryHashAttestation(ctx context.Context, req HashAttestationQueryRequest) (*HashAttestationQueryResult, error)
|
||||
|
||||
// CreateKVAttestation 发起 KV 存证。
|
||||
CreateKVAttestation(ctx context.Context, req KVAttestationRequest) (*AttestationResult, error)
|
||||
|
||||
// GetHashCert 根据 Hash 存证 evId 获取官方证书地址。
|
||||
GetHashCert(ctx context.Context, evID string) (*HashCertResult, error)
|
||||
|
||||
// CreateHashAttestationCertificate 发起 Hash 存证,并生成 Popi 自定义证书。
|
||||
CreateHashAttestationCertificate(ctx context.Context, req HashAttestationCertificateRequest) (*HashAttestationCertificateResult, error)
|
||||
|
||||
// CreateSimpleHashAttestationCertificate 简化的一站式证书生成方法,只需要最少的业务字段。
|
||||
CreateSimpleHashAttestationCertificate(ctx context.Context, req SimpleHashAttestationCertificateRequest) (*HashAttestationCertificateResult, error)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package zxchainsdk
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrMissingBizID = errors.New("missing biz_id")
|
||||
ErrMissingContent = errors.New("missing content")
|
||||
ErrMissingEvID = errors.New("missing ev_id")
|
||||
ErrMissingHash = errors.New("missing hash")
|
||||
ErrMissingScene = errors.New("missing scene")
|
||||
ErrMissingKVFields = errors.New("missing kv fields")
|
||||
ErrMissingFileName = errors.New("missing file_name")
|
||||
ErrMissingQueryKey = errors.New("missing ev_id, hash or tx_id")
|
||||
ErrEmptyAttestation = errors.New("empty attestation request")
|
||||
)
|
||||
@@ -0,0 +1,64 @@
|
||||
package zxchainsdk
|
||||
|
||||
// HashAttestationRequest 定义 Hash 存证请求模型。
|
||||
type HashAttestationRequest struct {
|
||||
BizID string // BizID 表示业务主键。
|
||||
Scene string // Scene 表示业务场景,会作为官方 EvSave 的 extendInfo。
|
||||
Content string // Content 表示待存证的文本内容。
|
||||
ContentBytes []byte // ContentBytes 表示待存证的二进制内容,优先级高于 Content。
|
||||
Hash string // Hash 表示业务侧传入的哈希值,SDK 实际上链仍按内容计算 SM3。
|
||||
FileName string // FileName 表示原始文件名。
|
||||
FileSize int64 // FileSize 表示原始文件大小。
|
||||
ContentType string // ContentType 表示原始内容类型。
|
||||
StorageURI string // StorageURI 表示业务侧保存原始文件的位置。
|
||||
CreatedAt string // CreatedAt 表示业务侧创建存证请求的时间。
|
||||
Metadata map[string]string // Metadata 表示附加元数据。
|
||||
}
|
||||
|
||||
// HashAttestationQueryRequest 定义 Hash 存证取证查询请求模型。
|
||||
type HashAttestationQueryRequest struct {
|
||||
EvID string // EvID 表示 Hash 存证回执中的 evId。
|
||||
Hash string // Hash 表示本次存证内容的 SM3 哈希。
|
||||
TxID string // TxID 表示 Hash 存证回执中的 txId。
|
||||
}
|
||||
|
||||
// KVAttestationRequest 定义 KV 存证请求模型。
|
||||
type KVAttestationRequest struct {
|
||||
BizID string // BizID 表示业务主键,同时作为链上 KV key。
|
||||
Scene string // Scene 表示业务场景。
|
||||
Fields map[string]string // Fields 表示待上链的键值字段集合。
|
||||
Metadata map[string]string // Metadata 表示附加元数据。
|
||||
}
|
||||
|
||||
// AttestationResult 定义存证结果模型。
|
||||
type AttestationResult struct {
|
||||
ReceiptID string // ReceiptID 表示链上回执编号,Hash 存证时对应 evId。
|
||||
RequestID string // RequestID 表示请求追踪编号,通常对应 txId。
|
||||
Status string // Status 表示存证状态。
|
||||
RawResponse []byte // RawResponse 保存官方 SDK 原始响应内容。
|
||||
ExternalRefID string // ExternalRefID 表示外部引用标识。
|
||||
BlockHeight int64 `json:"blockHeight,omitempty"` // BlockHeight 表示上链区块高度。
|
||||
TxTime string `json:"txTime,omitempty"` // TxTime 表示官方返回的上链时间。
|
||||
FileName string `json:"fileName,omitempty"` // FileName 表示原始文件名。
|
||||
FileSize int64 `json:"fileSize,omitempty"` // FileSize 表示原始文件大小。
|
||||
ContentType string `json:"contentType,omitempty"` // ContentType 表示原始内容类型。
|
||||
StorageURI string `json:"storageUri,omitempty"` // StorageURI 表示业务侧保存原始文件的位置。
|
||||
SM3Hash string `json:"sm3Hash,omitempty"` // SM3Hash 表示本次上链内容的 SM3 哈希。
|
||||
CreatedAt string `json:"createdAt,omitempty"` // CreatedAt 表示业务侧创建存证请求的时间。
|
||||
}
|
||||
|
||||
// HashAttestationQueryResult 定义 Hash 存证取证查询结果。
|
||||
type HashAttestationQueryResult struct {
|
||||
EvID string `json:"evId,omitempty"` // EvID 表示 Hash 存证回执中的 evId。
|
||||
TxID string `json:"txId,omitempty"` // TxID 表示 Hash 存证回执中的 txId。
|
||||
BlockHeight int64 `json:"blockHeight,omitempty"` // BlockHeight 表示上链区块高度。
|
||||
TxTime string `json:"txTime,omitempty"` // TxTime 表示官方返回的上链时间。
|
||||
ExtendInfo string `json:"extendInfo,omitempty"` // ExtendInfo 表示存证时传入的业务场景信息。
|
||||
RawResponse []byte `json:"-"` // RawResponse 保存官方 SDK 原始响应内容。
|
||||
}
|
||||
|
||||
// HashCertResult 定义 Hash 存证证书查询结果。
|
||||
type HashCertResult struct {
|
||||
EvID string // EvID 表示 Hash 存证回执中的 evId。
|
||||
CertURL string // CertURL 表示官方证书访问地址。
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package zxchainsdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"wallet_sdk"
|
||||
"zxsdk"
|
||||
)
|
||||
|
||||
// SDKClient 基于官方至信链 Go SDK 实现 Client。
|
||||
type SDKClient struct {
|
||||
client *zxsdk.ZxChainClient
|
||||
}
|
||||
|
||||
// NewSDKClient 使用官方凭据创建至信链 SDK 客户端。
|
||||
func NewSDKClient(secretID, secretKey, privateKey string) (*SDKClient, error) {
|
||||
client, err := zxsdk.NewZxChainClient(secretID, secretKey, privateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &SDKClient{client: client}, nil
|
||||
}
|
||||
|
||||
// CreateHashAttestation 使用官方 EvSave 发起 Hash 存证。
|
||||
func (c *SDKClient) CreateHashAttestation(_ context.Context, req HashAttestationRequest) (*AttestationResult, error) {
|
||||
data := req.ContentBytes
|
||||
if len(data) == 0 && req.Content != "" {
|
||||
data = []byte(req.Content)
|
||||
}
|
||||
if len(data) == 0 {
|
||||
return nil, ErrMissingContent
|
||||
}
|
||||
|
||||
sm3Hash, err := wallet_sdk.SM3Hash(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
createdAt := req.CreatedAt
|
||||
if createdAt == "" {
|
||||
createdAt = time.Now().Format(time.RFC3339)
|
||||
}
|
||||
|
||||
resp, err := c.client.EvSave(data, req.Scene)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
raw, _ := json.Marshal(resp)
|
||||
return &AttestationResult{
|
||||
ReceiptID: resp.EvId, // ReceiptID 保存官方 EvSave 返回的 evId。
|
||||
RequestID: resp.TxId, // RequestID 保存官方 EvSave 返回的 txId。
|
||||
Status: "success", // Status 表示本次 SDK 调用成功。
|
||||
RawResponse: raw, // RawResponse 保存官方 SDK 原始响应 JSON。
|
||||
ExternalRefID: req.BizID, // ExternalRefID 关联业务侧传入的 BizID。
|
||||
BlockHeight: resp.BlockHeight, // BlockHeight 表示上链区块高度。
|
||||
TxTime: resp.TxTime, // TxTime 表示官方返回的上链时间。
|
||||
FileName: req.FileName, // FileName 回传业务侧传入的文件名。
|
||||
FileSize: int64(len(data)), // FileSize 表示本次参与 Hash 存证的内容字节数。
|
||||
ContentType: req.ContentType, // ContentType 回传业务侧传入的内容类型。
|
||||
StorageURI: req.StorageURI, // StorageURI 回传业务侧保存原始文件的位置。
|
||||
SM3Hash: sm3Hash, // SM3Hash 表示本次上链内容的 SM3 哈希。
|
||||
CreatedAt: createdAt, // CreatedAt 表示业务侧传入或 SDK 自动生成的创建时间。
|
||||
}, nil
|
||||
}
|
||||
|
||||
// QueryHashAttestation 使用官方 EvQuery 查询 Hash 存证链上记录。
|
||||
func (c *SDKClient) QueryHashAttestation(_ context.Context, req HashAttestationQueryRequest) (*HashAttestationQueryResult, error) {
|
||||
if req.EvID == "" && req.Hash == "" && req.TxID == "" {
|
||||
return nil, ErrMissingQueryKey
|
||||
}
|
||||
|
||||
resp, err := c.client.EvQuery(req.EvID, req.Hash, req.TxID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
raw, _ := json.Marshal(resp)
|
||||
return &HashAttestationQueryResult{
|
||||
EvID: resp.EvId, // EvID 表示查询到的 Hash 存证 evId。
|
||||
TxID: resp.TxId, // TxID 表示查询到的链上交易 txId。
|
||||
BlockHeight: resp.BlockHeight, // BlockHeight 表示查询到的上链区块高度。
|
||||
TxTime: resp.TxTime, // TxTime 表示查询到的上链时间。
|
||||
ExtendInfo: resp.ExtendInfo, // ExtendInfo 表示存证时传入的业务场景信息。
|
||||
RawResponse: raw, // RawResponse 保存官方 SDK 原始响应 JSON。
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateKVAttestation 使用官方 KvSave 发起 KV 存证。
|
||||
func (c *SDKClient) CreateKVAttestation(_ context.Context, req KVAttestationRequest) (*AttestationResult, error) {
|
||||
kvValueBytes, _ := json.Marshal(req.Fields)
|
||||
err := c.client.KvSave(req.BizID, string(kvValueBytes), req.Scene)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := c.client.KvQuery(req.BizID, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
raw, _ := json.Marshal(resp)
|
||||
return &AttestationResult{
|
||||
ReceiptID: resp.TxId, // ReceiptID 保存 KV 查询结果中的 txId。
|
||||
RequestID: resp.TxId, // RequestID 保存 KV 查询结果中的 txId。
|
||||
Status: "success", // Status 表示本次 SDK 调用成功。
|
||||
RawResponse: raw, // RawResponse 保存官方 SDK 原始响应 JSON。
|
||||
ExternalRefID: resp.KvKey, // ExternalRefID 保存链上的 kvKey。
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetHashCert 根据 evId 获取官方证书地址。
|
||||
func (c *SDKClient) GetHashCert(_ context.Context, evID string) (*HashCertResult, error) {
|
||||
if evID == "" {
|
||||
return nil, ErrMissingEvID
|
||||
}
|
||||
|
||||
certURL, err := c.client.GetEvCert(evID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &HashCertResult{
|
||||
EvID: evID, // EvID 表示 Hash 存证回执中的 evId。
|
||||
CertURL: certURL, // CertURL 表示官方证书访问地址。
|
||||
}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user