154b0b62ad
build / build (push) Successful in 1m20s
新增北京时间自然周校验与缓存隔离,默认展开到业务二级部门。 选中部门在统计页就地绑定;区域名称列表原位增删改,双版本锁、自动预览、独立读回及删除确认。 筛选和两张区域看板使用同一已保存目录,删除失效筛选回全部。 验证:完整 make verify、全 mirror 隔离 PostgreSQL race、自然周边界与缓存键负例通过;真实恢复库327部门逐行对拍及六项浏览器验收通过。 schema57无迁移,不改真实公司区域绑定或原始问答,不连接DobotAI生产源库;PNG导出本轮不冒充已验证。
102 lines
3.2 KiB
Go
102 lines
3.2 KiB
Go
package qaopsmirror
|
|
|
|
import (
|
|
"context"
|
|
"knowledge-governance/internal/dobotaiops"
|
|
"knowledge-governance/internal/qaopsorg"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type DepartmentStatisticsQuery struct {
|
|
Period string `json:"period"`
|
|
Month string `json:"month,omitempty"`
|
|
Week string `json:"week,omitempty"`
|
|
}
|
|
type DepartmentStatisticsSnapshot struct {
|
|
Regions []qaopsorg.RegionDefinition `json:"regions"`
|
|
qaopsorg.DepartmentStatistics
|
|
Organization dobotaiops.OrganizationVersion `json:"organization"`
|
|
Period string `json:"period"`
|
|
Month string `json:"month"`
|
|
Week string `json:"week"`
|
|
Start *time.Time `json:"start"`
|
|
End time.Time `json:"end"`
|
|
GeneratedAt time.Time `json:"generated_at"`
|
|
ExpiresAt time.Time `json:"expires_at"`
|
|
}
|
|
type DepartmentStatisticsResult struct {
|
|
CacheKey string `json:"cache_key"`
|
|
State string `json:"state"`
|
|
CacheHit bool `json:"cache_hit"`
|
|
RetryAfter int `json:"retry_after"`
|
|
Snapshot *DepartmentStatisticsSnapshot `json:"snapshot"`
|
|
}
|
|
type departmentStatisticsEntry struct {
|
|
snapshot *DepartmentStatisticsSnapshot
|
|
err error
|
|
done bool
|
|
expires time.Time
|
|
}
|
|
type departmentStatisticsCache struct {
|
|
mu sync.Mutex
|
|
entries map[string]*departmentStatisticsEntry
|
|
slots chan struct{}
|
|
}
|
|
|
|
func (c *departmentStatisticsCache) get(key string, now time.Time, load func(context.Context) (DepartmentStatisticsSnapshot, error)) (DepartmentStatisticsResult, error) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if c.entries == nil {
|
|
c.entries = map[string]*departmentStatisticsEntry{}
|
|
c.slots = make(chan struct{}, 1)
|
|
}
|
|
if entry := c.entries[key]; entry != nil && (!entry.done || now.Before(entry.expires)) {
|
|
if entry.err != nil {
|
|
return DepartmentStatisticsResult{}, dobotaiops.ErrUnavailable
|
|
}
|
|
if entry.done {
|
|
return DepartmentStatisticsResult{State: "ready", CacheKey: key, CacheHit: true, Snapshot: entry.snapshot}, nil
|
|
}
|
|
return DepartmentStatisticsResult{State: "pending", CacheKey: key, RetryAfter: 1}, nil
|
|
}
|
|
for k, v := range c.entries {
|
|
if v.done && !now.Before(v.expires) {
|
|
delete(c.entries, k)
|
|
}
|
|
}
|
|
if len(c.entries) >= 32 {
|
|
return DepartmentStatisticsResult{}, dobotaiops.ErrUnavailable
|
|
}
|
|
entry := &departmentStatisticsEntry{}
|
|
c.entries[key] = entry
|
|
go func() {
|
|
started := time.Now()
|
|
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
|
defer cancel()
|
|
var data DepartmentStatisticsSnapshot
|
|
var err error
|
|
select {
|
|
case c.slots <- struct{}{}:
|
|
data, err = load(ctx)
|
|
<-c.slots
|
|
case <-ctx.Done():
|
|
err = ctx.Err()
|
|
}
|
|
finished := now.Add(time.Since(started))
|
|
data.GeneratedAt = finished
|
|
data.ExpiresAt = finished.Add(time.Minute)
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
entry.done = true
|
|
entry.err = err
|
|
entry.expires = data.ExpiresAt
|
|
if err != nil {
|
|
entry.expires = finished.Add(5 * time.Second)
|
|
} else {
|
|
entry.snapshot = &data
|
|
}
|
|
}()
|
|
return DepartmentStatisticsResult{State: "pending", CacheKey: key, RetryAfter: 1}, nil
|
|
}
|