package dailyreport import ( "math" "strings" "server/models" "github.com/beego/beego/v2/client/orm" ) // TeamQuery 团队日报汇总查询。 type TeamQuery struct { Date string OrgID uint64 Keyword string Page int PageSize int } // TeamStats 团队提交统计。 type TeamStats struct { Total int `json:"total"` Submitted int `json:"submitted"` Missing int `json:"missing"` Rate float64 `json:"rate"` // 提交率百分比,保留一位小数 } // TeamRow 团队汇总中的一名成员。 type TeamRow struct { UserID uint64 `json:"user_id"` UserName string `json:"user_name"` Account string `json:"account"` OrgID uint64 `json:"org_id"` OrgName string `json:"org_name"` Submitted bool `json:"submitted"` ReportID uint64 `json:"report_id"` Status int8 `json:"status"` // -1未填写 0草稿 1已提交 SubmitTime string `json:"submit_time"` HasIssue bool `json:"has_issue"` } // TeamResult 团队汇总结果。 type TeamResult struct { Date string `json:"date"` List []TeamRow `json:"list"` Total int64 `json:"total"` Stats TeamStats `json:"stats"` } // OrgOption 团队筛选用的部门选项(前端按 parent_id 组装树)。 type OrgOption struct { ID uint64 `json:"id"` ParentID uint64 `json:"parent_id"` Name string `json:"name"` MemberCount int `json:"member_count"` } // TeamStatus 团队日报汇总:按日期返回管辖范围内每位成员的提交情况。 // 管理员不选部门时统计全租户(含未分配部门的成员)。 func TeamStatus(u *UserContext, q TeamQuery) (*TeamResult, error) { if !u.CanViewTeam { return nil, errForbidden("无权查看团队日报") } date, err := ParseDate(q.Date) if err != nil { return nil, err } if date > Today() { return nil, errBad("不能查看未来日期的团队日报") } if q.Page < 1 { q.Page = 1 } if q.PageSize < 1 || q.PageSize > 100 { q.PageSize = 20 } orgs, err := loadOrgs(u.Tid) if err != nil { return nil, err } orgByID := make(map[uint64]*models.BackendOrganization, len(orgs)) for i := range orgs { orgByID[orgs[i].ID] = &orgs[i] } var scopeIDs []uint64 if q.OrgID > 0 { if !u.IsAdmin && !u.InTeamOrg(q.OrgID) { return nil, errForbidden("无权查看该部门") } if _, ok := orgByID[q.OrgID]; !ok { return nil, errBad("部门不存在") } scopeIDs = collectSubtree([]uint64{q.OrgID}, orgs) } else if !u.IsAdmin { scopeIDs = u.TeamOrgIDs } if scopeIDs != nil && len(scopeIDs) == 0 { return nil, errForbidden("你还没有管辖的部门") } cond := orm.NewCondition(). And("tid", u.Tid). And("delete_time__isnull", true). And("status", 1). AndNot("uid", u.UID) if scopeIDs != nil { cond = cond.And("org_id__in", scopeIDs) } if kw := strings.TrimSpace(q.Keyword); kw != "" { cond = cond.AndCond(orm.NewCondition(). Or("name__icontains", kw). Or("account__icontains", kw)) } base := models.Orm.QueryTable(new(models.SystemTenantUser)).SetCond(cond) total, _ := base.Count() var members []models.SystemTenantUser if _, err := base.OrderBy("org_id", "uid"). Limit(q.PageSize).Offset((q.Page - 1) * q.PageSize). All(&members); err != nil { return nil, err } // 统计口径覆盖全部成员,不受分页影响。 var allMembers []models.SystemTenantUser if _, err := base.OrderBy("uid").All(&allMembers, "uid"); err != nil { return nil, err } reports, err := loadReportsByDate(u.Tid, date) if err != nil { return nil, err } stats := TeamStats{Total: len(allMembers)} for _, m := range allMembers { if r := reports[m.Uid]; r != nil && r.Status == 1 { stats.Submitted++ } } stats.Missing = stats.Total - stats.Submitted if stats.Total > 0 { stats.Rate = math.Round(float64(stats.Submitted)*1000/float64(stats.Total)) / 10 } list := make([]TeamRow, 0, len(members)) for i := range members { m := &members[i] row := TeamRow{ UserID: m.Uid, UserName: userNameOf(m), Account: ptrString(m.Account), OrgID: m.OrgID, Status: -1, } if o := orgByID[m.OrgID]; o != nil { row.OrgName = o.OrgName } if r := reports[m.Uid]; r != nil { row.ReportID = r.ID row.Status = r.Status row.Submitted = r.Status == 1 row.SubmitTime = formatTime(r.SubmitTime) row.HasIssue = strings.TrimSpace(r.Issues) != "" } list = append(list, row) } return &TeamResult{Date: date, List: list, Total: total, Stats: stats}, nil } // TeamOrgs 返回团队筛选可选部门;管理员为全租户,主管仅自己管辖的部门子树。 func TeamOrgs(u *UserContext) ([]OrgOption, error) { if !u.CanViewTeam { return nil, errForbidden("无权查看团队日报") } orgs, err := loadOrgs(u.Tid) if err != nil { return nil, err } allow := make(map[uint64]bool, len(orgs)) if u.IsAdmin { for _, o := range orgs { allow[o.ID] = true } } else { for _, id := range u.TeamOrgIDs { allow[id] = true } } var users []models.SystemTenantUser if _, err := models.Orm.QueryTable(new(models.SystemTenantUser)). Filter("tid", u.Tid). Filter("delete_time__isnull", true). Filter("status", 1). All(&users, "org_id"); err != nil { return nil, err } counts := make(map[uint64]int, len(orgs)) for _, m := range users { if m.OrgID > 0 { counts[m.OrgID]++ } } out := make([]OrgOption, 0, len(allow)) for i := range orgs { o := &orgs[i] if !allow[o.ID] { continue } out = append(out, OrgOption{ ID: o.ID, ParentID: o.ParentID, Name: o.OrgName, MemberCount: counts[o.ID], }) } return out, nil } // loadReportsByDate 一次取回某日全部已提交/草稿日报,按作者 uid 建索引。 func loadReportsByDate(tid uint64, date string) (map[uint64]*models.OaDailyReport, error) { var rows []models.OaDailyReport if _, err := models.Orm.QueryTable(new(models.OaDailyReport)). Filter("tid", tid). Filter("report_date", date). Filter("is_deleted", 0). All(&rows); err != nil { return nil, err } out := make(map[uint64]*models.OaDailyReport, len(rows)) for i := range rows { out[rows[i].UserID] = &rows[i] } return out, nil } func userNameOf(m *models.SystemTenantUser) string { if n := strings.TrimSpace(ptrString(m.Name)); n != "" { return n } if a := strings.TrimSpace(ptrString(m.Account)); a != "" { return a } return "未命名成员" }