package dailyreport import ( "errors" "fmt" "strings" "time" "server/models" "server/services" "github.com/beego/beego/v2/client/orm" ) // 日报补写窗口:含今天在内的最近 7 天。 const BackfillDays = 7 // 业务错误:控制器按 Code 决定 HTTP 状态码与提示。 type Error struct { Code int Msg string } func (e *Error) Error() string { return e.Msg } func errBad(msg string) error { return &Error{Code: 400, Msg: msg} } func errForbidden(msg string) error { return &Error{Code: 403, Msg: msg} } func errNotFound(msg string) error { return &Error{Code: 404, Msg: msg} } // StatusOf 取业务错误的 HTTP 状态码,非业务错误回退 fallback。 func StatusOf(err error, fallback int) int { var e *Error if errors.As(err, &e) { return e.Code } return fallback } // UserContext 当前操作人在日报模块内的身份与可见范围。 // // UID 是 yz_system_tenant_user.uid(老 uid),不是 JWT 的 identity_id。 // TeamOrgIDs 是「当前用户作为负责人的部门」及其全部下级部门; // 为空且非管理员时,只能查看/操作自己的日报。 type UserContext struct { Tid uint64 UID uint64 Name string OrgID uint64 OrgName string IsAdmin bool TeamOrgIDs []uint64 CanViewTeam bool } // InTeamOrg 判断部门是否落在当前用户的团队范围内。 func (u *UserContext) InTeamOrg(orgID uint64) bool { for _, id := range u.TeamOrgIDs { if id == orgID { return true } } return false } // Today 本地日期 "YYYY-MM-DD"。 func Today() string { return time.Now().Format("2006-01-02") } // WindowStart 补写窗口起点(今天往前 BackfillDays-1 天)。 func WindowStart() string { return time.Now().AddDate(0, 0, -(BackfillDays - 1)).Format("2006-01-02") } // InWindow 判断日期是否在补写窗口内。 func InWindow(date string) bool { return date >= WindowStart() && date <= Today() } // ParseDate 校验 "YYYY-MM-DD" 并返回规范化的字符串。 func ParseDate(date string) (string, error) { date = strings.TrimSpace(date) if date == "" { return Today(), nil } t, err := time.ParseInLocation("2006-01-02", date, time.Local) if err != nil { return "", errBad("日报日期格式应为 YYYY-MM-DD") } return t.Format("2006-01-02"), nil } // dateOnly 把驱动返回的 DATE 值统一成 "YYYY-MM-DD"。 // go-sql-driver 在 parseTime=true 时把 DATE 读成 time.Time, // Beego ORM 赋给 string 字段后会得到 "2026-10-21 00:00:00 +0800 CST", // 直接参与 InWindow 的字符串比较会误判为“不在窗口内”。 func dateOnly(s string) string { s = strings.TrimSpace(s) if len(s) >= 10 { return s[:10] } return s } // BuildContext 组装当前用户的日报身份与团队范围。 // identityUserID 为 JWT 中的 UserID,platformAdmin 表示平台端管理员。 func BuildContext(tid uint64, identityUserID uint64, fallbackName string, platformAdmin bool) (*UserContext, error) { if models.Orm == nil || tid == 0 { return nil, errBad("缺少租户信息") } uid := services.ResolveTenantUID(tid, identityUserID) if uid == 0 { return nil, errBad("无法识别当前用户") } u := &UserContext{Tid: tid, UID: uid, Name: strings.TrimSpace(fallbackName), IsAdmin: platformAdmin} var tu models.SystemTenantUser err := models.Orm.QueryTable(new(models.SystemTenantUser)). Filter("tid", tid). Filter("uid", uid). Filter("delete_time__isnull", true). One(&tu) if err == nil { u.OrgID = tu.OrgID if n := strings.TrimSpace(ptrString(tu.Name)); n != "" { u.Name = n } else if a := strings.TrimSpace(ptrString(tu.Account)); a != "" { u.Name = a } if !u.IsAdmin { u.IsAdmin = tenantAdmin(tid, &tu) } } else if err != orm.ErrNoRows { return nil, err } if u.Name == "" { u.Name = fmt.Sprintf("用户%d", uid) } orgs, err := loadOrgs(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] } if o := orgByID[u.OrgID]; o != nil { u.OrgName = o.OrgName } if !u.IsAdmin { leaderKeys := leaderKeys(tid, &tu, identityUserID, uid) roots := make([]uint64, 0, 2) for i := range orgs { if orgs[i].LeaderID != nil && containsID(leaderKeys, *orgs[i].LeaderID) { roots = append(roots, orgs[i].ID) } } u.TeamOrgIDs = collectSubtree(roots, orgs) } u.CanViewTeam = u.IsAdmin || len(u.TeamOrgIDs) > 0 return u, nil } // tenantAdmin 与 controllers.isTenantAdmin 判定口径一致: // 未分配角色 / 租户管理员角色 / 角色未配置权限,均视为全权限。 // // 角色查询额外兼容全局预置角色:「租户管理员」「租户用户」在 // yz_system_admin_role 中是 cid=2 且 tenant_id=0 的全局记录, // 只有租户自建角色才带自己的 tenant_id,因此这里同时接受 0 与本租户。 func tenantAdmin(tid uint64, tu *models.SystemTenantUser) bool { if tu.GroupID == 0 { return true } cond := orm.NewCondition(). And("id", tu.GroupID). And("cid", 2). AndCond(orm.NewCondition().Or("tenant_id", 0).Or("tenant_id", tid)) var role models.AdminRole if err := models.Orm.QueryTable(new(models.AdminRole)).SetCond(cond).One(&role); err != nil { return false } if role.Name == "租户管理员" { return true } rights := "" if role.Rights != nil { rights = strings.TrimSpace(*role.Rights) } return rights == "" || rights == "[]" || rights == "null" } // leaderKeys 负责人在库里可能存员工ID,也可能存租户用户 uid / 认证中心 uid, // 这里把三种可能都收进来,避免因历史数据差异漏判主管身份。 func leaderKeys(tid uint64, tu *models.SystemTenantUser, identityUserID, uid uint64) []uint64 { keys := []uint64{uid} if identityUserID > 0 && identityUserID != uid { keys = append(keys, identityUserID) } account := strings.TrimSpace(ptrString(tu.Account)) if account == "" { return keys } var emps []models.BackendEmployee if _, err := models.Orm.QueryTable(new(models.BackendEmployee)). Filter("tid", tid). Filter("account", account). Filter("delete_time__isnull", true). All(&emps); err != nil { return keys } for _, e := range emps { keys = append(keys, uint64(e.ID)) } return keys } // loadOrgs 读取租户内未删除的部门。 func loadOrgs(tid uint64) ([]models.BackendOrganization, error) { var orgs []models.BackendOrganization _, err := models.Orm.QueryTable(new(models.BackendOrganization)). Filter("tid", tid). Filter("delete_time__isnull", true). OrderBy("sort", "id"). All(&orgs) if err != nil { return nil, err } return orgs, nil } // collectSubtree 返回 roots 及其全部下级部门 ID。 func collectSubtree(roots []uint64, orgs []models.BackendOrganization) []uint64 { if len(roots) == 0 { return nil } children := make(map[uint64][]uint64, len(orgs)) for _, o := range orgs { if o.ParentID > 0 { children[o.ParentID] = append(children[o.ParentID], o.ID) } } seen := make(map[uint64]bool, len(roots)) queue := make([]uint64, 0, len(roots)) for _, id := range roots { if id > 0 && !seen[id] { seen[id] = true queue = append(queue, id) } } out := make([]uint64, 0, len(queue)) for len(queue) > 0 { id := queue[0] queue = queue[1:] out = append(out, id) for _, child := range children[id] { if !seen[child] { seen[child] = true queue = append(queue, child) } } } return out } func containsID(ids []uint64, id uint64) bool { for _, v := range ids { if v == id { return true } } return false } func ptrString(p *string) string { if p == nil { return "" } return *p }