chore: 并入 iotlcm-admin-svr 历史到 admin-svr/(原提交 49818c6)
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
package tasks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterInnerTask("settle_profit_account_month_bill", "SettleProfitAccountMonthBill")
|
||||
RegisterInnerTask("settle_consume_account_month_bill", "SettleConsumeAccountMonthBill")
|
||||
RegisterInnerTask("settle_commission", "SettleCommission")
|
||||
}
|
||||
|
||||
// SettleProfitAccountMonthBill 结算利润账户月账单
|
||||
func (t Task) SettleProfitAccountMonthBill(accountPeriod string) {
|
||||
ctx := context.Background()
|
||||
g.Log().Debugf(ctx, "执行任务:结算利润账户月账单,账期: %s", accountPeriod)
|
||||
|
||||
param := &model.TProfitAccountMonthBillSettleParam{}
|
||||
if accountPeriod != "" {
|
||||
param.AccountPeriod = accountPeriod
|
||||
}
|
||||
if err := param.Filter(ctx); err != nil {
|
||||
g.Log().Errorf(ctx, "Task.SettleProfitAccountMonthBill 参数错误: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := service.CommonProfitAccountMonthBill().SettleProfitAccountMonthBill(ctx, param)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "Task.SettleProfitAccountMonthBill 执行失败: %v", err)
|
||||
return
|
||||
}
|
||||
g.Log().Debugf(ctx, "执行任务:结算利润账户月账单完成,处理机构数: %d, 新增: %d, 更新: %d",
|
||||
result.DeptCount, result.InsertCount, result.UpdateCount)
|
||||
}
|
||||
|
||||
// SettleConsumeAccountMonthBill 结算消费账户月账单
|
||||
func (t Task) SettleConsumeAccountMonthBill(accountPeriod string) {
|
||||
ctx := context.Background()
|
||||
g.Log().Debugf(ctx, "执行任务:结算消费账户月账单,账期: %s", accountPeriod)
|
||||
|
||||
param := &model.TConsumeAccountMonthBillSettleParam{}
|
||||
if accountPeriod != "" {
|
||||
param.AccountPeriod = accountPeriod
|
||||
}
|
||||
if err := param.Filter(ctx); err != nil {
|
||||
g.Log().Errorf(ctx, "Task.SettleConsumeAccountMonthBill 参数错误: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := service.CommonConsumeAccountMonthBill().SettleConsumeAccountMonthBill(ctx, param)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "Task.SettleConsumeAccountMonthBill 执行失败: %v", err)
|
||||
return
|
||||
}
|
||||
g.Log().Debugf(ctx, "执行任务:结算消费账户月账单完成,处理机构数: %d, 新增: %d, 更新: %d",
|
||||
result.DeptCount, result.InsertCount, result.UpdateCount)
|
||||
}
|
||||
|
||||
// SettleCommission 结算分润
|
||||
// 参数格式:startPeriod:endPeriod 如 "2026-06|2025-07"
|
||||
func (t Task) SettleCommission(params string) {
|
||||
ctx := context.Background()
|
||||
g.Log().Debugf(ctx, "执行任务:结算分润,参数: %s", params)
|
||||
|
||||
param := &model.TAgentBalanceCommissionSettlementParam{}
|
||||
|
||||
// 解析参数,格式: startPeriod|endPeriod
|
||||
if params != "" {
|
||||
parts := splitParams(params)
|
||||
if len(parts) >= 1 && parts[0] != "" {
|
||||
param.StartPeriod = parts[0]
|
||||
}
|
||||
if len(parts) >= 2 && parts[1] != "" {
|
||||
param.EndPeriod = parts[1]
|
||||
}
|
||||
}
|
||||
|
||||
if err := param.Filter(ctx); err != nil {
|
||||
g.Log().Errorf(ctx, "Task.SettleCommission 参数错误: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := service.CommonAgentBalance().SettleCommissionByPeriod(ctx, param)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "Task.SettleCommission 执行失败: %v", err)
|
||||
return
|
||||
}
|
||||
g.Log().Debugf(ctx, "执行任务:结算分润完成,账期: %s 至 %s,扫描: %d, 成功分组: %d, 失败分组: %d, 已结算金额: %.2f",
|
||||
result.StartPeriod, result.EndPeriod, result.ScanCount, result.SuccessGroupCount, result.FailGroupCount, result.SettledAmount)
|
||||
}
|
||||
|
||||
// splitParams 分割参数字符串
|
||||
func splitParams(params string) []string {
|
||||
var result []string
|
||||
var current string
|
||||
for _, ch := range params {
|
||||
if ch == '|' {
|
||||
result = append(result, current)
|
||||
current = ""
|
||||
} else {
|
||||
current += string(ch)
|
||||
}
|
||||
}
|
||||
result = append(result, current)
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package tasks
|
||||
|
||||
var (
|
||||
innerTaskNames = map[string]string{}
|
||||
)
|
||||
|
||||
func RegisterInnerTask(name string, methodName string) {
|
||||
innerTaskNames[name] = methodName
|
||||
}
|
||||
|
||||
func GetInnerTaskNames() map[string]string {
|
||||
return innerTaskNames
|
||||
}
|
||||
|
||||
func GetInnerTaskName(name string) string {
|
||||
return innerTaskNames[name]
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package tasks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterInnerTask("clear_operation_log_by_days", "ClearOperationLogByDays")
|
||||
RegisterInnerTask("test", "Test")
|
||||
}
|
||||
|
||||
func (t Task) ClearOperationLogByDays(days string) {
|
||||
ctx := context.Background()
|
||||
g.Log().Debugf(ctx, "执行任务:清理超过%v天的操作日志", days)
|
||||
err := service.SysOperLog().ClearOperationLogByDays(ctx, gconv.Int(days))
|
||||
if err != nil {
|
||||
g.Log().Error(ctx, "TaskJob.ClearOperationLogByDays", err)
|
||||
}
|
||||
time.Sleep(time.Second * 30)
|
||||
g.Log().Debugf(ctx, "执行任务:完成")
|
||||
}
|
||||
|
||||
func (t Task) Test() {
|
||||
ctx := context.Background()
|
||||
// 测试代码
|
||||
g.Log().Debugf(ctx, "执行任务:测试")
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package tasks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterInnerTask("sync_flow_pool_card", "SyncFlowPoolCard")
|
||||
}
|
||||
|
||||
func (t Task) SyncFlowPoolCard(deptId int64) {
|
||||
ctx := context.Background()
|
||||
if deptId <= 0 {
|
||||
g.Log().Errorf(ctx, "SyncFlowPoolCard failed: invalid deptId %d", deptId)
|
||||
return
|
||||
}
|
||||
|
||||
ctx = contexts.Set(ctx, &model.Context{
|
||||
User: &model.Identity{
|
||||
BaseClaims: model.BaseClaims{
|
||||
ID: consts.SuperAdminRoleId,
|
||||
Username: "system",
|
||||
NickName: "system",
|
||||
DeptId: deptId,
|
||||
TenantId: consts.DefaultSystemTenantCode,
|
||||
Roles: []model.Role{
|
||||
{RoleId: consts.SuperAdminRoleId, DataScope: consts.SysRoleDataScopeAll},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err := service.IotTSimFlowPool().SyncPoolCard(ctx, deptId); err != nil {
|
||||
g.Log().Errorf(ctx, "SyncFlowPoolCard failed, deptId:%d, err:%+v", deptId, err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package tasks
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestAllFullSyncTaskRegistrations(t *testing.T) {
|
||||
for name, method := range map[string]string{
|
||||
"start_traffic_usage_full_sync": "StartTrafficUsageFullSync",
|
||||
"syns_sim_status_queue": "SynsSimStatusQueue",
|
||||
"syns_imei_queue": "SynsImeiQueue",
|
||||
"sync_traffic_usage_daily": "SyncTrafficUsageDaily",
|
||||
"sync_traffic_usage_monthly": "SyncTrafficUsageMonthly",
|
||||
"start_traffic_usage_monthly_history": "StartTrafficUsageMonthlyHistory",
|
||||
} {
|
||||
if got := GetInnerTaskName(name); got != method {
|
||||
t.Errorf("task %q registered as %q, want %q", name, got, method)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package tasks
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
)
|
||||
|
||||
func init() { RegisterInnerTask("syns_imei_queue", "SynsImeiQueue") }
|
||||
|
||||
// SynsImeiQueue is the scheduled entry point for the full IMEI sync.
|
||||
//
|
||||
// It publishes an empty scope, which the Worker treats as a full sync — the
|
||||
// same contract as the status and traffic chains.
|
||||
//
|
||||
// The IMEI chain re-polls every card (including cards that already have a
|
||||
// value), so a device swap is detected. Schedule it less often than the status
|
||||
// sync: IMEI only changes when the device changes, while the fixed per-run cost
|
||||
// is the same as a status run.
|
||||
func (t Task) SynsImeiQueue() {
|
||||
if _, err := service.ImeiQueue().StartImeiRun(context.Background()); err != nil {
|
||||
g.Log().Errorf(context.Background(), "start imei queue sync failed: %+v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
package tasks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/uuid32"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
const jobLockKeyPrefix = "iotlcm:job-lock"
|
||||
|
||||
type redisJobLockOptions struct {
|
||||
InvokeTarget string
|
||||
Period time.Duration
|
||||
TTL time.Duration
|
||||
}
|
||||
|
||||
type redisJobLock struct {
|
||||
key string
|
||||
value string
|
||||
ttl time.Duration
|
||||
}
|
||||
|
||||
func (t Task) runWithRedisJobLock(ctx context.Context, options redisJobLockOptions, run func(context.Context)) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
if run == nil {
|
||||
return
|
||||
}
|
||||
invokeTarget := strings.TrimSpace(options.InvokeTarget)
|
||||
if strings.TrimSpace(invokeTarget) == "" {
|
||||
invokeTarget = taskInvokeTarget(t.ID, "")
|
||||
}
|
||||
if options.TTL <= 0 {
|
||||
options.TTL = 30 * time.Minute
|
||||
}
|
||||
|
||||
lock := redisJobLock{
|
||||
key: jobLockKey(invokeTarget, time.Now(), options.Period),
|
||||
value: jobLockValue(),
|
||||
ttl: options.TTL,
|
||||
}
|
||||
acquired, err := lock.acquire(ctx)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "job lock acquire failed, invokeTarget:%s, key:%s, err:%+v", invokeTarget, lock.key, err)
|
||||
return
|
||||
}
|
||||
if !acquired {
|
||||
g.Log().Infof(ctx, "job skipped by redis lock, invokeTarget:%s, key:%s", invokeTarget, lock.key)
|
||||
return
|
||||
}
|
||||
|
||||
runCtx, cancel := context.WithCancel(ctx)
|
||||
stopRenew := lock.startRenew(runCtx, cancel, invokeTarget)
|
||||
defer stopRenew()
|
||||
defer func() {
|
||||
if err := lock.release(context.Background()); err != nil {
|
||||
g.Log().Warningf(ctx, "job lock release failed, invokeTarget:%s, key:%s, err:%+v", invokeTarget, lock.key, err)
|
||||
}
|
||||
}()
|
||||
|
||||
g.Log().Infof(ctx, "job lock acquired, invokeTarget:%s, key:%s, ttl:%s", invokeTarget, lock.key, lock.ttl)
|
||||
run(runCtx)
|
||||
}
|
||||
|
||||
func (l redisJobLock) acquire(ctx context.Context) (bool, error) {
|
||||
res, err := g.Redis().Do(ctx, "SET", l.key, l.value, "NX", "PX", l.ttl.Milliseconds())
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return strings.EqualFold(strings.TrimSpace(res.String()), "OK"), nil
|
||||
}
|
||||
|
||||
func (l redisJobLock) startRenew(ctx context.Context, cancel context.CancelFunc, invokeTarget string) func() {
|
||||
interval := l.ttl / 3
|
||||
if interval <= 0 {
|
||||
interval = time.Minute
|
||||
}
|
||||
stop := make(chan struct{})
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-stop:
|
||||
return
|
||||
case <-ticker.C:
|
||||
renewCtx, renewCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
ok, err := l.renew(renewCtx)
|
||||
renewCancel()
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "job lock renew failed, invokeTarget:%s, key:%s, err:%+v", invokeTarget, l.key, err)
|
||||
continue
|
||||
}
|
||||
if !ok {
|
||||
g.Log().Warningf(ctx, "job lock lost, cancel current run, invokeTarget:%s, key:%s", invokeTarget, l.key)
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
return func() {
|
||||
close(stop)
|
||||
<-done
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
func (l redisJobLock) renew(ctx context.Context) (bool, error) {
|
||||
const script = `if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("PEXPIRE", KEYS[1], ARGV[2]) else return 0 end`
|
||||
res, err := g.Redis().Do(ctx, "EVAL", script, 1, l.key, l.value, strconv.FormatInt(l.ttl.Milliseconds(), 10))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return res.Int() == 1, nil
|
||||
}
|
||||
|
||||
func (l redisJobLock) release(ctx context.Context) error {
|
||||
const script = `if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("DEL", KEYS[1]) else return 0 end`
|
||||
_, err := g.Redis().Do(ctx, "EVAL", script, 1, l.key, l.value)
|
||||
return err
|
||||
}
|
||||
|
||||
func jobLockKey(invokeTarget string, now time.Time, period time.Duration) string {
|
||||
return fmt.Sprintf("%s:%s:%s", jobLockKeyPrefix, normalizeLockPart(invokeTarget), jobLockPeriodKey(now, period))
|
||||
}
|
||||
|
||||
func jobLockPeriodKey(now time.Time, period time.Duration) string {
|
||||
if now.IsZero() {
|
||||
now = time.Now()
|
||||
}
|
||||
if period < 0 {
|
||||
return "active"
|
||||
}
|
||||
if period >= 24*time.Hour {
|
||||
return now.Format("20060102")
|
||||
}
|
||||
if period > 0 {
|
||||
return strconv.FormatInt(now.Truncate(period).Unix(), 10)
|
||||
}
|
||||
return now.Format("20060102150405")
|
||||
}
|
||||
|
||||
func taskInvokeTarget(taskID string, fallback string) string {
|
||||
taskID = strings.TrimSpace(taskID)
|
||||
if taskID == "" {
|
||||
return fallback
|
||||
}
|
||||
if index := strings.LastIndex(taskID, "-job-"); index > 0 {
|
||||
return taskID[:index]
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func normalizeLockPart(value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
value = strings.ReplaceAll(value, ":", "_")
|
||||
value = strings.ReplaceAll(value, " ", "_")
|
||||
if value == "" {
|
||||
return "unknown"
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func jobLockValue() string {
|
||||
host, _ := os.Hostname()
|
||||
if strings.TrimSpace(host) == "" {
|
||||
host = "unknown"
|
||||
}
|
||||
return host + ":" + uuid32.S()
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package tasks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
)
|
||||
|
||||
const (
|
||||
monthlyExpiryJobPeriod = 24 * time.Hour
|
||||
monthlyExpiryJobTTL = 6 * time.Hour
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterInnerTask("NotifyMonthlyPackageExpiry", "NotifyMonthlyPackageExpiry")
|
||||
}
|
||||
|
||||
// NotifyMonthlyPackageExpiry uses an independent sys_job target and Redis lock.
|
||||
func (t Task) NotifyMonthlyPackageExpiry() {
|
||||
t.runWithRedisJobLock(context.Background(), redisJobLockOptions{
|
||||
InvokeTarget: "NotifyMonthlyPackageExpiry",
|
||||
Period: monthlyExpiryJobPeriod,
|
||||
TTL: monthlyExpiryJobTTL,
|
||||
}, service.SimWarning().NotifyMonthlyExpiry)
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package tasks
|
||||
|
||||
import (
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
_ "time/tzdata"
|
||||
|
||||
"github.com/robfig/cron/v3"
|
||||
)
|
||||
|
||||
func monthlyExpiryMigration(t *testing.T) string {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile("../../resource/migrations/20261014_monthly_package_expiry_notice.sql")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return regexp.MustCompile(`(?m)--[^\r\n]*`).ReplaceAllString(string(data), "")
|
||||
}
|
||||
|
||||
func TestMonthlyExpiryDeploymentSchedule(t *testing.T) {
|
||||
sql := monthlyExpiryMigration(t)
|
||||
matches := regexp.MustCompile(`'CRON_TZ=[^']+'`).FindString(sql)
|
||||
if matches == "" {
|
||||
t.Fatal("migration must specify cron expression")
|
||||
}
|
||||
parser := cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor)
|
||||
schedule, err := parser.Parse(strings.Trim(matches, "'"))
|
||||
if err != nil {
|
||||
t.Fatalf("deployment expression must parse with the real scheduler: %v", err)
|
||||
}
|
||||
for _, tc := range []struct{ start, expected string }{
|
||||
{"2026-10-14T01:00:00Z", "2026-10-15T01:00:00Z"},
|
||||
{"2026-10-15T01:00:00Z", "2026-11-15T01:00:00Z"},
|
||||
{"2026-12-15T01:00:00Z", "2027-01-15T01:00:00Z"},
|
||||
{"2028-02-15T01:00:00Z", "2028-03-15T01:00:00Z"},
|
||||
} {
|
||||
start, _ := time.Parse(time.RFC3339, tc.start)
|
||||
expected, _ := time.Parse(time.RFC3339, tc.expected)
|
||||
if got := schedule.Next(start); !got.Equal(expected) {
|
||||
t.Errorf("from %s: got %s, want %s", tc.start, got, expected)
|
||||
}
|
||||
}
|
||||
for _, alias := range []string{"ScanSimExpireCandidateWarning", "scan_sim_expire_candidate_warning"} {
|
||||
if got := GetInnerTaskName(alias); got != "ScanSimExpireCandidateWarning" {
|
||||
t.Errorf("existing alias %s no longer registered: %s", alias, got)
|
||||
}
|
||||
}
|
||||
if got := GetInnerTaskName("NotifyMonthlyPackageExpiry"); got != "NotifyMonthlyPackageExpiry" {
|
||||
t.Errorf("monthly task not registered independently: %s", got)
|
||||
}
|
||||
if got, want := jobLockKey("NotifyMonthlyPackageExpiry", time.Date(2026, 10, 15, 9, 0, 0, 0, time.UTC), monthlyExpiryJobPeriod), jobLockKey("scan_sim_expire_candidate_warning", time.Date(2026, 10, 15, 9, 0, 0, 0, time.UTC), warningExpireScanJobPeriod); got == want {
|
||||
t.Fatal("monthly and legacy tasks must not share the same lock")
|
||||
}
|
||||
}
|
||||
|
||||
// 只检查迁移脚本结构,不连接数据库或执行 SQL;不替代部署前的 MySQL 验证。
|
||||
func TestMonthlyExpiryDeploymentInsertOnly(t *testing.T) {
|
||||
sql := strings.Join(strings.Fields(monthlyExpiryMigration(t)), " ")
|
||||
if strings.Count(strings.ToUpper(sql), "INSERT INTO SYS_JOB") != 1 {
|
||||
t.Fatal("migration must insert only one independent sys_job")
|
||||
}
|
||||
if regexp.MustCompile(`(?i)\bUPDATE\s+sys_job\b|\bDELETE\s+FROM\s+sys_job\b|\bjob_id\b`).MatchString(sql) {
|
||||
t.Fatal("migration must not update/delete jobs or provide a generated job_id")
|
||||
}
|
||||
for _, required := range []string{
|
||||
"'NotifyMonthlyPackageExpiry'",
|
||||
"'CRON_TZ=Asia/Shanghai 0 0 9 15 * *'",
|
||||
"1, 1, '1'",
|
||||
"WHERE NOT EXISTS ( SELECT 1 FROM sys_job WHERE invoke_target = 'NotifyMonthlyPackageExpiry' )",
|
||||
} {
|
||||
if !strings.Contains(sql, required) {
|
||||
t.Errorf("migration missing required independent paused job behavior: %q", required)
|
||||
}
|
||||
}
|
||||
if strings.Contains(sql, "ScanSimExpireCandidateWarning") || strings.Contains(sql, "scan_sim_expire_candidate_warning") {
|
||||
t.Fatal("migration must not touch legacy job targets")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package tasks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
)
|
||||
|
||||
const (
|
||||
packageLifecycleJobPeriod = 15 * time.Minute
|
||||
packageLifecycleJobTTL = 45 * time.Minute
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterInnerTask("sync_package_lifecycle", "SyncPackageLifecycle")
|
||||
RegisterInnerTask("SyncPackageLifecycle", "SyncPackageLifecycle")
|
||||
}
|
||||
|
||||
func (t Task) SyncPackageLifecycle() {
|
||||
t.runWithRedisJobLock(context.Background(), redisJobLockOptions{
|
||||
InvokeTarget: "sync_package_lifecycle",
|
||||
Period: packageLifecycleJobPeriod,
|
||||
TTL: packageLifecycleJobTTL,
|
||||
}, service.PackageLifecycle().Sync)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package tasks
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
)
|
||||
|
||||
func init() { RegisterInnerTask("syns_sim_status_queue", "SynsSimStatusQueue") }
|
||||
|
||||
// SynsSimStatusQueue is the sole scheduled entry point for full status sync.
|
||||
// The legacy admin-process implementation was removed after the queue path
|
||||
// became the supported production flow.
|
||||
func (t Task) SynsSimStatusQueue() {
|
||||
if err := service.SimStatusQueue().StartFullSync(context.Background()); err != nil {
|
||||
g.Log().Errorf(context.Background(), "start sim status queue sync failed: %+v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package tasks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
warningTrafficJobPeriod = -time.Second
|
||||
warningTrafficJobTTL = 45 * time.Minute
|
||||
warningExpirePreloadJobPeriod = 24 * time.Hour
|
||||
warningExpirePreloadJobTTL = 6 * time.Hour
|
||||
warningExpireScanJobPeriod = time.Hour
|
||||
warningExpireScanJobTTL = 30 * time.Minute
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterInnerTask("scan_sim_warning", "ScanSimWarning")
|
||||
RegisterInnerTask("scan_sim_traffic_warning", "ScanSimTrafficWarning")
|
||||
RegisterInnerTask("scan_sim_flow_pool_warning", "ScanSimFlowPoolWarning")
|
||||
RegisterInnerTask("scan_sim_expire_warning", "ScanSimExpireWarning")
|
||||
RegisterInnerTask("preload_sim_expire_warning", "PreloadSimExpireWarning")
|
||||
RegisterInnerTask("scan_sim_expire_candidate_warning", "ScanSimExpireCandidateWarning")
|
||||
RegisterInnerTask("ScanSimWarning", "ScanSimWarning")
|
||||
RegisterInnerTask("ScanSimTrafficWarning", "ScanSimTrafficWarning")
|
||||
RegisterInnerTask("ScanSimFlowPoolWarning", "ScanSimFlowPoolWarning")
|
||||
RegisterInnerTask("ScanSimExpireWarning", "ScanSimExpireWarning")
|
||||
RegisterInnerTask("PreloadSimExpireWarning", "PreloadSimExpireWarning")
|
||||
RegisterInnerTask("ScanSimExpireCandidateWarning", "ScanSimExpireCandidateWarning")
|
||||
RegisterInnerTask("check_flow_pool_warning", "ScanSimFlowPoolWarning")
|
||||
RegisterInnerTask("notify_package_expiry", "ScanSimExpireWarning")
|
||||
}
|
||||
|
||||
func (t Task) ScanSimWarning() {
|
||||
t.runWithRedisJobLock(context.Background(), redisJobLockOptions{
|
||||
InvokeTarget: "scan_sim_warning",
|
||||
Period: warningTrafficJobPeriod,
|
||||
TTL: warningTrafficJobTTL,
|
||||
}, service.SimWarning().Scan)
|
||||
}
|
||||
|
||||
func (t Task) ScanSimTrafficWarning() {
|
||||
t.runWithRedisJobLock(context.Background(), redisJobLockOptions{
|
||||
InvokeTarget: "scan_sim_warning",
|
||||
Period: warningTrafficJobPeriod,
|
||||
TTL: warningTrafficJobTTL,
|
||||
}, service.SimWarning().ScanTraffic)
|
||||
}
|
||||
|
||||
func (t Task) ScanSimFlowPoolWarning() {
|
||||
t.runWithRedisJobLock(context.Background(), redisJobLockOptions{
|
||||
InvokeTarget: "scan_sim_warning",
|
||||
Period: warningTrafficJobPeriod,
|
||||
TTL: warningTrafficJobTTL,
|
||||
}, service.SimWarning().ScanFlowPool)
|
||||
}
|
||||
|
||||
func (t Task) ScanSimExpireWarning() {
|
||||
t.runWithRedisJobLock(context.Background(), redisJobLockOptions{
|
||||
InvokeTarget: "notify_package_expiry",
|
||||
Period: warningExpireScanJobPeriod,
|
||||
TTL: warningExpireScanJobTTL,
|
||||
}, service.SimWarning().ScanExpireCandidates)
|
||||
}
|
||||
|
||||
func (t Task) PreloadSimExpireWarning() {
|
||||
t.runWithRedisJobLock(context.Background(), redisJobLockOptions{
|
||||
InvokeTarget: "preload_sim_expire_warning",
|
||||
Period: warningExpirePreloadJobPeriod,
|
||||
TTL: warningExpirePreloadJobTTL,
|
||||
}, service.SimWarning().PreloadExpireCandidates)
|
||||
}
|
||||
|
||||
func (t Task) ScanSimExpireCandidateWarning() {
|
||||
t.runWithRedisJobLock(context.Background(), redisJobLockOptions{
|
||||
InvokeTarget: "scan_sim_expire_candidate_warning",
|
||||
Period: warningExpireScanJobPeriod,
|
||||
TTL: warningExpireScanJobTTL,
|
||||
}, service.SimWarning().ScanExpireCandidates)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package tasks
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type TaskLog struct {
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func (l *TaskLog) Info(msg string, keysAndValues ...interface{}) {
|
||||
g.Log().Infof(l.ctx, "%s, %v", msg, keysAndValues)
|
||||
}
|
||||
|
||||
func (l *TaskLog) Error(err error, msg string, keysAndValues ...interface{}) {
|
||||
g.Log().Errorf(l.ctx, "%s, %v", msg, keysAndValues)
|
||||
}
|
||||
@@ -0,0 +1,395 @@
|
||||
package tasks
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/robfig/cron/v3"
|
||||
"go.opentelemetry.io/otel"
|
||||
)
|
||||
|
||||
// Task 任务结构体 内置任务
|
||||
type Task struct {
|
||||
ID string //任务ID
|
||||
TaskType string //任务类型
|
||||
MethodName string //方法名
|
||||
Params []interface{} //参数
|
||||
Explain string //任务描述
|
||||
}
|
||||
|
||||
type TaskOptions struct {
|
||||
Uid string
|
||||
Payload []byte
|
||||
Expr *string // 仅周期任务
|
||||
In *time.Duration // 仅一次性任务 当前时间的延迟
|
||||
At *time.Time // 仅一次性任务 指定时间
|
||||
Now *bool // 仅一次性任务 当前时间
|
||||
Replace bool // 仅一次性任务 是否替换已存在的任务
|
||||
Ctx context.Context // 仅一次性任务
|
||||
MaxRetry int
|
||||
Timeout int // 限制并发堵塞超时时间
|
||||
Concurrent int // 0不限制并发 1限制并发
|
||||
}
|
||||
type taskInfo struct {
|
||||
entryID int
|
||||
option TaskOptions
|
||||
run bool
|
||||
}
|
||||
|
||||
type Tasks struct {
|
||||
cron *cron.Cron
|
||||
lock sync.Mutex
|
||||
taskMap map[string]*taskInfo
|
||||
}
|
||||
|
||||
var (
|
||||
instance *Tasks
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
func TasksInstance(ctx context.Context) *Tasks {
|
||||
once.Do(func() {
|
||||
instance, _ = NewTasks(ctx) // Initialize only once
|
||||
})
|
||||
return instance
|
||||
}
|
||||
|
||||
func taskExecuteContext(ctx context.Context) context.Context {
|
||||
if ctx == nil {
|
||||
return context.Background()
|
||||
}
|
||||
return context.WithoutCancel(ctx)
|
||||
}
|
||||
|
||||
// Once 注册一个任务以运行一次。
|
||||
func (tk *Tasks) Once(ctx context.Context, taskOptions TaskOptions) error {
|
||||
// return tk.worker.Once(options...)
|
||||
info := &taskInfo{
|
||||
entryID: 0,
|
||||
option: taskOptions,
|
||||
run: false,
|
||||
}
|
||||
spec := ""
|
||||
if taskOptions.Expr != nil && *taskOptions.Expr != "" {
|
||||
spec = *taskOptions.Expr
|
||||
} else if taskOptions.In != nil {
|
||||
spec = fmt.Sprintf("@every %s", *taskOptions.In)
|
||||
} else if taskOptions.At != nil {
|
||||
spec = fmt.Sprintf("@every %s", time.Until(*taskOptions.At).String())
|
||||
} else if taskOptions.Now != nil && *taskOptions.Now {
|
||||
runCtx := taskExecuteContext(ctx)
|
||||
go func() {
|
||||
tk.process(runCtx, info)
|
||||
}()
|
||||
g.Log().Infof(ctx, "Tasks.Once end. info: %+v", info)
|
||||
return nil
|
||||
} else {
|
||||
return errors.New("expr is required")
|
||||
}
|
||||
tk.lock.Lock()
|
||||
tk.taskMap[taskOptions.Uid] = info
|
||||
tk.lock.Unlock()
|
||||
runCtx := taskExecuteContext(ctx)
|
||||
entryID, _ := tk.cron.AddFunc(spec, func() {
|
||||
tk.process(runCtx, info)
|
||||
tk.lock.Lock()
|
||||
delete(tk.taskMap, taskOptions.Uid)
|
||||
tk.lock.Unlock()
|
||||
})
|
||||
tk.lock.Lock()
|
||||
info, ok := tk.taskMap[taskOptions.Uid]
|
||||
if ok {
|
||||
info.entryID = int(entryID)
|
||||
}
|
||||
tk.lock.Unlock()
|
||||
g.Log().Infof(ctx, "Tasks.Once end. info: %+v", info)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 注册一个任务以由cron表达式运行。
|
||||
func (tk *Tasks) Cron(ctx context.Context, taskOptions TaskOptions) (id string, err error) {
|
||||
spec := ""
|
||||
if taskOptions.Expr != nil && *taskOptions.Expr != "" {
|
||||
spec = *taskOptions.Expr
|
||||
} else {
|
||||
return "", errors.New("expr is required")
|
||||
}
|
||||
tk.lock.Lock()
|
||||
tk.taskMap[taskOptions.Uid] = &taskInfo{
|
||||
entryID: 0,
|
||||
option: taskOptions,
|
||||
run: false,
|
||||
}
|
||||
tk.lock.Unlock()
|
||||
runCtx := taskExecuteContext(ctx)
|
||||
entryID, _ := tk.cron.AddFunc(spec, func() {
|
||||
tk.lock.Lock()
|
||||
info, ok := tk.taskMap[taskOptions.Uid]
|
||||
g.Log().Infof(ctx, "Tasks.Cron AddFunc func begin. uid: %s, spec: %s, ok: %v", taskOptions.Uid, spec, ok)
|
||||
if !ok {
|
||||
tk.lock.Unlock()
|
||||
return
|
||||
}
|
||||
tk.lock.Unlock()
|
||||
start := time.Now()
|
||||
for {
|
||||
if taskOptions.Timeout > 0 && time.Since(start) > time.Second*time.Duration(taskOptions.Timeout) {
|
||||
g.Log().Infof(ctx, "Tasks.Cron AddFunc func timeout. uid: %s, spec: %s, timeout: %d, start: %s", taskOptions.Uid, spec, taskOptions.Timeout, start.Format("2006-01-02 15:04:05"))
|
||||
return
|
||||
}
|
||||
tk.lock.Lock()
|
||||
if info.run && info.option.Concurrent == 1 {
|
||||
tk.lock.Unlock()
|
||||
time.Sleep(time.Second * 1)
|
||||
continue
|
||||
}
|
||||
info.run = true
|
||||
tk.lock.Unlock()
|
||||
break
|
||||
}
|
||||
tk.process(runCtx, info)
|
||||
tk.lock.Lock()
|
||||
info.run = false
|
||||
tk.lock.Unlock()
|
||||
})
|
||||
tk.lock.Lock()
|
||||
info, ok := tk.taskMap[taskOptions.Uid]
|
||||
if ok {
|
||||
info.entryID = int(entryID)
|
||||
}
|
||||
tk.lock.Unlock()
|
||||
nextTime := tk.cron.Entry(entryID).Next
|
||||
nextTimeStr := nextTime.Format("2006-01-02 15:04:05")
|
||||
sub := nextTime.Sub(time.Now())
|
||||
g.Log().Infof(ctx, "Tasks.Cron end. entryID: %d, uid: %s, spec: %s, nextTime:%s, sub:%v", info.entryID, info.option.Uid, spec, nextTimeStr, sub)
|
||||
return gconv.String(info.entryID), nil
|
||||
}
|
||||
|
||||
// 从任务队列中删除一个任务。
|
||||
func (tk *Tasks) Remove(uid string) error {
|
||||
tk.lock.Lock()
|
||||
info, ok := tk.taskMap[uid]
|
||||
if ok {
|
||||
tk.cron.Remove(cron.EntryID(info.entryID))
|
||||
delete(tk.taskMap, uid)
|
||||
}
|
||||
tk.lock.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tk *Tasks) RemoveCron(entryID string) error {
|
||||
tk.lock.Lock()
|
||||
for _, info := range tk.taskMap {
|
||||
if gconv.String(info.entryID) == entryID {
|
||||
tk.cron.Remove(cron.EntryID(info.entryID))
|
||||
delete(tk.taskMap, info.option.Uid)
|
||||
break
|
||||
}
|
||||
}
|
||||
tk.lock.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tk Tasks) GetTask(uid string) (task *Task, err error) {
|
||||
tk.lock.Lock()
|
||||
info, ok := tk.taskMap[uid]
|
||||
if !ok {
|
||||
return nil, errors.New("task not found")
|
||||
}
|
||||
tk.lock.Unlock()
|
||||
|
||||
taskRun := &Task{}
|
||||
|
||||
err = json.Unmarshal(info.option.Payload, &taskRun)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return taskRun, nil
|
||||
}
|
||||
|
||||
func NewTasks(ctx context.Context) (tk *Tasks, err error) {
|
||||
defer func() {
|
||||
e := recover()
|
||||
if e != nil {
|
||||
err = errors.Errorf("%v", e)
|
||||
}
|
||||
}()
|
||||
// logger := &TaskLog{ctx: ctx}
|
||||
c := cron.New(cron.WithLocation(time.Local),
|
||||
cron.WithSeconds(),
|
||||
// cron.WithChain(cron.SkipIfStillRunning(logger)),
|
||||
)
|
||||
// w := worker.New(
|
||||
// worker.WithWorkerHandler(func(ctx context.Context, p worker.Payload) error {
|
||||
// return process(task{
|
||||
// ctx: ctx,
|
||||
// payload: p,
|
||||
// })
|
||||
// }),
|
||||
// worker.WithWorkerRedisPeriodKey("TaskSchedule"),
|
||||
// )
|
||||
// if w.Error != nil {
|
||||
// err = errors.WithMessage(w.Error, "initialize worker failed")
|
||||
// return
|
||||
// }
|
||||
c.Start()
|
||||
tk = &Tasks{
|
||||
cron: c,
|
||||
taskMap: make(map[string]*taskInfo),
|
||||
}
|
||||
g.Log().Debug(ctx, "NewTasks initialize tasks success")
|
||||
return
|
||||
}
|
||||
|
||||
func (tk *Tasks) process(ctx context.Context, t *taskInfo) (err error) {
|
||||
g.Log().Debug(ctx, "Tasks.process payload", t.option.Payload)
|
||||
tr := otel.Tracer("task")
|
||||
_, span := tr.Start(ctx, "Task")
|
||||
defer span.End()
|
||||
var taskRun Task
|
||||
err = json.Unmarshal(t.option.Payload, &taskRun)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
g.Log().Debug(ctx, "Tasks.process task", taskRun)
|
||||
err = CallMethod(ctx, &taskRun)
|
||||
if err != nil {
|
||||
fmt.Println("CallMethod err:", err.Error())
|
||||
return err
|
||||
}
|
||||
g.Log().Debug(ctx, "Tasks.process end. task", taskRun)
|
||||
return
|
||||
}
|
||||
|
||||
// CallMethod 调用任务的实际方法
|
||||
func CallMethod(ctx context.Context, task *Task) (err error) {
|
||||
// 获取TaskModel的反射值
|
||||
taskValue := reflect.ValueOf(task)
|
||||
// 准备要调用的方法的参数
|
||||
var args []reflect.Value
|
||||
for _, param := range task.Params {
|
||||
if !reflect.ValueOf(param).IsValid() {
|
||||
err = errors.New("invalid parameter")
|
||||
return
|
||||
}
|
||||
args = append(args, reflect.ValueOf(param))
|
||||
}
|
||||
// 查找并执行方法
|
||||
method := taskValue.MethodByName(task.MethodName)
|
||||
if method.IsValid() {
|
||||
if method.Type().NumIn() == 0 {
|
||||
method.Call(nil)
|
||||
return
|
||||
} else if method.Type().NumIn() != len(args) {
|
||||
err = errors.New("incorrect number of parameters")
|
||||
return
|
||||
}
|
||||
g.Log().Debug(context.Background(), "执行的任务:", task.MethodName)
|
||||
for i := 0; i < method.Type().NumIn(); i++ {
|
||||
switch method.Type().In(i).Kind() {
|
||||
case reflect.Float32:
|
||||
args[i] = reflect.ValueOf(gconv.Float32(task.Params[i]))
|
||||
case reflect.Float64:
|
||||
args[i] = reflect.ValueOf(gconv.Float64(task.Params[i]))
|
||||
case reflect.Uint64:
|
||||
args[i] = reflect.ValueOf(gconv.Uint64(task.Params[i]))
|
||||
case reflect.Int64:
|
||||
args[i] = reflect.ValueOf(gconv.Int64(task.Params[i]))
|
||||
case reflect.Int:
|
||||
args[i] = reflect.ValueOf(gconv.Int(task.Params[i]))
|
||||
case reflect.Int32:
|
||||
args[i] = reflect.ValueOf(gconv.Int32(task.Params[i]))
|
||||
case reflect.Int16:
|
||||
args[i] = reflect.ValueOf(gconv.Int16(task.Params[i]))
|
||||
case reflect.Int8:
|
||||
args[i] = reflect.ValueOf(gconv.Int8(task.Params[i]))
|
||||
case reflect.Uint:
|
||||
args[i] = reflect.ValueOf(gconv.Uint(task.Params[i]))
|
||||
case reflect.Uint32:
|
||||
args[i] = reflect.ValueOf(gconv.Uint32(task.Params[i]))
|
||||
case reflect.Uint16:
|
||||
args[i] = reflect.ValueOf(gconv.Uint16(task.Params[i]))
|
||||
case reflect.Uint8:
|
||||
args[i] = reflect.ValueOf(gconv.Uint8(task.Params[i]))
|
||||
case reflect.String:
|
||||
args[i] = reflect.ValueOf(gconv.String(task.Params[i]))
|
||||
case reflect.Bool:
|
||||
args[i] = reflect.ValueOf(gconv.Bool(task.Params[i]))
|
||||
default:
|
||||
args[i] = reflect.ValueOf(task.Params[i])
|
||||
}
|
||||
}
|
||||
if len(task.Params) > 0 {
|
||||
method.Call(args)
|
||||
} else {
|
||||
method.Call(nil)
|
||||
}
|
||||
} else {
|
||||
errInfo := fmt.Sprintf("Method not found: %s", task.MethodName)
|
||||
err = errors.New(errInfo)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// UnmarshalTask 解析TaskJob,使用自定义JSON解析来处理类型问题
|
||||
func UnmarshalTask(data []byte) (*Task, error) {
|
||||
var task Task
|
||||
dec := json.NewDecoder(bytes.NewReader(data))
|
||||
// 使用json.Decoder逐个解析Token,自定义解析逻辑
|
||||
for {
|
||||
t, err := dec.Token()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 根据Token类型处理
|
||||
switch v := t.(type) {
|
||||
case float64:
|
||||
// 如果是数字 ,检查是否可以转换为整数
|
||||
if v == float64(int64(v)) {
|
||||
// 如果可以转换为整数,则转换为int类型
|
||||
task.Params = append(task.Params, int64(v))
|
||||
} else {
|
||||
// 如果不能转换为整数,则保留为float64类型
|
||||
task.Params = append(task.Params, v)
|
||||
}
|
||||
case string, bool, nil:
|
||||
// 对于字符串、布尔和空值,直接添加到Params中
|
||||
task.Params = append(task.Params, v)
|
||||
// 可以添加更多的类型处理
|
||||
}
|
||||
}
|
||||
return &task, nil
|
||||
}
|
||||
|
||||
// CheckFuncName 检查方法名是否存在
|
||||
func (tk *Tasks) CheckFuncName(funcName string) (actualfuncName string, exists bool) {
|
||||
exist := GetInnerTaskName(funcName)
|
||||
g.Log().Debug(context.Background(), "CheckFuncName", exist)
|
||||
if exist != "" {
|
||||
return exist, true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ParseParameters 解析参数
|
||||
func (tk *Tasks) ParseParameters(parseData string) (params []interface{}, err error) {
|
||||
parts := strings.Split(parseData, "|")
|
||||
params = make([]interface{}, len(parts))
|
||||
for i, part := range parts {
|
||||
params[i] = part
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package tasks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
trafficCutoffJobPeriod = 15 * time.Minute
|
||||
trafficCutoffJobTTL = 45 * time.Minute
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterInnerTask("check_traffic_cutoff", "CheckTrafficCutoff")
|
||||
RegisterInnerTask("CheckTrafficCutoff", "CheckTrafficCutoff")
|
||||
}
|
||||
|
||||
func (t Task) CheckTrafficCutoff() {
|
||||
t.runWithRedisJobLock(context.Background(), redisJobLockOptions{
|
||||
InvokeTarget: "check_traffic_cutoff",
|
||||
Period: trafficCutoffJobPeriod,
|
||||
TTL: trafficCutoffJobTTL,
|
||||
}, service.TrafficCutoff().Check)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package tasks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterInnerTask("sync_traffic_pool_usage_daily", "SyncTrafficPoolUsageDaily")
|
||||
RegisterInnerTask("sync_traffic_pool_usage_monthly", "SyncTrafficPoolUsageMonthly")
|
||||
}
|
||||
|
||||
func (t Task) SyncTrafficPoolUsageDaily() {
|
||||
service.TrafficPoolUsageSync().SyncDaily(context.Background())
|
||||
}
|
||||
|
||||
func (t Task) SyncTrafficPoolUsageMonthly() {
|
||||
service.TrafficPoolUsageSync().SyncMonthly(context.Background())
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package tasks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterInnerTask("sync_traffic_usage_daily", "SyncTrafficUsageDaily")
|
||||
}
|
||||
|
||||
// SyncTrafficUsageDaily publishes one command for Worker to execute the
|
||||
// previous-day usage full sync. Admin no longer scans cards itself: Worker owns
|
||||
// the scan, per-channel pacing, rounds, failure pool and replay.
|
||||
func (t Task) SyncTrafficUsageDaily() {
|
||||
t.runWithRedisJobLock(context.Background(), redisJobLockOptions{
|
||||
InvokeTarget: "sync_traffic_usage_daily",
|
||||
Period: -1,
|
||||
TTL: 30 * time.Minute,
|
||||
}, func(ctx context.Context) {
|
||||
if err := service.TrafficUsageDailySync().StartDaily(ctx); err != nil {
|
||||
// The timer must fail visibly when the start command cannot be
|
||||
// published; Worker owns the actual full scan and overlap decision.
|
||||
g.Log().Errorf(ctx, "start daily usage full sync failed: %+v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package tasks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterInnerTask("sync_traffic_usage_monthly", "SyncTrafficUsageMonthly")
|
||||
RegisterInnerTask("start_traffic_usage_full_sync", "StartTrafficUsageFullSync")
|
||||
RegisterInnerTask("start_traffic_usage_monthly_history", "StartTrafficUsageMonthlyHistory")
|
||||
}
|
||||
|
||||
// SyncTrafficUsageMonthly publishes one command for Worker to execute the
|
||||
// previous-month usage history sync. Admin no longer scans cards itself: Worker
|
||||
// owns the scan, per-channel pacing, rounds, failure pool and replay.
|
||||
//
|
||||
// The timer name is unchanged so an existing sys_job row keeps working.
|
||||
func (t Task) SyncTrafficUsageMonthly() {
|
||||
t.runWithRedisJobLock(context.Background(), redisJobLockOptions{
|
||||
InvokeTarget: "sync_traffic_usage_monthly",
|
||||
Period: -1,
|
||||
TTL: 30 * time.Minute,
|
||||
}, func(ctx context.Context) {
|
||||
if err := service.TrafficUsageMonthlySync().StartMonthlyHistory(ctx); err != nil {
|
||||
// The timer must fail visibly when the start command cannot be
|
||||
// published; Worker owns the actual full scan and overlap decision.
|
||||
g.Log().Errorf(ctx, "start monthly usage history full sync failed: %+v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (t Task) StartTrafficUsageFullSync() {
|
||||
if err := service.TrafficUsageMonthlySync().StartMonthly(context.Background()); err != nil {
|
||||
// The timer should fail visibly when the start command cannot be published;
|
||||
// Worker owns the actual full scan and run-overlap decision.
|
||||
g.Log().Errorf(context.Background(), "start monthly usage full sync failed: %+v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// StartTrafficUsageMonthlyHistory is the explicit entry point for the monthly
|
||||
// history chain, kept separate from the legacy-named timer above so operators
|
||||
// can trigger a backfill without touching the scheduled row.
|
||||
func (t Task) StartTrafficUsageMonthlyHistory() {
|
||||
if err := service.TrafficUsageMonthlySync().StartMonthlyHistory(context.Background()); err != nil {
|
||||
g.Log().Errorf(context.Background(), "start monthly usage history full sync failed: %+v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package tasks
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestTrafficUsageMonthlyTaskRegistrations(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
"sync_traffic_usage_monthly": "SyncTrafficUsageMonthly",
|
||||
"start_traffic_usage_full_sync": "StartTrafficUsageFullSync",
|
||||
}
|
||||
for name, method := range tests {
|
||||
if actual := GetInnerTaskName(name); actual != method {
|
||||
t.Fatalf("task %q registered as %q, want %q", name, actual, method)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrafficUsageJobLockUsesStableActiveScope(t *testing.T) {
|
||||
first := time.Date(2026, 9, 20, 23, 59, 0, 0, time.Local)
|
||||
second := first.Add(2 * time.Minute)
|
||||
if got := jobLockPeriodKey(first, -1); got != "active" {
|
||||
t.Fatalf("first active key = %q", got)
|
||||
}
|
||||
if got := jobLockPeriodKey(second, -1); got != "active" {
|
||||
t.Fatalf("second active key = %q", got)
|
||||
}
|
||||
if jobLockKey("sync_traffic_usage_daily", first, -1) != jobLockKey("sync_traffic_usage_daily", second, -1) {
|
||||
t.Fatal("active job lock key must remain stable across midnight")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user