366 lines
8.5 KiB
Go
366 lines
8.5 KiB
Go
package testweb
|
||
|
||
import (
|
||
"encoding/json"
|
||
"io"
|
||
"os"
|
||
"path/filepath"
|
||
"regexp"
|
||
"strconv"
|
||
"strings"
|
||
"sync"
|
||
)
|
||
|
||
const prescriptionIndexFile = "prescription-audit.jsonl"
|
||
|
||
// 监管 method(与 transit hy.StepMethods 一致)。
|
||
const (
|
||
MethodRecipe = "uploadRecipeIndicators"
|
||
MethodVerification = "uploadRecipeVerificationIndicators"
|
||
)
|
||
|
||
var (
|
||
prescriptionIndexMu sync.Mutex
|
||
filenameRE = regexp.MustCompile(`filename="([^"]+)"`)
|
||
)
|
||
|
||
// PrescriptionRecord 处方相关业务索引行。
|
||
type PrescriptionRecord struct {
|
||
Rel string `json:"rel"`
|
||
BizType string `json:"bizType"`
|
||
Channel string `json:"channel"`
|
||
ServiceMethod string `json:"serviceMethod,omitempty"`
|
||
PrescriptionHint string `json:"prescriptionHint,omitempty"`
|
||
HTTPStatus int `json:"httpStatus"`
|
||
MsgCode int `json:"msgCode,omitempty"`
|
||
TraceID string `json:"traceId,omitempty"`
|
||
FileID string `json:"fileId,omitempty"`
|
||
OK bool `json:"ok"`
|
||
DurationMs int64 `json:"durationMs"`
|
||
At string `json:"at"`
|
||
ErrorSummary string `json:"errorSummary,omitempty"`
|
||
}
|
||
|
||
// AppendPrescriptionFromLogContent 写 api-log 后追加处方业务索引。
|
||
func AppendPrescriptionFromLogContent(root, rel string, raw []byte) error {
|
||
rec, ok := classifyPrescriptionLog(rel, raw)
|
||
if !ok {
|
||
return nil
|
||
}
|
||
return appendPrescriptionRecord(root, rec)
|
||
}
|
||
|
||
func classifyPrescriptionLog(rel string, raw []byte) (PrescriptionRecord, bool) {
|
||
parsed, err := ParseAPILogBytes(rel, raw)
|
||
if err != nil {
|
||
return PrescriptionRecord{}, false
|
||
}
|
||
path := parsed.Meta["Path"]
|
||
kind := parsed.Meta["Kind"]
|
||
if path == "" && kind == "file" {
|
||
path = "/mng/file/auth/upload"
|
||
}
|
||
if path == "" && kind == "supervise" {
|
||
path = "/province/supervise/data"
|
||
}
|
||
|
||
var rec PrescriptionRecord
|
||
rec.Rel = rel
|
||
rec.At = parsed.Meta["Time"]
|
||
if d := parsed.Meta["DurationMs"]; d != "" {
|
||
if n, err := strconv.ParseInt(d, 10, 64); err == nil {
|
||
rec.DurationMs = n
|
||
}
|
||
}
|
||
|
||
switch {
|
||
case path == "/mng/file/auth/upload" || kind == "file":
|
||
rec.BizType = "pdf"
|
||
rec.Channel = "file"
|
||
case path == "/province/supervise/data" || kind == "supervise":
|
||
method := headerVal(parsed.Inbound.Headers, "X-Service-Method")
|
||
if method == "" {
|
||
method = headerVal(parsed.Outbound.Headers, "X-Service-Method")
|
||
}
|
||
rec.ServiceMethod = method
|
||
rec.Channel = "supervise"
|
||
switch method {
|
||
case MethodRecipe:
|
||
rec.BizType = "recipe"
|
||
case MethodVerification:
|
||
rec.BizType = "verification"
|
||
default:
|
||
return PrescriptionRecord{}, false
|
||
}
|
||
default:
|
||
return PrescriptionRecord{}, false
|
||
}
|
||
|
||
fillPrescriptionResponse(&rec, parsed)
|
||
if rec.BizType == "pdf" {
|
||
rec.PrescriptionHint = extractPrescriptionHint(parsed.Inbound.BodyBytes)
|
||
if rec.PrescriptionHint == "" {
|
||
rec.PrescriptionHint = extractPrescriptionHint(parsed.Outbound.BodyBytes)
|
||
}
|
||
}
|
||
return rec, true
|
||
}
|
||
|
||
func fillPrescriptionResponse(rec *PrescriptionRecord, p *ParsedAPILog) {
|
||
resp := p.Response
|
||
rec.HTTPStatus = resp.Status
|
||
if rec.HTTPStatus == 0 {
|
||
if st := p.Meta["Status"]; st != "" {
|
||
if n, err := strconv.Atoi(st); err == nil {
|
||
rec.HTTPStatus = n
|
||
}
|
||
}
|
||
}
|
||
rec.TraceID = headerVal(resp.Headers, "X-Ca-Request-Id")
|
||
if rec.TraceID == "" {
|
||
rec.TraceID = headerVal(resp.Headers, "x-ca-request-id")
|
||
}
|
||
|
||
body := resp.BodyJSON
|
||
if body == "" {
|
||
body = resp.Body
|
||
}
|
||
if body == "" {
|
||
rec.OK = rec.HTTPStatus >= 200 && rec.HTTPStatus < 300
|
||
return
|
||
}
|
||
|
||
var v map[string]any
|
||
if err := json.Unmarshal([]byte(body), &v); err != nil {
|
||
rec.OK = rec.HTTPStatus >= 200 && rec.HTTPStatus < 300
|
||
return
|
||
}
|
||
if rec.BizType == "pdf" {
|
||
fillPDFFromJSON(rec, v)
|
||
return
|
||
}
|
||
fillSuperviseFromJSON(rec, v)
|
||
}
|
||
|
||
func fillPDFFromJSON(rec *PrescriptionRecord, v map[string]any) {
|
||
if success, ok := v["success"].(bool); ok && success {
|
||
rec.OK = true
|
||
} else if rec.HTTPStatus >= 200 && rec.HTTPStatus < 300 {
|
||
rec.OK = true
|
||
}
|
||
if row, ok := v["record"].(map[string]any); ok {
|
||
if fid, ok := row["fileId"].(string); ok {
|
||
rec.FileID = fid
|
||
}
|
||
}
|
||
if !rec.OK {
|
||
if msg, ok := v["message"].(string); ok {
|
||
rec.ErrorSummary = msg
|
||
}
|
||
}
|
||
}
|
||
|
||
func fillSuperviseFromJSON(rec *PrescriptionRecord, v map[string]any) {
|
||
if mc, ok := jsonInt(v["msgCode"]); ok {
|
||
rec.MsgCode = mc
|
||
}
|
||
if code, ok := jsonInt(v["code"]); ok && rec.MsgCode == 0 {
|
||
rec.MsgCode = code
|
||
}
|
||
gwOK := rec.HTTPStatus >= 200 && rec.HTTPStatus < 300
|
||
bizOK := rec.MsgCode == 0 || rec.MsgCode == 200
|
||
if data, ok := v["data"].(map[string]any); ok {
|
||
if mc, ok := jsonInt(data["msgCode"]); ok {
|
||
rec.MsgCode = mc
|
||
bizOK = mc == 0 || mc == 200
|
||
}
|
||
}
|
||
rec.OK = gwOK && bizOK
|
||
if !rec.OK {
|
||
rec.ErrorSummary = firstJSONString(v, "msg", "message", "errorMessage")
|
||
}
|
||
}
|
||
|
||
func firstJSONString(v map[string]any, keys ...string) string {
|
||
for _, k := range keys {
|
||
if s, ok := v[k].(string); ok && s != "" {
|
||
return s
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func jsonInt(v any) (int, bool) {
|
||
switch x := v.(type) {
|
||
case float64:
|
||
return int(x), true
|
||
case int:
|
||
return x, true
|
||
case string:
|
||
n, err := strconv.Atoi(strings.TrimSpace(x))
|
||
return n, err == nil
|
||
default:
|
||
return 0, false
|
||
}
|
||
}
|
||
|
||
func headerVal(h map[string]string, key string) string {
|
||
if h == nil {
|
||
return ""
|
||
}
|
||
for k, v := range h {
|
||
if strings.EqualFold(k, key) {
|
||
return v
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func extractPrescriptionHint(body []byte) string {
|
||
if len(body) == 0 {
|
||
return ""
|
||
}
|
||
text := string(body)
|
||
if m := filenameRE.FindStringSubmatch(text); len(m) > 1 {
|
||
name := m[1]
|
||
if idx := strings.LastIndex(name, "_"); idx >= 0 && idx < len(name)-1 {
|
||
suffix := strings.TrimSuffix(name[idx+1:], ".pdf")
|
||
if suffix != "" && suffix != "prescription" {
|
||
return suffix
|
||
}
|
||
}
|
||
return name
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func appendPrescriptionRecord(root string, rec PrescriptionRecord) error {
|
||
line, err := json.Marshal(rec)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
prescriptionIndexMu.Lock()
|
||
defer prescriptionIndexMu.Unlock()
|
||
path := filepath.Join(root, prescriptionIndexFile)
|
||
f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer f.Close()
|
||
_, err = f.Write(append(line, '\n'))
|
||
return err
|
||
}
|
||
|
||
// RebuildPrescriptionIndex 扫描 api-logs 重建 prescription-audit.jsonl。
|
||
func RebuildPrescriptionIndex(root string) (int, error) {
|
||
root, err := filepath.Abs(root)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
var rels []string
|
||
err = filepath.WalkDir(root, func(path string, d os.DirEntry, walkErr error) error {
|
||
if walkErr != nil {
|
||
return walkErr
|
||
}
|
||
if d.IsDir() {
|
||
return nil
|
||
}
|
||
name := d.Name()
|
||
if (!strings.Contains(name, "_file_") && !strings.Contains(name, "_supervise_")) || !strings.HasSuffix(name, ".log") {
|
||
return nil
|
||
}
|
||
rel, err := filepath.Rel(root, path)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
rel = filepath.ToSlash(rel)
|
||
if !logRelRE.MatchString(rel) {
|
||
return nil
|
||
}
|
||
rels = append(rels, rel)
|
||
return nil
|
||
})
|
||
if err != nil && !os.IsNotExist(err) {
|
||
return 0, err
|
||
}
|
||
|
||
indexPath := filepath.Join(root, prescriptionIndexFile)
|
||
tmpPath := indexPath + ".tmp"
|
||
out, err := os.Create(tmpPath)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
count := 0
|
||
for _, rel := range rels {
|
||
full := filepath.Join(root, filepath.FromSlash(rel))
|
||
raw, err := os.ReadFile(full)
|
||
if err != nil {
|
||
continue
|
||
}
|
||
rec, ok := classifyPrescriptionLog(rel, raw)
|
||
if !ok {
|
||
continue
|
||
}
|
||
line, err := json.Marshal(rec)
|
||
if err != nil {
|
||
continue
|
||
}
|
||
if _, err := out.Write(append(line, '\n')); err != nil {
|
||
_ = out.Close()
|
||
_ = os.Remove(tmpPath)
|
||
return count, err
|
||
}
|
||
count++
|
||
}
|
||
if err := out.Close(); err != nil {
|
||
return count, err
|
||
}
|
||
if err := os.Rename(tmpPath, indexPath); err != nil {
|
||
return count, err
|
||
}
|
||
return count, nil
|
||
}
|
||
|
||
// ListPrescriptionRecords 读取 jsonl 列表。
|
||
func ListPrescriptionRecords(root, bizType string, limit int) ([]PrescriptionRecord, error) {
|
||
if limit <= 0 {
|
||
limit = 100
|
||
}
|
||
path := filepath.Join(root, prescriptionIndexFile)
|
||
f, err := os.Open(path)
|
||
if err != nil {
|
||
if os.IsNotExist(err) {
|
||
return nil, nil
|
||
}
|
||
return nil, err
|
||
}
|
||
defer f.Close()
|
||
raw, err := io.ReadAll(f)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
lines := strings.Split(string(raw), "\n")
|
||
var all []PrescriptionRecord
|
||
for _, line := range lines {
|
||
line = strings.TrimSpace(line)
|
||
if line == "" {
|
||
continue
|
||
}
|
||
var rec PrescriptionRecord
|
||
if err := json.Unmarshal([]byte(line), &rec); err != nil {
|
||
continue
|
||
}
|
||
if bizType != "" && rec.BizType != bizType {
|
||
continue
|
||
}
|
||
all = append(all, rec)
|
||
}
|
||
for i, j := 0, len(all)-1; i < j; i, j = i+1, j-1 {
|
||
all[i], all[j] = all[j], all[i]
|
||
}
|
||
if len(all) > limit {
|
||
all = all[:limit]
|
||
}
|
||
return all, nil
|
||
}
|