Files
xk-hy-forward-go/internal/forward/testweb/prescription_index.go
2026-07-21 16:35:46 +08:00

366 lines
8.5 KiB
Go
Raw 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 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
}