Skip to content

Commit 7f7b72c

Browse files
committed
新增「抢到后自动付款」开关,默认关
checkout 一直是 autoPayWithPreferredPaymentMethod:false —— 下单成功后要用户 自己在订单过期前付款,半夜抢到的机器可能因为没人醒着付款而作废。现在把 auto-pay 做成显式开关(schema 描述:"order will be automatically paid with preferred payment method",需要 OVH 账户已设置默认支付方式)。 默认不自动付款。自动扣钱必须是用户显式打开的,不能是隐含行为: 字段默认 false、数据库列 DEFAULT 0、三个界面的勾选框默认不勾、 Telegram 文本下单和批量订阅不提供此开关(没有界面确认的路径不给扣钱能力)。 开关在三个下单入口各自独立: - 服务器下单对话框(逐任务) - 服务器监控订阅(触发自动下单时生效,可 PUT 编辑) - VPS 监控订阅(同上) 自动付款依附于自动下单:关掉自动下单时强制清掉自动付款。 通知按真实状态措辞:开了开关的说「已请求自动付款,请核对扣款是否成功」—— 只承诺我们真正知道的(请求了≠扣成了,默认支付方式失效时 OVH 不会扣); 没开的仍提醒「订单尚未付款,逾期作废」。队列任务和订阅卡片都显示 「自动付款」标记,让人一眼看出哪些任务会动钱。 链路:界面开关 → 队列任务/订阅字段(落库,重启保留) → 监控触发时经 quick-order 透传 → checkout。端到端验证过建/改/重启的完整往返 —— 顺带修掉一个快照深拷贝漏字段的 bug(Snapshot 少拷 AutoPay, GET 永远读到 false,靠实测抓出来的)。
1 parent 28a4ea5 commit 7f7b72c

25 files changed

Lines changed: 165 additions & 38 deletions

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.1.9
1+
0.1.10

server/internal/db/db.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,13 @@ func (db *DB) migrate() error {
7171
// retraction_time:撤销权截止日。老库没有这一列,而 ListHistory 用 SELECT * + sqlx 严格映射,
7272
// 只加结构体字段不加列会让整个历史列表报 missing destination name,两者必须同一次上线。
7373
// failure_count:只记真正提交并失败的次数(无货轮次不计),MaxRetries 靠它封顶
74+
// auto_pay:下单后用默认支付方式自动付款的显式开关
75+
if err := db.addColumnIfMissing("queue", "auto_pay", "INTEGER NOT NULL DEFAULT 0"); err != nil {
76+
return err
77+
}
78+
if err := db.addColumnIfMissing("monitor_subscriptions", "auto_pay", "INTEGER NOT NULL DEFAULT 0"); err != nil {
79+
return err
80+
}
7481
if err := db.addColumnIfMissing("queue", "failure_count", "INTEGER NOT NULL DEFAULT 0"); err != nil {
7582
return err
7683
}

server/internal/db/monitor.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ type monitorSubRow struct {
1919
AutoOrder int `db:"auto_order"`
2020
Quantity int `db:"quantity"`
2121
AutoOrderAccountID string `db:"auto_order_account_id"`
22+
AutoPay int `db:"auto_pay"`
2223
}
2324

2425
func rowToMonitorSub(r monitorSubRow) types.Subscription {
@@ -43,6 +44,7 @@ func rowToMonitorSub(r monitorSubRow) types.Subscription {
4344
AutoOrder: r.AutoOrder == 1,
4445
Quantity: r.Quantity,
4546
AutoOrderAccountID: r.AutoOrderAccountID,
47+
AutoPay: r.AutoPay == 1,
4648
}
4749
}
4850

@@ -86,6 +88,7 @@ func monitorSubToRow(s types.Subscription) (monitorSubRow, error) {
8688
AutoOrder: bi(s.AutoOrder),
8789
Quantity: s.Quantity,
8890
AutoOrderAccountID: s.AutoOrderAccountID,
91+
AutoPay: bi(s.AutoPay),
8992
}, nil
9093
}
9194

