更新
This commit is contained in:
1 parent
b6b528490c
commit
4ab63e77ed
28 files changed
+1858
-695
No files matched your search
+11
-7
@@ -15,7 +15,7 @@
|
||||
//
|
||||
// 用法:
|
||||
//
|
||||
// deploy -host 10.31.100.2 -user root -dir /www/wwwroot/api.yunzer.cn -local .
|
||||
// deploy -host 212.64.112.158 -port 8822 -user root -dir /www/wwwroot/api.yunzer.cn -local .
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -51,8 +51,8 @@ var defaultExcludeFiles = []string{
|
||||
"*.exe", "*.log", "*.pid", "*.bak", "*.tmp", "*.pub",
|
||||
"nohup.out", "output.log", "go.log",
|
||||
"test-output-*.txt", "migrate_*.csv",
|
||||
"server", // 无扩展名的编译产物
|
||||
"*_id_ed25519", // SSH 私钥:绝对不能传到服务器
|
||||
"server", // 无扩展名的编译产物
|
||||
"*_id_ed25519", // SSH 私钥:绝对不能传到服务器
|
||||
".deploy_known_hosts",
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ var defaultExcludeFiles = []string{
|
||||
|
||||
type options struct {
|
||||
host string
|
||||
port string // ← 新增
|
||||
user string
|
||||
pass string
|
||||
keyFile string
|
||||
@@ -79,7 +80,8 @@ var errCancelled = errors.New("用户取消")
|
||||
|
||||
func main() {
|
||||
var opt options
|
||||
flag.StringVar(&opt.host, "host", "10.31.100.2", "服务器地址")
|
||||
flag.StringVar(&opt.host, "host", "212.64.112.158", "服务器地址")
|
||||
flag.StringVar(&opt.port, "port", "22", "SSH 端口") // ← 新增
|
||||
flag.StringVar(&opt.user, "user", "root", "SSH 用户名")
|
||||
flag.StringVar(&opt.pass, "pass", "", "SSH 密码(留空则读环境变量 DEPLOY_PASS)")
|
||||
flag.StringVar(&opt.keyFile, "key", "", "SSH 私钥路径(指定后用密钥登录,忽略密码)")
|
||||
@@ -137,7 +139,7 @@ func run(opt options) error {
|
||||
}
|
||||
|
||||
fmt.Printf("本地目录 : %s\n", localRoot)
|
||||
fmt.Printf("远程目录 : %s@%s:%s\n", opt.user, opt.host, opt.dir)
|
||||
fmt.Printf("远程目录 : %s@%s:%s:%s\n", opt.user, opt.host, opt.port, opt.dir) // ← 输出带端口
|
||||
|
||||
client, err := dial(opt, localRoot)
|
||||
if err != nil {
|
||||
@@ -583,10 +585,12 @@ func dial(opt options, localRoot string) (*ssh.Client, error) {
|
||||
HostKeyCallback: hostKeyChecker(localRoot),
|
||||
Timeout: 20 * time.Second,
|
||||
}
|
||||
addr := fmt.Sprintf("%s:%d", opt.host, 22)
|
||||
|
||||
// ← 关键:用 net.JoinHostPort 支持自定义端口(同时兼容 IPv6)
|
||||
addr := net.JoinHostPort(opt.host, opt.port)
|
||||
client, err := ssh.Dial("tcp", addr, cfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("连接服务器失败: %w", err)
|
||||
return nil, fmt.Errorf("连接服务器失败(%s): %w", addr, err)
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,11 +4,11 @@ import "time"
|
||||
|
||||
// BackendAiChatMessage AI聊天消息表
|
||||
type BackendAiChatMessage struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"`
|
||||
SessionID uint64 `orm:"column(session_id)" json:"session_id"`
|
||||
Role string `orm:"column(role);size(20)" json:"role"` // user / assistant
|
||||
Content string `orm:"column(content);type(text)" json:"content"`
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"`
|
||||
SessionID uint64 `orm:"column(session_id)" json:"session_id"`
|
||||
Role string `orm:"column(role);size(20)" json:"role"` // user / assistant
|
||||
Content string `orm:"column(content);type(text)" json:"content"`
|
||||
// Images 用户上传的图片(base64 dataURL 的 JSON 数组),仅 user 消息使用
|
||||
Images string `orm:"column(images);type(text)" json:"images"`
|
||||
// Tokens 本次响应消耗的 token 总数(0 表示未统计到)
|
||||
|
||||
@@ -60,7 +60,7 @@ var PlatformSiteSettingDefs = []PlatformSiteSettingDef{
|
||||
// 备案信息
|
||||
{Tag: "icp", Title: "ICP 备案号", Remark: "如:苏ICP备2026000000号-1,常用于页脚 | group:备案信息"},
|
||||
{Tag: "policeicp", Title: "公安备案", Remark: "如:苏公网安备32010000000000号,常用于页脚 | group:备案信息"},
|
||||
{Tag: "copyright", Title: "版权信息", Remark: "如:© 2026 公司名称 版权所有 | group:备案信息"},
|
||||
{Tag: "copyright", Title: "版权信息", Remark: "如:© 2026 云泽网 版权所有 | group:备案信息"},
|
||||
|
||||
// 政策条款
|
||||
{Tag: "privacy", Title: "隐私政策", Remark: "隐私政策链接,如 /page/privacy | group:政策条款"},
|
||||
|
||||
@@ -163,6 +163,7 @@ func RegisterAuthRoutes() {
|
||||
beego.Router("/backend/weather/now", &controllers.BackendWeatherController{}, "get:Now")
|
||||
|
||||
// 用户接口
|
||||
beego.Router("/backend/syncTenantUsersToEmployees", &controllers.BackendAdminUserController{}, "post:SyncTenantUsersToEmployees")
|
||||
beego.Router("/backend/getTenantUsers/:tid", &controllers.BackendAdminUserController{}, "get:GetTenantUsers")
|
||||
beego.Router("/backend/getAllUsers", &controllers.BackendAdminUserController{}, "get:GetAllUsers")
|
||||
beego.Router("/backend/getUserInfo/:id", &controllers.BackendAdminUserController{}, "get:GetUserInfo")
|
||||
|
||||
@@ -0,0 +1,379 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"server/models"
|
||||
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
)
|
||||
|
||||
// OnEmployeeCreatedHook 员工创建后的外部回调(由 controllers 包在初始化时注册,如同步通讯录)
|
||||
var OnEmployeeCreatedHook func(tid uint64, employeeID uint64, emp *models.BackendEmployee)
|
||||
|
||||
// parseDate 解析日期字符串为 *time.Time
|
||||
func parseDate(v string) *time.Time {
|
||||
v = strings.TrimSpace(v)
|
||||
if v == "" {
|
||||
return nil
|
||||
}
|
||||
if t, err := time.Parse("2006-01-02", v); err == nil {
|
||||
return &t
|
||||
}
|
||||
if t, err := time.Parse("2006-01-02 15:04:05", v); err == nil {
|
||||
return &t
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnsureEmployeeForTenantUser 确保租户用户在 yz_backend_employee 中有对应的人员记录。
|
||||
// 规则:先有用户,后自动生成员工,以 (tid, account) 严格匹配。
|
||||
func EnsureEmployeeForTenantUser(tu *models.SystemTenantUser) (*models.BackendEmployee, bool, error) {
|
||||
if tu == nil || tu.Account == nil {
|
||||
return nil, false, nil
|
||||
}
|
||||
account := strings.TrimSpace(*tu.Account)
|
||||
if account == "" {
|
||||
return nil, false, nil
|
||||
}
|
||||
tid := int(tu.Tid)
|
||||
if tid <= 0 {
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
var emp models.BackendEmployee
|
||||
// 员工表的 uk_account 是全局唯一索引(不是 tid+account 复合索引)。
|
||||
// 因此先按账号查全表,避免已有的历史/其他租户同账号记录被误判为缺失后再 INSERT。
|
||||
err := models.Orm.QueryTable(new(models.BackendEmployee)).
|
||||
Filter("account", account).
|
||||
One(&emp)
|
||||
if err == nil && emp.Tid != nil && *emp.Tid > 0 && *emp.Tid != tid {
|
||||
return nil, false, fmt.Errorf("员工账号 %s 已被其他租户使用,不能重复创建", account)
|
||||
}
|
||||
|
||||
name := account
|
||||
if tu.Name != nil && strings.TrimSpace(*tu.Name) != "" {
|
||||
name = strings.TrimSpace(*tu.Name)
|
||||
}
|
||||
|
||||
var birthday *time.Time
|
||||
if tu.Birth != nil && strings.TrimSpace(*tu.Birth) != "" {
|
||||
birthday = parseDate(strings.TrimSpace(*tu.Birth))
|
||||
}
|
||||
|
||||
status := int8(1)
|
||||
if tu.Status == 0 {
|
||||
status = 0
|
||||
}
|
||||
|
||||
var deptPtr *string
|
||||
if tu.OrgID > 0 {
|
||||
deptStr := strconv.FormatUint(tu.OrgID, 10)
|
||||
deptPtr = &deptStr
|
||||
}
|
||||
|
||||
if err == orm.ErrNoRows {
|
||||
// 不存在 -> 自动创建新员工
|
||||
newEmp := models.BackendEmployee{
|
||||
Tid: &tid,
|
||||
Account: account,
|
||||
Name: name,
|
||||
Gender: int8(tu.Sex),
|
||||
Birthday: birthday,
|
||||
Department: deptPtr,
|
||||
Phone: tu.Phone,
|
||||
Email: tu.Email,
|
||||
AccountStatus: status,
|
||||
}
|
||||
id, insertErr := models.Orm.Insert(&newEmp)
|
||||
if insertErr != nil {
|
||||
return nil, false, insertErr
|
||||
}
|
||||
newEmp.ID = uint(id)
|
||||
|
||||
if OnEmployeeCreatedHook != nil {
|
||||
OnEmployeeCreatedHook(tu.Tid, uint64(newEmp.ID), &newEmp)
|
||||
}
|
||||
|
||||
return &newEmp, true, nil
|
||||
} else if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
// 已存在 -> 只同步用户侧明确提供的基础资料;不覆盖部门、职位及其他员工专属字段。
|
||||
update := orm.Params{}
|
||||
if emp.Tid == nil || *emp.Tid <= 0 {
|
||||
// 历史无租户归属的员工记录:被当前租户用户明确匹配后,补上租户归属。
|
||||
update["tid"] = tid
|
||||
}
|
||||
if emp.DeleteTime != nil {
|
||||
update["delete_time"] = nil
|
||||
}
|
||||
if emp.Name != name {
|
||||
update["name"] = name
|
||||
}
|
||||
if emp.Gender != int8(tu.Sex) {
|
||||
update["gender"] = int8(tu.Sex)
|
||||
}
|
||||
if birthday != nil && (emp.Birthday == nil || !emp.Birthday.Equal(*birthday)) {
|
||||
update["birthday"] = birthday
|
||||
}
|
||||
if tu.Phone != nil && strings.TrimSpace(*tu.Phone) != "" && (emp.Phone == nil || *emp.Phone != *tu.Phone) {
|
||||
update["phone"] = *tu.Phone
|
||||
}
|
||||
if tu.Email != nil && strings.TrimSpace(*tu.Email) != "" && (emp.Email == nil || *emp.Email != *tu.Email) {
|
||||
update["email"] = *tu.Email
|
||||
}
|
||||
if tu.OrgID > 0 && (emp.Department == nil || *emp.Department == "") {
|
||||
update["department"] = *deptPtr
|
||||
}
|
||||
if emp.AccountStatus != status && emp.AccountStatus != 2 {
|
||||
update["account_status"] = status
|
||||
}
|
||||
if len(update) > 0 {
|
||||
update["update_time"] = time.Now()
|
||||
query := models.Orm.QueryTable(new(models.BackendEmployee)).Filter("id", emp.ID)
|
||||
if emp.Tid != nil && *emp.Tid > 0 {
|
||||
query = query.Filter("tid", tid)
|
||||
}
|
||||
if _, err := query.Update(update); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
}
|
||||
|
||||
return &emp, false, nil
|
||||
}
|
||||
|
||||
// SyncSelectedTenantUsersToEmployees 对指定租户用户进行比对;仅对缺失或基础资料有差异的员工执行写入。
|
||||
func SyncSelectedTenantUsersToEmployees(tid uint64, userIDs []uint64) (map[string]interface{}, error) {
|
||||
counts := map[string]interface{}{"created": 0, "updated": 0, "unchanged": 0, "failed": 0, "errors": []string{}}
|
||||
if tid == 0 || len(userIDs) == 0 {
|
||||
return counts, nil
|
||||
}
|
||||
seen := make(map[uint64]struct{}, len(userIDs))
|
||||
for _, userID := range userIDs {
|
||||
if userID == 0 {
|
||||
counts["failed"] = counts["failed"].(int) + 1
|
||||
counts["errors"] = append(counts["errors"].([]string), "用户ID无效")
|
||||
continue
|
||||
}
|
||||
if _, duplicate := seen[userID]; duplicate {
|
||||
continue
|
||||
}
|
||||
seen[userID] = struct{}{}
|
||||
|
||||
var user models.SystemTenantUser
|
||||
err := models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("id", userID).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
One(&user)
|
||||
if err != nil {
|
||||
counts["failed"] = counts["failed"].(int) + 1
|
||||
counts["errors"] = append(counts["errors"].([]string), fmt.Sprintf("用户记录 %d 不存在于当前租户或已删除", userID))
|
||||
continue
|
||||
}
|
||||
|
||||
differs := tenantUserEmployeeDiffers(&user)
|
||||
_, created, err := EnsureEmployeeForTenantUser(&user)
|
||||
if err != nil {
|
||||
counts["failed"] = counts["failed"].(int) + 1
|
||||
account := ""
|
||||
if user.Account != nil {
|
||||
account = strings.TrimSpace(*user.Account)
|
||||
}
|
||||
counts["errors"] = append(counts["errors"].([]string), fmt.Sprintf("用户 %s(ID %d)同步失败: %v", account, userID, err))
|
||||
continue
|
||||
}
|
||||
if created {
|
||||
counts["created"] = counts["created"].(int) + 1
|
||||
} else if differs {
|
||||
counts["updated"] = counts["updated"].(int) + 1
|
||||
} else {
|
||||
counts["unchanged"] = counts["unchanged"].(int) + 1
|
||||
}
|
||||
}
|
||||
return counts, nil
|
||||
}
|
||||
|
||||
func tenantUserEmployeeDiffers(user *models.SystemTenantUser) bool {
|
||||
if user == nil || user.Account == nil {
|
||||
return true
|
||||
}
|
||||
var employee models.BackendEmployee
|
||||
if err := models.Orm.QueryTable(new(models.BackendEmployee)).
|
||||
Filter("tid", int(user.Tid)).
|
||||
Filter("account", strings.TrimSpace(*user.Account)).
|
||||
Filter("delete_time__isnull", true).
|
||||
One(&employee); err != nil {
|
||||
return true
|
||||
}
|
||||
name := strings.TrimSpace(*user.Account)
|
||||
if user.Name != nil && strings.TrimSpace(*user.Name) != "" {
|
||||
name = strings.TrimSpace(*user.Name)
|
||||
}
|
||||
status := int8(1)
|
||||
if user.Status == 0 {
|
||||
status = 0
|
||||
}
|
||||
if employee.Name != name || employeeStatusDiffers(employee.AccountStatus, status) || int(employee.Gender) != int(user.Sex) {
|
||||
return true
|
||||
}
|
||||
if user.Phone != nil && (employee.Phone == nil || *employee.Phone != *user.Phone) {
|
||||
return true
|
||||
}
|
||||
if user.Email != nil && (employee.Email == nil || *employee.Email != *user.Email) {
|
||||
return true
|
||||
}
|
||||
if user.Birth != nil && strings.TrimSpace(*user.Birth) != "" {
|
||||
birthday := parseDate(*user.Birth)
|
||||
if birthday != nil && (employee.Birthday == nil || !employee.Birthday.Equal(*birthday)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func employeeStatusDiffers(employeeStatus, userStatus int8) bool {
|
||||
if employeeStatus == 2 {
|
||||
return false // 离职员工由人事管理维护,不通过用户同步自动复职
|
||||
}
|
||||
return employeeStatus != userStatus
|
||||
}
|
||||
|
||||
// SyncEmployeeOnUserUpdate 当用户在用户管理中被编辑时,同步修改对应员工
|
||||
func SyncEmployeeOnUserUpdate(tid uint64, oldAccount string, tu *models.SystemTenantUser) error {
|
||||
if tu == nil || tid == 0 {
|
||||
return nil
|
||||
}
|
||||
acc := oldAccount
|
||||
if acc == "" && tu.Account != nil {
|
||||
acc = *tu.Account
|
||||
}
|
||||
acc = strings.TrimSpace(acc)
|
||||
if acc == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
var emp models.BackendEmployee
|
||||
err := models.Orm.QueryTable(new(models.BackendEmployee)).
|
||||
Filter("tid", int(tid)).
|
||||
Filter("account", acc).
|
||||
One(&emp)
|
||||
if err != nil {
|
||||
// 员工不存在则自动创建
|
||||
_, _, err = EnsureEmployeeForTenantUser(tu)
|
||||
return err
|
||||
}
|
||||
|
||||
changed := false
|
||||
if tu.Account != nil && strings.TrimSpace(*tu.Account) != "" && emp.Account != strings.TrimSpace(*tu.Account) {
|
||||
emp.Account = strings.TrimSpace(*tu.Account)
|
||||
changed = true
|
||||
}
|
||||
if tu.Name != nil && strings.TrimSpace(*tu.Name) != "" && emp.Name != strings.TrimSpace(*tu.Name) {
|
||||
emp.Name = strings.TrimSpace(*tu.Name)
|
||||
changed = true
|
||||
}
|
||||
if tu.Sex > 0 && emp.Gender != int8(tu.Sex) {
|
||||
emp.Gender = int8(tu.Sex)
|
||||
changed = true
|
||||
}
|
||||
if tu.Birth != nil {
|
||||
tb := parseDate(strings.TrimSpace(*tu.Birth))
|
||||
if tb != nil && (emp.Birthday == nil || !emp.Birthday.Equal(*tb)) {
|
||||
emp.Birthday = tb
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
if tu.Phone != nil && (emp.Phone == nil || *emp.Phone != *tu.Phone) {
|
||||
emp.Phone = tu.Phone
|
||||
changed = true
|
||||
}
|
||||
if tu.Email != nil && (emp.Email == nil || *emp.Email != *tu.Email) {
|
||||
emp.Email = tu.Email
|
||||
changed = true
|
||||
}
|
||||
if tu.Status == 1 && emp.AccountStatus == 0 {
|
||||
emp.AccountStatus = 1
|
||||
changed = true
|
||||
} else if tu.Status == 0 && emp.AccountStatus == 1 {
|
||||
emp.AccountStatus = 0
|
||||
changed = true
|
||||
}
|
||||
|
||||
if changed {
|
||||
emp.UpdateTime = time.Now()
|
||||
_, err = models.Orm.Update(&emp)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SyncEmployeeOnUserDelete 当租户用户被删除时,将员工置为离职状态(2)
|
||||
func SyncEmployeeOnUserDelete(tid uint64, account string) error {
|
||||
account = strings.TrimSpace(account)
|
||||
if tid == 0 || account == "" {
|
||||
return nil
|
||||
}
|
||||
_, err := models.Orm.QueryTable(new(models.BackendEmployee)).
|
||||
Filter("tid", int(tid)).
|
||||
Filter("account", account).
|
||||
Filter("delete_time__isnull", true).
|
||||
Update(orm.Params{
|
||||
"account_status": int8(2), // 2 = 离职
|
||||
"update_time": time.Now(),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// SyncUserFromEmployee 当在人员管理中编辑了员工的基本信息时,同步回用户表
|
||||
func SyncUserFromEmployee(tid uint64, emp *models.BackendEmployee) error {
|
||||
if emp == nil || tid == 0 {
|
||||
return nil
|
||||
}
|
||||
account := strings.TrimSpace(emp.Account)
|
||||
if account == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
update := orm.Params{
|
||||
"update_time": time.Now(),
|
||||
}
|
||||
if emp.Name != "" {
|
||||
update["name"] = emp.Name
|
||||
}
|
||||
if emp.Phone != nil {
|
||||
update["phone"] = *emp.Phone
|
||||
}
|
||||
if emp.Email != nil {
|
||||
update["email"] = *emp.Email
|
||||
}
|
||||
if emp.Gender > 0 {
|
||||
update["sex"] = uint8(emp.Gender)
|
||||
}
|
||||
if emp.Birthday != nil {
|
||||
update["birth"] = emp.Birthday.Format("2006-01-02")
|
||||
}
|
||||
if emp.Department != nil && *emp.Department != "" {
|
||||
if orgId, err := strconv.ParseUint(*emp.Department, 10, 64); err == nil && orgId > 0 {
|
||||
update["org_id"] = orgId
|
||||
}
|
||||
}
|
||||
// 员工状态 1: 启用 -> 用户 1; 员工状态 0: 禁用 / 2: 离职 -> 用户 0 (禁用登录)
|
||||
if emp.AccountStatus == 1 {
|
||||
update["status"] = int8(1)
|
||||
} else {
|
||||
update["status"] = int8(0)
|
||||
}
|
||||
|
||||
_, err := models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("tid", tid).
|
||||
Filter("account", account).
|
||||
Filter("delete_time__isnull", true).
|
||||
Update(update)
|
||||
return err
|
||||
}
|
||||
+31
-34
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<title>{{.LoginTitle}} - 统一认证中心</title>
|
||||
<title>温新云科技 - 统一认证中心 - 统一认证中心</title>
|
||||
<style>
|
||||
:root {
|
||||
--primary: #3973ff;
|
||||
@@ -212,18 +212,20 @@
|
||||
to { opacity: 1; transform: none; }
|
||||
}
|
||||
|
||||
/* 小屏时左侧品牌区隐藏,在表单上方展示紧凑品牌头 */
|
||||
.mobile-brand { display: none; align-items: center; gap: 12px; margin-bottom: 28px; }
|
||||
.mobile-brand .brand-img { height: 36px; max-width: 180px; }
|
||||
.mobile-brand .brand-logo {
|
||||
/* 表单上方品牌 Logo 区:显示在「登录」字样上方 */
|
||||
.form-brand { display: flex; align-items: center; margin-bottom: 24px; }
|
||||
.form-brand-img { height: 42px; max-width: 240px; object-fit: contain; display: block; }
|
||||
.form-brand-fallback { display: inline-flex; align-items: center; gap: 12px; }
|
||||
.form-brand-logo {
|
||||
width: 40px; height: 40px;
|
||||
border-radius: 11px;
|
||||
background: rgba(57, 115, 255, 0.10);
|
||||
border-color: rgba(57, 115, 255, 0.18);
|
||||
border: 1px solid rgba(57, 115, 255, 0.18);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.mobile-brand .brand-logo svg { stroke: var(--primary); }
|
||||
.mobile-brand-text { display: flex; flex-direction: column; gap: 2px; }
|
||||
.mobile-brand-name { font-size: 15px; font-weight: 600; }
|
||||
.mobile-brand-sub { font-size: 12px; color: var(--muted); }
|
||||
.form-brand-logo svg { width: 22px; height: 22px; stroke: var(--primary); }
|
||||
.form-brand-name { font-size: 16px; font-weight: 600; color: var(--text); }
|
||||
|
||||
.form-foot {
|
||||
position: absolute;
|
||||
@@ -535,14 +537,13 @@
|
||||
padding: 40px 24px 72px;
|
||||
}
|
||||
.form-inner { max-width: 440px; margin: 0 auto; }
|
||||
.mobile-brand { display: flex; }
|
||||
.panel-title { font-size: 26px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-page">
|
||||
<!-- 左侧品牌区:背景图 / Logo / 主副标题由 platform「平台设置 → 登录页设置」提供 -->
|
||||
<!-- 左侧品牌区:背景图 / 主副标题由 platform「平台设置 → 登录页设置」提供 -->
|
||||
<aside class="brand-panel{{if .LoginBg}} has-bg{{end}}">
|
||||
{{if .LoginBg}}
|
||||
<img class="brand-bg" src="{{.LoginBg}}" alt="" />
|
||||
@@ -551,20 +552,16 @@
|
||||
|
||||
<div class="brand-inner">
|
||||
<div class="brand">
|
||||
{{if .LoginLogo}}
|
||||
<img class="brand-img" src="{{.LoginLogo}}" alt="{{.LoginTitle}}" />
|
||||
{{else}}
|
||||
<span class="brand-logo">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 3l7 3v6c0 4.2-2.9 7.6-7 9-4.1-1.4-7-4.8-7-9V6l7-3z"/>
|
||||
<path d="M9.2 12.2l2 2 3.6-4"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="brand-name">温新云统一认证中心</span>
|
||||
{{end}}
|
||||
<span class="brand-name">统一认证中心</span>
|
||||
</div>
|
||||
|
||||
<h1 class="brand-title">{{.LoginTitle}}</h1>
|
||||
<h1 class="brand-title">温新云</h1>
|
||||
<p class="brand-sub">{{.LoginSubtitle}}</p>
|
||||
</div>
|
||||
|
||||
@@ -573,28 +570,28 @@
|
||||
<rect x="4" y="11" width="16" height="10" rx="2"/>
|
||||
<path d="M8 11V8a4 4 0 018 0v3"/>
|
||||
</svg>
|
||||
<span>© 2026 Yunzer · 连接已加密</span>
|
||||
<span>© 2026 WEIXIN TECH · 连接已加密</span>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- 右侧表单区 -->
|
||||
<main class="form-panel">
|
||||
<div class="form-inner">
|
||||
<div class="mobile-brand">
|
||||
<!-- 品牌 Logo 区:显示在「登录」字样上方 -->
|
||||
<div class="form-brand">
|
||||
{{if .LoginLogo}}
|
||||
<img class="brand-img" src="{{.LoginLogo}}" alt="{{.LoginTitle}}" />
|
||||
<img class="form-brand-img" src="{{.LoginLogo}}" alt="温新云科技 - 统一认证中心" />
|
||||
{{else}}
|
||||
<span class="brand-logo">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 3l7 3v6c0 4.2-2.9 7.6-7 9-4.1-1.4-7-4.8-7-9V6l7-3z"/>
|
||||
<path d="M9.2 12.2l2 2 3.6-4"/>
|
||||
</svg>
|
||||
</span>
|
||||
<div class="form-brand-fallback">
|
||||
<span class="form-brand-logo">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 3l7 3v6c0 4.2-2.9 7.6-7 9-4.1-1.4-7-4.8-7-9V6l7-3z"/>
|
||||
<path d="M9.2 12.2l2 2 3.6-4"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="form-brand-name">温新云统一认证中心</span>
|
||||
</div>
|
||||
{{end}}
|
||||
<span class="mobile-brand-text">
|
||||
<span class="mobile-brand-name">温新云统一认证中心</span>
|
||||
<span class="mobile-brand-sub">{{.LoginTitle}}</span>
|
||||
</span>
|
||||
</div>
|
||||
{{if eq .Step "tenant"}}
|
||||
<div class="panel-head">
|
||||
@@ -633,8 +630,8 @@
|
||||
<div class="back"><a class="link" onclick="logout()">不是我的账号?切换账号</a></div>
|
||||
{{else}}
|
||||
<div class="panel-head">
|
||||
<h1 class="panel-title">登录</h1>
|
||||
<p class="panel-sub">请选择登录方式,进入您的工作空间</p>
|
||||
<h1 class="panel-title">欢迎登陆您的工作空间</h1>
|
||||
<!-- <p class="panel-sub"></p> -->
|
||||
</div>
|
||||
|
||||
<div class="error" id="error">
|
||||
@@ -799,7 +796,7 @@
|
||||
</div>
|
||||
|
||||
<div class="form-foot">
|
||||
<span>© 2026 云泽 版权所有</span>
|
||||
<span>© 2027 云泽网 版权所有</span>
|
||||
<button id="cacheClearBtn" class="cache-clear-btn" type="button" onclick="clearSiteCache()" title="仅清除本站在当前浏览器中的缓存数据">清除缓存</button>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
Reference in new issue
Block a user