小程序一些功能更新

This commit is contained in:
李琦
2026-09-29 10:57:01 +08:00
parent 0cb70a7618
commit 3bb7fa8db3
96 changed files with 13890 additions and 213 deletions

View File

@@ -0,0 +1,218 @@
package logic
import (
"encoding/json"
"reflect"
"strings"
"testing"
"time"
v1 "tool-api/api/user/v1"
"tool-api/internal/consts"
)
// TestJudgeSeatGuardRate 覆盖判据④:≥5 次/分钟 或 ≥20 次/小时 → 4006。
func TestJudgeSeatGuardRate(t *testing.T) {
now := time.Now()
if code := judgeSeatGuard(seatGuardInput{CountLastMinute: 6, PerMinute: 5, PerHour: 20, Now: now}); code != consts.CodeSeatRateLimited {
t.Fatalf("1 分钟第 6 次应 4006,got %d", code)
}
if code := judgeSeatGuard(seatGuardInput{CountLastMinute: 5, PerMinute: 5, PerHour: 20, Now: now}); code != consts.CodeSeatRateLimited {
t.Fatalf("1 分钟第 5 次(≥阈值)应 4006,got %d", code)
}
if code := judgeSeatGuard(seatGuardInput{CountLastHour: 20, PerMinute: 5, PerHour: 20, Now: now}); code != consts.CodeSeatRateLimited {
t.Fatalf("1 小时第 20 次应 4006,got %d", code)
}
if code := judgeSeatGuard(seatGuardInput{CountLastMinute: 4, CountLastHour: 19, PerMinute: 5, PerHour: 20, Now: now}); code != 0 {
t.Fatalf("未达阈值应放行,got %d", code)
}
}
// TestJudgeSeatGuardCooling 覆盖判据⑤:连续失败 5 次 → 4007 冷却 10 分钟;冷却结束恢复。
func TestJudgeSeatGuardCooling(t *testing.T) {
now := time.Now()
in := seatGuardInput{
CountLastMinute: 1, CountLastHour: 3,
ConsecutiveFails: 5, Threshold: 5, CooldownMinutes: 10,
PerMinute: 5, PerHour: 20, Now: now,
}
// 最近一次失败在冷却窗口内 → 4007(即使输入正确也应冷却)
recentFail := now.Add(-1 * time.Minute)
in.LastFailAt = &recentFail
if code := judgeSeatGuard(in); code != consts.CodeSeatCooling {
t.Fatalf("冷却期内应 4007,got %d", code)
}
// 冷却已过 → 放行
oldFail := now.Add(-11 * time.Minute)
in.LastFailAt = &oldFail
if code := judgeSeatGuard(in); code != 0 {
t.Fatalf("冷却结束应放行,got %d", code)
}
// 连续失败不足阈值 → 放行
in.ConsecutiveFails = 4
in.LastFailAt = &recentFail
if code := judgeSeatGuard(in); code != 0 {
t.Fatalf("失败次数不足阈值应放行,got %d", code)
}
}
// TestConsecutiveFails 最近连续失败计数(倒序)。
func TestConsecutiveFails(t *testing.T) {
if n := consecutiveFails([]int{0, 0, 0, 1, 0}); n != 3 {
t.Fatalf("got %d want 3", n)
}
if n := consecutiveFails([]int{1, 0}); n != 0 {
t.Fatalf("got %d want 0", n)
}
if n := consecutiveFails(nil); n != 0 {
t.Fatalf("got %d want 0", n)
}
}
// TestMaskPhone 覆盖判据⑦的脱敏:中间 4 位打码。
func TestMaskPhone(t *testing.T) {
cases := map[string]string{
"13800000000": "138****0000",
"13912345678": "139****5678",
"": "",
"1234": "****",
}
for in, want := range cases {
if got := maskPhone(in); got != want {
t.Fatalf("maskPhone(%q)=%q want %q", in, got, want)
}
}
}
// TestParseStage 舞台解析:无舞台/非法 JSON 返回 nil,不阻断查座。
func TestParseStage(t *testing.T) {
layout := `{"canvas":{"w":1600,"h":1200},"stage":{"x":10,"y":20,"w":300,"h":80,"shape":"rect","rotation":0,"color":"#fff"}}`
s := parseStage(layout)
if s == nil || s.X != 10 || s.W != 300 || s.Shape != "rect" {
t.Fatalf("解析舞台失败: %+v", s)
}
if parseStage("") != nil {
t.Fatalf("空布局应返回 nil")
}
if parseStage("{not json") != nil {
t.Fatalf("非法 JSON 应返回 nil")
}
if parseStage(`{"canvas":{"w":1600,"h":1200}}`) != nil {
t.Fatalf("无 stage 应返回 nil")
}
}
// TestEventSeatQueryParseCanvas 大厅尺寸解析:取 canvas.w/h;空串/非法/≤0 一律回退 1600/1200。
func TestEventSeatQueryParseCanvas(t *testing.T) {
// 设计器/测试样本:canvas:{w:1600,h:1200}
if w, h := parseCanvas(`{"canvas":{"w":1600,"h":1200},"stage":{"x":10,"y":20,"w":300,"h":80,"shape":"rect"}}`); w != 1600 || h != 1200 {
t.Fatalf("标准样本应解析为 1600x1200,got %dx%d", w, h)
}
// 空串 → 回退缺省
if w, h := parseCanvas(""); w != 1600 || h != 1200 {
t.Fatalf("空布局应回退 1600x1200,got %dx%d", w, h)
}
// 非法 JSON → 回退缺省(不阻断)
if w, h := parseCanvas("{not json"); w != 1600 || h != 1200 {
t.Fatalf("非法 JSON 应回退 1600x1200,got %dx%d", w, h)
}
// 值为 0/负数 → 回退缺省
if w, h := parseCanvas(`{"canvas":{"w":0,"h":0}}`); w != 1600 || h != 1200 {
t.Fatalf("非正值应回退缺省,got %dx%d", w, h)
}
// 自定义尺寸 → 原样返回
if w, h := parseCanvas(`{"canvas":{"w":800,"h":600}}`); w != 800 || h != 600 {
t.Fatalf("自定义尺寸应原样返回,got %dx%d", w, h)
}
}
// TestEventSeatQueryResTableCoordTags 固化与前端约定一致的下划线字段名与取值
// (json tag 写错会静默变 undefined → 前端永远走降级;此测试防回归)。
func TestEventSeatQueryResTableCoordTags(t *testing.T) {
b, err := json.Marshal(v1.EventSeatQueryRes{Found: true, TableX: 1100, TableY: 850, HallW: 1600, HallH: 1200})
if err != nil {
t.Fatalf("marshal 失败: %v", err)
}
var m map[string]any
if err := json.Unmarshal(b, &m); err != nil {
t.Fatalf("unmarshal 失败: %v", err)
}
for _, k := range []string{"table_x", "table_y", "hall_w", "hall_h"} {
if _, ok := m[k]; !ok {
t.Fatalf("缺字段 %q(json tag 可能写错): %s", k, string(b))
}
}
if v, _ := m["table_x"].(float64); v != 1100 {
t.Fatalf("table_x=%v want 1100", m["table_x"])
}
if v, _ := m["hall_h"].(float64); v != 1200 {
t.Fatalf("hall_h=%v want 1200", m["hall_h"])
}
// 未命中(零值)时 4 字段应为 0(前端据此降级)
b2, _ := json.Marshal(v1.EventSeatQueryRes{Found: false, Message: seatNotFoundMessage()})
var m2 map[string]any
if err := json.Unmarshal(b2, &m2); err != nil {
t.Fatalf("unmarshal 失败: %v", err)
}
for _, k := range []string{"table_x", "table_y", "hall_w", "hall_h"} {
if v, _ := m2[k].(float64); v != 0 {
t.Fatalf("未命中 %s 应为 0,got %v", k, m2[k])
}
}
}
// TestBuildSeatCells 桌面示意图:只含座位号/是否本人/是否有人。
func TestBuildSeatCells(t *testing.T) {
seats := []seatBrief{
{Id: 1, TableId: 1, SeatNo: 1},
{Id: 2, TableId: 1, SeatNo: 2, EmployeeId: 99},
{Id: 3, TableId: 1, SeatNo: 3},
}
cells := buildSeatCells(seats, 2)
if len(cells) != 3 {
t.Fatalf("len=%d want 3", len(cells))
}
if cells[0].SeatNo != 1 || cells[0].IsSelf || cells[0].Filled {
t.Fatalf("1 号座应有/无人标记正确: %+v", cells[0])
}
if cells[1].SeatNo != 2 || !cells[1].IsSelf || !cells[1].Filled {
t.Fatalf("2 号座应为本人且有人: %+v", cells[1])
}
}
// TestSeatQueryResultMinimized 覆盖判据⑥:返回结构不含任何他人/隐私字段。
func TestSeatQueryResultMinimized(t *testing.T) {
forbidden := map[string]bool{
"name": true, "phone": true, "mobile": true, "nickname": true,
"realname": true, "username": true, "employee_name": true, "employeename": true,
}
seen := map[reflect.Type]bool{}
var walk func(rt reflect.Type, path string)
walk = func(rt reflect.Type, path string) {
if rt.Kind() == reflect.Ptr {
rt = rt.Elem()
}
if rt.Kind() != reflect.Struct || seen[rt] {
return
}
seen[rt] = true
for i := 0; i < rt.NumField(); i++ {
f := rt.Field(i)
tag := strings.Split(f.Tag.Get("json"), ",")[0]
if tag == "" || tag == "-" {
tag = f.Name
}
low := strings.ToLower(tag)
if forbidden[low] {
t.Fatalf("EventSeatQueryRes 含潜在他人/隐私字段: %s.%s", path, tag)
}
walk(f.Type, path+"."+tag)
}
}
walk(reflect.TypeOf(v1.EventSeatQueryRes{}), "res")
// SeatCell 必须恰好 3 个字段(seat_no / is_self / filled)
if n := reflect.TypeOf(v1.SeatCell{}).NumField(); n != 3 {
t.Fatalf("SeatCell 字段数=%d want 3", n)
}
}