修复个人中心问题

This commit is contained in:
2026-09-29 18:25:09 +08:00
parent 0dc5c2f075
commit ca4220f92c
44 changed files with 1793 additions and 310 deletions
+15 -2
View File
@@ -32,11 +32,24 @@ type SyncTenantUserInput struct {
// 注意:密码属于身份层(一人一份),因此在任一企业修改密码,
// 该用户在其他企业的登录密码会同步变化——这是统一认证的预期行为。
func SyncTenantUser(in SyncTenantUserInput) error {
_, err := SyncTenantUserAndGetIdentity(in)
return err
}
// SyncTenantUserAndGetIdentity 同 SyncTenantUser,但额外返回认证中心身份ID。
//
// 用途:新增租户用户时先把身份建出来,再用 identity_id 作为
// yz_system_tenant_user.uid——业务表(文件/客户/合同…)已在 uid 全量迁移中
// 统一成 identity_id,老表 uid 只有与它一致,业务侧才不需要任何换算。
func SyncTenantUserAndGetIdentity(in SyncTenantUserInput) (uint64, error) {
identity, err := findOrCreateIdentity(in)
if err != nil {
return err
return 0, err
}
return upsertTenantBinding(identity, in)
if err := upsertTenantBinding(identity, in); err != nil {
return 0, err
}
return identity.ID, nil
}
// RemoveTenantUser 删除该用户在指定企业的绑定(保留身份本身,
+3 -2
View File
@@ -61,9 +61,10 @@ func NewOaDocActor(tid int, uid uint64) OaDocActor {
actor := OaDocActor{Tid: tid, UID: uid, OrgIDs: []uint64{}}
var user models.SystemTenantUser
// uid 来自令牌(identity_id),查老表前先换算成老 uid
if err := models.Orm.QueryTable(new(models.SystemTenantUser)).
Filter("tid", tid).
Filter("uid", uid).
Filter("uid", ResolveTenantUID(uint64(tid), uid)).
Filter("delete_time__isnull", true).
One(&user); err != nil {
// 查不到归属信息时退化为"普通用户",仅能看公开文档
@@ -1472,7 +1473,7 @@ func oaDocShareTargetName(tid int, shareType int8, targetID uint64) string {
}
var u models.SystemTenantUser
if err := models.Orm.QueryTable(new(models.SystemTenantUser)).
Filter("tid", tid).Filter("uid", targetID).Filter("delete_time__isnull", true).One(&u); err == nil {
Filter("tid", tid).Filter("uid", ResolveTenantUID(uint64(tid), targetID)).Filter("delete_time__isnull", true).One(&u); err == nil {
if u.Name != nil {
return *u.Name
}
+2 -1
View File
@@ -11,9 +11,10 @@ func OperationLogUser(tid *uint64, userID uint64) (account, name string) {
if tid != nil && *tid != 0 {
var user models.SystemTenantUser
// userID 是令牌里的 identity_id,查老表前先换算成老 uid
if err := models.Orm.QueryTable(new(models.SystemTenantUser)).
Filter("tid", *tid).
Filter("uid", userID).
Filter("uid", ResolveTenantUID(*tid, userID)).
One(&user); err == nil {
if user.Account != nil {
account = *user.Account
+59
View File
@@ -153,6 +153,59 @@ func paybackPlanOverdueFor(plan models.TenantCrmPayback, today string) bool {
return false
}
// crmProjectStatusText 项目状态文案(与控制器层 projectStatusName 保持一致,用于操作日志)。
func crmProjectStatusText(s int8) string {
switch s {
case 1:
return "未开始"
case 2:
return "进行中"
case 3:
return "已完成"
case 4:
return "已暂停"
case 5:
return "异常"
}
return "未知"
}
// crmContractStatusText 合同状态文案(与控制器层 contractStatusName 保持一致,用于操作日志)。
func crmContractStatusText(s int8) string {
switch s {
case 1:
return "草稿"
case 2:
return "已完成"
case 3:
return "已作废"
case 4:
return "履约中"
case 5:
return "异常"
}
return "未知"
}
// writeOverdueStatusLog 逾期联动自动改状态时写入 CRM 操作日志。
// 自动置「异常」与人工流转同一口径:日志里必须写明原因,操作者记为「系统自动」,便于事后追溯。
// relatedType=3 与控制器层一致(项目 / 合同的操作日志同属该分类)。
func writeOverdueStatusLog(tenantID string, relatedID uint64, content string, now time.Time) {
if models.Orm == nil || relatedID == 0 {
return
}
log := models.TenantCrmOperateLog{
TenantID: tenantID,
RelatedType: 3,
RelatedID: relatedID,
Action: "status",
Content: content,
OperatorName: "系统自动",
CreateTime: now,
}
_, _ = models.Orm.Insert(&log)
}
// applyContractOverdue 合同置「异常」(5),并联动项目置「异常」(5)。
// 返回 (合同是否更新, 项目是否更新)。
func applyContractOverdue(tenantID string, contractID uint64, now time.Time) (int, int) {
@@ -174,6 +227,9 @@ func applyContractOverdue(tenantID string, contractID uint64, now time.Time) (in
return 0, 0
}
contractUpdated = 1
// 自动改异常同样写明原因,否则日志里只看到"变异常"却不知为何
writeOverdueStatusLog(tenantID, contractID,
"合同状态自动流转:"+crmContractStatusText(contract.Status)+" → 异常;异常原因:回款计划逾期未收齐", now)
}
// 合同关联项目联动置异常
projectUpdated := 0
@@ -186,6 +242,9 @@ func applyContractOverdue(tenantID string, contractID uint64, now time.Time) (in
Filter("id", proj.ID).Filter("tenant_id", tenantID).
Update(map[string]interface{}{"status": int8(5), "update_time": now}); err == nil {
projectUpdated = 1
writeOverdueStatusLog(tenantID, proj.ID,
"项目状态自动流转:"+crmProjectStatusText(proj.Status)+" → 异常;异常原因:关联合同「"+
contract.ContractName+"」回款计划逾期未收齐", now)
}
}
}
+74 -1
View File
@@ -5,6 +5,7 @@ import (
"math"
"sort"
"strings"
"sync"
"time"
"server/models"
@@ -151,6 +152,75 @@ func NormalizeModuleCode(code string) string {
return strings.ToLower(strings.TrimSpace(code))
}
// ---------------------------------------------------------------- 模块权益鉴权
// moduleCodesCacheTTL 模块权益缓存时长。
// 模块鉴权中间件会在每个业务请求上判断开通状态,直查数据库代价太高;
// 60 秒延迟对「购买后立即可用」没有实际影响(前端刷新即可)。
const moduleCodesCacheTTL = 60 * time.Second
type tenantModuleCodesItem struct {
codes map[string]bool
expireAt time.Time
}
var (
tenantModuleCodesMu sync.RWMutex
tenantModuleCodesCache = map[uint64]tenantModuleCodesItem{}
)
// GetTenantModuleCodesCached 带进程内缓存的模块权益查询(供请求级鉴权使用)
func GetTenantModuleCodesCached(tid uint64) map[string]bool {
now := time.Now()
tenantModuleCodesMu.RLock()
item, ok := tenantModuleCodesCache[tid]
tenantModuleCodesMu.RUnlock()
if ok && now.Before(item.expireAt) {
return item.codes
}
codes := GetTenantModuleCodes(tid)
tenantModuleCodesMu.Lock()
// 顺带清理过期项,避免长期运行后内存堆积
if len(tenantModuleCodesCache) > 5000 {
for k, v := range tenantModuleCodesCache {
if now.After(v.expireAt) {
delete(tenantModuleCodesCache, k)
}
}
}
tenantModuleCodesCache[tid] = tenantModuleCodesItem{codes: codes, expireAt: now.Add(moduleCodesCacheTTL)}
tenantModuleCodesMu.Unlock()
return codes
}
// InvalidateTenantModuleCodes 清除某租户的模块权益缓存(套餐 / 订单变更后调用)
func InvalidateTenantModuleCodes(tid uint64) {
if tid == 0 {
return
}
tenantModuleCodesMu.Lock()
delete(tenantModuleCodesCache, tid)
tenantModuleCodesMu.Unlock()
}
// IsModuleEnabled 判断租户是否已开通指定功能模块。
//
// 口径与 /backend/modules/getTenantList 保持一致:租户没有任何模块权益时视为「全开」,
// 避免历史租户或未配置套餐的环境被一刀切拦截。
func IsModuleEnabled(tid uint64, moduleCode string) bool {
code := NormalizeModuleCode(moduleCode)
if code == "" || tid == 0 {
return true
}
codes := GetTenantModuleCodesCached(tid)
if len(codes) == 0 {
return true
}
return codes[code]
}
// DayStart 取时间所在自然日的 0 点(有效期按「日」计算,不精确到秒)
func DayStart(t time.Time) time.Time {
return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
@@ -277,9 +347,10 @@ func CheckTenantUserQuota(tid, uid uint64) (*TenantQuotaCheck, error) {
result.Quota = quota
if uid > 0 {
// uid 可能来自令牌(identity_id),查老表前先换算
exist, err := models.Orm.QueryTable(new(models.SystemTenantUser)).
Filter("tid", tid).
Filter("uid", uid).
Filter("uid", ResolveTenantUID(tid, uid)).
Filter("delete_time__isnull", true).
Count()
if err == nil && exist > 0 {
@@ -442,6 +513,8 @@ func ApplyPackageToTenant(tid, packageID uint64, syncQuota bool) (int, error) {
Update(update); err != nil {
return 0, err
}
// 套餐变更后清掉模块权益缓存,避免「刚换套餐仍被拦/仍在放行」的延迟
InvalidateTenantModuleCodes(tid)
return newQuota, nil
}
+18
View File
@@ -4,8 +4,26 @@ import (
"strings"
"server/models"
authsvc "server/services/auth"
)
// ResolveTenantUID 把「令牌中的 user_id(认证中心 identity_id)」换算成
// 老表 yz_system_tenant_user.uid。
//
// 背景:uid 全量迁移脚本(scripts/uidmigrate)刻意跳过 yz_system_tenant_user
// (见其 skipTables),业务表列迁成 identity_id,而老表 uid 仍是老值。
// 因此凡是用 identity_id 去查老表的场景都必须先经过这里换算;
// 两者已经对齐的环境(或传入的本来就是老 uid)原值返回。
func ResolveTenantUID(tid, userID uint64) uint64 {
if tid == 0 || userID == 0 {
return 0
}
if legacy := authsvc.FindLegacyUID(tid, userID); legacy > 0 {
return legacy
}
return userID
}
// BindTenantUser 绑定用户到租户(若已存在则更新状态/默认值)
func BindTenantUser(tid, uid uint64, account, name, phone, email *string, sex *uint8, birth *string, password *string, isDefault, status int8, remark *string) (uint64, error) {
var existed models.SystemTenantUser
+45
View File
@@ -0,0 +1,45 @@
package services
import (
"errors"
"strings"
"time"
"server/models"
)
// 用户头像统一存在认证中心身份表 yz_auth_identity.avatar:
// /auth/userinfo 会返回该字段,前端登录回跳、顶部栏、个人中心都从这里取,
// 业务侧(yz_system_tenant_user)不再另存一份,避免两处不一致。
// UpdateIdentityAvatar 更新身份头像
func UpdateIdentityAvatar(identityID uint64, avatarURL string) error {
if identityID == 0 {
return errors.New("身份ID不能为空")
}
url := strings.TrimSpace(avatarURL)
if url == "" {
return errors.New("头像地址不能为空")
}
_, err := models.Orm.QueryTable(new(models.AuthIdentity)).
Filter("id", identityID).
Update(map[string]interface{}{
"avatar": url,
"update_time": time.Now(),
})
return err
}
// GetIdentityAvatar 读取身份头像;未设置或身份不存在时返回空字符串
func GetIdentityAvatar(identityID uint64) string {
if identityID == 0 {
return ""
}
var identity models.AuthIdentity
if err := models.Orm.QueryTable(new(models.AuthIdentity)).
Filter("id", identityID).
One(&identity); err != nil || identity.Avatar == nil {
return ""
}
return strings.TrimSpace(*identity.Avatar)
}