fix(clothes): 保护失败原因并可靠回退额度
仅属主可查看失败原因,分享只读场景置空;额度回退改用独立 ctx,避免客户端断开导致回退失败;新增 ImageProcessStatus 与 FailReason 响应字段,并抽取 detachedCtx 复用后台任务上下文。
This commit is contained in:
+21
-17
@@ -29,23 +29,23 @@ type ClothesDeleteByUUIDResponse struct{}
|
||||
// ClothesUpdateByUUIDRequest 修改衣物
|
||||
type ClothesUpdateByUUIDRequest struct {
|
||||
ClothesUUID string `json:"clothes_uuid" binding:"required"`
|
||||
CategoryMain string `json:"category_main" binding:"max=64"` // 衣物大类 上装/下装/连衣裙/外套/鞋/配饰
|
||||
CategorySub string `json:"category_sub" binding:"max=64"` // 衣物小类 T恤/牛仔裤/运动鞋/发带/戒指等
|
||||
Style string `json:"style" binding:"max=64"` // 风格
|
||||
ColorMain string `json:"color" binding:"max=64"` // 主颜色
|
||||
ColorSub string `json:"color_sub" binding:"max=64"` // 副颜色
|
||||
Thickness string `json:"thickness" binding:"max=64"` // 厚度
|
||||
Version string `json:"version" binding:"max=64"` // 版型 宽松/修身/阔腿/紧身/常规合身
|
||||
Material string `json:"material" binding:"max=64"` // 材质 牛仔布/针织/皮质/纯棉/羊毛等
|
||||
Pattern string `json:"pattern" binding:"max=64"` // 花纹
|
||||
SleeveType string `json:"sleeve_type" binding:"max=64"` // 袖型
|
||||
ClothesType string `json:"clothes_type" binding:"max=64"` // 衣长类型 短款/常规/中长/长款
|
||||
NecklineType string `json:"neckline_type" binding:"max=64"` // 领型
|
||||
Formality string `json:"formality" binding:"max=16"` // 正式程度 1-5
|
||||
Occasions string `json:"occasions" binding:"max=255"` // 适用场合
|
||||
VisualImpact string `json:"visual_impact" binding:"max=16"` // 视觉冲击力 1-5
|
||||
Name string `json:"name" binding:"max=64"` // 名称
|
||||
Remark string `json:"remark" binding:"max=500"` // 备注
|
||||
CategoryMain string `json:"category_main" binding:"max=64"` // 衣物大类 上装/下装/连衣裙/外套/鞋/配饰
|
||||
CategorySub string `json:"category_sub" binding:"max=64"` // 衣物小类 T恤/牛仔裤/运动鞋/发带/戒指等
|
||||
Style string `json:"style" binding:"max=64"` // 风格
|
||||
ColorMain string `json:"color" binding:"max=64"` // 主颜色
|
||||
ColorSub string `json:"color_sub" binding:"max=64"` // 副颜色
|
||||
Thickness string `json:"thickness" binding:"max=64"` // 厚度
|
||||
Version string `json:"version" binding:"max=64"` // 版型 宽松/修身/阔腿/紧身/常规合身
|
||||
Material string `json:"material" binding:"max=64"` // 材质 牛仔布/针织/皮质/纯棉/羊毛等
|
||||
Pattern string `json:"pattern" binding:"max=64"` // 花纹
|
||||
SleeveType string `json:"sleeve_type" binding:"max=64"` // 袖型
|
||||
ClothesType string `json:"clothes_type" binding:"max=64"` // 衣长类型 短款/常规/中长/长款
|
||||
NecklineType string `json:"neckline_type" binding:"max=64"` // 领型
|
||||
Formality string `json:"formality" binding:"max=16"` // 正式程度 1-5
|
||||
Occasions string `json:"occasions" binding:"max=255"` // 适用场合
|
||||
VisualImpact string `json:"visual_impact" binding:"max=16"` // 视觉冲击力 1-5
|
||||
Name string `json:"name" binding:"max=64"` // 名称
|
||||
Remark string `json:"remark" binding:"max=500"` // 备注
|
||||
}
|
||||
|
||||
type ClothesUpdateByUUIDResponse struct{}
|
||||
@@ -76,6 +76,10 @@ type ClothesGetByUUIDResponse struct {
|
||||
Name string `json:"name"`
|
||||
Remark string `json:"remark"`
|
||||
ProcessedImage string `json:"processed_image"`
|
||||
// ImageProcessStatus 处理状态:1=抠图中 2=分类中 3=排队中 4=成功 5=失败
|
||||
ImageProcessStatus int8 `json:"image_process_status"`
|
||||
// FailReason 处理失败原因:仅属主可见,非属主(分享只读)场景为空
|
||||
FailReason string `json:"fail_reason"`
|
||||
}
|
||||
|
||||
// ClothesListRequest 衣物列表
|
||||
|
||||
@@ -172,7 +172,12 @@ func (s *ClosetShareService) Detail(ctx context.Context, userID int64, shareUUID
|
||||
}
|
||||
clothesResp := make([]dto.ClothesGetByUUIDResponse, 0, len(clothesList))
|
||||
for i := range clothesList {
|
||||
clothesResp = append(clothesResp, buildClothesResponse(&clothesList[i]))
|
||||
item := buildClothesResponse(&clothesList[i])
|
||||
// 非属主只读查看:不暴露内部失败原因
|
||||
if !isOwner {
|
||||
item.FailReason = ""
|
||||
}
|
||||
clothesResp = append(clothesResp, item)
|
||||
}
|
||||
|
||||
// 记录接收关系:登录且非属主用户成功访问即视为收到,幂等,失败不阻断详情返回。
|
||||
|
||||
@@ -103,7 +103,7 @@ func (s *ClothesService) Upload(ctx context.Context, userID int64, fileHeader *m
|
||||
originalPath, serr := upload.FileSaveTmpBytes(ctx, imageData, originalName, fmt.Sprintf("pending/%d", userID))
|
||||
if serr != nil {
|
||||
slog.ErrorContext(ctx, "clothes upload: save original image failed", "user_id", userID, "error", serr)
|
||||
refundCosQuota(ctx, userID, 1, 0, "原图持久化失败回退")
|
||||
refundUploadQuotaOnFail(ctx, userID, "原图持久化失败回退")
|
||||
return nil, errcode.UploadFileSaveFailed
|
||||
}
|
||||
clothes := &mysqlmodel.Clothes{
|
||||
@@ -117,8 +117,8 @@ func (s *ClothesService) Upload(ctx context.Context, userID int64, fileHeader *m
|
||||
slog.ErrorContext(ctx, "clothes upload: create clothes record failed",
|
||||
"user_id", userID, "clothes_uuid", clothesUUID, "error", err)
|
||||
_ = os.Remove(originalPath)
|
||||
// 建记录失败,回退已扣额度。
|
||||
refundCosQuota(ctx, userID, 1, 0, "上传失败回退")
|
||||
// 建记录失败,回退已扣额度(独立 ctx:客户端中途断开也要保证退回)。
|
||||
refundUploadQuotaOnFail(ctx, userID, "上传失败回退")
|
||||
return nil, errcode.ClothesCreateFailed
|
||||
}
|
||||
|
||||
@@ -133,6 +133,23 @@ func (s *ClothesService) Upload(ctx context.Context, userID int64, fileHeader *m
|
||||
}, nil
|
||||
}
|
||||
|
||||
// detachedCtx 基于 Background 派生独立 ctx:继承 trace 链路但切断请求取消信号,
|
||||
// 供「与客户端连接解耦」的后台任务与「必须完成」的收尾操作使用。
|
||||
func detachedCtx(parent context.Context, timeout time.Duration) (context.Context, context.CancelFunc) {
|
||||
return context.WithTimeout(
|
||||
trace.ContextWithSpanContext(context.Background(), trace.SpanContextFromContext(parent)),
|
||||
timeout,
|
||||
)
|
||||
}
|
||||
|
||||
// refundUploadQuotaOnFail 用不受请求取消影响的 ctx 回退上传额度:
|
||||
// 客户端中途断开导致请求 ctx 已取消时,仍要保证额度退回成功。
|
||||
func refundUploadQuotaOnFail(parent context.Context, userID int64, reason string) {
|
||||
ctx, cancel := detachedCtx(parent, 30*time.Second)
|
||||
defer cancel()
|
||||
refundCosQuota(ctx, userID, 1, 0, reason)
|
||||
}
|
||||
|
||||
// processImageAsync 后台执行抠图、上传 COS、分类,并回填衣物记录。
|
||||
func (s *ClothesService) processImageAsync(parentCtx context.Context, userID int64, clothesUUID string, imageData []byte, originalPath string) {
|
||||
// 限流:获取信号量,超出并发上限时阻塞等待。
|
||||
@@ -140,7 +157,7 @@ func (s *ClothesService) processImageAsync(parentCtx context.Context, userID int
|
||||
defer func() { <-imageProcessSem }()
|
||||
|
||||
// 派生独立 ctx:继承父 trace 信息,但切断取消信号,避免复用已结束的请求 ctx。
|
||||
ctx, cancel := context.WithTimeout(trace.ContextWithSpanContext(context.Background(), trace.SpanContextFromContext(parentCtx)), 3*time.Minute)
|
||||
ctx, cancel := detachedCtx(parentCtx, 3*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
ctx, span := global.Tracer.Start(ctx, "svc.ClothesProcessImageAsync")
|
||||
@@ -392,6 +409,10 @@ func (s *ClothesService) GetByUUID(ctx context.Context, userID int64, request *d
|
||||
}
|
||||
}
|
||||
response := buildClothesResponse(clothes)
|
||||
// 非属主只读查看(穿搭分享):不暴露内部失败原因
|
||||
if clothes.UserID != userID {
|
||||
response.FailReason = ""
|
||||
}
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
@@ -610,6 +631,9 @@ func buildClothesResponse(c *mysqlmodel.Clothes) dto.ClothesGetByUUIDResponse {
|
||||
Name: c.Name,
|
||||
Remark: c.Remark,
|
||||
ProcessedImage: processed,
|
||||
|
||||
ImageProcessStatus: c.ImageProcessStatus,
|
||||
FailReason: c.FailReason,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
// =============================================================================
|
||||
// Grafana Alloy 采集配置 —— 只采集 zt-closet-backend 的本地日志文件
|
||||
//
|
||||
// ⚠️ 本文件有两种「花括号」,语法规则相反,混用会直接启动失败:
|
||||
// 1) 块(block) endpoint { } / stage.json { } / logging { }
|
||||
// → 参数之间用【换行】分隔,【不能加逗号】
|
||||
// 加了会报:Argument definitions must be separated by newlines, not commas
|
||||
// 2) 对象字面量(= 号右边的表达式)
|
||||
// values = { } / expressions = { } / external_labels = { } / path_targets 里的 { }
|
||||
// → 字段之间【必须用逗号】分隔,否则报:missing ',' in field list
|
||||
// 判断方法:`名字 {` 后面直接换行 = 块;`名字 = {` 是对象字面量。
|
||||
// 改动本文件时务必按此规则,否则 Alloy 起不来。
|
||||
//
|
||||
// 部署方式:Alloy 以 systemd 服务运行(不是 Docker)
|
||||
// 配置路径 /etc/alloy/config.alloy (把本文件拷到这里)
|
||||
// 存储路径 --storage.path=/var/lib/alloy/data(见 systemctl cat alloy)
|
||||
//
|
||||
// 采集对象:lumberjack 滚动写出的 JSON 日志(见 main.go setupSlogLogger)
|
||||
// 宿主路径 /opt/zt-closet/storage/logs/app.log
|
||||
// 容器/服务内读取路径由你挂载或直接指向该目录
|
||||
// 归档 app-*.log.gz(Compress: true,默认不采集,见 loki.source.file 注释)
|
||||
// 实测格式 {"time":"2026-09-09T01:52:52.772742663+08:00","level":"INFO",
|
||||
// "source":{"function":"...","file":"...","line":44},
|
||||
// "msg":"GIN_REQUEST","request_id":"...","method":"GET","path":"/pods",
|
||||
// "query":"","status":404,"ip":"...","latency":"4.559µs","user_agent":""}
|
||||
// =============================================================================
|
||||
|
||||
// 若 Alloy 与应用不在同一文件系统视图下(比如 Alloy 装在别的主机/容器),
|
||||
// 请把下面 local.file_match 里的 __path__ 改成实际可见的路径。
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// logging —— Alloy 自身的运行日志(不是被采集的业务日志)
|
||||
// -----------------------------------------------------------------------------
|
||||
logging {
|
||||
level = "info" // Alloy 自身日志级别:debug / info / warn / error,排查采集问题时改 debug
|
||||
format = "logfmt" // Alloy 自身日志格式,logfmt 便于人读
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// local.file_match —— 「发现」要采集哪些文件,并给它们打上静态标签
|
||||
// 只负责找文件、产出 target 列表,不读内容。
|
||||
// -----------------------------------------------------------------------------
|
||||
local.file_match "app_logs" {
|
||||
// ↓ 这是一个「对象字面量」,字段之间必须用逗号分隔
|
||||
path_targets = [{
|
||||
// ⚠️ 必须限定为 app*.log,不能用 *.log:
|
||||
// 同目录下还有 classification.log(Python 服务输出,是
|
||||
// "INFO:__main__:..." 纯文本 + tqdm 进度条,不是 JSON)。
|
||||
// 用 *.log 会把它一起采走,解析不出任何字段,还会混入 \r 进度条垃圾。
|
||||
__path__ = "/opt/zt-closet/storage/logs/app*.log",
|
||||
|
||||
// 以下三个是自定义标签,会附加到每一条日志上(低基数,可以安全做标签)。
|
||||
job = "zt-closet-backend", // 采集任务名,多服务时用于区分
|
||||
service_name = "zt-closet-backend", // 服务名,推荐保留(Loki 生态的通用标签名)
|
||||
env = "prod", // 环境标识,prod / test / dev
|
||||
}]
|
||||
|
||||
// 每隔多久重新扫描一次目录,发现新建/轮转出来的新文件。
|
||||
// 该值就是官方默认值,保留只为显式可见。
|
||||
sync_period = "10s"
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// loki.source.file —— 真正「读」文件内容,按行产出日志
|
||||
// -----------------------------------------------------------------------------
|
||||
loki.source.file "app_logs" {
|
||||
// 上游 local.file_match 发现的文件列表。
|
||||
targets = local.file_match.app_logs.targets
|
||||
|
||||
// 下游处理管道;解析 / 打标签 / 降噪都在 loki.process 里做。
|
||||
forward_to = [loki.process.app_logs.receiver]
|
||||
|
||||
// 从文件末尾开始读:首次部署时不重放历史日志,
|
||||
// 避免把几十 MB 旧日志一次性灌进 Loki。false 则会从头回放。
|
||||
tail_from_end = true
|
||||
|
||||
// 读取位置(offset)记录在 --storage.path 指定的目录,服务重启后接着读、不重复。
|
||||
// 该目录必须持久化(systemd 下通常是 /var/lib/alloy/data,属主为 alloy 用户),
|
||||
// 否则 offset 丢失,配合 tail_from_end=true 会直接跳过一段日志。
|
||||
|
||||
// ↓↓↓ 可选:需要采集 lumberjack 压缩归档的 app-*.log.gz 时启用
|
||||
// 注意:官方明确说明 tail_from_end 与 decompression 不可同时使用,
|
||||
// 启用下面这段时需要把上面的 tail_from_end 删掉。
|
||||
// decompression {
|
||||
// enabled = true // 注意:块参数只用换行分隔,不要加逗号
|
||||
// initial_delay = "10s" // 文件写入后延迟多久开始解压读取,避免读到半截文件
|
||||
// format = "gz" // 合法值只有 gz / z / bz2
|
||||
// }
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// loki.process —— 日志处理管道:把 JSON 拆成字段,决定哪些做标签、哪些做元数据
|
||||
// 这是整个配置里唯一需要按业务调整的地方。
|
||||
// -----------------------------------------------------------------------------
|
||||
loki.process "app_logs" {
|
||||
// ---- 第一步:解析 JSON:把一行 JSON 的每个 key 提取成可用字段 ----
|
||||
// 左侧是管道内的字段名,右侧是 JMESPath 表达式(点号表示取嵌套字段)。
|
||||
stage.json {
|
||||
// ↓ 对象字面量:字段之间必须用逗号
|
||||
expressions = {
|
||||
time = "time", // 日志自身时间戳(RFC3339Nano),供 stage.timestamp 使用
|
||||
level = "level", // 日志级别:DEBUG/INFO/WARN/ERROR
|
||||
msg = "msg", // 日志消息,如 GIN_REQUEST
|
||||
trace_id = "trace_id", // 链路 ID,仅 slog.XxxContext(ctx,...) 且 ctx 内有 span 时存在
|
||||
span_id = "span_id", // span ID,同上
|
||||
request_id = "request_id", // 单请求 ID,见 middleware/logger.go
|
||||
method = "method", // HTTP 方法
|
||||
path = "path", // ⚠️ 高基数字段:打的是 c.Request.URL.Path 原始路径,
|
||||
// 不是 gin 路由模板,含 UUID 等动态段,
|
||||
// 所以只能做元数据,绝不能提升为标签
|
||||
status = "status", // HTTP 状态码
|
||||
ip = "ip", // 客户端 IP
|
||||
latency = "latency", // 处理耗时,字符串如 "4.559µs"(非数字,不能直接聚合)
|
||||
source_file = "source.file", // 因为 AddSource:true,日志自带 文件:行号
|
||||
source_line = "source.line", // ↑ 同上,定位代码位置用
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 第二步:用日志自身的时间戳,而不是 Alloy 读到它的时间 ----
|
||||
// 不加这个 stage,Loki 会用「采集时刻」当作日志时间:一旦 Alloy 重启、
|
||||
// 限流重试或追赶积压,历史日志会被写成"现在",时间范围查询与
|
||||
// count_over_time 告警全部错位,且没有任何报错提示。
|
||||
// format 用 RFC3339Nano 精确匹配实测的 "2026-09-09T01:52:52.772742663+08:00"。
|
||||
// 注意:若配合 decompression 回放超过 14 天的归档,Loki 会以
|
||||
// "entry too far behind" 拒收整批(reject_old_samples_max_age 默认 14d)。
|
||||
// ⚠️ 注意这是「块」不是对象字面量,参数之间【只能用换行分隔,禁止加逗号】
|
||||
stage.timestamp {
|
||||
source = "time"
|
||||
format = "RFC3339Nano"
|
||||
}
|
||||
|
||||
// ---- 第三步:把 level / status 提升为「索引标签」----
|
||||
// 标签参与索引、筛选最快,但每个不同的标签组合都会形成一个独立 stream。
|
||||
// 这里 level(4 种) × status(约 8 种) ≈ 32 个 stream,量级很小可以接受;
|
||||
// 注意 path 是高基数字段,绝不能加进来。
|
||||
stage.labels {
|
||||
values = {
|
||||
level = "", // 空字符串表示「沿用同名字段」,等价于 level = "level"
|
||||
status = "",
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 第四步:其余字段降级为「结构化元数据」(structured metadata) ----
|
||||
// 元数据不建索引、不加剧 stream 基数,但 LogQL 里照样能用
|
||||
// | trace_id="xxx" 过滤,也能点击跳转 Tempo 查链路。
|
||||
//
|
||||
// ⚠️ 启用前提(缺一不可,否则 distributor 直接返回 400 拒收整批,外部
|
||||
// 表现为「Alloy 不停重试、Loki 里一条日志都没有」,很难定位):
|
||||
// 1) Loki ≥ 3.0(2.9 起为实验特性)
|
||||
// 2) schema 版本 ≥ v13,且索引类型必须为 tsdb(不支持已废弃的 boltdb-shipper)
|
||||
// 3) limits_config.allow_structured_metadata: true
|
||||
// 确认不了就先整段注释掉:其余配置照常工作,只是查询时少了这些字段。
|
||||
// 另外元数据上限为每行 64KB / 128 条,这里 9 个字段远未触及。
|
||||
stage.structured_metadata {
|
||||
values = {
|
||||
trace_id = "",
|
||||
span_id = "",
|
||||
request_id = "", // 按 request_id 串起一次请求的全部日志
|
||||
method = "",
|
||||
path = "", // 高基数字段放这里最合适:可过滤,但不产生新 stream
|
||||
ip = "",
|
||||
latency = "",
|
||||
source_file = "", // 出错时直接看是哪个文件哪一行打的
|
||||
source_line = "",
|
||||
// 注意:status 已在上一 stage 提升为标签,此处不再重复设置,
|
||||
// 避免同一字段既是标签又是元数据导致语义混淆
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 第五步(当前已启用):丢弃 Docker HEALTHCHECK 产生的探针噪音 ----
|
||||
// 若服务以 systemd 直接运行而非 Docker,没有 HEALTHCHECK,这段可删除。
|
||||
// 需要保留这层可用性信号时,也把本段整段删除即可。
|
||||
//
|
||||
// 说明:stage.drop 的 source 只能引用「extracted map」中的字段
|
||||
// (官方文档明确限定为 extracted data,且该 stage 没有 source_type 参数,
|
||||
// 读不到标签、也读不到 structured metadata)。path 正是第一步
|
||||
// stage.json 提取出来的,因此这里可用。多个条件之间是 AND 关系。
|
||||
// 丢弃计数见 Alloy UI:loki_process_dropped_lines_total{reason="healthcheck"}。
|
||||
// ⚠️ 同样这是「块」,参数之间只用换行,禁止加逗号
|
||||
stage.drop {
|
||||
source = "path" // 引用 stage.json 提取出的字段
|
||||
value = "/ping" // 与 source 值完全相等时丢弃
|
||||
drop_counter_reason = "healthcheck"
|
||||
}
|
||||
|
||||
// 处理结果推给出口。
|
||||
forward_to = [loki.write.default.receiver]
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// loki.write —— 出口:把日志批量推送到 Loki 的 push 接口
|
||||
//
|
||||
// 层级注意:顶层只有 external_labels / max_streams;
|
||||
// url、batch_*、*_backoff_*、remote_timeout、tenant_id、basic_auth 等
|
||||
// 都必须写在 endpoint 块内。写到顶层 Alloy 启动即报 unrecognized attribute。
|
||||
// -----------------------------------------------------------------------------
|
||||
loki.write "default" {
|
||||
endpoint {
|
||||
// Loki 写入地址。
|
||||
url = "http://119.45.6.212:3100/loki/api/v1/push"
|
||||
|
||||
// ---- 批量:攒批降低请求数 ----
|
||||
batch_wait = "60s" // 最多攒 60s 才发:请求数最省,但日志最多延迟 1 分钟才可见;
|
||||
// 真实延迟 = min(batch_wait, 攒满 batch_size)。
|
||||
// 需要近实时排障就调到 1s~5s。
|
||||
batch_size = "1MiB" // 攒够 1MiB 立即发,与 batch_wait 谁先满足用谁
|
||||
|
||||
// 请求超时,默认 10s;跨公网写入或 Loki 较慢时可放大。
|
||||
remote_timeout = "10s"
|
||||
|
||||
// ---- 重试:Loki 短暂不可用时退避重试,避免丢日志 ----
|
||||
// 必须保证 min < max:退避实现中一旦 nextDelayMin >= nextDelayMax
|
||||
// 就直接返回 MinBackoff 且永不增长(max 被静默忽略)。
|
||||
// ⚠️ endpoint 是「块」,参数只用换行分隔,禁止加逗号
|
||||
min_backoff_period = "500ms" // 退避起点,官方默认 500ms
|
||||
max_backoff_period = "5m" // 退避上限,官方默认 5m
|
||||
max_backoff_retries = 10 // 重试 10 次后仍失败则丢弃该批
|
||||
|
||||
// ⚠️ 单位陷阱:Go duration 中 m=分钟、ms=毫秒,
|
||||
// "500m" 是 500 分钟(8h20m),"500ms" 才是 500 毫秒。
|
||||
//
|
||||
// 提示:退避不是固定倍数,而是在 [min, max) 区间取随机抖动值、区间再翻倍。
|
||||
// 以 min=500ms / retries=10 为例,实际发生的 9 次等待区间依次为
|
||||
// [0.5s,1s) → [1s,2s) → … → [128s,256s),最大也只到 256s,
|
||||
// 即使把 max 设成 5m 或 500m 也不会触及上限。真正决定「最多重试多久」
|
||||
// 的是 max_backoff_retries。
|
||||
|
||||
// Loki 开启多租户鉴权时才需要:
|
||||
// tenant_id = "zt-closet"
|
||||
// basic_auth {
|
||||
// username = "zt-closet"
|
||||
// password = sys.env("LOKI_PASSWORD") // 密钥只从环境变量读,不要写进本文件
|
||||
// }
|
||||
}
|
||||
|
||||
// 顶层参数:附加到所有日志的全局标签,便于区分集群/机房。
|
||||
external_labels = {
|
||||
cluster = "prod",
|
||||
}
|
||||
}
|
||||
@@ -401,6 +401,10 @@ Authorization: Bearer <access_token>
|
||||
| name | string | 名称 |
|
||||
| remark | string | 备注 |
|
||||
| processed_image | string | 抠图后的图片完整 URL(可直接展示) |
|
||||
| image_process_status | int | 处理状态:`1`=抠图中 `2`=分类中 `3`=排队中 `4`=成功 `5`=失败 |
|
||||
| fail_reason | string | 处理失败原因;**仅属主可见**,非属主(穿搭分享 / 衣柜分享只读)场景为空串 |
|
||||
|
||||
> 衣物**列表与详情**响应均带 `image_process_status`:前端应据此区分「处理中(1/2/3)」与「处理失败(5)」,不要用属性/图片是否为空来推断。`fail_reason` 仅在属主场景返回。
|
||||
|
||||
### 4.3 穿搭对象 `OutfitItem`
|
||||
|
||||
|
||||
Reference in New Issue
Block a user