diff --git a/cmd/jintai-visitor-proxy/main.go b/cmd/jintai-visitor-proxy/main.go index 760938e..1aafe4d 100644 --- a/cmd/jintai-visitor-proxy/main.go +++ b/cmd/jintai-visitor-proxy/main.go @@ -22,6 +22,8 @@ import ( "strings" "sync" "time" + + qrcode "github.com/skip2/go-qrcode" ) const ( @@ -328,13 +330,118 @@ func renderResultHTML(raw []byte) template.HTML { return template.HTML(`
返回内容不是 JSON,已显示在原始响应里。
`) } + result := value + message := "" + if obj, ok := value.(map[string]any); ok { + if msg, _ := obj["message"].(string); msg != "" { + message = msg + } + if v, ok := obj["result"]; ok { + result = v + } + } + var b strings.Builder b.WriteString(`
`) - b.WriteString(renderAny(value, "查询结果")) + if message != "" { + b.WriteString(`
` + html.EscapeString(message) + `
`) + } + + records := collectRecords(result) + if len(records) > 0 { + for i, record := range records { + b.WriteString(renderVisitorCard(record, i+1, len(records))) + } + b.WriteString(`
详细字段`) + b.WriteString(renderAny(result, "访客记录")) + b.WriteString(`
`) + } else { + b.WriteString(renderAny(result, "访客记录")) + } b.WriteString(`
`) return template.HTML(b.String()) } +func collectRecords(v any) []map[string]any { + switch x := v.(type) { + case []any: + records := make([]map[string]any, 0, len(x)) + for _, item := range x { + if record, ok := item.(map[string]any); ok { + records = append(records, record) + } + } + return records + case map[string]any: + if len(x) == 0 { + return nil + } + return []map[string]any{x} + case string: + var nested any + if json.Unmarshal([]byte(x), &nested) == nil { + return collectRecords(nested) + } + } + return nil +} + +func renderVisitorCard(obj map[string]any, index, total int) string { + name := firstField(obj, "姓名", "访客姓名") + if name == "" { + name = "访客" + } + gender := firstField(obj, "性别", "性别_dict", "性别_dictText") + title := "访客" + if gender == "男" { + title = "先生" + } else if gender == "女" { + title = "女士" + } + + company := firstField(obj, "访客公司", "公司", "单位") + start := firstField(obj, "开始时间", "来访时间") + end := firstField(obj, "结束时间", "离开时间") + reason := firstField(obj, "来访事由_dictText", "来访事由_dict", "来访事由") + host := firstField(obj, "接待人名称", "接待人", "person") + qrText := firstField(obj, "备注", "二维码", "通行码", "访客码", "QRCode", "qrCode", "qr") + qrSrc, qrErr := qrImageDataURL(qrText) + + var b strings.Builder + b.WriteString(`
`) + b.WriteString(`
访客证`) + if total > 1 { + b.WriteString(`` + html.EscapeString(fmt.Sprintf("%d / %d", index, total)) + ``) + } + b.WriteString(`
`) + b.WriteString(`
尊敬的` + html.EscapeString(name) + `` + html.EscapeString(title) + `
`) + if qrSrc != "" { + b.WriteString(`
访客二维码
`) + } else { + msg := "记录里没有可生成二维码的备注字段。" + if qrErr != nil { + msg = qrErr.Error() + } + b.WriteString(`
` + html.EscapeString(msg) + `
`) + } + b.WriteString(`
进园区时,请出示此二维码
`) + b.WriteString(`
` + html.EscapeString(firstNonEmpty(company, "-")) + `
`) + b.WriteString(`
`) + b.WriteString(renderMetaRow("开始时间", start)) + b.WriteString(renderMetaRow("结束时间", end)) + b.WriteString(renderMetaRow("来访事由", reason)) + b.WriteString(renderMetaRow("接待人", host)) + b.WriteString(`
`) + return b.String() +} + +func renderMetaRow(label, value string) string { + if strings.TrimSpace(value) == "" { + return "" + } + return `
` + html.EscapeString(label) + `` + html.EscapeString(value) + `
` +} + func renderAny(v any, title string) string { switch x := v.(type) { case map[string]any: @@ -391,6 +498,31 @@ func renderObject(obj map[string]any, title string) string { return b.String() } +func firstField(obj map[string]any, keys ...string) string { + for _, key := range keys { + if v, ok := obj[key]; ok { + if s := strings.TrimSpace(stringify(v)); s != "" { + return s + } + } + } + return "" +} + +func qrImageDataURL(text string) (string, error) { + text = strings.TrimSpace(text) + if text == "" { + return "", errors.New("记录里没有可生成二维码的备注字段。") + } + + png, err := qrcode.Encode(text, qrcode.Medium, 220) + if err != nil { + return "", fmt.Errorf("二维码生成失败:%w", err) + } + + return "data:image/png;base64," + base64.StdEncoding.EncodeToString(png), nil +} + func stringify(v any) string { switch x := v.(type) { case nil: @@ -568,6 +700,103 @@ var pageTemplate = template.Must(template.New("page").Parse(` .status.err { color: var(--err); } .status.muted { color: var(--muted); } .result-block { margin-top: 18px; } + .visitor-card { + max-width: 390px; + margin: 0 auto 14px; + background: #fff; + border: 1px solid #dcc698; + border-radius: 8px; + box-shadow: 0 18px 50px rgba(85, 61, 23, .14); + overflow: hidden; + } + .card-head { + display: flex; + align-items: center; + justify-content: space-between; + min-height: 48px; + padding: 0 18px; + color: #fff; + background: #8f6f34; + font-size: 18px; + font-weight: 700; + } + .card-head strong { font-size: 13px; font-weight: 600; } + .welcome { + display: flex; + align-items: baseline; + justify-content: center; + gap: 8px; + padding: 20px 18px 12px; + font-size: 16px; + } + .welcome b { font-size: 22px; } + .qr-wrap { + display: flex; + justify-content: center; + padding: 8px 0 10px; + background: #fff; + } + .qr-code { + width: 220px; + height: 220px; + display: block; + image-rendering: pixelated; + } + .qr-missing { + margin: 12px 18px; + padding: 18px; + border: 1px dashed var(--line); + border-radius: 8px; + color: var(--err); + text-align: center; + line-height: 1.6; + background: #fff7f7; + } + .qr-tip { + padding: 0 18px 14px; + color: #8f6f34; + text-align: center; + font-size: 14px; + } + .company { + padding: 14px 18px; + border-top: 1px solid #efe2c8; + border-bottom: 1px solid #efe2c8; + text-align: center; + font-size: 15px; + line-height: 1.6; + word-break: break-word; + } + .card-meta { padding: 12px 18px 16px; } + .meta-row { + display: flex; + justify-content: space-between; + gap: 14px; + padding: 6px 0; + color: var(--muted); + font-size: 14px; + line-height: 1.5; + } + .meta-row strong { + color: var(--text); + font-weight: 500; + text-align: right; + word-break: break-word; + } + .field-details, .raw-json { + margin-top: 12px; + background: #fff; + border: 1px solid var(--line); + border-radius: 8px; + overflow: hidden; + } + .field-details summary, .raw-json summary { + cursor: pointer; + padding: 12px 14px; + color: var(--primary); + font-size: 14px; + user-select: none; + } .api-message, .empty, .single-value { padding: 14px 16px; color: var(--muted); @@ -606,7 +835,7 @@ var pageTemplate = template.Must(template.New("page").Parse(` } .image-text { color: var(--muted); font-size: 12px; } pre { - margin: 18px 0 0; + margin: 0; max-height: 360px; overflow: auto; white-space: pre-wrap; @@ -622,6 +851,15 @@ var pageTemplate = template.Must(template.New("page").Parse(` .captcha-grid, .actions, .fields { grid-template-columns: 1fr; } .field-key { border-bottom: 0; padding-bottom: 4px; } .field-value { padding-top: 4px; } + .visitor-card { max-width: 100%; } + .meta-row { + display: block; + } + .meta-row strong { + display: block; + margin-top: 2px; + text-align: left; + } } @@ -669,7 +907,7 @@ var pageTemplate = template.Must(template.New("page").Parse(` {{.ResultHTML}} - {{if .RawJSON}}
{{.RawJSON}}
{{end}} + {{if .RawJSON}}
原始响应
{{.RawJSON}}
{{end}} `)) diff --git a/go.mod b/go.mod index 2ec0e3d..85393eb 100644 --- a/go.mod +++ b/go.mod @@ -58,6 +58,7 @@ require ( github.com/olekukonko/ll v0.0.9 // indirect github.com/olekukonko/tablewriter v1.1.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect + github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect github.com/syndtr/goleveldb v1.0.0 // indirect github.com/tjfoc/gmsm v1.4.1 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect diff --git a/go.sum b/go.sum index 9d2042d..2f71e15 100644 --- a/go.sum +++ b/go.sum @@ -241,6 +241,8 @@ github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1 h1:NVK+OqnavpyFmUiKfUMHrpvbCi2VFoWTrcpI7aDaJ2I= github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=