This commit is contained in:
hero920103 committed 2026-11-13 20:26:16 +08:00
1 parent b6b528490c
commit 4ab63e77ed
28 files changed
+1858 -695

No files matched your search

+58 -1
View File
@@ -224,6 +224,51 @@ func findBackendTenantUser(idOrUid uint64, jwtTid uint64, byID bool) (*models.Sy
// GetAllUsers 获取当前租户后台用户列表
// GET /backend/getAllUsers
// SyncTenantUsersToEmployees 手动同步当前租户选中的用户到人员档案。
// POST /backend/syncTenantUsersToEmployees {"user_ids":[...]};服务层再次按 JWT tid 校验每个 ID。
func (c *BackendAdminUserController) SyncTenantUsersToEmployees() {
_, tid, ok := c.requireBackendIdentity()
if !ok {
return
}
if tid == 0 {
c.Data["json"] = map[string]interface{}{"code": 403, "msg": "登录态未绑定企业"}
_ = c.ServeJSON()
return
}
var request struct {
UserIDs []uint64 `json:"user_ids"`
}
body := c.Ctx.Input.RequestBody
if len(body) == 0 {
var readErr error
body, readErr = io.ReadAll(c.Ctx.Request.Body)
if readErr != nil {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "读取同步请求失败"}
_ = c.ServeJSON()
return
}
}
if err := json.Unmarshal(body, &request); err != nil || len(request.UserIDs) == 0 {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "请先选择要同步的用户"}
_ = c.ServeJSON()
return
}
if len(request.UserIDs) > 500 {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "单次最多同步500个用户"}
_ = c.ServeJSON()
return
}
result, err := services.SyncSelectedTenantUsersToEmployees(tid, request.UserIDs)
if err != nil {
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "同步失败: " + err.Error()}
_ = c.ServeJSON()
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": result}
_ = c.ServeJSON()
}
func (c *BackendAdminUserController) GetAllUsers() {
_, jwtTid, ok := c.requireBackendIdentity()
if !ok {
@@ -557,6 +602,8 @@ func (c *BackendAdminUserController) AddUser() {
if e := models.Orm.QueryTable(new(models.SystemTenantUser)).Filter("id", id).One(&created); e == nil {
syncTenantUserToAuth(&created, false)
}
// 自动生成员工并同步通讯录(先有用户,后自动生成员工)
_, _, _ = services.EnsureEmployeeForTenantUser(&created)
c.Data["json"] = map[string]interface{}{
"code": 200,
@@ -687,6 +734,12 @@ func (c *BackendAdminUserController) EditUser() {
var edited models.SystemTenantUser
if e := models.Orm.QueryTable(new(models.SystemTenantUser)).Filter("id", row.ID).One(&edited); e == nil {
syncTenantUserToAuth(&edited, false)
oldAccount := ""
if row.Account != nil {
oldAccount = *row.Account
}
_ = services.SyncEmployeeOnUserUpdate(row.Tid, oldAccount, &edited)
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success"}
@@ -729,6 +782,11 @@ func (c *BackendAdminUserController) DeleteUser() {
return
}
// 同步将员工状态置为离职(2)
if row.Account != nil {
_ = services.SyncEmployeeOnUserDelete(row.Tid, *row.Account)
}
// 双写:删除认证中心中的企业绑定
removeTenantUserFromAuth(row)
@@ -790,7 +848,6 @@ func (c *BackendAdminUserController) ChangePassword() {
_ = c.ServeJSON()
return
}
// 双写:同步新密码到统一认证中心(改密场景,显式允许更新身份层密码)
var updated models.SystemTenantUser
if e := models.Orm.QueryTable(new(models.SystemTenantUser)).Filter("id", row.ID).One(&updated); e == nil {
+58 -40
View File
@@ -167,7 +167,7 @@ const maxToolRounds = 6
// aiUsage 一次 AI 调用的 token 用量与真实模型
type aiUsage struct {
Model string `json:"model,omitempty"` // 接口实际返回的使用模型(如网关把 auto 解析后的真实模型)
Model string `json:"model,omitempty"` // 接口实际返回的使用模型(如网关把 auto 解析后的真实模型)
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
TotalTokens int `json:"total_tokens"`
@@ -197,10 +197,10 @@ type usedMcpTool struct {
// aiToolUsage 一次 AI 工具循环调用的元信息:用了什么模型、调用了哪些 MCP 工具
type aiToolUsage struct {
Model string `json:"model"` // 实际使用的模型
Rounds int `json:"rounds"` // 工具循环轮数
Tools []usedMcpTool `json:"tools"` // 实际被调用的 MCP 工具(去重)
ToolCount int `json:"tool_count"` // 工具调用种类数
Model string `json:"model"` // 实际使用的模型
Rounds int `json:"rounds"` // 工具循环轮数
Tools []usedMcpTool `json:"tools"` // 实际被调用的 MCP 工具(去重)
ToolCount int `json:"tool_count"` // 工具调用种类数
}
// sanitizeToolName 工具名规范化为 OpenAI 允许的字符集
@@ -260,8 +260,8 @@ func collectMcpTools(claims *jwtutil.Claims, enabled bool, serverIDs []uint64) (
if err != nil {
// 单个服务连接失败不阻断整体,跳过并记录到摘要(标记不可用)
summaries = append(summaries, mcpToolSummary{
ServerID: srv.ID,
ServerName: srv.Name,
ServerID: srv.ID,
ServerName: srv.Name,
Description: "连接失败:" + err.Error(),
})
continue
@@ -446,8 +446,25 @@ func (c *BackendAiChatController) MessageList() {
c.chatJsonErr(500, 500, "查询失败: "+err.Error())
return
}
providerName := ""
if session.ProviderID > 0 {
provider := models.BackendAiProvider{ID: session.ProviderID}
if models.Orm.Read(&provider) == nil {
providerName = provider.Name
}
}
responseList := make([]map[string]interface{}, 0, len(list))
for _, message := range list {
b, _ := json.Marshal(message)
var item map[string]interface{}
_ = json.Unmarshal(b, &item)
if message.Role == "assistant" && providerName != "" {
item["provider_name"] = providerName
}
responseList = append(responseList, item)
}
c.chatOk(map[string]interface{}{"list": list, "title": session.Title})
c.chatOk(map[string]interface{}{"list": responseList, "title": session.Title})
}
// DeleteMessage DELETE /backend/ai/chat/message/:id
@@ -702,14 +719,14 @@ func (c *BackendAiChatController) Send() {
durationMs := int(time.Since(startTime).Milliseconds())
assistantMsg := models.BackendAiChatMessage{
TenantID: fmt.Sprintf("%d", claims.TenantId),
SessionID: session.ID,
Role: "assistant",
Content: reply,
Tokens: totalUsage.total(),
DurationMs: durationMs,
Model: useModel,
CreateTime: time.Now(),
TenantID: fmt.Sprintf("%d", claims.TenantId),
SessionID: session.ID,
Role: "assistant",
Content: reply,
Tokens: totalUsage.total(),
DurationMs: durationMs,
Model: useModel,
CreateTime: time.Now(),
}
_, _ = models.Orm.Insert(&assistantMsg)
@@ -886,14 +903,14 @@ func (c *BackendAiChatController) SendStream() {
// 保存AI回复
durationMs := int(time.Since(startTime).Milliseconds())
assistantMsg := models.BackendAiChatMessage{
TenantID: fmt.Sprintf("%d", claims.TenantId),
SessionID: session.ID,
Role: "assistant",
Content: fullReply,
Tokens: totalUsage.total(),
DurationMs: durationMs,
Model: useModel,
CreateTime: time.Now(),
TenantID: fmt.Sprintf("%d", claims.TenantId),
SessionID: session.ID,
Role: "assistant",
Content: fullReply,
Tokens: totalUsage.total(),
DurationMs: durationMs,
Model: useModel,
CreateTime: time.Now(),
}
_, _ = models.Orm.Insert(&assistantMsg)
@@ -901,11 +918,12 @@ func (c *BackendAiChatController) SendStream() {
_, _ = models.Orm.Update(&session, "update_time")
writeJSON("done", map[string]interface{}{
"session_id": session.ID,
"message_id": assistantMsg.ID,
"tokens": totalUsage.total(),
"duration_ms": durationMs,
"model": useModel,
"session_id": session.ID,
"message_id": assistantMsg.ID,
"tokens": totalUsage.total(),
"duration_ms": durationMs,
"model": useModel,
"provider_name": provider.Name,
})
}
@@ -1044,10 +1062,10 @@ func callAITools(provider models.BackendAiProvider, model string, systemPrompt s
}
var result struct {
Content []struct {
Type string `json:"type"`
Text string `json:"text"`
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Text string `json:"text"`
ID string `json:"id"`
Name string `json:"name"`
Input map[string]interface{} `json:"input"`
} `json:"content"`
Usage *struct {
@@ -1303,9 +1321,9 @@ func callAnthropicStreamTools(provider models.BackendAiProvider, model string, s
var text strings.Builder
var usage aiUsage
type toolUseAcc struct {
ID string
Name string
Input strings.Builder
ID string
Name string
Input strings.Builder
}
acc := make(map[int]*toolUseAcc)
var order []int
@@ -1326,11 +1344,11 @@ func callAnthropicStreamTools(provider models.BackendAiProvider, model string, s
switch currentEvent {
case "content_block_start":
var block struct {
Index int `json:"index"`
Index int `json:"index"`
ContentBlock struct {
Type string `json:"type"`
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
ID string `json:"id"`
Name string `json:"name"`
} `json:"content_block"`
Usage *struct {
InputTokens int `json:"input_tokens"`
+39 -34
View File
@@ -10,6 +10,7 @@ import (
"server/models"
"server/pkg/identity"
"server/pkg/jwtutil"
"server/services"
"github.com/beego/beego/v2/client/orm"
beego "github.com/beego/beego/v2/server/web"
@@ -39,15 +40,15 @@ func (c *BackendErpContactController) contactTenantID() (uint64, error) {
}
type erpContactDTO struct {
ID uint64 `json:"id"`
Tid uint64 `json:"tid"`
EmployeeID uint64 `json:"employee_id"`
OrgID uint64 `json:"org_id"`
OrgName string `json:"org_name"`
ContactType int8 `json:"contact_type"`
ContactName string `json:"contact_name"`
Gender int8 `json:"gender"`
Phone string `json:"phone"`
ID uint64 `json:"id"`
Tid uint64 `json:"tid"`
EmployeeID uint64 `json:"employee_id"`
OrgID uint64 `json:"org_id"`
OrgName string `json:"org_name"`
ContactType int8 `json:"contact_type"`
ContactName string `json:"contact_name"`
Gender int8 `json:"gender"`
Phone string `json:"phone"`
// 手机号可登记多个(有的人有多个号码):JSON 数组,最多 5 个;phone 保留为第一个
Mobiles string `json:"mobiles"`
WorkPhone string `json:"work_phone"`
@@ -585,6 +586,10 @@ func (c *BackendErpContactController) contactDTO(row models.BackendErpContact) e
// --- Sync from Employee ---
func init() {
services.OnEmployeeCreatedHook = SyncContactOnEmployeeCreate
}
// SyncContactOnEmployeeCreate 员工创建后同步到通讯录(由组织架构控制器调用)
func SyncContactOnEmployeeCreate(tid uint64, employeeID uint64, emp *models.BackendEmployee) {
syncContactFromEmployee(tid, employeeID, emp)
@@ -765,13 +770,13 @@ func syncContactFromEmployee(tid uint64, employeeID uint64, emp *models.BackendE
// getEmployeeFileForContact 获取员工人事档案数据(用于通讯录同步)
// 返回档案中与通讯录相关的字段
func getEmployeeFileForContact(employeeID uint64) struct {
WorkEmail string
HouseholdAddress string
CurrentAddress string
EmergencyContact string
EmergencyPhone string
WorkEmail string
HouseholdAddress string
CurrentAddress string
EmergencyContact string
EmergencyPhone string
EmergencyRelation string
EmploymentStatus int8
EmploymentStatus int8
} {
var file models.BackendEmployeeFile
err := models.Orm.QueryTable(new(models.BackendEmployeeFile)).
@@ -782,32 +787,32 @@ func getEmployeeFileForContact(employeeID uint64) struct {
if err != nil {
// 档案不存在,返回空值
return struct {
WorkEmail string
HouseholdAddress string
CurrentAddress string
EmergencyContact string
EmergencyPhone string
WorkEmail string
HouseholdAddress string
CurrentAddress string
EmergencyContact string
EmergencyPhone string
EmergencyRelation string
EmploymentStatus int8
EmploymentStatus int8
}{}
}
return struct {
WorkEmail string
HouseholdAddress string
CurrentAddress string
EmergencyContact string
EmergencyPhone string
WorkEmail string
HouseholdAddress string
CurrentAddress string
EmergencyContact string
EmergencyPhone string
EmergencyRelation string
EmploymentStatus int8
EmploymentStatus int8
}{
WorkEmail: file.WorkEmail,
HouseholdAddress: file.HouseholdAddress,
CurrentAddress: file.CurrentAddress,
EmergencyContact: file.EmergencyContact,
EmergencyPhone: file.EmergencyPhone,
WorkEmail: file.WorkEmail,
HouseholdAddress: file.HouseholdAddress,
CurrentAddress: file.CurrentAddress,
EmergencyContact: file.EmergencyContact,
EmergencyPhone: file.EmergencyPhone,
EmergencyRelation: file.EmergencyRelation,
EmploymentStatus: file.EmploymentStatus,
EmploymentStatus: file.EmploymentStatus,
}
}
@@ -982,4 +987,4 @@ func contactFirstNonEmptyPtr(values ...*string) *string {
}
}
return nil
}
}
+70 -17
View File
@@ -5,9 +5,12 @@ import (
"strconv"
"strings"
"time"
"server/models"
"server/pkg/identity"
"server/pkg/jwtutil"
"server/services"
"github.com/beego/beego/v2/client/orm"
beego "github.com/beego/beego/v2/server/web"
@@ -66,21 +69,21 @@ func defaultOrgSettings() orgSettings {
}
type organizationDTO struct {
ID uint64 `json:"id"`
Tid uint64 `json:"tid"`
TenantID uint64 `json:"tenant_id"`
OrgName string `json:"org_name"`
OrgCode string `json:"org_code"`
ParentID uint64 `json:"parent_id"`
ParentName string `json:"parent_name"`
LeaderID uint64 `json:"leader_id"`
LeaderName string `json:"leader_name"`
IsCompany int `json:"is_company"`
Sort uint `json:"sort"`
Status int8 `json:"status"`
Remark string `json:"remark"`
CreateTime string `json:"create_time"`
UpdateTime string `json:"update_time"`
ID uint64 `json:"id"`
Tid uint64 `json:"tid"`
TenantID uint64 `json:"tenant_id"`
OrgName string `json:"org_name"`
OrgCode string `json:"org_code"`
ParentID uint64 `json:"parent_id"`
ParentName string `json:"parent_name"`
LeaderID uint64 `json:"leader_id"`
LeaderName string `json:"leader_name"`
IsCompany int `json:"is_company"`
Sort uint `json:"sort"`
Status int8 `json:"status"`
Remark string `json:"remark"`
CreateTime string `json:"create_time"`
UpdateTime string `json:"update_time"`
}
type employeeDTO struct {
@@ -785,7 +788,7 @@ func (c *BackendOrganizationController) GetEmployee() {
return
}
cond := orm.NewCondition().And("tid", tid).And("delete_time__isnull", true)
cond := orm.NewCondition().AndCond(orm.NewCondition().Or("tid", tid).Or("tid__isnull", true)).And("delete_time__isnull", true)
orgID, _ := c.GetUint64("org_id")
if orgID == 0 {
@@ -820,6 +823,29 @@ func (c *BackendOrganizationController) GetEmployee() {
return
}
// uk_account 为全局唯一索引,历史员工可能 tid 为空;只在当前用户账号可确认归属时认领这类历史记录。
var tenantUsers []models.SystemTenantUser
_, _ = models.Orm.QueryTable(new(models.SystemTenantUser)).Filter("tid", tid).Filter("delete_time__isnull", true).All(&tenantUsers, "account")
ownedAccounts := make(map[string]struct{}, len(tenantUsers))
for _, user := range tenantUsers {
if user.Account != nil && strings.TrimSpace(*user.Account) != "" {
ownedAccounts[strings.TrimSpace(*user.Account)] = struct{}{}
}
}
filtered := make([]models.BackendEmployee, 0, len(rows))
for _, row := range rows {
if row.Tid != nil && *row.Tid > 0 && *row.Tid != int(tid) {
continue
}
if row.Tid == nil || *row.Tid == 0 {
if _, belongsToTenant := ownedAccounts[strings.TrimSpace(row.Account)]; !belongsToTenant {
continue
}
}
filtered = append(filtered, row)
}
rows = filtered
c.jsonOK(c.employeeDTOList(tid, rows))
}
@@ -909,6 +935,17 @@ func (c *BackendOrganizationController) CreateEmployee() {
c.jsonError(400, "账号已存在")
return
}
// 校验是否先有用户:必须在「用户管理」中已存在该账号(先有用户,后自动生成员工)
var tenantUser models.SystemTenantUser
tuErr := models.Orm.QueryTable(new(models.SystemTenantUser)).
Filter("tid", tid).
Filter("account", account).
Filter("delete_time__isnull", true).
One(&tenantUser)
if tuErr != nil {
c.jsonError(400, "该账号在「用户管理」中不存在,请先前往「用户管理」添加用户")
return
}
gender, hasGender := c.getIntValue(body, "gender", "sex")
status, hasStatus := c.getIntValue(body, "account_status", "status")
@@ -1072,13 +1109,19 @@ func (c *BackendOrganizationController) EditEmployee() {
// 同步到通讯录(字段优先级:人事档案 > 员工)
SyncContactOnEmployeeRefresh(tid, uint64(id))
// 反向同步到租户用户表(保持人员与用户一致)
var updatedEmp models.BackendEmployee
if err := c.employeeQuery(tid).Filter("id", id).One(&updatedEmp); err == nil {
_ = services.SyncUserFromEmployee(tid, &updatedEmp)
}
c.jsonOK(nil)
}
// DeleteEmployee 员工离职/禁用:员工不做删除,只改账号状态。
//
// 说明:员工属于企业数据,人事档案(yz_backend_employee_file)与通讯录
//(yz_backend_erp_contact)都以 employee_id 关联员工。一旦把员工软删除(写 delete_time),
// (yz_backend_erp_contact)都以 employee_id 关联员工。一旦把员工软删除(写 delete_time),
// 档案页与通讯录就查不到对应的人,等于连带删掉了企业数据。
// 因此这里只把账号状态置为"离职"(2),不写 delete_time,也不触碰档案与通讯录记录。
// 启用/禁用请走 editEmployee 改 account_status(1 启用 / 0 禁用 / 2 离职)。
@@ -1106,6 +1149,16 @@ func (c *BackendOrganizationController) DeleteEmployee() {
return
}
// 员工置为离职状态后,同步将对应用户的状态置为禁用(0),防止离职人员继续登录系统
var emp models.BackendEmployee
if err := c.employeeQuery(tid).Filter("id", id).One(&emp); err == nil && emp.Account != "" {
_, _ = models.Orm.QueryTable(new(models.SystemTenantUser)).
Filter("tid", tid).
Filter("account", emp.Account).
Filter("delete_time__isnull", true).
Update(orm.Params{"status": int8(0), "update_time": time.Now()})
}
c.jsonOK(nil)
}