This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user