package main import ( "encoding/json" "flag" "fmt" "os" "sort" "strconv" "strings" "time" ) type remoteOrder struct { OrderNo string `json:"order_no"` OutOrderNo string `json:"out_order_no"` OrderMoney json.Number `json:"order_money"` OrderStatus string `json:"order_status"` PaymentTime *int64 `json:"payment_time"` CreateTime int64 `json:"create_time"` } type recoveryFile struct { Orders []recoveryOrder `json:"orders"` } type recoveryOrder struct { OutOrderNo string `json:"out_order_no"` OrderNo string `json:"order_no"` OrderStatus string `json:"order_status"` SubscribeId int64 `json:"subscribe_id"` ExpireAt int64 `json:"expire_at"` Quantity int64 `json:"quantity"` PaidAt int64 `json:"paid_at,omitempty"` Note string `json:"note,omitempty"` PaymentOrderNo string `json:"payment_order_no,omitempty"` OrderMoneyCents int64 `json:"order_money_cents,omitempty"` } type adminOrderResponse struct { Code uint32 `json:"code"` Msg string `json:"msg"` Data struct { Total int64 `json:"total"` List []adminOrder `json:"list"` } `json:"data"` } type adminOrder struct { OrderNo string `json:"order_no"` } func main() { var ( input string output string adminOrders string subscribeID int64 onlyMissing bool ) flag.StringVar(&input, "in", "aaa.json", "input payment platform orders JSON") flag.StringVar(&output, "out", "internal/recovery/recovery_orders.json", "output recovery orders JSON") flag.StringVar(&adminOrders, "admin-orders", "", "optional admin order list response JSON; when provided, matched out_order_no values are skipped") flag.Int64Var(&subscribeID, "subscribe-id", 1, "subscribe id to use for recovered subscriptions") flag.BoolVar(&onlyMissing, "only-missing", true, "only output payment success orders missing from admin order list when -admin-orders is set") flag.Parse() orders, err := readOrders(input) must(err) existingOrders := map[string]struct{}{} if adminOrders != "" { existingOrders, err = readAdminOrderSet(adminOrders) must(err) } daysByAmount := map[int64]int64{ 1953: 7, 4193: 30, 9093: 90, 31500: 365, } out := recoveryFile{Orders: make([]recoveryOrder, 0, len(orders))} var skipped []string var matchedExisting int seen := make(map[string]struct{}) for _, item := range orders { if strings.ToLower(strings.TrimSpace(item.OrderStatus)) != "success" { continue } localOrderNo := strings.TrimSpace(item.OutOrderNo) if localOrderNo == "" { skipped = append(skipped, fmt.Sprintf("%s missing out_order_no", item.OrderNo)) continue } if _, ok := seen[localOrderNo]; ok { skipped = append(skipped, fmt.Sprintf("%s duplicate out_order_no", localOrderNo)) continue } if _, exists := existingOrders[localOrderNo]; exists && onlyMissing { matchedExisting++ seen[localOrderNo] = struct{}{} continue } amount := decimalYuanToCents(item.OrderMoney) days, ok := daysByAmount[amount] if !ok { skipped = append(skipped, fmt.Sprintf("%s unknown amount %s", localOrderNo, item.OrderMoney.String())) continue } base := item.CreateTime if item.PaymentTime != nil && *item.PaymentTime > 0 { base = *item.PaymentTime } if base <= 0 { skipped = append(skipped, fmt.Sprintf("%s missing payment/create time", localOrderNo)) continue } seen[localOrderNo] = struct{}{} out.Orders = append(out.Orders, recoveryOrder{ OutOrderNo: localOrderNo, OrderNo: strings.TrimSpace(item.OrderNo), OrderStatus: "success", SubscribeId: subscribeID, ExpireAt: time.Unix(base, 0).Add(time.Duration(days) * 24 * time.Hour).UnixMilli(), Quantity: days, PaidAt: base, Note: "data recovery", PaymentOrderNo: strings.TrimSpace(item.OrderNo), OrderMoneyCents: amount, }) } sort.Slice(out.Orders, func(i, j int) bool { return out.Orders[i].OutOrderNo < out.Orders[j].OutOrderNo }) must(writeJSON(output, out)) fmt.Printf("converted recovery orders=%d matched_existing=%d skipped=%d output=%s\n", len(out.Orders), matchedExisting, len(skipped), output) for _, msg := range skipped { fmt.Printf("skip: %s\n", msg) } } func readAdminOrderSet(path string) (map[string]struct{}, error) { data, err := os.ReadFile(path) if err != nil { return nil, err } var resp adminOrderResponse if err := json.Unmarshal(data, &resp); err == nil && resp.Data.List != nil { if resp.Code != 0 && resp.Code != 200 { return nil, fmt.Errorf("admin order response failed: code=%d msg=%s", resp.Code, resp.Msg) } return adminOrdersToSet(resp.Data.List), nil } var list []adminOrder if err := json.Unmarshal(data, &list); err != nil { return nil, err } return adminOrdersToSet(list), nil } func adminOrdersToSet(list []adminOrder) map[string]struct{} { set := make(map[string]struct{}, len(list)) for _, item := range list { orderNo := strings.TrimSpace(item.OrderNo) if orderNo != "" { set[orderNo] = struct{}{} } } return set } func readOrders(path string) ([]remoteOrder, error) { data, err := os.ReadFile(path) if err != nil { return nil, err } var orders []remoteOrder if err := json.Unmarshal(data, &orders); err != nil { return nil, err } return orders, nil } func writeJSON(path string, data recoveryFile) error { bytes, err := json.MarshalIndent(data, "", " ") if err != nil { return err } bytes = append(bytes, '\n') return os.WriteFile(path, bytes, 0644) } func decimalYuanToCents(n json.Number) int64 { s := strings.TrimSpace(n.String()) if s == "" { return 0 } parts := strings.SplitN(s, ".", 2) yuan, _ := strconv.ParseInt(parts[0], 10, 64) cents := yuan * 100 if len(parts) == 1 { return cents } frac := parts[1] if len(frac) > 2 { frac = frac[:2] } for len(frac) < 2 { frac += "0" } f, _ := strconv.ParseInt(frac, 10, 64) if strings.HasPrefix(parts[0], "-") { return cents - f } return cents + f } func must(err error) { if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } }