修人脸识别排队被误判为失败:识别中应继续等待

问题(162 服务器日志实证):
  ERROR 人脸识别被系统驳回,判定签到失败 userId=1043 message=识别中,预计等待4位
  ERROR 人脸识别被系统驳回,判定签到失败 userId=1043 message=识别中,预计等待8位

「识别中,预计等待N位」是服务端识别队列的进度提示,不是失败。
但 evalFaceIdentify 里只要 recognitionProgress 非空就判 Rejected+Settled,
于是提交后的复核立刻判定签到失败,用户看到报错,实际签到可能仍在排队后成功。

根因:
  if res.Progress != "" { res.Rejected = true; res.Settled = true; return }

修复:
1. 新增 faceProgressIsPending / faceProgressIsTerminalFailure 两个判定:
   - 含「识别中/排队/处理中/等待」等 → 进行中,继续等待;
   - 含「未通过/失败/驳回/人脸不匹配/未检测到人脸」等 → 终止失败;
   - 两者皆无的未知文案 → 保守按进行中处理,不误杀(超时后仍走原有
     「未出结果按成功」兜底);
   - 判定失败前先排除排队文案,避免「识别中」被误伤。
2. handler 轮询策略调整:原先固定 4 轮(0/1/2/3 秒)就收手,排队靠后
   必然等不到结果。现在前 4 轮快速取结果,之后每 3 秒一轮,最长再等
   90 秒;期间把最新排队提示透传给前端(queued=true),用户能看见进度。

