99 lines
2.8 KiB
Go
99 lines
2.8 KiB
Go
package teamsalarysettlement
|
||
|
||
import (
|
||
"context"
|
||
"log/slog"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
)
|
||
|
||
const autoRunnerInterval = time.Hour
|
||
|
||
// AutoRunner 是 admin-server 内的轻量自动结算循环;真正的幂等不靠 runner,而靠 wallet 结算进度表。
|
||
type AutoRunner struct {
|
||
service *Service
|
||
appCode string
|
||
worker string
|
||
cancel context.CancelFunc
|
||
wg sync.WaitGroup
|
||
}
|
||
|
||
// NewAutoRunner 只保存运行参数,不立刻启动 goroutine,便于 main 按 jobs.enabled 控制生命周期。
|
||
func NewAutoRunner(service *Service, appCode string, worker string) *AutoRunner {
|
||
return &AutoRunner{
|
||
service: service,
|
||
appCode: normalizeAppCode(appCode),
|
||
worker: strings.TrimSpace(worker),
|
||
}
|
||
}
|
||
|
||
// Start 启动后台循环;重复调用不创建第二个 worker,避免同进程内重复扫描。
|
||
func (r *AutoRunner) Start() {
|
||
if r == nil || r.service == nil || r.cancel != nil {
|
||
return
|
||
}
|
||
ctx, cancel := context.WithCancel(context.Background())
|
||
r.cancel = cancel
|
||
r.wg.Add(1)
|
||
go r.loop(ctx)
|
||
}
|
||
|
||
// Close 停止后台循环并等待退出,保证 admin-server 优雅关闭时不会截断正在执行的事务。
|
||
func (r *AutoRunner) Close() {
|
||
if r == nil || r.cancel == nil {
|
||
return
|
||
}
|
||
r.cancel()
|
||
r.wg.Wait()
|
||
r.cancel = nil
|
||
}
|
||
|
||
// loop 启动后先立即检查一次,再按小时检查;是否真正发薪由 RunAutomaticDue 按日期判断。
|
||
func (r *AutoRunner) loop(ctx context.Context) {
|
||
defer r.wg.Done()
|
||
r.runOnce(ctx, time.Now())
|
||
ticker := time.NewTicker(autoRunnerInterval)
|
||
defer ticker.Stop()
|
||
for {
|
||
select {
|
||
case <-ctx.Done():
|
||
return
|
||
case now := <-ticker.C:
|
||
r.runOnce(ctx, now)
|
||
}
|
||
}
|
||
}
|
||
|
||
// runOnce 给单次自动结算设置超时,避免数据库异常时 goroutine 永久卡住。
|
||
func (r *AutoRunner) runOnce(parent context.Context, now time.Time) {
|
||
ctx, cancel := context.WithTimeout(parent, 10*time.Minute)
|
||
defer cancel()
|
||
// 多节点同时触发时不单独抢锁;结算进度行和 command_id 唯一索引会保证同一周期同一用户只发一次。
|
||
result, err := r.service.RunAutomaticDue(ctx, r.appCode, now)
|
||
if err != nil {
|
||
slog.Error("team_salary_auto_settlement_failed", "worker", r.worker, "error", err)
|
||
return
|
||
}
|
||
if !result.Due {
|
||
return
|
||
}
|
||
slog.Info("team_salary_auto_settlement_completed",
|
||
"worker", r.worker,
|
||
"cycle_key", result.CycleKey,
|
||
"bd_success", result.Results[policyTypeBD].SuccessCount,
|
||
"bd_skipped", result.Results[policyTypeBD].SkippedCount,
|
||
"admin_success", result.Results[policyTypeAdmin].SuccessCount,
|
||
"admin_skipped", result.Results[policyTypeAdmin].SkippedCount,
|
||
)
|
||
}
|
||
|
||
// normalizeAppCode 兜底默认应用编码,保证自动任务和后台手动入口都使用同一 app 维度。
|
||
func normalizeAppCode(value string) string {
|
||
value = strings.ToLower(strings.TrimSpace(value))
|
||
if value == "" {
|
||
return "lalu"
|
||
}
|
||
return value
|
||
}
|