228 lines
6.0 KiB
Go
228 lines
6.0 KiB
Go
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)
|
|
}
|
|
}
|