@@ -114,10 +117,10 @@ func (db *DB) UpsertMonitorSubscription(s types.Subscription) error {
114117
_, err = db.NamedExec(`
115118
INSERT INTO monitor_subscriptions
116119
(plan_code, datacenters, notify_available, notify_unavailable, last_status,
117-
created_at, history, server_name, auto_order, quantity, auto_order_account_id)
120+
created_at, history, server_name, auto_order, quantity, auto_order_account_id, auto_pay)
118121
VALUES
119122
(:plan_code, :datacenters, :notify_available, :notify_unavailable, :last_status,
120-
:created_at, :history, :server_name, :auto_order, :quantity, :auto_order_account_id)
123+
:created_at, :history, :server_name, :auto_order, :quantity, :auto_order_account_id, :auto_pay)
121124
ON CONFLICT(plan_code) DO UPDATE SET
122125
datacenters = excluded.datacenters,
123126
notify_available = excluded.notify_available,
@@ -127,7 +130,8 @@ func (db *DB) UpsertMonitorSubscription(s types.Subscription) error {
127130
server_name = excluded.server_name,
128131
auto_order = excluded.auto_order,
129132
quantity = excluded.quantity,
130-
auto_order_account_id = excluded.auto_order_account_id
133+
auto_order_account_id = excluded.auto_order_account_id,
134+
auto_pay = excluded.auto_pay
131135
`, r)
132136
if err != nil {
133137
return fmt.Errorf("upsert monitor sub %s: %w", s.PlanCode, err)
@@ -153,10 +157,10 @@ func (db *DB) ReplaceMonitorSubscriptions(subs []types.Subscription) error {
153157
_, err = tx.NamedExec(`
154158
INSERT INTO monitor_subscriptions
155159
(plan_code, datacenters, notify_available, notify_unavailable, last_status,
156-
created_at, history, server_name, auto_order, quantity, auto_order_account_id)
160+
created_at, history, server_name, auto_order, quantity, auto_order_account_id, auto_pay)
157161
VALUES
158162
(:plan_code, :datacenters, :notify_available, :notify_unavailable, :last_status,
159-
:created_at, :history, :server_name, :auto_order, :quantity, :auto_order_account_id)
163+
:created_at, :history, :server_name, :auto_order, :quantity, :auto_order_account_id, :auto_pay)
160164
`, r)
161165
if err != nil {
162166
return fmt.Errorf("insert monitor sub %s: %w", s.PlanCode, err)

server/internal/db/queue.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ type queueRow struct {
2626
Priority int `db:"priority"`
2727
FromTelegram int `db:"from_telegram"`
2828
ConfigSniperTaskID string `db:"config_sniper_task_id"`
29+
AutoPay int `db:"auto_pay"`
2930
}
3031

3132
func rowToQueueItem(r queueRow) types.QueueItem {
@@ -54,6 +55,7 @@ func rowToQueueItem(r queueRow) types.QueueItem {
5455
Priority: r.Priority,
5556
FromTelegram: r.FromTelegram == 1,
5657
ConfigSniperTaskID: r.ConfigSniperTaskID,
58+
AutoPay: r.AutoPay == 1,
5759
}
5860
}
5961

@@ -89,6 +91,7 @@ func queueItemToRow(q types.QueueItem) (queueRow, error) {
8991
Priority: q.Priority,
9092
FromTelegram: bi(q.FromTelegram),
9193
ConfigSniperTaskID: q.ConfigSniperTaskID,
94+
AutoPay: bi(q.AutoPay),
9295
}, nil
9396
}
9497

@@ -125,11 +128,11 @@ func (db *DB) ReplaceQueue(items []types.QueueItem) error {
125128
INSERT INTO queue
126129
(id, account_id, plan_code, datacenter, options, status, created_at, updated_at,
127130
retry_interval, retry_count, failure_count, max_retries, last_check_time,
128-
quick_order, priority, from_telegram, config_sniper_task_id)
131+
quick_order, priority, from_telegram, config_sniper_task_id, auto_pay)
129132
VALUES
130133
(:id, :account_id, :plan_code, :datacenter, :options, :status, :created_at, :updated_at,
131134
:retry_interval, :retry_count, :failure_count, :max_retries, :last_check_time,
132-
:quick_order, :priority, :from_telegram, :config_sniper_task_id)
135+
:quick_order, :priority, :from_telegram, :config_sniper_task_id, :auto_pay)
133136
`, r)
134137
if err != nil {
135138
return fmt.Errorf("insert queue %s: %w", q.ID, err)

server/internal/handlers/monitor.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ func AddSubscription(state *app.State, mon *monitor.Monitor) gin.HandlerFunc {
3939
AutoOrder bool `json:"autoOrder"`
4040
Quantity int `json:"quantity"`
4141
AutoOrderAccountID string `json:"autoOrderAccountId"` // 空 = 触发时只通知不下单
42+
// AutoPay 下单成功后用默认支付方式自动付款。默认 false ——
43+
// 自动扣钱必须是显式打开的开关
44+
AutoPay bool `json:"autoPay"`
4245
}
4346
_ = c.ShouldBindJSON(&body)
4447
if body.PlanCode == "" {
@@ -84,7 +87,7 @@ func AddSubscription(state *app.State, mon *monitor.Monitor) gin.HandlerFunc {
8487
region, subsidiary, regionWarning := mon.PreflightRegion(body.PlanCode, body.AutoOrderAccountID)
8588

8689
mon.AddSubscription(body.PlanCode, body.Datacenters, notifyAvailable, notifyUnavailable,
87-
serverName, nil, nil, body.AutoOrder, body.Quantity, body.AutoOrderAccountID)
90+
serverName, nil, nil, body.AutoOrder, body.Quantity, body.AutoOrderAccountID, body.AutoPay)
8891
mon.SaveToDB()
8992

9093
if !mon.Running() {
@@ -172,7 +175,7 @@ func BatchAddAll(state *app.State, mon *monitor.Monitor) gin.HandlerFunc {
172175
continue
173176
}
174177
mon.AddSubscription(pc, []string{}, notifyAvailable, notifyUnavailable,
175-
server.Name, nil, nil, body.AutoOrder, 1, body.AutoOrderAccountID)
178+
server.Name, nil, nil, body.AutoOrder, 1, body.AutoOrderAccountID, false)
176179
added++
177180
state.Logger.Debug("批量添加订阅: "+pc+" ("+server.Name+")", "monitor")
178181
}

server/internal/handlers/queue.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ func AddQueueItem(state *app.State) gin.HandlerFunc {
2424
Datacenter string `json:"datacenter"`
2525
Options []string `json:"options"`
2626
RetryInterval int `json:"retryInterval"`
27+
// AutoPay 下单成功后用默认支付方式自动付款(显式开关,默认关)
28+
AutoPay bool `json:"autoPay"`
2729
}
2830
_ = c.ShouldBindJSON(&body)
2931
if body.AccountID == "" {
@@ -67,6 +69,7 @@ func AddQueueItem(state *app.State) gin.HandlerFunc {
6769
RetryInterval: body.RetryInterval,
6870
RetryCount: 0,
6971
LastCheckTime: 0,
72+
AutoPay: body.AutoPay,
7073
}
7174
state.QueueMu.Lock()
7275
state.Queue = append(state.Queue, item)

server/internal/handlers/quick_order.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ func QuickOrder(state *app.State) gin.HandlerFunc {
3232
Options []string `json:"options"`
3333
FromMonitor bool `json:"fromMonitor"`
3434
SkipDuplicateCheck bool `json:"skipDuplicateCheck"`
35+
AutoPay bool `json:"autoPay"`
3536
}
3637
_ = c.ShouldBindJSON(&body)
3738
if body.PlanCode == "" || body.Datacenter == "" {
@@ -196,6 +197,7 @@ func QuickOrder(state *app.State) gin.HandlerFunc {
196197
Options: options,
197198
Status: "running",
198199
RetryCount: 0,
200+
AutoPay: body.AutoPay,
199201
// MaxRetries 封顶的是**真正提交并失败的次数**(FailureCount),无货轮次不计。
200202
// 早先按"检查轮次"封顶是错的:监控只在 无货→有货 的跳变上重新触发 quick-order,
201203
// 一旦库存持续可见而下单侧连续吃 429/5xx,任务用尽轮次置 failed 后

server/internal/handlers/subscription_edit.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ func UpdateSubscription(state *app.State, mon *monitor.Monitor) gin.HandlerFunc
4242
AutoOrder *bool `json:"autoOrder"`
4343
Quantity *int `json:"quantity"`
4444
AutoOrderAccountID *string `json:"autoOrderAccountId"`
45+
AutoPay *bool `json:"autoPay"`
4546
}
4647
if err := c.ShouldBindJSON(&body); err != nil {
4748
c.JSON(http.StatusBadRequest, gin.H{"status": "error", "message": "请求体格式错误: " + err.Error()})
@@ -56,6 +57,7 @@ func UpdateSubscription(state *app.State, mon *monitor.Monitor) gin.HandlerFunc
5657
autoOrder := cur.AutoOrder
5758
quantity := cur.Quantity
5859
accountID := cur.AutoOrderAccountID
60+
autoPay := cur.AutoPay
5961

6062
if body.Datacenters != nil {
6163
datacenters = *body.Datacenters
@@ -72,6 +74,9 @@ func UpdateSubscription(state *app.State, mon *monitor.Monitor) gin.HandlerFunc
7274
if body.Quantity != nil {
7375
quantity = *body.Quantity
7476
}
77+
if body.AutoPay != nil {
78+
autoPay = *body.AutoPay
79+
}
7580
if body.AutoOrderAccountID != nil {
7681
accountID = *body.AutoOrderAccountID
7782
// 空串是合法值:表示「触发时只通知、不下单」
@@ -94,7 +99,7 @@ func UpdateSubscription(state *app.State, mon *monitor.Monitor) gin.HandlerFunc
9499

95100
// AddSubscription 对已存在的 planCode 是就地改配置,不会重置 LastStatus / History
96101
mon.AddSubscription(planCode, datacenters, notifyAvailable, notifyUnavailable,
97-
cur.ServerName, nil, nil, autoOrder, quantity, accountID)
102+
cur.ServerName, nil, nil, autoOrder, quantity, accountID, autoPay)
98103
mon.SaveToDB()
99104
state.Logger.Info("更新服务器订阅配置: "+planCode, "monitor")
100105

@@ -131,6 +136,7 @@ func UpdateVPSSubscription(state *app.State) gin.HandlerFunc {
131136
AutoOrder *bool `json:"autoOrder"`
132137
Quantity *int `json:"quantity"`
133138
OS *string `json:"os"`
139+
AutoPay *bool `json:"autoPay"`
134140
}
135141
if err := c.ShouldBindJSON(&body); err != nil {
136142
c.JSON(http.StatusBadRequest, gin.H{"status": "error", "message": "请求体格式错误: " + err.Error()})
@@ -182,11 +188,18 @@ func UpdateVPSSubscription(state *app.State) gin.HandlerFunc {
182188
if body.OS != nil {
183189
next.OS = strings.TrimSpace(*body.OS)
184190
}
191+
if body.AutoPay != nil {
192+
next.AutoPay = *body.AutoPay
193+
}
185194
// 没有下单账户就不可能自动下单 —— 让这两个字段自洽,
186195
// 否则界面上会出现"开着自动下单但买不了"的状态
187196
if next.AutoOrderAccountID == "" {
188197
next.AutoOrder = false
189198
}
199+
// 自动付款依附于自动下单
200+
if !next.AutoOrder {
201+
next.AutoPay = false
202+
}
190203
if next.AutoOrder && next.Quantity < 1 {
191204
next.Quantity = 1
192205
}
@@ -261,6 +274,7 @@ func UpdateVPSSubscription(state *app.State) gin.HandlerFunc {
261274
live.AutoOrder = next.AutoOrder
262275
live.Quantity = next.Quantity
263276
live.OS = next.OS
277+
live.AutoPay = next.AutoPay
264278
if resetStatus {
265279
live.LastStatus = map[string]string{}
266280
}

server/internal/handlers/vps_monitor.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ func AddVPSSubscription(state *app.State) gin.HandlerFunc {
8888
AutoOrder bool `json:"autoOrder"`
8989
Quantity int `json:"quantity"`
9090
OS string `json:"os"`
91+
AutoPay bool `json:"autoPay"`
9192
}
9293
_ = c.ShouldBindJSON(&body)
9394
if body.PlanCode == "" {
@@ -192,6 +193,7 @@ func AddVPSSubscription(state *app.State) gin.HandlerFunc {
192193
AutoOrder: body.AutoOrder && body.AutoOrderAccountID != "",
193194
Quantity: body.Quantity,
194195
OS: strings.TrimSpace(body.OS),
196+
AutoPay: body.AutoPay && body.AutoOrder && body.AutoOrderAccountID != "",
195197
}
196198
if sub.AutoOrder && sub.Quantity < 1 {
197199
sub.Quantity = 1

server/internal/monitor/check.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -672,7 +672,7 @@ func (m *Monitor) CheckAvailabilityChange(sub *Subscription, traceID string) {
672672
"[monitor] %s 跳过自动下单:验价用的是账户 %q,而订阅指定的下单账户是 %q,两者不一致(下单账户可能已被删除)",
673673
planCode, choice.accountID, cfg.AutoOrderAccountID), "monitor")
674674
default:
675-
m.batchOrder(planCode, configInfo, orderTargets, cfg.Quantity, cfg.AutoOrderAccountID)
675+
m.batchOrder(planCode, configInfo, orderTargets, cfg.Quantity, cfg.AutoOrderAccountID, cfg.AutoPay)
676676
}
677677
}
678678

0 commit comments

Comments
 (0)