更新若干功能

This commit is contained in:
hero920103 committed 2026-10-21 17:45:50 +08:00
1 parent 892ba5886d
commit 79d5ae6b66
45 files changed
+6755 -38

No files matched your search

+282
View File
@@ -0,0 +1,282 @@
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
}