Files
xk-hy-transit-go/internal/sync/sync.go

667 lines
21 KiB
Go
Raw Normal View History

2026-05-22 08:06:07 +08:00
// Package syncer 编排监管中转主流程(与 xk-api HyTransitRecordService 配合)。
//
// 每个 stepconsult/referral/recipe/verification在 runStep 中顺序执行:
//
// ① xkapi.BatchCreate → 云端 xk_hy_transit_batch 建批次
// ② xkapi.Pull → 云端组包并写入 xk_hy_transit_record返回 record_id
// ③ hy.PostForward逐条 → 经 forward-go 报政务云;本地 hy_push_* 留痕
// ④ xkapi.BatchCallback → 整批回写 callback_status非逐条 HTTP
// ⑤ xkapi.BatchFinish → 标记批次 pull 结束
//
// 运营可见状态在云端表;本机表仅用于外网机审计与排错。
package syncer
import (
"encoding/json"
"fmt"
"strings"
2026-05-28 16:39:51 +08:00
"sync"
2026-05-22 08:06:07 +08:00
"time"
"xk-hy-transit-go/internal/applog"
"xk-hy-transit-go/internal/config"
"xk-hy-transit-go/internal/db"
2026-05-28 16:39:51 +08:00
"xk-hy-transit-go/internal/fileauth"
2026-05-22 08:06:07 +08:00
"xk-hy-transit-go/internal/hy"
"xk-hy-transit-go/internal/hyfile"
"xk-hy-transit-go/internal/xkapi"
)
// Runner 单次或定时同步执行器。
type Runner struct {
cfg config.Config
xk *xkapi.Client
store *db.Store
organ hy.OrganConfig
fileUploadURL string // 实际上传地址(经 forward 或直连政务云)
2026-05-28 17:03:08 +08:00
uploadToken string // 本地按 FileAuth.java 生成
2026-05-28 16:39:51 +08:00
cloudUploadToken string // xk-api 预生成值,仅诊断对比
fileBucket string
2026-05-22 08:06:07 +08:00
fileUploadViaForward bool
2026-05-28 16:39:51 +08:00
hyAppKey string
hyAppSecret string
hyAesKey string
lastCloud xkapi.ConfigResponse
lastCloudOK bool
lastConfigErr error
configRefreshedAt time.Time
configMu sync.RWMutex
2026-05-22 08:06:07 +08:00
}
// StoreClose 关闭本机 MySQL 连接。
func (r *Runner) StoreClose() {
if r.store != nil {
_ = r.store.Close()
}
}
2026-05-22 09:17:39 +08:00
// Store 返回本机审计库(供 Web 控制台只读查询)。
func (r *Runner) Store() *db.Store {
return r.store
}
2026-05-22 08:06:07 +08:00
// NewRunner 加载机构配置并创建 Runner含 forward 文件上传地址解析)。
func NewRunner(cfg config.Config, store *db.Store) (*Runner, error) {
2026-05-28 16:39:51 +08:00
fileauth.SetB64Mode(cfg.FileAuthB64)
2026-05-22 08:06:07 +08:00
if p := strings.TrimSpace(cfg.ChromePath); p != "" {
hyfile.SetChromePath(p)
applog.Appf("chrome path from config: %s", p)
}
hyfile.SetUploadInsecureSkipVerify(cfg.FileUploadInsecureSkipVerify)
2026-05-28 16:39:51 +08:00
hyfile.SetUploadMinInterval(cfg.FileUploadMinInterval)
if cfg.FileUploadMinInterval > 0 {
applog.Appf("file upload min interval: %s", cfg.FileUploadMinInterval)
}
2026-05-22 08:06:07 +08:00
xk := xkapi.New(cfg.XkAPIBaseURL, cfg.XkAPIToken, cfg.XkAPICallbackPath)
2026-05-28 16:39:51 +08:00
r := &Runner{
cfg: cfg,
xk: xk,
store: store,
fileUploadViaForward: cfg.FileUploadViaForward,
hyAppKey: cfg.HyAppKey,
hyAppSecret: cfg.HyAppSecret,
hyAesKey: cfg.HyAesKey,
}
if err := r.ReloadCloudConfig(); err != nil {
2026-05-22 08:06:07 +08:00
return nil, fmt.Errorf("load organ config: %w", err)
}
2026-05-28 16:39:51 +08:00
if strings.TrimSpace(r.uploadToken) == "" {
2026-05-22 08:06:07 +08:00
applog.Appf("warning: uploadToken empty, recipe file upload will fail (check xk-api HY_APP_KEY/HY_APP_SECRET/HY_FILE_BUCKET)")
}
if cfg.FileUploadViaForward {
2026-05-28 16:39:51 +08:00
applog.Appf("file upload via forward: %s (cloud fileUploadUrl ignored)", r.fileUploadURL)
}
return r, nil
2026-05-22 08:06:07 +08:00
}
// resolveFileUploadURL 决定 PDF 实际上传地址:默认经 forward-go避免外网机直连政务云 28211。
func resolveFileUploadURL(cfg config.Config, cloudFileUploadURL string) string {
if cfg.FileUploadViaForward {
return hy.ForwardFileUploadURL(cfg.ForwardBaseURL)
}
return strings.TrimSpace(cloudFileUploadURL)
}
// Run 执行一个或多个 step。
func (r *Runner) Run(step, anchorDate string) error {
if step == "all" {
for _, s := range []string{"consult", "referral", "recipe", "verification"} {
if err := r.runStep(s, anchorDate); err != nil {
return err
}
}
return nil
}
return r.runStep(step, anchorDate)
}
// runStep 执行单个监管类型的完整同步(见包注释 ①~⑤)。
func (r *Runner) runStep(step, anchorDate string) error {
2026-06-05 16:36:39 +08:00
_, err := r.executeStep(step, anchorDate, StepRunOptions{ItemLimit: 0, CollectTrace: false})
return err
}
// executeStep 单 step 完整链路;自动与联调共用,开头刷新云端配置与 uploadToken。
func (r *Runner) executeStep(step, anchorDate string, opts StepRunOptions) (*StepTestResult, error) {
2026-05-22 08:06:07 +08:00
method, ok := hy.StepMethods[step]
if !ok {
2026-06-05 16:36:39 +08:00
return nil, fmt.Errorf("unknown step: %s", step)
}
if err := r.ReloadCloudConfig(); err != nil {
return nil, fmt.Errorf("reload config: %w", err)
}
var result *StepTestResult
if opts.CollectTrace {
result = &StepTestResult{
Step: step,
StepLabel: hy.LabelStep(step),
AnchorDate: anchorDate,
}
2026-05-22 08:06:07 +08:00
}
applog.Appf("sync start step=%s date=%s", step, anchorDate)
2026-06-05 16:36:39 +08:00
if r.store != nil {
_, _ = r.store.UpsertJob(anchorDate, step, "running", 0, 0, 0, "")
}
2026-05-22 08:06:07 +08:00
batchID, err := r.xk.BatchCreate(anchorDate, step)
if err != nil {
2026-06-05 16:36:39 +08:00
if r.store != nil {
_ = r.store.FinishJob(anchorDate, step, "failed")
}
2026-05-22 08:06:07 +08:00
applog.Pullf("batch create failed step=%s date=%s err=%v", step, anchorDate, err)
2026-06-05 16:36:39 +08:00
return result, fmt.Errorf("batch create: %w", err)
}
if result != nil {
result.BatchID = batchID
2026-05-22 08:06:07 +08:00
}
applog.Pullf("batch created step=%s date=%s batch_id=%d", step, anchorDate, batchID)
pull, err := r.xk.Pull(step, anchorDate, batchID)
if err != nil {
_ = r.xk.BatchFinish(batchID, "failed")
2026-06-05 16:36:39 +08:00
if r.store != nil {
_ = r.store.FinishJob(anchorDate, step, "failed")
}
2026-05-22 08:06:07 +08:00
applog.Pullf("pull failed step=%s batch_id=%d err=%v", step, batchID, err)
2026-06-05 16:36:39 +08:00
return result, err
2026-05-22 08:06:07 +08:00
}
validationCount := 0
for _, item := range pull.Items {
if len(item.ValidationErrors) > 0 {
validationCount++
}
}
2026-07-15 08:47:41 +08:00
followUpCount := 0
if step == "verification" {
followUpCount = len(pull.FollowUpRecipeItems)
}
applog.Pullf("pull done step=%s batch_id=%d items=%d validation_failed=%d follow_up_recipe=%d", step, batchID, len(pull.Items), validationCount, followUpCount)
2026-06-05 16:36:39 +08:00
if result != nil {
result.PullSummary = PullSummary{
Total: len(pull.Items),
ValidationFailed: validationCount,
}
}
items := pull.Items
if opts.ItemLimit > 0 && len(items) > opts.ItemLimit {
items = items[:opts.ItemLimit]
}
if result != nil {
result.PullSummary.Processed = len(items)
}
2026-05-22 08:06:07 +08:00
var callbacks []xkapi.CallbackItem
success, failed, skipped := 0, 0, 0
2026-07-15 08:47:41 +08:00
appendProcessed := func(processStep, processMethod string, batchItems []xkapi.PullItem) {
for _, item := range batchItems {
var tr *itemTraceCtx
if opts.CollectTrace {
tr = newItemTraceCtx(item)
if item.BizKey == "" && item.Payload != nil {
tr.out.BizKey = hy.BizKey(processStep, anchorDate, item.Payload)
}
}
cb := r.processItem(processStep, anchorDate, processMethod, batchID, item, tr)
callbacks = append(callbacks, cb)
if tr != nil {
tr.finish(cb)
result.Items = append(result.Items, *tr.out)
}
switch cb.PushStatus {
case "success":
success++
case "skipped":
skipped++
default:
failed++
2026-06-05 16:36:39 +08:00
}
}
2026-07-15 08:47:41 +08:00
}
appendProcessed(step, method, items)
if step == "verification" && len(pull.FollowUpRecipeItems) > 0 {
followUpMethod, ok := hy.StepMethods["recipe"]
if !ok {
return result, fmt.Errorf("unknown follow-up step: recipe")
2026-05-22 08:06:07 +08:00
}
2026-07-15 08:47:41 +08:00
applog.Pullf("verification follow-up recipe items=%d", len(pull.FollowUpRecipeItems))
appendProcessed("recipe", followUpMethod, pull.FollowUpRecipeItems)
2026-05-22 08:06:07 +08:00
}
if err := r.xk.BatchCallback(batchID, callbacks); err != nil {
2026-06-05 16:36:39 +08:00
applog.Appf("batch callback failed step=%s batch_id=%d err=%v", step, batchID, err)
if r.store != nil {
_, _ = r.store.UpsertJob(anchorDate, step, "failed", success+failed+skipped, success, failed, err.Error())
_ = r.store.FinishJob(anchorDate, step, "failed")
}
if result != nil {
result.CallbackError = err.Error()
result.Summary = StepOutcomeSummary{Success: success, Failed: failed, Skipped: skipped}
}
_ = r.xk.BatchFinish(batchID, "failed")
return result, fmt.Errorf("batch callback: %w", err)
2026-05-22 08:06:07 +08:00
}
2026-06-05 16:36:39 +08:00
_ = r.xk.BatchFinish(batchID, "done")
2026-05-22 08:06:07 +08:00
total := success + failed + skipped
2026-06-05 16:36:39 +08:00
if r.store != nil {
_, _ = r.store.UpsertJob(anchorDate, step, "done", total, success, failed, "")
_ = r.store.FinishJob(anchorDate, step, "done")
}
2026-05-22 08:06:07 +08:00
applog.Pullf("step done step=%s batch_id=%d total=%d success=%d failed=%d skipped=%d", step, batchID, total, success, failed, skipped)
2026-06-05 16:36:39 +08:00
if result != nil {
result.Summary = StepOutcomeSummary{Success: success, Failed: failed, Skipped: skipped}
}
return result, nil
2026-05-22 08:06:07 +08:00
}
// processItem 处理单条:校验失败则 skipped否则 BuildUpload + PostForward组装 CallbackItem。
2026-06-05 16:36:39 +08:00
func (r *Runner) processItem(step, anchorDate, method string, batchID int, item xkapi.PullItem, tr *itemTraceCtx) xkapi.CallbackItem {
2026-05-22 08:06:07 +08:00
payload := item.Payload
bizKey := item.BizKey
if bizKey == "" && payload != nil {
bizKey = hy.BizKey(step, anchorDate, payload)
}
recordID := item.RecordID
cb := xkapi.CallbackItem{
RecordID: recordID,
BizKey: bizKey,
CallbackStatus: "waiting",
}
if len(item.ValidationErrors) > 0 || payload == nil {
bussID := ""
if payload != nil {
bussID = fmt.Sprintf("%v", payload["bussID"])
}
payloadJSON, _ := json.Marshal(payload)
2026-06-05 16:36:39 +08:00
if r.store != nil {
_ = r.store.SaveRecord(int64(batchID), anchorDate, method, method, bussID, bizKey, "", string(payloadJSON), item.ValidationErrors)
}
2026-05-22 08:06:07 +08:00
cb.PushStatus = "skipped"
cb.CallbackStatus = "failed"
cb.ErrorMessage = "validation failed"
if len(item.ValidationErrors) > 0 {
cb.ErrorMessage = item.ValidationErrors[0]
}
applog.Pullf("skip validation biz_key=%s errors=%v", bizKey, item.ValidationErrors)
return cb
}
2026-06-05 16:36:39 +08:00
if tr != nil {
tr.setPayloadSnapshot(payload)
}
if step == "recipe" {
if metaBool(item.Meta, "needs_recipe_upload") {
if err := r.ensureRecipeFileID(item, payload, tr); err != nil {
if r.store != nil {
_ = r.store.SaveRecord(int64(batchID), anchorDate, method, method, fmt.Sprintf("%v", payload["bussID"]), bizKey, "", mustJSON(payload), nil)
}
cb.PushStatus = "failed"
cb.CallbackStatus = "failed"
cb.ErrorMessage = "recipe file upload: " + err.Error()
applog.Pushf("recipe upload fail biz_key=%s err=%v", bizKey, err)
return cb
}
} else if tr != nil {
tr.setRecipeSkipped("needs_recipe_upload=false")
2026-05-22 08:06:07 +08:00
}
}
payloadJSON, _ := json.Marshal(payload)
2026-06-05 16:36:39 +08:00
if r.store != nil {
_ = r.store.SaveRecord(int64(batchID), anchorDate, method, method, fmt.Sprintf("%v", payload["bussID"]), bizKey, "", string(payloadJSON), nil)
}
2026-05-22 08:06:07 +08:00
2026-05-28 16:39:51 +08:00
r.configMu.RLock()
appKey, appSecret, aesKey := r.hyAppKey, r.hyAppSecret, r.hyAesKey
r.configMu.RUnlock()
req, err := hy.BuildUpload(method, payload, r.organ, appKey, appSecret, aesKey)
2026-05-22 08:06:07 +08:00
if err != nil {
2026-06-05 16:36:39 +08:00
if r.store != nil {
_ = r.store.UpdateRecordStatus(bizKey, "failed", err.Error())
}
2026-05-22 08:06:07 +08:00
cb.PushStatus = "failed"
cb.CallbackStatus = "failed"
cb.ErrorMessage = err.Error()
applog.Pushf("build upload fail biz_key=%s err=%v", bizKey, err)
return cb
}
2026-06-05 16:36:39 +08:00
if tr != nil {
tr.setPushReq(req)
}
2026-05-22 08:06:07 +08:00
start := time.Now()
2026-05-22 08:42:14 +08:00
result, err := hy.PostForward(r.cfg.ForwardBaseURL, req, r.cfg.ForwardSharedSecret)
2026-05-22 08:06:07 +08:00
duration := int(time.Since(start).Milliseconds())
if err != nil {
2026-06-05 16:36:39 +08:00
if r.store != nil {
_ = r.store.UpdateRecordStatus(bizKey, "failed", err.Error())
}
2026-05-22 08:06:07 +08:00
cb.PushStatus = "failed"
cb.CallbackStatus = "failed"
cb.ErrorMessage = err.Error()
applog.Pushf("post forward fail biz_key=%s method=%s err=%v", bizKey, method, err)
return cb
}
2026-06-05 16:36:39 +08:00
if r.store != nil {
recordLocalID, _ := r.store.GetRecordID(bizKey)
headersJSON, _ := json.Marshal(req.Headers)
_ = r.store.SaveLog(recordLocalID, result.HTTPCode, result.MsgCode, result.Msg, result.TraceID, len(req.Body), req.PlainJSON, string(headersJSON), result.Body, duration)
}
2026-05-22 08:06:07 +08:00
cb.HTTPCode = result.HTTPCode
cb.MsgCode = result.MsgCode
cb.Msg = result.Msg
cb.TraceID = result.TraceID
cb.ResponseBody = result.Body
if hy.IsSuperviseOK(result) {
2026-06-05 16:36:39 +08:00
if r.store != nil {
_ = r.store.UpdateRecordStatus(bizKey, "success", "")
}
2026-05-22 08:06:07 +08:00
cb.PushStatus = "success"
cb.CallbackStatus = "success"
applog.Pushf("ok biz_key=%s method=%s code=%d msgCode=%d trace=%s %dms", bizKey, method, result.GatewayCode, result.MsgCode, result.TraceID, duration)
} else {
errText := hy.SuperviseErrorSummary(result)
2026-06-05 16:36:39 +08:00
if r.store != nil {
_ = r.store.UpdateRecordStatus(bizKey, "failed", errText)
}
2026-05-22 08:06:07 +08:00
cb.PushStatus = "failed"
cb.CallbackStatus = "failed"
cb.ErrorMessage = errText
applog.Pushf("fail biz_key=%s method=%s %s %dms", bizKey, method, errText, duration)
}
return cb
}
// ResolveAnchorDate 解析锚定日:命令行优先,否则按配置向前推 N 天。
func ResolveAnchorDate(cfg config.Config, dateArg string) string {
if dateArg != "" {
return dateArg
}
return time.Now().AddDate(0, 0, -cfg.AnchorOffsetDays).Format("2006-01-02")
}
// ensureRecipeFileID 处方 PDF 子流程HTML→PDF→本地落盘→经 forward 上传→写回 recipeFileId。
2026-06-05 16:36:39 +08:00
func (r *Runner) ensureRecipeFileID(item xkapi.PullItem, payload map[string]any, tr *itemTraceCtx) error {
2026-05-22 08:06:07 +08:00
// 步骤 1解析处方 ID必要时从云端拉取打印 HTML
prescriptionID := metaInt(item.Meta, "prescription_id")
if prescriptionID <= 0 {
prescriptionID = metaIntFromPayload(payload, "recipeID")
}
html, _ := item.Meta["recipe_file_html"].(string)
if strings.TrimSpace(html) == "" {
if prescriptionID <= 0 {
return fmt.Errorf("recipe_file_html missing and no prescription_id")
}
applog.Pushf("fetch recipe html prescription_id=%d", prescriptionID)
detail, err := r.xk.GetPrescriptionPrintDetail(prescriptionID)
if err != nil {
return fmt.Errorf("fetch recipe html: %w", err)
}
html = detail.RecipeFileHTML
}
// 步骤 2chromedp 将监管打印 HTML 转为 PDF 字节
applog.Pushf("recipe pdf: prescription_id=%d html_len=%d save_root=%s", prescriptionID, len(html), hyfile.ProgramDir())
pdf, err := hyfile.HtmlToPDF(html)
if err != nil {
return err
}
// 步骤 3落盘到本机 pdf/ 目录,便于运营核对
pdfName := buildPDFNameInput(item.Meta, payload)
var localPath string
if saved, err := hyfile.SavePDFLocal(pdf, pdfName); err != nil {
applog.Pushf("save local pdf warning prescription_id=%d err=%v", prescriptionID, err)
} else {
localPath = saved
applog.Pushf("saved local pdf: %s", saved)
}
2026-06-04 08:15:50 +08:00
// 步骤 4经 forward-go 上传至政务云文件服务,得到 recipeFileId§2.3.4 multipart 字段 file
2026-05-22 08:06:07 +08:00
filename := hyfile.BuildPDFFilename(pdfName)
2026-05-28 16:39:51 +08:00
token, err := r.uploadTokenForFile(filename)
if err != nil {
return err
}
r.configMu.RLock()
url := r.fileUploadURL
r.configMu.RUnlock()
2026-06-05 16:36:39 +08:00
upRes, err := hyfile.UploadPDFDetail(pdf, filename, url, token, r.cfg.ForwardSharedSecret)
if tr != nil {
tr.setRecipeUpload(upRes, localPath, err)
}
2026-05-22 08:06:07 +08:00
if err != nil {
applog.Pushf("upload pdf fail prescription_no=%s local=%s err=%v", pdfName.PrescriptionNo, localPath, err)
return err
}
2026-06-05 16:36:39 +08:00
fileID := upRes.FileID
2026-05-22 08:06:07 +08:00
applog.Pushf("upload pdf ok prescription_no=%s fileId=%s local=%s url=%s", pdfName.PrescriptionNo, fileID, localPath, r.fileUploadURL)
// 步骤 5写入组包 payload并回写云端处方表
payload["recipeFileId"] = fileID
if prescriptionID > 0 {
if err := r.xk.SaveRecipeFile(prescriptionID, fileID, item.RecordID); err != nil {
applog.Pushf("save recipe file cloud warning prescription_id=%d err=%v", prescriptionID, err)
}
}
return nil
}
func metaBool(meta map[string]any, key string) bool {
if meta == nil {
return false
}
switch v := meta[key].(type) {
case bool:
return v
case float64:
return v != 0
case int:
return v != 0
case string:
s := strings.TrimSpace(strings.ToLower(v))
return s == "true" || s == "1" || s == "yes"
default:
return false
}
}
func metaString(meta map[string]any, key string) string {
if meta == nil {
return ""
}
if s, ok := meta[key].(string); ok {
return s
}
return fmt.Sprintf("%v", meta[key])
}
func metaInt(meta map[string]any, key string) int {
if meta == nil {
return 0
}
switch v := meta[key].(type) {
case float64:
return int(v)
case int:
return v
case string:
var n int
fmt.Sscanf(v, "%d", &n)
return n
default:
return 0
}
}
func buildPDFNameInput(meta map[string]any, payload map[string]any) hyfile.PDFNameInput {
in := hyfile.PDFNameInput{At: parseMetaTime(meta, payload)}
in.PatientName = metaString(meta, "patient_name")
if in.PatientName == "" {
if s, ok := payload["patientName"].(string); ok {
in.PatientName = s
}
}
in.StoreName = metaString(meta, "store_name")
in.PrescriptionNo = metaString(meta, "prescription_no")
if in.PrescriptionNo == "" {
in.PrescriptionNo = fmt.Sprintf("%v", payload["recipeUniqueID"])
}
return in
}
func parseMetaTime(meta map[string]any, payload map[string]any) time.Time {
if meta != nil {
if t := hyfile.ParseTimeString(metaString(meta, "created_at")); !t.IsZero() {
return t
}
}
if s, ok := payload["datein"].(string); ok {
if t := hyfile.ParseTimeString(s); !t.IsZero() {
return t
}
}
return time.Now()
}
func metaIntFromPayload(payload map[string]any, key string) int {
switch v := payload[key].(type) {
case float64:
return int(v)
case int:
return v
case string:
var n int
fmt.Sscanf(v, "%d", &n)
return n
default:
return 0
}
}
func mustJSON(payload map[string]any) string {
b, _ := json.Marshal(payload)
return string(b)
}
// FileUploadURL 返回实际使用的文件上传地址upload-test 与排错用)。
func (r *Runner) FileUploadURL() string { return r.fileUploadURL }
// FileUploadViaForward 是否经内网 forward 上传 PDF。
func (r *Runner) FileUploadViaForward() bool { return r.fileUploadViaForward }
// UploadToken 返回监管文件上传凭证。
func (r *Runner) UploadToken() string { return r.uploadToken }
2026-05-22 08:42:14 +08:00
// ForwardSharedSecret 与 forward-go FORWARD_SHARED_SECRET 一致(可选)。
func (r *Runner) ForwardSharedSecret() string { return r.cfg.ForwardSharedSecret }
2026-05-28 16:39:51 +08:00
// Cfg 返回本地环境配置(只读副本)。
func (r *Runner) Cfg() config.Config { return r.cfg }
// FileBucket 返回生效的文件 bucketuploadToken scope
func (r *Runner) FileBucket() string {
r.configMu.RLock()
defer r.configMu.RUnlock()
return r.fileBucket
}
// CloudUploadToken 返回 xk-api 预生成的 uploadToken仅诊断对比
func (r *Runner) CloudUploadToken() string {
r.configMu.RLock()
defer r.configMu.RUnlock()
return r.cloudUploadToken
}
// UploadTokenSource 说明 uploadToken 生成方式。
2026-05-28 17:03:08 +08:00
func (r *Runner) UploadTokenSource() string { return "local-fileauth-java" }
2026-05-28 16:39:51 +08:00
// RegenerateUploadToken 供诊断:用当前凭证重新生成 token与 refreshUploadToken 相同)。
func (r *Runner) RegenerateUploadToken() error {
return r.refreshUploadToken()
}
// RetryRecord 对本机 hy_push_record 整条重试:刷新配置 → 处理 → 回调云端。
func (r *Runner) RetryRecord(localRecordID int64) error {
if r.store == nil {
return fmt.Errorf("no local store")
}
if err := r.ReloadCloudConfig(); err != nil {
return fmt.Errorf("reload config: %w", err)
}
detail, err := r.store.GetRecordDetail(localRecordID)
if err != nil {
return err
}
rec := detail.Record
step := db.StepForMethod(rec.Method)
if step == "" {
return fmt.Errorf("unknown method: %s", rec.Method)
}
method := rec.Method
batchID := int(rec.JobID)
if batchID <= 0 {
return fmt.Errorf("invalid batch_id (job_id) on record %d", localRecordID)
}
var payload map[string]any
if rec.PayloadJSON != "" {
if err := json.Unmarshal([]byte(rec.PayloadJSON), &payload); err != nil {
return fmt.Errorf("parse payload_json: %w", err)
}
}
var validationErrors []string
if rec.ValidationErrors.Valid && rec.ValidationErrors.String != "" {
_ = json.Unmarshal([]byte(rec.ValidationErrors.String), &validationErrors)
}
meta := buildRetryMeta(step, payload)
item := xkapi.PullItem{
RecordID: 0,
BizKey: rec.BizKey,
Payload: payload,
Meta: meta,
ValidationErrors: validationErrors,
}
2026-06-05 16:36:39 +08:00
cb := r.processItem(step, rec.AnchorDate, method, batchID, item, nil)
2026-05-28 16:39:51 +08:00
if err := r.xk.BatchCallback(batchID, []xkapi.CallbackItem{cb}); err != nil {
2026-06-05 16:36:39 +08:00
applog.Appf("retry batch callback failed record_id=%d err=%v", localRecordID, err)
return fmt.Errorf("batch callback: %w", err)
2026-05-28 16:39:51 +08:00
}
_ = r.store.IncRetry(rec.BizKey)
if cb.PushStatus != "success" {
if cb.ErrorMessage != "" {
return fmt.Errorf("%s", cb.ErrorMessage)
}
return fmt.Errorf("retry failed push_status=%s", cb.PushStatus)
}
return nil
}
func buildRetryMeta(step string, payload map[string]any) map[string]any {
meta := map[string]any{}
if step != "recipe" || payload == nil {
return meta
}
rid := metaIntFromPayload(payload, "recipeID")
if rid > 0 {
meta["prescription_id"] = rid
}
if v, ok := payload["recipeFileId"]; !ok || v == nil || fmt.Sprintf("%v", v) == "" {
meta["needs_recipe_upload"] = true
}
return meta
}