feat(t214): persist candidate observation identities
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
"cmroubao/backend-api/internal/domain"
|
||||
"cmroubao/backend-api/internal/usecase"
|
||||
@@ -93,6 +94,25 @@ func storeCandidateDecisionDataset(
|
||||
if err != nil {
|
||||
return repositoryFailure(err)
|
||||
}
|
||||
_, err = tx.ExecContext(
|
||||
ctx,
|
||||
`INSERT INTO candidate_observation_identities (
|
||||
candidate_key, execution_id, candidate_ordinal,
|
||||
card_signature, detail_signature, detail_evidence_sha256,
|
||||
specification_evidence_sha256, identity_version, created_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, 1, ?)`,
|
||||
candidateObservationKey(write.ExecutionID, candidate),
|
||||
write.ExecutionID,
|
||||
candidate.Ordinal,
|
||||
candidate.CardSignature,
|
||||
candidate.DetailSignature,
|
||||
candidate.DetailEvidenceSHA256,
|
||||
candidate.SpecificationEvidenceSHA256,
|
||||
formatTimestamp(write.Now),
|
||||
)
|
||||
if err != nil {
|
||||
return repositoryFailure(err)
|
||||
}
|
||||
}
|
||||
if batch.ProvenanceJSON != nil {
|
||||
var provenance usecase.ExecutionProvenance
|
||||
@@ -567,12 +587,21 @@ func getCandidateDecisionDataset(
|
||||
}
|
||||
rows, err := queryer.QueryContext(
|
||||
ctx,
|
||||
`SELECT task_id, execution_id, ordinal, title, sku_text, price_text,
|
||||
product_url, image_url, evidence_asset_ids_json,
|
||||
collection_status, observed_at
|
||||
FROM candidate_observations
|
||||
WHERE task_id = ? AND execution_id = ?
|
||||
ORDER BY ordinal ASC`,
|
||||
`SELECT observation.task_id, observation.execution_id,
|
||||
observation.ordinal, observation.title, observation.sku_text,
|
||||
observation.price_text, observation.product_url,
|
||||
observation.image_url, observation.evidence_asset_ids_json,
|
||||
observation.collection_status, observation.observed_at,
|
||||
identity.candidate_key, identity.card_signature,
|
||||
identity.detail_signature, identity.detail_evidence_sha256,
|
||||
identity.specification_evidence_sha256,
|
||||
identity.identity_version, identity.created_at
|
||||
FROM candidate_observations AS observation
|
||||
LEFT JOIN candidate_observation_identities AS identity
|
||||
ON identity.execution_id = observation.execution_id
|
||||
AND identity.candidate_ordinal = observation.ordinal
|
||||
WHERE observation.task_id = ? AND observation.execution_id = ?
|
||||
ORDER BY observation.ordinal ASC`,
|
||||
taskID,
|
||||
executionID,
|
||||
)
|
||||
@@ -582,6 +611,9 @@ func getCandidateDecisionDataset(
|
||||
for rows.Next() {
|
||||
var observation domain.CandidateObservation
|
||||
var evidenceJSON, observedAt string
|
||||
var candidateKey, cardSignature, detailSignature sql.NullString
|
||||
var detailHash, specificationHash, identityAt sql.NullString
|
||||
var identityVersion sql.NullInt64
|
||||
if err := rows.Scan(
|
||||
&observation.TaskID,
|
||||
&observation.ExecutionID,
|
||||
@@ -594,6 +626,13 @@ func getCandidateDecisionDataset(
|
||||
&evidenceJSON,
|
||||
&observation.CollectionStatus,
|
||||
&observedAt,
|
||||
&candidateKey,
|
||||
&cardSignature,
|
||||
&detailSignature,
|
||||
&detailHash,
|
||||
&specificationHash,
|
||||
&identityVersion,
|
||||
&identityAt,
|
||||
); err != nil {
|
||||
_ = rows.Close()
|
||||
return nil, repositoryFailure(err)
|
||||
@@ -610,6 +649,24 @@ func getCandidateDecisionDataset(
|
||||
_ = rows.Close()
|
||||
return nil, repositoryFailure(err)
|
||||
}
|
||||
if candidateKey.Valid {
|
||||
identityCreatedAt, parseErr := parseTimestamp(identityAt.String)
|
||||
if parseErr != nil {
|
||||
_ = rows.Close()
|
||||
return nil, repositoryFailure(parseErr)
|
||||
}
|
||||
observation.Identity = &domain.CandidateObservationIdentity{
|
||||
CandidateKey: candidateKey.String,
|
||||
ExecutionID: observation.ExecutionID,
|
||||
CandidateOrdinal: observation.Ordinal,
|
||||
CardSignature: cardSignature.String,
|
||||
DetailSignature: detailSignature.String,
|
||||
DetailEvidenceSHA256: detailHash.String,
|
||||
SpecificationEvidenceSHA256: specificationHash.String,
|
||||
IdentityVersion: int(identityVersion.Int64),
|
||||
CreatedAt: identityCreatedAt,
|
||||
}
|
||||
}
|
||||
dataset.Observations = append(dataset.Observations, observation)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
@@ -823,6 +880,23 @@ func hashCandidateResult(candidates string, recommendation *string) string {
|
||||
return hex.EncodeToString(digest.Sum(nil))
|
||||
}
|
||||
|
||||
func candidateObservationKey(
|
||||
executionID string,
|
||||
candidate usecase.ExecutionCandidate,
|
||||
) string {
|
||||
digest := sha256.New()
|
||||
_, _ = digest.Write([]byte("cmroubao-candidate-v1"))
|
||||
_, _ = digest.Write([]byte{0})
|
||||
_, _ = digest.Write([]byte(executionID))
|
||||
_, _ = digest.Write([]byte{0})
|
||||
_, _ = digest.Write([]byte(strconv.Itoa(candidate.Ordinal)))
|
||||
_, _ = digest.Write([]byte{0})
|
||||
_, _ = digest.Write([]byte(candidate.DetailSignature))
|
||||
_, _ = digest.Write([]byte{0})
|
||||
_, _ = digest.Write([]byte(candidate.SpecificationEvidenceSHA256))
|
||||
return hex.EncodeToString(digest.Sum(nil))
|
||||
}
|
||||
|
||||
func nullableSQLString(value sql.NullString) any {
|
||||
if !value.Valid {
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user