Browse Source

feat:直播计费

master
刘浩东 3 weeks ago
parent
commit
800670e5c2
  1. 1
      common/config.go
  2. 1
      config-prod.yaml
  3. 1
      config.yaml
  4. 27
      mock/main.go
  5. 5
      service/live_service.go
  6. 8
      service/scheduler.go
  7. 16
      sql/003_seed.sql
  8. 2
      sql/009_account_security_invoice.sql

1
common/config.go

@ -83,6 +83,7 @@ type Live struct {
ViewerLeaseSeconds int `mapstructure:"viewer-lease-seconds"` ViewerLeaseSeconds int `mapstructure:"viewer-lease-seconds"`
StopGraceSeconds int `mapstructure:"stop-grace-seconds"` StopGraceSeconds int `mapstructure:"stop-grace-seconds"`
ReconcileIntervalSeconds int `mapstructure:"reconcile-interval-seconds"` ReconcileIntervalSeconds int `mapstructure:"reconcile-interval-seconds"`
BillingIntervalSeconds int `mapstructure:"billing-interval-seconds"`
RecordPolicy string `mapstructure:"record-policy"` RecordPolicy string `mapstructure:"record-policy"`
CallbackAuthToken string `mapstructure:"callback-auth-token"` CallbackAuthToken string `mapstructure:"callback-auth-token"`
AllowRealCloud bool `mapstructure:"allow-real-cloud"` AllowRealCloud bool `mapstructure:"allow-real-cloud"`

1
config-prod.yaml

@ -43,6 +43,7 @@ live:
viewer-lease-seconds: 30 viewer-lease-seconds: 30
stop-grace-seconds: 15 stop-grace-seconds: 15
reconcile-interval-seconds: 10 reconcile-interval-seconds: 10
billing-interval-seconds: 60
record-policy: disabled record-policy: disabled
callback-auth-token: "db570a886e3d65c3f6c55d648ddb41506a658f6be4892719345056602fc2e7ac" callback-auth-token: "db570a886e3d65c3f6c55d648ddb41506a658f6be4892719345056602fc2e7ac"
allow-real-cloud: true allow-real-cloud: true

1
config.yaml

@ -44,6 +44,7 @@ live:
viewer-lease-seconds: 30 viewer-lease-seconds: 30
stop-grace-seconds: 15 stop-grace-seconds: 15
reconcile-interval-seconds: 10 reconcile-interval-seconds: 10
billing-interval-seconds: 60
record-policy: disabled record-policy: disabled
allow-real-cloud: false allow-real-cloud: false
heartbeat: heartbeat:

27
mock/main.go

@ -4,6 +4,7 @@ import (
"log" "log"
"os" "os"
"os/signal" "os/signal"
"strings"
"syscall" "syscall"
"time" "time"
) )
@ -50,7 +51,7 @@ func main() {
}, },
} }
for _, spec := range specs {
for _, spec := range selectDockSpecs(specs, os.Getenv("MOCK_DOCKS")) {
dock := newMockDock(spec) dock := newMockDock(spec)
if err := dock.start(broker, username, password); err != nil { if err := dock.start(broker, username, password); err != nil {
log.Fatalf("启动机巢 %s 失败: %v", spec.DockID, err) log.Fatalf("启动机巢 %s 失败: %v", spec.DockID, err)
@ -65,6 +66,30 @@ func main() {
time.Sleep(200 * time.Millisecond) time.Sleep(200 * time.Millisecond)
} }
func selectDockSpecs(specs []dockSpec, selected string) []dockSpec {
if strings.TrimSpace(selected) == "" {
return specs
}
allowed := make(map[string]struct{})
for _, dockID := range strings.Split(selected, ",") {
if dockID = strings.TrimSpace(dockID); dockID != "" {
allowed[dockID] = struct{}{}
}
}
selectedSpecs := make([]dockSpec, 0, len(allowed))
for _, spec := range specs {
if _, ok := allowed[spec.DockID]; ok {
selectedSpecs = append(selectedSpecs, spec)
}
}
if len(selectedSpecs) == 0 {
log.Fatalf("MOCK_DOCKS 未匹配任何设备: %s", selected)
}
return selectedSpecs
}
func envOr(key, def string) string { func envOr(key, def string) string {
if v := os.Getenv(key); v != "" { if v := os.Getenv(key); v != "" {
return v return v

5
service/live_service.go

@ -246,6 +246,11 @@ func (s *LiveService) reconcileCloudSession(provider liveprovider.Adapter, sessi
if session.DeviceStreaming && online.Online { if session.DeviceStreaming && online.Online {
updates["cloud_confirmed_at"] = now updates["cloud_confirmed_at"] = now
} }
if provider.Provider() == "fake" && session.Phase == "stopping" && !session.DeviceStreaming {
updates["cloud_online"] = false
updates["phase"] = "stopped"
updates["stopped_at"] = now
}
switch session.Phase { switch session.Phase {
case "starting", "reconnecting": case "starting", "reconnecting":
if session.DeviceStreaming && online.Online { if session.DeviceStreaming && online.Online {

8
service/scheduler.go

@ -32,8 +32,12 @@ func (s *Scheduler) Start() {
} }
_, _ = s.cron.AddFunc("@every "+(time.Duration(interval)*time.Second).String(), DefaultLiveService.ReconcileLeases) _, _ = s.cron.AddFunc("@every "+(time.Duration(interval)*time.Second).String(), DefaultLiveService.ReconcileLeases)
_, _ = s.cron.AddFunc("@every "+(time.Duration(interval)*time.Second).String(), DefaultLiveService.ReconcileCloudSessions) _, _ = s.cron.AddFunc("@every "+(time.Duration(interval)*time.Second).String(), DefaultLiveService.ReconcileCloudSessions)
// 直播计费:每 60s 扫描进行中的 live_session 按码率扣减
_, _ = s.cron.AddFunc("@every 60s", DefaultBillingService.chargeLiveSessions)
// 直播计费:按配置周期扫描进行中的 live_session 按码率扣减
billingInterval := common.AppConf.Live.BillingIntervalSeconds
if billingInterval <= 0 {
billingInterval = 60
}
_, _ = s.cron.AddFunc("@every "+(time.Duration(billingInterval)*time.Second).String(), DefaultBillingService.chargeLiveSessions)
// 余额快照:每 30min 将 Redis 权威余额回写 MySQL // 余额快照:每 30min 将 Redis 权威余额回写 MySQL
_, _ = s.cron.AddFunc("@every 30m", DefaultBillingService.flushBalanceSnapshot) _, _ = s.cron.AddFunc("@every 30m", DefaultBillingService.flushBalanceSnapshot)
// SIM 卡用量:每 1h 从运营商同步用量 // SIM 卡用量:每 1h 从运营商同步用量

16
sql/003_seed.sql

@ -36,11 +36,11 @@ SET @demo_user_id = (SELECT id FROM `user` WHERE phone = '13900000000');
-- 演示机巢 / 无人机(与 mock 设备 dock-1/2/3 对应) -- 演示机巢 / 无人机(与 mock 设备 dock-1/2/3 对应)
-- 若 MQTT 已自发现同 dock_id 的 pending 记录,则认领并补全名称坐标。 -- 若 MQTT 已自发现同 dock_id 的 pending 记录,则认领并补全名称坐标。
-- --------------------------------------------------------------- -- ---------------------------------------------------------------
INSERT INTO dock (id, user_id, dock_id, name, code, sn, iccid, longitude, latitude, altitude, location, status, register_status, dock_id_source, software_ver, protocol_ver, created_at, updated_at)
INSERT INTO dock (id, user_id, dock_id, name, code, sn, iccid, longitude, latitude, altitude, location, register_status, dock_id_source, software_ver, protocol_ver, created_at, updated_at)
VALUES VALUES
(1001, @demo_user_id, 'dock-1', '青山湖 01 号机巢', 'JG-DOCK-001', 'dock-1', '8986000000000000001', 119.802631, 30.262438, 15, '青山湖科技城 A 区', 'offline', 'registered', 'seed', '1.3.0', '1.0', NOW(), NOW()),
(1002, @demo_user_id, 'dock-2', '青山湖 02 号机巢', 'JG-DOCK-002', 'dock-2', '8986000000000000002', 119.818264, 30.255917, 18, '青山湖科技城 B 区', 'offline', 'registered', 'seed', '1.3.0', '1.0', NOW(), NOW()),
(1003, @demo_user_id, 'dock-3', '青山湖 03 号机巢', 'JG-DOCK-003', 'dock-3', '8986000000000000003', 119.791450, 30.248610, 12, '青山湖科技城 C 区', 'offline', 'registered', 'seed', '1.3.0', '1.0', NOW(), NOW())
(1001, @demo_user_id, 'dock-1', '青山湖 01 号机巢', 'JG-DOCK-001', 'dock-1', '8986000000000000001', 119.802631, 30.262438, 15, '青山湖科技城 A 区', 'registered', 'seed', '1.3.0', '1.0', NOW(), NOW()),
(1002, @demo_user_id, 'dock-2', '青山湖 02 号机巢', 'JG-DOCK-002', 'dock-2', '8986000000000000002', 119.818264, 30.255917, 18, '青山湖科技城 B 区', 'registered', 'seed', '1.3.0', '1.0', NOW(), NOW()),
(1003, @demo_user_id, 'dock-3', '青山湖 03 号机巢', 'JG-DOCK-003', 'dock-3', '8986000000000000003', 119.791450, 30.248610, 12, '青山湖科技城 C 区', 'registered', 'seed', '1.3.0', '1.0', NOW(), NOW())
ON DUPLICATE KEY UPDATE ON DUPLICATE KEY UPDATE
user_id = @demo_user_id, user_id = @demo_user_id,
name = VALUES(name), name = VALUES(name),
@ -52,11 +52,11 @@ ON DUPLICATE KEY UPDATE
location = VALUES(location), location = VALUES(location),
register_status = 'registered'; register_status = 'registered';
INSERT INTO drone (id, user_id, drone_sn, dock_id, name, code, model, status, battery, firmware_ver, created_at, updated_at)
INSERT INTO drone (id, user_id, drone_sn, dock_id, name, code, model, battery, firmware_ver, created_at, updated_at)
VALUES VALUES
(2001, @demo_user_id, 'JG-UAV-001', 'dock-1', '巡检无人机 001', 'JG-UAV-001', 'LAIC-X1', 'offline', 86, '1.0.0', NOW(), NOW()),
(2002, @demo_user_id, 'JG-UAV-002', 'dock-2', '巡检无人机 002', 'JG-UAV-002', 'LAIC-X1', 'offline', 72, '1.0.0', NOW(), NOW()),
(2003, @demo_user_id, 'JG-UAV-003', 'dock-3', '巡检无人机 003', 'JG-UAV-003', 'LAIC-X1', 'offline', 41, '1.0.0', NOW(), NOW())
(2001, @demo_user_id, 'JG-UAV-001', 'dock-1', '巡检无人机 001', 'JG-UAV-001', 'LAIC-X1', 86, '1.0.0', NOW(), NOW()),
(2002, @demo_user_id, 'JG-UAV-002', 'dock-2', '巡检无人机 002', 'JG-UAV-002', 'LAIC-X1', 72, '1.0.0', NOW(), NOW()),
(2003, @demo_user_id, 'JG-UAV-003', 'dock-3', '巡检无人机 003', 'JG-UAV-003', 'LAIC-X1', 41, '1.0.0', NOW(), NOW())
ON DUPLICATE KEY UPDATE ON DUPLICATE KEY UPDATE
user_id = @demo_user_id, user_id = @demo_user_id,
dock_id = VALUES(dock_id), dock_id = VALUES(dock_id),

2
sql/009_account_security_invoice.sql

@ -1,4 +1,4 @@
ALTER TABLE `user` ADD COLUMN `password_changed_at` DATETIME NULL COMMENT '密码最后修改时间' AFTER `last_login`;
-- 001_schema.sql 已包含 password_changed_at;本脚本仅补充账户会话和发票表。
CREATE TABLE IF NOT EXISTS `user_login_session` ( CREATE TABLE IF NOT EXISTS `user_login_session` (
id BIGINT PRIMARY KEY COMMENT '主键 ID', id BIGINT PRIMARY KEY COMMENT '主键 ID',

Loading…
Cancel
Save