测试:用日志里的真实文案断言不判驳回;同时保留 FRS.0036 等真实失败
文案必须判驳回;progressCode=2 无文案时仍判驳回;未知文案不误杀。
This commit is contained in:
becomer
2026-10-17 21:37:07 +08:00
parent 152878cc7c
commit 9bf2d46f84
3 changed files with 171 additions and 6 deletions
+26 -3
View File
@@ -785,12 +785,28 @@ func executeSignTask(c *fiber.Ctx) error {
// 但今日校园侧实为未签到。故此处提交成功后回查详情确认结果,只有真正成功才扣费。
if cpd.NeedsFaceIdentify(detail.Rows) {
sendEvent("verifying", map[string]interface{}{"status": "verifying", "message": "正在核验人脸识别结果..."})
// 轮询等待:0s → 1s → 2s → 3s(即查询时刻 ≈ t0/t1/t3/t6,逐次间隔递增到 3 秒)
// 轮询等待:识别在服务端排队,可能返回「识别中,预计等待N位」。
// 这种情况必须继续等待而不是判失败,因此:
// - 前 4 轮间隔 0/1/2/3 秒,快速拿到常见情形(多数几秒内出结果);
// - 之后转为每 3 秒一轮,最多再等 faceQueueMaxWait,
// 覆盖排队靠后的情况;
// - 期间若出现明确失败文案则立即判失败,出结果(signStatus=4)则成功。
delays := []time.Duration{0, 1 * time.Second, 2 * time.Second, 3 * time.Second}
const faceQueueMaxWait = 90 * time.Second
deadline := time.Now().Add(faceQueueMaxWait)
pendingHint := ""
rejected := false
var rejectMsg string
settledOK := false
for _, d := range delays {
for i := 0; ; i++ {
var d time.Duration
if i < len(delays) {
d = delays[i]
} else if time.Now().Before(deadline) {
d = 3 * time.Second
} else {
break // 等够了,走「未出结果按成功」兜底
}
if d > 0 {
time.Sleep(d)
}
@@ -809,7 +825,14 @@ func executeSignTask(c *fiber.Ctx) error {
settledOK = true
break
}
// 尚未出结果:继续等待下一轮
// 尚未出结果(含「识别中,预计等待N位」排队提示):继续等待。
// 把最新排队提示透传给前端,让用户看到进度而不是干等。
if res.Message != "" && res.Message != pendingHint {
pendingHint = res.Message
sendEvent("verifying", map[string]interface{}{
"status": "verifying", "message": pendingHint, "queued": true,
})
}
}
if rejected {
if rejectMsg == "" {
@@ -120,3 +120,88 @@ func TestRawCodeString(t *testing.T) {
}
}
}
// 线上实证:人脸识别在服务端排队时,recognitionProgress 会返回
// 「识别中,预计等待N位」。这类文案表示仍在进行,绝不能判为驳回。
// 依据 162 服务器日志(v1.2.160 之前版本误判为签到失败)。
func TestEvalFaceIdentifyQueueing(t *testing.T) {
cases := []string{
"识别中,预计等待4位",
"识别中,预计等待8位",
"识别中,预计等待1位",
"识别中",
"排队中,请稍候",
"处理中",
"预计等待3位",
}
for _, msg := range cases {
t.Run(msg, func(t *testing.T) {
got := evalFaceIdentify(map[string]interface{}{
"isFaceIdentify": float64(1),
"recognitionProgress": msg,
"recognitionProgressCode": float64(0),
"signStatus": "0",
})
if got.Rejected {
t.Fatalf("排队文案 %q 不应判为驳回", msg)
}
if got.Settled {
t.Fatalf("排队文案 %q 不应视为已出结果", msg)
}
if got.Message != msg {
t.Fatalf("Message 应透传排队提示,got %q want %q", got.Message, msg)
}
})
}
}
// 真正的失败文案仍必须判驳回,不能因为放宽而漏判。
func TestEvalFaceIdentifyRealFailure(t *testing.T) {
cases := []string{
"系统自动驳回。被驳回原因:网络连接故障,请重新尝试签到(FRS.0036)",
"人脸识别未通过",
"人脸不匹配",
"未检测到人脸",
"签到失败",
}
for _, msg := range cases {
t.Run(msg, func(t *testing.T) {
got := evalFaceIdentify(map[string]interface{}{
"isFaceIdentify": float64(1),
"recognitionProgress": msg,
"signStatus": "0",
})
if !got.Rejected {
t.Fatalf("失败文案 %q 应判驳回", msg)
}
if !got.Settled {
t.Fatalf("失败文案 %q 应视为已出结果", msg)
}
})
}
}
// 未识别文案保守处理:不确定就当进行中,不误杀正常签到。
func TestEvalFaceIdentifyUnknownMessageNotRejected(t *testing.T) {
got := evalFaceIdentify(map[string]interface{}{
"isFaceIdentify": float64(1),
"recognitionProgress": "系统繁忙,请稍后再试一下",
"signStatus": "0",
})
if got.Rejected {
t.Fatal("未知文案不应判驳回(保守当进行中)")
}
}
// recognitionProgressCode=2 仍应独立判驳回(无文案时靠码判断)。
func TestEvalFaceIdentifyCode2WithoutMessage(t *testing.T) {
got := evalFaceIdentify(map[string]interface{}{
"isFaceIdentify": float64(1),
"recognitionProgressCode": float64(2),
"recognitionProgress": "",
"signStatus": "0",
})
if !got.Rejected || !got.Settled {
t.Fatalf("progressCode=2 应判驳回,got %+v", got)
}
}
+59 -2
View File
@@ -565,11 +565,23 @@ func evalFaceIdentify(rows map[string]interface{}) SignCheckResult {
res.Progress = strings.TrimSpace(mapValueStr(rows, "recognitionProgress"))
res.ProgressCode = mapValueInt(rows["recognitionProgressCode"])
// 有驳回文案 → 明确驳回
// 有进展文案时不能一律判驳回:今日校园在服务端识别排队期间,
// recognitionProgress 会是「识别中,预计等待4位」这类排队提示
// (线上日志实证)。此时应视为「仍在进行中」,交由调用方继续等待。
// 只有文案本身表达失败/驳回语义时才判定为驳回。
if res.Progress != "" {
res.Message = res.Progress
if faceProgressIsTerminalFailure(res.Progress) {
res.Rejected = true
res.Settled = true
res.Message = res.Progress
return res
}
if faceProgressIsPending(res.Progress) {
// 明确排队中:未出结果,继续等待
return res
}
// 无法识别的文案:保守按「仍在进行中」处理,不误杀;
// 上层轮询到超时后按提交成功处理(现有兜底逻辑)。
return res
}
// 有明确进展码(非 0)→ 已出结果;2 为驳回,其余视为已处理
@@ -1661,3 +1673,48 @@ func SubmitSign(cookieMap map[string]string, host string, taskType TaskType, sub
return nil
}
// faceProgressIsPending 判断 receptionProgress 文案是否表示「仍在排队/识别中」。
//
// 线上实证文案:「识别中,预计等待4位」「识别中,预计等待8位」。
// 这类提示表示服务端识别队列尚未处理完,不是失败,必须继续等待。
func faceProgressIsPending(msg string) bool {
s := strings.TrimSpace(msg)
if s == "" {
return false
}
// 常见排队/进行中关键词
pendingWords := []string{
"识别中", "排队中", "排队", "处理中", "等待", "预计等待", "审核中",
}
for _, w := range pendingWords {
if strings.Contains(s, w) {
return true
}
}
return false
}
// faceProgressIsTerminalFailure 判断文案是否明确表示失败/驳回(终止态)。
//
// 只有明确的否定结论才算终止,避免把排队提示误判成失败。
func faceProgressIsTerminalFailure(msg string) bool {
s := strings.TrimSpace(msg)
if s == "" {
return false
}
// 先排除排队类文案(可能同时含"识别"字样)
if faceProgressIsPending(s) {
return false
}
failWords := []string{
"未通过", "不通过", "失败", "驳回", "驳回申请", "无效",
"未检测到人脸", "人脸不匹配", "请重试", "请重新", "超时",
}
for _, w := range failWords {
if strings.Contains(s, w) {
return true
}
}
return false
}