feat(t245): use ERP images as procurement references

This commit is contained in:
QiuSW
2026-07-29 17:29:20 +08:00
parent a20590b4b7
commit 15074692a3
22 changed files with 1057 additions and 117 deletions
@@ -5,6 +5,7 @@ import (
"errors"
"strconv"
"strings"
"time"
"unicode/utf8"
"cmroubao/backend-api/internal/domain"
@@ -21,10 +22,25 @@ var (
type ProcurementService struct {
repository ProcurementRepository
store ReferenceImageStore
clock Clock
ids IDGenerator
}
type ProcurementServiceOption func(*ProcurementService) error
func WithProcurementReferenceStore(
store ReferenceImageStore,
) ProcurementServiceOption {
return func(service *ProcurementService) error {
if store == nil {
return errors.New("procurement reference store is required")
}
service.store = store
return nil
}
}
type CreateProcurementRequestCommand struct {
CreatorSubject string
ActorUserID string
@@ -60,15 +76,25 @@ func NewProcurementService(
repository ProcurementRepository,
clock Clock,
ids IDGenerator,
options ...ProcurementServiceOption,
) (*ProcurementService, error) {
if repository == nil || clock == nil || ids == nil {
return nil, errors.New("procurement service dependencies are required")
}
return &ProcurementService{
service := &ProcurementService{
repository: repository,
clock: clock,
ids: ids,
}, nil
}
for _, option := range options {
if option == nil {
return nil, errors.New("procurement service option is required")
}
if err := option(service); err != nil {
return nil, err
}
}
return service, nil
}
func (service *ProcurementService) CreateRequest(
@@ -136,13 +162,27 @@ func (service *ProcurementService) CreateRequest(
request.Status = domain.ProcurementBlocked
request.BlockingCode = &code
}
stored, created, err := service.repository.CreateProcurementRequest(
ctx,
request,
)
var auto *AutoProcurementReference
if request.Status == domain.ProcurementNeedsImage &&
service.store != nil {
auto, err = service.copyFreightImageReference(ctx, request, now)
if err != nil {
return CreateProcurementRequestResult{}, err
}
}
stored, created, referenceUsed, err :=
service.repository.CreateProcurementRequest(
ctx,
request,
auto,
)
if err != nil {
service.cleanupAutomaticReference(auto)
return CreateProcurementRequestResult{}, wrapProcurementError(err)
}
if !referenceUsed {
service.cleanupAutomaticReference(auto)
}
return CreateProcurementRequestResult{
Request: stored,
Replayed: !created,
@@ -207,19 +247,95 @@ func (service *ProcurementService) BindReference(
fields,
)
}
request, err := service.repository.BindProcurementReference(
ctx,
command.CreatorSubject,
command.RequestID,
command.ImageAssetID,
service.clock.Now().UTC(),
)
request, replacedStorageKey, err :=
service.repository.BindProcurementReference(
ctx,
command.CreatorSubject,
command.RequestID,
command.ImageAssetID,
service.clock.Now().UTC(),
)
if err != nil {
return domain.ProcurementRequest{}, wrapProcurementError(err)
}
if replacedStorageKey != nil {
service.deleteStoredReference(*replacedStorageKey)
}
return request, nil
}
func (service *ProcurementService) copyFreightImageReference(
ctx context.Context,
request domain.ProcurementRequest,
now time.Time,
) (*AutoProcurementReference, error) {
image, err := service.repository.GetReadyFreightItemImage(
ctx,
request.CreatorSubject,
request.FreightOrderItemID,
)
if errors.Is(err, ErrRepositoryNotFound) {
return nil, nil
}
if err != nil {
return nil, wrapRepositoryError(err)
}
assetID, err := service.ids.NewID()
if err != nil {
return nil, procurementInternal(err)
}
content, err := service.store.Open(ctx, image.StorageKey)
if err != nil {
return nil, mapImageStoreError(err)
}
normalized, putErr := service.store.Put(
ctx,
assetID,
image.MediaType,
content,
)
closeErr := content.Close()
if putErr != nil {
return nil, mapImageStoreError(putErr)
}
if closeErr != nil {
service.deleteStoredReference(normalized.StorageKey)
return nil, procurementInternal(closeErr)
}
return &AutoProcurementReference{
Asset: domain.Asset{
ID: assetID,
CreatorSubject: request.CreatorSubject,
Purpose: domain.AssetPurposeTaskReference,
MediaType: normalized.MediaType,
SizeBytes: normalized.SizeBytes,
SHA256: normalized.SHA256,
StorageKey: normalized.StorageKey,
CreatedAt: now,
},
ProductThumbRef: image.ProductThumbRef,
SourceImageSHA256: image.SHA256,
SourceImageStorageKey: image.StorageKey,
}, nil
}
func (service *ProcurementService) cleanupAutomaticReference(
auto *AutoProcurementReference,
) {
if auto != nil {
service.deleteStoredReference(auto.Asset.StorageKey)
}
}
func (service *ProcurementService) deleteStoredReference(storageKey string) {
if service.store == nil || storageKey == "" {
return
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = service.store.Delete(ctx, storageKey)
}
func (service *ProcurementService) CreateTask(
ctx context.Context,
command CreateProcurementTaskCommand,