Files
2026-05-22 09:17:39 +08:00

203 lines
6.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package logweb
import (
"database/sql"
"encoding/json"
"fmt"
"xk-hy-transit-go/internal/db"
"xk-hy-transit-go/internal/hy"
)
// RunListItem 同步流水列表项(任务 + 业务 + 最新日志)。
type RunListItem struct {
Job db.SyncJobRow `json:"job,omitempty"`
HasJob bool `json:"hasJob"`
Record db.RecordWithLog `json:"record"`
Step string `json:"step"`
StepLabel string `json:"stepLabel"`
PushStatusLabel string `json:"pushStatusLabel"`
CallbackStatusLabel string `json:"callbackStatusLabel"`
Callback CallbackView `json:"callback"`
Payload JSONBlock `json:"payload"`
Errors JSONBlock `json:"validationErrors"`
Response JSONBlock `json:"responseBody"`
PushPlain JSONBlock `json:"pushPlain"`
HasPushPlain bool `json:"hasPushPlain"`
}
// RecordDetailView 记录详情 API 响应。
type RecordDetailView struct {
Record db.RecordWithLog `json:"record"`
Step string `json:"step"`
StepLabel string `json:"stepLabel"`
PushStatusLabel string `json:"pushStatusLabel"`
CallbackStatusLabel string `json:"callbackStatusLabel"`
Logs []db.PushLogRow `json:"logs"`
Payload JSONBlock `json:"payload"`
Errors JSONBlock `json:"validationErrors"`
Callback CallbackView `json:"callback"`
LogViews []LogEntryView `json:"logViews"`
PushPlain JSONBlock `json:"pushPlain"`
}
// LogEntryView 单条推送日志 + 格式化明文/头/响应。
type LogEntryView struct {
Log db.PushLogRow `json:"log"`
PlainJSON JSONBlock `json:"plainJson"`
Headers JSONBlock `json:"headers"`
Response JSONBlock `json:"responseBody"`
CipherPreview string `json:"cipherPreview,omitempty"`
}
// BuildCallbackView 从记录与最新日志合成回调视图。
func BuildCallbackView(r db.RecordWithLog) CallbackView {
cbStatus := InferCallbackStatus(r.PushStatus)
cb := CallbackView{
BizKey: r.BizKey,
PushStatus: r.PushStatus,
PushStatusLabel: hy.LabelPushStatus(r.PushStatus),
CallbackStatus: cbStatus,
CallbackStatusLabel: hy.LabelCallbackStatus(cbStatus),
Note: "本机未持久化云端 record_id完整 callback_status 以 xk-api 为准",
}
if r.LastError.Valid && r.LastError.String != "" {
cb.ErrorMessage = r.LastError.String
}
if r.LastLog != nil {
cb.HTTPCode = r.LastLog.HTTPCode
if r.LastLog.MsgCode.Valid {
cb.MsgCode = int(r.LastLog.MsgCode.Int64)
}
cb.Msg = r.LastLog.Msg
cb.TraceID = r.LastLog.TraceID
if r.LastLog.ResponseBody.Valid {
cb.ResponseBody = r.LastLog.ResponseBody.String
}
}
return cb
}
func nullStringJSON(ns sql.NullString) JSONBlock {
if !ns.Valid || ns.String == "" {
return JSONBlock{Raw: "", Valid: true}
}
return FormatJSON(ns.String)
}
// ListRuns 组装流水列表:优先按记录查,附带匹配的 job。
func ListRuns(store *db.Store, anchorDate, step string, limit int) ([]RunListItem, error) {
var records []db.RecordWithLog
var err error
if step != "" {
method, mErr := db.MethodForStep(step)
if mErr != nil {
return nil, mErr
}
if anchorDate == "" {
records, err = store.ListRecordsRecent(limit, "")
} else {
records, err = store.ListRecordsByAnchorMethod(anchorDate, method, limit)
}
} else {
records, err = store.ListRecordsRecent(limit, anchorDate)
}
if err != nil {
return nil, err
}
jobs, _ := store.ListJobs(50, anchorDate, step)
jobIndex := make(map[string]db.SyncJobRow)
for _, j := range jobs {
key := j.AnchorDate + "|" + j.Step
jobIndex[key] = j
}
out := make([]RunListItem, 0, len(records))
for _, rec := range records {
st := db.StepForMethod(rec.Method)
cb := BuildCallbackView(rec)
item := RunListItem{
Record: rec,
Step: st,
StepLabel: hy.LabelStep(st),
PushStatusLabel: hy.LabelPushStatus(rec.PushStatus),
CallbackStatusLabel: cb.CallbackStatusLabel,
Callback: cb,
Payload: FormatJSON(rec.PayloadJSON),
Errors: nullStringJSON(rec.ValidationErrors),
}
if rec.LastLog != nil {
if rec.LastLog.ResponseBody.Valid {
item.Response = FormatJSON(rec.LastLog.ResponseBody.String)
}
item.PushPlain = nullStringJSON(rec.LastLog.RequestPlainJSON)
item.HasPushPlain = rec.LastLog.RequestPlainJSON.Valid && rec.LastLog.RequestPlainJSON.String != ""
}
key := rec.AnchorDate + "|" + st
if j, ok := jobIndex[key]; ok {
item.Job = j
item.HasJob = true
}
out = append(out, item)
}
return out, nil
}
// GetRecordDetail 记录详情。
func GetRecordDetail(store *db.Store, recordID int64) (*RecordDetailView, error) {
d, err := store.GetRecordDetail(recordID)
if err != nil {
return nil, err
}
st := db.StepForMethod(d.Record.Method)
cb := BuildCallbackView(d.Record)
view := &RecordDetailView{
Record: d.Record,
Step: st,
StepLabel: hy.LabelStep(st),
PushStatusLabel: hy.LabelPushStatus(d.Record.PushStatus),
CallbackStatusLabel: cb.CallbackStatusLabel,
Logs: d.Logs,
Payload: FormatJSON(d.Record.PayloadJSON),
Errors: nullStringJSON(d.Record.ValidationErrors),
Callback: cb,
}
if len(d.Logs) > 0 {
view.PushPlain = nullStringJSON(d.Logs[0].RequestPlainJSON)
}
for _, l := range d.Logs {
resp := JSONBlock{Raw: "", Valid: true}
if l.ResponseBody.Valid {
resp = FormatJSON(l.ResponseBody.String)
}
lv := LogEntryView{
Log: l,
PlainJSON: nullStringJSON(l.RequestPlainJSON),
Headers: nullStringJSON(l.RequestHeadersJSON),
Response: resp,
}
if l.RequestBodyLen > 0 && l.RequestHeadersJSON.Valid {
// requestBody 在 headers JSON 的 requestBody 字段(密文)
lv.CipherPreview = cipherPreviewFromHeaders(l.RequestHeadersJSON.String, l.RequestBodyLen)
}
view.LogViews = append(view.LogViews, lv)
}
return view, nil
}
func cipherPreviewFromHeaders(headersJSON string, bodyLen int) string {
var h map[string]string
if err := json.Unmarshal([]byte(headersJSON), &h); err != nil {
return fmt.Sprintf("密文长度 %d 字节", bodyLen)
}
rb := h["requestBody"]
if rb == "" {
return fmt.Sprintf("密文长度 %d 字节", bodyLen)
}
if len(rb) > 200 {
return rb[:200] + "…"
}
return rb
}