feat(t240): cache freight item images

This commit is contained in:
QiuSW
2026-07-29 15:12:05 +08:00
parent 5f17fc11aa
commit c7e152aaa1
28 changed files with 1728 additions and 30 deletions
@@ -30,6 +30,7 @@ type AdminServices struct {
Results *usecase.ExecutionResultService
Authorizations *usecase.OrderAuthorizationService
Freight *usecase.FreightService
FreightImages *usecase.FreightImageService
Procurement *usecase.ProcurementService
ERP *shunyunbao.SessionManager
}
@@ -75,6 +76,12 @@ func registerAdminAPI(routes gin.IRoutes, services AdminServices) error {
routes.GET("/api/v1/freight-orders", handler.listFreightOrders)
routes.GET("/api/v1/freight-orders/:id", handler.freightOrderDetail)
}
if services.FreightImages != nil {
routes.GET(
"/api/v1/freight-items/:id/image",
handler.freightItemImage,
)
}
if services.Procurement != nil {
routes.POST(
"/api/v1/freight-items/:id/procurement-request",
@@ -92,6 +99,29 @@ func registerAdminAPI(routes gin.IRoutes, services AdminServices) error {
return nil
}
func (h *adminHandlers) freightItemImage(ctx *gin.Context) {
result, err := h.services.FreightImages.OpenItemImage(
ctx.Request.Context(),
localAdminSubject,
ctx.Param("id"),
)
if err != nil {
writeUsecaseError(ctx, err)
return
}
defer result.Content.Close()
ctx.Header("Cache-Control", "private, no-store")
ctx.Header("Content-Type", result.Image.MediaType)
ctx.Header(
"Content-Length",
strconv.FormatInt(result.Image.SizeBytes, 10),
)
ctx.Header("ETag", `"`+result.Image.SHA256+`"`)
ctx.Header("X-Content-Type-Options", "nosniff")
ctx.Status(http.StatusOK)
_, _ = io.Copy(ctx.Writer, result.Content)
}
func (h *adminHandlers) evidenceContent(ctx *gin.Context) {
result, err := h.services.Results.OpenEvidence(
ctx.Request.Context(),
@@ -479,6 +479,41 @@ func TestAdminFreightAPIImportsAllItemsWithoutPII(t *testing.T) {
detail.Header().Get("Cache-Control") != "no-store" {
t.Fatalf("freight detail status/body = %d / %s", detail.Code, detail.Body)
}
var detailBody struct {
Items []struct {
ID string `json:"id"`
ImageStatus string `json:"image_status"`
ImageURL string `json:"image_url"`
} `json:"items"`
}
decodeResponse(t, detail, &detailBody)
if len(detailBody.Items) != 2 ||
detailBody.Items[0].ImageStatus != "READY" ||
detailBody.Items[0].ImageURL == "" {
t.Fatalf("freight detail images = %+v", detailBody.Items)
}
imageResponse := performAdminRequest(
t,
router,
http.MethodGet,
detailBody.Items[0].ImageURL,
"",
nil,
"",
)
if imageResponse.Code != http.StatusOK ||
imageResponse.Header().Get("Content-Type") != "image/jpeg" ||
imageResponse.Header().Get("Cache-Control") != "private, no-store" ||
imageResponse.Header().Get("X-Content-Type-Options") != "nosniff" ||
imageResponse.Header().Get("ETag") == "" ||
!bytes.HasPrefix(imageResponse.Body.Bytes(), []byte{0xff, 0xd8}) {
t.Fatalf(
"freight image status/headers/body = %d / %#v / %x",
imageResponse.Code,
imageResponse.Header(),
imageResponse.Body.Bytes(),
)
}
replay := performAdminRequest(
t,
router,
@@ -875,6 +910,9 @@ func TestAdminOrderAuthorizationIsIdempotentAndRevisioned(t *testing.T) {
if err != nil {
t.Fatalf("migration.New() error = %v", err)
}
if err := runner.Down(context.Background()); err != nil {
t.Fatalf("image migration down: %v", err)
}
if err := runner.Down(context.Background()); err != nil {
t.Fatalf("metadata migration down: %v", err)
}
@@ -1211,12 +1249,22 @@ func newAdminIntegrationFixture(t *testing.T) *adminIntegrationFixture {
if err != nil {
t.Fatalf("usecase.NewOrderAuthorizationService() error = %v", err)
}
freightImages, err := usecase.NewFreightImageService(
repositories,
staticFreightSource{},
files,
clock,
)
if err != nil {
t.Fatalf("usecase.NewFreightImageService() error = %v", err)
}
freight, err := usecase.NewFreightService(
repositories,
staticFreightSource{},
clock,
ids,
time.Second,
usecase.WithFreightImageCache(freightImages),
)
if err != nil {
t.Fatalf("usecase.NewFreightService() error = %v", err)
@@ -1236,6 +1284,7 @@ func newAdminIntegrationFixture(t *testing.T) *adminIntegrationFixture {
Results: results,
Authorizations: authorizations,
Freight: freight,
FreightImages: freightImages,
Procurement: procurement,
},
emptyAdminWeb{},
@@ -1270,6 +1319,8 @@ func (staticFreightSource) QueryOrder(
quantityTwo := 2
priceOne := int64(12950)
priceTwo := int64(8800)
thumbOne := "190"
thumbTwo := "191"
return domain.FreightSourceBatch{
SchemaVersion: 1,
Query: domain.FreightSourceQuery{
@@ -1289,6 +1340,7 @@ func (staticFreightSource) QueryOrder(
Quantity: &quantityOne,
OriginalUnitPriceMinor: &priceOne,
OriginalCurrency: domain.FreightCurrencyTWD,
ProductThumbRef: &thumbOne,
},
{
ExternalItemID: "89",
@@ -1298,12 +1350,41 @@ func (staticFreightSource) QueryOrder(
Quantity: &quantityTwo,
OriginalUnitPriceMinor: &priceTwo,
OriginalCurrency: domain.FreightCurrencyTWD,
ProductThumbRef: &thumbTwo,
},
},
}},
}, nil
}
func (staticFreightSource) FetchProductImage(
context.Context,
string,
) (usecase.FreightSourceImage, error) {
var imageBytes bytes.Buffer
source := image.NewRGBA(image.Rect(0, 0, 8, 6))
for y := 0; y < 6; y++ {
for x := 0; x < 8; x++ {
source.Set(
x,
y,
color.RGBA{R: uint8(x * 20), G: 80, B: 160, A: 255},
)
}
}
if err := jpeg.Encode(
&imageBytes,
source,
&jpeg.Options{Quality: 85},
); err != nil {
return usecase.FreightSourceImage{}, err
}
return usecase.FreightSourceImage{
Content: io.NopCloser(bytes.NewReader(imageBytes.Bytes())),
MediaType: "image/jpeg",
}, nil
}
func (staticFreightSource) QueryCreatedRange(
_ context.Context,
createdFrom, createdTo string,
@@ -713,6 +713,9 @@ func TestDeviceExecutionResultsAreIdempotentAndAuditable(t *testing.T) {
if err != nil {
t.Fatalf("migration.New() after review error = %v", err)
}
if err := runner.Down(context.Background()); err != nil {
t.Fatalf("image migration down: %v", err)
}
if err := runner.Down(context.Background()); err != nil {
t.Fatalf("metadata migration down: %v", err)
}
@@ -739,8 +742,8 @@ func TestDeviceExecutionResultsAreIdempotentAndAuditable(t *testing.T) {
}
if applied, err := runner.Up(context.Background()); err != nil {
t.Fatalf("restore device command migration: %v", err)
} else if applied != 7 {
t.Fatalf("restored migrations = %d, want 7", applied)
} else if applied != 8 {
t.Fatalf("restored migrations = %d, want 8", applied)
}
completePayload := fmt.Sprintf(
@@ -1429,6 +1432,9 @@ func TestDeviceOrderCommandDeliveryAndAcknowledgementAreRecoverable(
if err != nil {
t.Fatalf("migration.New() error = %v", err)
}
if err := runner.Down(context.Background()); err != nil {
t.Fatalf("image migration down: %v", err)
}
if err := runner.Down(context.Background()); err != nil {
t.Fatalf("metadata migration down: %v", err)
}
@@ -205,6 +205,10 @@ func (h *adminHandlers) freightOrderDetail(ctx *gin.Context) {
}
items := make([]gin.H, 0, len(detail.Items))
for _, item := range detail.Items {
var imageURL any
if item.ImageStatus == domain.FreightItemImageReady {
imageURL = "/api/v1/freight-items/" + item.ID + "/image"
}
items = append(items, gin.H{
"id": item.ID,
"external_item_id": item.ExternalItemID,
@@ -215,6 +219,9 @@ func (h *adminHandlers) freightOrderDetail(ctx *gin.Context) {
"product_thumb_ref": item.ProductThumbRef,
"original_unit_price_minor": item.OriginalUnitPriceMinor,
"original_currency": item.OriginalCurrency,
"image_status": item.ImageStatus,
"image_error_code": item.ImageErrorCode,
"image_url": imageURL,
"purchase_status": item.PurchaseStatus,
"revision": item.Revision,
"canonical_sha256": item.CanonicalSHA256,