Files
yunzerwebsiteallinone/go/services/dailyreport/context.go
T
2026-10-21 17:45:50 +08:00

283 lines
7.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}