Files
knowledge-governance/internal/qaopsmirror/department_statistics_cache.go
T
姚章浩 154b0b62ad
build / build (push) Successful in 1m20s
feat(qaops): 完成区域统计与部门绑定六项联动
新增北京时间自然周校验与缓存隔离,默认展开到业务二级部门。
选中部门在统计页就地绑定;区域名称列表原位增删改,双版本锁、自动预览、独立读回及删除确认。
筛选和两张区域看板使用同一已保存目录,删除失效筛选回全部。

验证:完整 make verify、全 mirror 隔离 PostgreSQL race、自然周边界与缓存键负例通过;真实恢复库327部门逐行对拍及六项浏览器验收通过。
schema57无迁移,不改真实公司区域绑定或原始问答,不连接DobotAI生产源库;PNG导出本轮不冒充已验证。
2026-10-19 23:41:01 +08:00

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
}