批量更新
This commit is contained in:
+72
-73
@@ -1,73 +1,72 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"server/models"
|
||||
"server/pkg/passwordutil"
|
||||
)
|
||||
|
||||
func NormalizeAccount(s string) string {
|
||||
return strings.TrimSpace(s)
|
||||
}
|
||||
|
||||
func CreateAdminUser(account, password string, name, phone, email, qq, avatar *string, sex uint8, roleID uint64, status uint8) (uint64, error) {
|
||||
hashed, err := passwordutil.Hash(password)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
u := &models.AdminUser{
|
||||
Account: NormalizeAccount(account),
|
||||
Password: hashed,
|
||||
Name: name,
|
||||
Phone: phone,
|
||||
Email: email,
|
||||
Qq: qq,
|
||||
Avatar: avatar,
|
||||
Sex: sex,
|
||||
RoleID: roleID,
|
||||
Status: status,
|
||||
}
|
||||
id, err := models.Orm.Insert(u)
|
||||
return uint64(id), err
|
||||
}
|
||||
|
||||
func GetAdminUserByID(id uint64) (*models.AdminUser, error) {
|
||||
u := &models.AdminUser{ID: id}
|
||||
if err := models.Orm.Read(u); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func UpdateAdminUser(id uint64, fields map[string]interface{}) error {
|
||||
_, err := models.Orm.QueryTable(new(models.AdminUser)).Filter("id", id).Update(fields)
|
||||
return err
|
||||
}
|
||||
|
||||
func DeleteAdminUser(id uint64) error {
|
||||
_, err := models.Orm.QueryTable(new(models.AdminUser)).Filter("id", id).Delete()
|
||||
return err
|
||||
}
|
||||
|
||||
func ChangeAdminUserPassword(id uint64, newPassword string) error {
|
||||
hashed, err := passwordutil.Hash(newPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = models.Orm.QueryTable(new(models.AdminUser)).Filter("id", id).Update(map[string]interface{}{
|
||||
"password": hashed,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func ListAdminUsers() ([]models.AdminUser, int64, error) {
|
||||
var rows []models.AdminUser
|
||||
total, err := models.Orm.QueryTable(new(models.AdminUser)).Count()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
_, err = models.Orm.QueryTable(new(models.AdminUser)).OrderBy("-id").All(&rows)
|
||||
return rows, total, err
|
||||
}
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"server/models"
|
||||
"server/pkg/passwordutil"
|
||||
)
|
||||
|
||||
func NormalizeAccount(s string) string {
|
||||
return strings.TrimSpace(s)
|
||||
}
|
||||
|
||||
func CreateAdminUser(account, password string, name, phone, email, qq, avatar *string, sex uint8, roleID uint64, status uint8) (uint64, error) {
|
||||
hashed, err := passwordutil.Hash(password)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
u := &models.AdminUser{
|
||||
Account: NormalizeAccount(account),
|
||||
Password: hashed,
|
||||
Name: name,
|
||||
Phone: phone,
|
||||
Email: email,
|
||||
Qq: qq,
|
||||
Avatar: avatar,
|
||||
Sex: sex,
|
||||
RoleID: roleID,
|
||||
Status: status,
|
||||
}
|
||||
id, err := models.Orm.Insert(u)
|
||||
return uint64(id), err
|
||||
}
|
||||
|
||||
func GetAdminUserByID(id uint64) (*models.AdminUser, error) {
|
||||
u := &models.AdminUser{ID: id}
|
||||
if err := models.Orm.Read(u); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func UpdateAdminUser(id uint64, fields map[string]interface{}) error {
|
||||
_, err := models.Orm.QueryTable(new(models.AdminUser)).Filter("id", id).Update(fields)
|
||||
return err
|
||||
}
|
||||
|
||||
func DeleteAdminUser(id uint64) error {
|
||||
_, err := models.Orm.QueryTable(new(models.AdminUser)).Filter("id", id).Delete()
|
||||
return err
|
||||
}
|
||||
|
||||
func ChangeAdminUserPassword(id uint64, newPassword string) error {
|
||||
hashed, err := passwordutil.Hash(newPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = models.Orm.QueryTable(new(models.AdminUser)).Filter("id", id).Update(map[string]interface{}{
|
||||
"password": hashed,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func ListAdminUsers() ([]models.AdminUser, int64, error) {
|
||||
var rows []models.AdminUser
|
||||
total, err := models.Orm.QueryTable(new(models.AdminUser)).Count()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
_, err = models.Orm.QueryTable(new(models.AdminUser)).OrderBy("-id").All(&rows)
|
||||
return rows, total, err
|
||||
}
|
||||
|
||||
@@ -119,6 +119,25 @@ func GetTenantUser(identityID, tid uint64) (*models.AuthTenantUser, error) {
|
||||
return &bind, nil
|
||||
}
|
||||
|
||||
// FindIdentityIDByAccount 按「企业 + 账号」查找认证中心身份ID。
|
||||
// 账号可以是企业内账号、手机号或邮箱(老登录入口按账号登录时使用)。
|
||||
func FindIdentityIDByAccount(tid uint64, account string) uint64 {
|
||||
account = strings.TrimSpace(account)
|
||||
if tid == 0 || account == "" {
|
||||
return 0
|
||||
}
|
||||
base := models.Orm.QueryTable(new(models.AuthTenantUser)).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true)
|
||||
var bind models.AuthTenantUser
|
||||
for _, field := range []string{"account", "phone", "email"} {
|
||||
if err := base.Filter(field, account).One(&bind); err == nil {
|
||||
return bind.IdentityID
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// BuildProfile 组装身份概要(含可选企业列表)
|
||||
func BuildProfile(m *models.AuthIdentity) (*IdentityProfile, error) {
|
||||
tenants, err := ListTenantOptions(m.ID)
|
||||
|
||||
@@ -0,0 +1,413 @@
|
||||
// Package idp 第三方身份源(微信/钉钉/飞书/QQ/GitHub/Google)统一适配层。
|
||||
//
|
||||
// 各平台 OAuth2 流程基本一致,差异只在端点地址、参数名与用户字段,
|
||||
// 因此用一份通用实现 + 预设配置覆盖,新增平台只需加一条 preset。
|
||||
package idp
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
)
|
||||
|
||||
// UserInfo 第三方返回的标准化用户信息
|
||||
type UserInfo struct {
|
||||
OpenID string // 该平台内的唯一 ID(必填)
|
||||
UnionID string // 跨平台唯一 ID(微信/钉钉有,GitHub/Google 用 OpenID 代替)
|
||||
Nickname string
|
||||
Avatar string
|
||||
Raw string // 原始 JSON,便于排查
|
||||
}
|
||||
|
||||
// Config 一个第三方身份源的配置
|
||||
type Config struct {
|
||||
Provider string
|
||||
AuthURL string
|
||||
TokenURL string
|
||||
UserinfoURL string
|
||||
Scopes []string
|
||||
AppID string
|
||||
AppSecret string
|
||||
ProxyURL string // GitHub/Google 在国内服务器需要代理时填写
|
||||
|
||||
// 参数风格:微信/QQ 用 appid+secret,其余用 client_id+client_secret
|
||||
UseAppIDStyle bool
|
||||
// 用户信息接口风格:query=拼在 URL 上(微信/QQ),bearer=放 Authorization 头
|
||||
UserinfoStyle string
|
||||
|
||||
// 用户信息字段映射(一级 JSON 字段)
|
||||
OpenIDField string
|
||||
UnionIDField string
|
||||
NicknameField string
|
||||
AvatarField string
|
||||
}
|
||||
|
||||
// presets 各平台预设(端点与字段映射)
|
||||
var presets = map[string]Config{
|
||||
models.IdPWechat: {
|
||||
Provider: models.IdPWechat,
|
||||
AuthURL: "https://open.weixin.qq.com/connect/oauth2/authorize",
|
||||
TokenURL: "https://api.weixin.qq.com/sns/oauth2/access_token",
|
||||
UserinfoURL: "https://api.weixin.qq.com/sns/userinfo",
|
||||
Scopes: []string{"snsapi_userinfo"},
|
||||
UseAppIDStyle: true,
|
||||
UserinfoStyle: "query",
|
||||
OpenIDField: "openid",
|
||||
UnionIDField: "unionid",
|
||||
NicknameField: "nickname",
|
||||
AvatarField: "headimgurl",
|
||||
},
|
||||
models.IdPQQ: {
|
||||
Provider: models.IdPQQ,
|
||||
AuthURL: "https://graph.qq.com/oauth2.0/authorize",
|
||||
TokenURL: "https://graph.qq.com/oauth2.0/token",
|
||||
UserinfoURL: "https://graph.qq.com/user/get_user_info",
|
||||
Scopes: []string{"get_user_info"},
|
||||
UseAppIDStyle: true,
|
||||
UserinfoStyle: "query",
|
||||
OpenIDField: "openid",
|
||||
NicknameField: "nickname",
|
||||
AvatarField: "figureurl_qq_2",
|
||||
},
|
||||
models.IdPGitHub: {
|
||||
Provider: models.IdPGitHub,
|
||||
AuthURL: "https://github.com/login/oauth/authorize",
|
||||
TokenURL: "https://github.com/login/oauth/access_token",
|
||||
UserinfoURL: "https://api.github.com/user",
|
||||
Scopes: []string{"read:user"},
|
||||
UserinfoStyle: "bearer",
|
||||
OpenIDField: "id",
|
||||
NicknameField: "login",
|
||||
AvatarField: "avatar_url",
|
||||
},
|
||||
models.IdPGoogle: {
|
||||
Provider: models.IdPGoogle,
|
||||
AuthURL: "https://accounts.google.com/o/oauth2/v2/auth",
|
||||
TokenURL: "https://oauth2.googleapis.com/token",
|
||||
UserinfoURL: "https://openidconnect.googleapis.com/v1/userinfo",
|
||||
Scopes: []string{"openid", "profile"},
|
||||
UserinfoStyle: "bearer",
|
||||
OpenIDField: "sub",
|
||||
NicknameField: "name",
|
||||
AvatarField: "picture",
|
||||
},
|
||||
models.IdPDingTalk: {
|
||||
Provider: models.IdPDingTalk,
|
||||
AuthURL: "https://login.dingtalk.com/oauth2/auth",
|
||||
TokenURL: "https://api.dingtalk.com/v1.0/oauth2/userAccessToken",
|
||||
UserinfoURL: "https://api.dingtalk.com/v1.0/contact/users/me",
|
||||
Scopes: []string{"openid", "profile"},
|
||||
UserinfoStyle: "bearer",
|
||||
OpenIDField: "unionId",
|
||||
UnionIDField: "unionId",
|
||||
NicknameField: "nick",
|
||||
AvatarField: "avatarUrl",
|
||||
},
|
||||
models.IdPFeishu: {
|
||||
Provider: models.IdPFeishu,
|
||||
AuthURL: "https://open.feishu.cn/open-apis/authen/v1/authorize",
|
||||
TokenURL: "https://open.feishu.cn/open-apis/authen/v2/oauth/token",
|
||||
UserinfoURL: "https://open.feishu.cn/open-apis/authen/v1/user_info",
|
||||
Scopes: []string{"contact:user.base:readonly"},
|
||||
UserinfoStyle: "bearer",
|
||||
OpenIDField: "open_id",
|
||||
UnionIDField: "union_id",
|
||||
NicknameField: "name",
|
||||
AvatarField: "avatar_url",
|
||||
},
|
||||
}
|
||||
|
||||
// Supported 返回支持的第三方平台列表
|
||||
func Supported() []string {
|
||||
return []string{
|
||||
models.IdPWechat, models.IdPDingTalk, models.IdPFeishu,
|
||||
models.IdPQQ, models.IdPGitHub, models.IdPGoogle,
|
||||
}
|
||||
}
|
||||
|
||||
// IsSupported 是否为已知平台
|
||||
func IsSupported(provider string) bool {
|
||||
_, ok := presets[provider]
|
||||
return ok
|
||||
}
|
||||
|
||||
// LoadConfig 读取身份源配置:
|
||||
// - tid > 0 时优先取租户自带身份源(yz_auth_tenant_idp)
|
||||
// - 取不到或 tid=0 时取平台全局配置(tid=0 的记录)
|
||||
//
|
||||
// 未配置任何记录时返回 false,调用方应提示「该登录方式未开通」。
|
||||
func LoadConfig(provider string, tid uint64) (Config, bool) {
|
||||
base, ok := presets[provider]
|
||||
if !ok {
|
||||
return Config{}, false
|
||||
}
|
||||
|
||||
var row models.AuthTenantIdp
|
||||
qs := models.Orm.QueryTable(new(models.AuthTenantIdp)).
|
||||
Filter("provider", provider).
|
||||
Filter("status", 1)
|
||||
|
||||
if tid > 0 {
|
||||
if err := qs.Filter("tid", tid).One(&row); err == nil {
|
||||
return applyDBConfig(base, &row), true
|
||||
}
|
||||
}
|
||||
// 回落到平台全局配置(tid=0)
|
||||
if err := qs.Filter("tid", 0).One(&row); err == nil {
|
||||
return applyDBConfig(base, &row), true
|
||||
}
|
||||
return base, false
|
||||
}
|
||||
|
||||
func applyDBConfig(base Config, row *models.AuthTenantIdp) Config {
|
||||
if row.AppID != nil && strings.TrimSpace(*row.AppID) != "" {
|
||||
base.AppID = strings.TrimSpace(*row.AppID)
|
||||
}
|
||||
if row.AppSecret != nil && strings.TrimSpace(*row.AppSecret) != "" {
|
||||
base.AppSecret = strings.TrimSpace(*row.AppSecret)
|
||||
}
|
||||
if row.ProxyURL != nil && strings.TrimSpace(*row.ProxyURL) != "" {
|
||||
base.ProxyURL = strings.TrimSpace(*row.ProxyURL)
|
||||
}
|
||||
if row.AuthURL != nil && strings.TrimSpace(*row.AuthURL) != "" {
|
||||
base.AuthURL = strings.TrimSpace(*row.AuthURL)
|
||||
}
|
||||
if row.TokenURL != nil && strings.TrimSpace(*row.TokenURL) != "" {
|
||||
base.TokenURL = strings.TrimSpace(*row.TokenURL)
|
||||
}
|
||||
if row.UserinfoURL != nil && strings.TrimSpace(*row.UserinfoURL) != "" {
|
||||
base.UserinfoURL = strings.TrimSpace(*row.UserinfoURL)
|
||||
}
|
||||
if row.Scopes != nil && strings.TrimSpace(*row.Scopes) != "" {
|
||||
base.Scopes = splitList(*row.Scopes)
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
func splitList(s string) []string {
|
||||
parts := strings.Split(s, ",")
|
||||
out := make([]string, 0, len(parts))
|
||||
for _, p := range parts {
|
||||
if v := strings.TrimSpace(p); v != "" {
|
||||
out = append(out, v)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// BuildAuthURL 生成跳转到第三方授权页的地址
|
||||
func BuildAuthURL(cfg Config, redirectURI, state string) string {
|
||||
v := url.Values{}
|
||||
if cfg.UseAppIDStyle {
|
||||
v.Set("appid", cfg.AppID)
|
||||
} else {
|
||||
v.Set("client_id", cfg.AppID)
|
||||
}
|
||||
v.Set("redirect_uri", redirectURI)
|
||||
v.Set("response_type", "code")
|
||||
v.Set("scope", strings.Join(cfg.Scopes, ","))
|
||||
v.Set("state", state)
|
||||
|
||||
addr := cfg.AuthURL + "?" + v.Encode()
|
||||
if cfg.Provider == models.IdPWechat {
|
||||
// 微信要求在 hash 后带 #wechat_redirect
|
||||
return addr + "#wechat_redirect"
|
||||
}
|
||||
return addr
|
||||
}
|
||||
|
||||
// Exchange 用授权码换取用户信息
|
||||
func Exchange(cfg Config, code string) (*UserInfo, error) {
|
||||
if cfg.AppID == "" || cfg.AppSecret == "" {
|
||||
return nil, errors.New("该登录方式尚未配置,请联系管理员")
|
||||
}
|
||||
|
||||
// 1. code 换 access_token
|
||||
v := url.Values{}
|
||||
if cfg.UseAppIDStyle {
|
||||
v.Set("appid", cfg.AppID)
|
||||
v.Set("secret", cfg.AppSecret)
|
||||
} else {
|
||||
v.Set("client_id", cfg.AppID)
|
||||
v.Set("client_secret", cfg.AppSecret)
|
||||
}
|
||||
v.Set("code", code)
|
||||
v.Set("grant_type", "authorization_code")
|
||||
|
||||
tokenBody, err := post(cfg, cfg.TokenURL, v, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tokenMap := parseMap(tokenBody)
|
||||
accessToken := firstStr(tokenMap, "access_token")
|
||||
if accessToken == "" {
|
||||
return nil, fmt.Errorf("换取令牌失败: %s", truncate(tokenBody, 200))
|
||||
}
|
||||
openID := firstStr(tokenMap, "openid", "unionId", "open_id", "sub", "id")
|
||||
|
||||
// 2. 取用户信息
|
||||
var infoMap map[string]interface{}
|
||||
if cfg.UserinfoStyle == "query" {
|
||||
q := url.Values{}
|
||||
q.Set("access_token", accessToken)
|
||||
if openID != "" {
|
||||
q.Set("openid", openID)
|
||||
}
|
||||
// QQ 需要额外带 oauth_consumer_key
|
||||
if cfg.Provider == models.IdPQQ {
|
||||
q.Set("oauth_consumer_key", cfg.AppID)
|
||||
}
|
||||
body, err := get(cfg, cfg.UserinfoURL+"?"+q.Encode(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
infoMap = parseMap(body)
|
||||
} else {
|
||||
body, err := get(cfg, cfg.UserinfoURL, map[string]string{
|
||||
"Authorization": "Bearer " + accessToken,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
infoMap = parseMap(body)
|
||||
// 飞书把数据包在 data 里
|
||||
if d, ok := infoMap["data"].(map[string]interface{}); ok {
|
||||
infoMap = d
|
||||
}
|
||||
}
|
||||
|
||||
info := &UserInfo{Raw: truncate(mustJSON(infoMap), 2000)}
|
||||
info.OpenID = strField(infoMap, cfg.OpenIDField)
|
||||
if cfg.UnionIDField != "" {
|
||||
info.UnionID = strField(infoMap, cfg.UnionIDField)
|
||||
}
|
||||
info.Nickname = strField(infoMap, cfg.NicknameField)
|
||||
info.Avatar = strField(infoMap, cfg.AvatarField)
|
||||
|
||||
if info.OpenID == "" && openID != "" {
|
||||
info.OpenID = openID
|
||||
}
|
||||
if info.OpenID == "" {
|
||||
return nil, errors.New("未能获取第三方账号标识")
|
||||
}
|
||||
if info.UnionID == "" {
|
||||
info.UnionID = info.OpenID
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- HTTP
|
||||
|
||||
func client(cfg Config) *http.Client {
|
||||
c := &http.Client{Timeout: 15 * time.Second}
|
||||
if cfg.ProxyURL != "" {
|
||||
if u, err := url.Parse(cfg.ProxyURL); err == nil {
|
||||
c.Transport = &http.Transport{Proxy: http.ProxyURL(u)}
|
||||
}
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func post(cfg Config, target string, form url.Values, headers map[string]string) (string, error) {
|
||||
req, err := http.NewRequest("POST", target, strings.NewReader(form.Encode()))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
if cfg.Provider == models.IdPGitHub {
|
||||
req.Header.Set("Accept", "application/json")
|
||||
}
|
||||
for k, v := range headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
resp, err := client(cfg).Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("请求第三方失败: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode >= 300 {
|
||||
return "", fmt.Errorf("第三方返回异常(%d): %s", resp.StatusCode, truncate(string(body), 200))
|
||||
}
|
||||
return string(body), nil
|
||||
}
|
||||
|
||||
func get(cfg Config, target string, headers map[string]string) (string, error) {
|
||||
req, err := http.NewRequest("GET", target, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for k, v := range headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
resp, err := client(cfg).Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("请求第三方失败: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode >= 300 {
|
||||
return "", fmt.Errorf("第三方返回异常(%d): %s", resp.StatusCode, truncate(string(body), 200))
|
||||
}
|
||||
return string(body), nil
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 解析工具
|
||||
|
||||
// parseMap 兼容 JSON 与 querystring 两种响应(微信/QQ 早期接口返回 form 格式)
|
||||
func parseMap(body string) map[string]interface{} {
|
||||
out := map[string]interface{}{}
|
||||
if err := json.Unmarshal([]byte(body), &out); err == nil {
|
||||
return out
|
||||
}
|
||||
if values, err := url.ParseQuery(body); err == nil {
|
||||
for k, v := range values {
|
||||
if len(v) > 0 {
|
||||
out[k] = v[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func firstStr(m map[string]interface{}, keys ...string) string {
|
||||
for _, k := range keys {
|
||||
if v, ok := m[k]; ok {
|
||||
switch t := v.(type) {
|
||||
case string:
|
||||
if t != "" {
|
||||
return t
|
||||
}
|
||||
case float64:
|
||||
return fmt.Sprintf("%.0f", t)
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func strField(m map[string]interface{}, field string) string {
|
||||
if field == "" {
|
||||
return ""
|
||||
}
|
||||
return firstStr(m, field)
|
||||
}
|
||||
|
||||
func mustJSON(m map[string]interface{}) string {
|
||||
b, _ := json.Marshal(m)
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func truncate(s string, n int) string {
|
||||
if len(s) <= n {
|
||||
return s
|
||||
}
|
||||
return s[:n]
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
"server/pkg/jwtutil"
|
||||
"server/pkg/passwordutil"
|
||||
)
|
||||
|
||||
@@ -127,9 +128,9 @@ func PasswordLogin(account, password, clientID, ip, userAgent string) (*LoginRes
|
||||
ClientID: clientID,
|
||||
Sid: sess.Sid,
|
||||
Username: profile.Nickname,
|
||||
UserType: "tenant",
|
||||
Amr: AmrPwd,
|
||||
AccessTTL: accessTTL,
|
||||
// 不指定 UserType:由 IssueTokens 按应用编码给出 backend / app
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -184,7 +185,6 @@ func ChooseTenant(sid string, tid uint64, clientID string) (*TokenPair, *Identit
|
||||
ClientID: clientID,
|
||||
Sid: sid,
|
||||
Username: profile.Nickname,
|
||||
UserType: "tenant",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@@ -262,7 +262,32 @@ func writeLoginLog(tid uint64, identityID uint64, account, userName, clientID, l
|
||||
return err
|
||||
}
|
||||
|
||||
// Logout 登出:吊销刷新令牌、会话与访问令牌
|
||||
// Logout 登出:吊销刷新令牌、会话与访问令牌,并通知其他应用(单点登出)
|
||||
func Logout(accessToken, refreshToken string) error {
|
||||
return RevokeTokenPair(refreshToken, accessToken, models.RevokeReasonLogout)
|
||||
err := RevokeTokenPair(refreshToken, accessToken, models.RevokeReasonLogout)
|
||||
|
||||
// 单点登出:通知该用户在其他应用的会话(如后台登出后 APP 同步下线)
|
||||
identityID := uint64(0)
|
||||
if claims, e := jwtutil.ParseTokenRaw(accessToken); e == nil && claims.UserID > 0 {
|
||||
identityID = uint64(claims.UserID)
|
||||
}
|
||||
if identityID == 0 && refreshToken != "" {
|
||||
if rt, e := findRefreshToken(refreshToken); e == nil {
|
||||
identityID = rt.IdentityID
|
||||
}
|
||||
}
|
||||
if identityID > 0 {
|
||||
NotifyBackchannelLogout(identityID)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// findRefreshToken 按明文查找刷新令牌记录
|
||||
func findRefreshToken(plain string) (*models.AuthRefreshToken, error) {
|
||||
var rt models.AuthRefreshToken
|
||||
if err := models.Orm.QueryTable(new(models.AuthRefreshToken)).
|
||||
Filter("token_hash", hashToken(plain)).One(&rt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &rt, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"server/models"
|
||||
"server/pkg/jwtutil"
|
||||
)
|
||||
|
||||
// BackchannelLogoutEvent OIDC 标准登出事件声明
|
||||
const BackchannelLogoutEvent = "http://schemas.openid.net/event/backchannel-logout"
|
||||
|
||||
// logoutTarget 需要收到登出通知的应用
|
||||
type logoutTarget struct {
|
||||
ClientID string
|
||||
Endpoint string
|
||||
Sid string
|
||||
Identity uint64
|
||||
}
|
||||
|
||||
// NotifyBackchannelLogout 单点登出:通知该用户已登录的所有应用。
|
||||
//
|
||||
// 流程:查库收集目标应用(同步)→ 异步逐个 POST logout_token。
|
||||
// 各应用收到后应清除本地会话,否则用户在这边登出了,其他应用仍显示已登录。
|
||||
//
|
||||
// 注意:查库必须在请求上下文内同步完成(beego 全局 Ormer 不适合跨 goroutine 使用),
|
||||
// 异步部分只做 HTTP 通知,不再触碰数据库。
|
||||
func NotifyBackchannelLogout(identityID uint64) {
|
||||
targets, err := collectLogoutTargets(identityID)
|
||||
if err != nil || len(targets) == 0 {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
for _, t := range targets {
|
||||
if err := sendLogoutToken(t); err != nil {
|
||||
log.Printf("[auth] 单点登出通知失败 client=%s: %v", t.ClientID, err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// collectLogoutTargets 收集该用户当前活跃会话涉及的应用(按 client_id 去重)
|
||||
func collectLogoutTargets(identityID uint64) ([]logoutTarget, error) {
|
||||
var sessions []models.AuthSession
|
||||
if _, err := models.Orm.QueryTable(new(models.AuthSession)).
|
||||
Filter("identity_id", identityID).
|
||||
Filter("revoked", 0).
|
||||
All(&sessions); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
seen := map[string]bool{}
|
||||
targets := make([]logoutTarget, 0)
|
||||
for _, s := range sessions {
|
||||
if s.ClientID == "" || seen[s.ClientID] {
|
||||
continue
|
||||
}
|
||||
var client models.AuthClient
|
||||
if err := models.Orm.QueryTable(new(models.AuthClient)).
|
||||
Filter("client_id", s.ClientID).One(&client); err != nil {
|
||||
continue
|
||||
}
|
||||
if client.BackchannelLogoutURI == nil || strings.TrimSpace(*client.BackchannelLogoutURI) == "" {
|
||||
continue
|
||||
}
|
||||
seen[s.ClientID] = true
|
||||
targets = append(targets, logoutTarget{
|
||||
ClientID: s.ClientID,
|
||||
Endpoint: strings.TrimSpace(*client.BackchannelLogoutURI),
|
||||
Sid: s.Sid,
|
||||
Identity: identityID,
|
||||
})
|
||||
}
|
||||
return targets, nil
|
||||
}
|
||||
|
||||
// sendLogoutToken 按 OIDC Back-Channel Logout 规范发送 logout_token
|
||||
func sendLogoutToken(t logoutTarget) error {
|
||||
token, err := jwtutil.SignToken(jwtutil.TokenOptions{
|
||||
Alg: jwtutil.AlgRS256,
|
||||
UserID: int(t.Identity),
|
||||
Subject: fmt.Sprintf("%d", t.Identity),
|
||||
Audience: []string{t.ClientID},
|
||||
ClientID: t.ClientID,
|
||||
Sid: t.Sid,
|
||||
Jti: uuid.NewString(),
|
||||
Events: map[string]interface{}{BackchannelLogoutEvent: map[string]interface{}{}},
|
||||
TTL: 5 * time.Minute,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("logout_token", token)
|
||||
|
||||
client := &http.Client{Timeout: 5 * time.Second}
|
||||
resp, err := client.PostForm(t.Endpoint, form)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode >= 300 {
|
||||
return fmt.Errorf("应用返回状态码 %d", resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -46,6 +47,10 @@ type TokenIssue struct {
|
||||
|
||||
// IssueTokens 签发访问令牌与刷新令牌。
|
||||
// 刷新令牌明文只在本次返回,库中仅存哈希。
|
||||
//
|
||||
// 令牌中的 user_id 为认证中心 identity_id(yz_auth_identity.id)。
|
||||
// 业务表(文件/客户/合同/日程等)中的 uid 已由 scripts/uidmigrate 全量迁移为
|
||||
// 同一套 ID,因此无需任何兼容换算。
|
||||
func IssueTokens(opt TokenIssue) (*TokenPair, error) {
|
||||
accessTTL := opt.AccessTTL
|
||||
if accessTTL <= 0 {
|
||||
@@ -56,13 +61,27 @@ func IssueTokens(opt TokenIssue) (*TokenPair, error) {
|
||||
refreshTTL = 2592000
|
||||
}
|
||||
|
||||
// 全量迁移后,令牌中的 user_id 统一为认证中心的 identity_id
|
||||
userID := int(opt.IdentityID)
|
||||
|
||||
// userType 需与现有业务接口的判定保持一致:
|
||||
// 后端接口普遍要求 user_type 为 "backend"(租户后台)或 "app"(移动端),
|
||||
// 若签发 "tenant" 会被判为无权访问。这里按应用编码给出默认值。
|
||||
userType := opt.UserType
|
||||
if userType == "" {
|
||||
userType = "backend"
|
||||
if opt.ClientID == "yz-uniapp" {
|
||||
userType = "app"
|
||||
}
|
||||
}
|
||||
|
||||
jti := uuid.NewString()
|
||||
access, err := jwtutil.SignToken(jwtutil.TokenOptions{
|
||||
Alg: jwtutil.AlgRS256,
|
||||
UserID: int(opt.IdentityID),
|
||||
UserID: userID,
|
||||
Username: opt.Username,
|
||||
TenantID: int(opt.Tid),
|
||||
UserType: opt.UserType,
|
||||
UserType: userType,
|
||||
ClientID: opt.ClientID,
|
||||
Sid: opt.Sid,
|
||||
Amr: opt.Amr,
|
||||
@@ -159,6 +178,46 @@ func RefreshTokens(plain, clientID string) (*TokenPair, error) {
|
||||
return pair, nil
|
||||
}
|
||||
|
||||
// FindLegacyUID 通过「身份 + 企业」在老表 yz_system_tenant_user 中找到对应 uid。
|
||||
//
|
||||
// 双轨期(老登录与统一认证并行)下业务接口仍以老 uid 识别用户,
|
||||
// 因此签发令牌、返回用户信息时都要换算回老 uid。
|
||||
// 匹配顺序:账号 → 手机号 → 邮箱;都匹配不到返回 0。
|
||||
func FindLegacyUID(tid, identityID uint64) uint64 {
|
||||
if tid == 0 || identityID == 0 {
|
||||
return 0
|
||||
}
|
||||
var bind models.AuthTenantUser
|
||||
if err := models.Orm.QueryTable(new(models.AuthTenantUser)).
|
||||
Filter("tid", tid).
|
||||
Filter("identity_id", identityID).
|
||||
Filter("delete_time__isnull", true).
|
||||
One(&bind); err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
qs := models.Orm.QueryTable(new(models.SystemTenantUser)).Filter("tid", tid)
|
||||
match := func(field string, value *string) uint64 {
|
||||
if value == nil || strings.TrimSpace(*value) == "" {
|
||||
return 0
|
||||
}
|
||||
var row models.SystemTenantUser
|
||||
if err := qs.Filter(field, strings.TrimSpace(*value)).
|
||||
Filter("delete_time__isnull", true).
|
||||
One(&row); err == nil {
|
||||
return row.Uid
|
||||
}
|
||||
return 0
|
||||
}
|
||||
if uid := match("account", bind.Account); uid > 0 {
|
||||
return uid
|
||||
}
|
||||
if uid := match("phone", bind.Phone); uid > 0 {
|
||||
return uid
|
||||
}
|
||||
return match("email", bind.Email)
|
||||
}
|
||||
|
||||
// RevokeTokenPair 登出:吊销刷新令牌、会话,并把 access token 的 jti 加入黑名单。
|
||||
func RevokeTokenPair(plain, accessToken, reason string) error {
|
||||
if plain != "" {
|
||||
|
||||
@@ -19,10 +19,10 @@ import (
|
||||
|
||||
// McpSession 一次已建立的 MCP 连接会话
|
||||
type McpSession struct {
|
||||
Server models.BackendMcpServer
|
||||
Client *mcpclient.Client
|
||||
Tools []models.McpToolInfo
|
||||
Finger string // 配置指纹,配置变更时自动重连
|
||||
Server models.BackendMcpServer
|
||||
Client *mcpclient.Client
|
||||
Tools []models.McpToolInfo
|
||||
Finger string // 配置指纹,配置变更时自动重连
|
||||
}
|
||||
|
||||
// McpManager MCP 客户端管理器(全局单例,带连接缓存)
|
||||
|
||||
@@ -832,14 +832,14 @@ type OaDocSaveParams struct {
|
||||
Tags string
|
||||
Summary string
|
||||
// Content 在线制作文档的编辑源(富文本 HTML),SetContent 为 true 时才覆盖
|
||||
SetContent bool
|
||||
Content string
|
||||
Status int8
|
||||
Version int
|
||||
OwnerID uint64
|
||||
OwnerName string
|
||||
IsStar int8
|
||||
Visibility int8 // 0-租户公开 1-私密
|
||||
SetContent bool
|
||||
Content string
|
||||
Status int8
|
||||
Version int
|
||||
OwnerID uint64
|
||||
OwnerName string
|
||||
IsStar int8
|
||||
Visibility int8 // 0-租户公开 1-私密
|
||||
}
|
||||
|
||||
// OaDocCreate 新增文档。
|
||||
|
||||
+50
-51
@@ -1,51 +1,50 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"server/models"
|
||||
)
|
||||
|
||||
// CheckUserPermission 校验用户是否拥有指定权限标识。
|
||||
// 兼容 rights 为 JSON 数组 / 逗号分隔字符串;解析失败时默认放行,避免历史数据阻断请求。
|
||||
func CheckUserPermission(userID int, permission string) (bool, error) {
|
||||
if permission == "" || userID <= 0 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
var user models.AdminUser
|
||||
if err := models.Orm.QueryTable(new(models.AdminUser)).Filter("id", userID).One(&user); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
var role models.AdminRole
|
||||
if err := models.Orm.QueryTable(new(models.AdminRole)).Filter("id", user.RoleID).One(&role); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if role.Rights == nil || strings.TrimSpace(*role.Rights) == "" {
|
||||
return true, nil
|
||||
}
|
||||
rightsRaw := strings.TrimSpace(*role.Rights)
|
||||
|
||||
// 1) JSON 数组格式
|
||||
var arr []string
|
||||
if err := json.Unmarshal([]byte(rightsRaw), &arr); err == nil {
|
||||
for _, p := range arr {
|
||||
if strings.TrimSpace(p) == permission {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// 2) 逗号分隔字符串
|
||||
for _, p := range strings.Split(rightsRaw, ",") {
|
||||
if strings.TrimSpace(p) == permission {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"server/models"
|
||||
)
|
||||
|
||||
// CheckUserPermission 校验用户是否拥有指定权限标识。
|
||||
// 兼容 rights 为 JSON 数组 / 逗号分隔字符串;解析失败时默认放行,避免历史数据阻断请求。
|
||||
func CheckUserPermission(userID int, permission string) (bool, error) {
|
||||
if permission == "" || userID <= 0 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
var user models.AdminUser
|
||||
if err := models.Orm.QueryTable(new(models.AdminUser)).Filter("id", userID).One(&user); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
var role models.AdminRole
|
||||
if err := models.Orm.QueryTable(new(models.AdminRole)).Filter("id", user.RoleID).One(&role); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if role.Rights == nil || strings.TrimSpace(*role.Rights) == "" {
|
||||
return true, nil
|
||||
}
|
||||
rightsRaw := strings.TrimSpace(*role.Rights)
|
||||
|
||||
// 1) JSON 数组格式
|
||||
var arr []string
|
||||
if err := json.Unmarshal([]byte(rightsRaw), &arr); err == nil {
|
||||
for _, p := range arr {
|
||||
if strings.TrimSpace(p) == permission {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// 2) 逗号分隔字符串
|
||||
for _, p := range strings.Split(rightsRaw, ",") {
|
||||
if strings.TrimSpace(p) == permission {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"server/models"
|
||||
"server/pkg/jwtutil"
|
||||
"server/pkg/passwordutil"
|
||||
authsvc "server/services/auth"
|
||||
)
|
||||
|
||||
type PlatformLoginUser struct {
|
||||
@@ -133,7 +134,14 @@ func BackendLogin(tenantName, account, password string) (string, *PlatformLoginU
|
||||
|
||||
tenantID := int(tenant.ID)
|
||||
const userType = "backend"
|
||||
token, err := jwtutil.GenerateToken(int(tenantUser.Uid), account, tenantID, userType)
|
||||
|
||||
// 令牌中的 user_id 统一使用认证中心 identity_id(与统一登录签发的一致)。
|
||||
// 业务表的 uid 已全量迁移为同一套 ID,因此老登录入口无需停用也不会错位。
|
||||
userID := int(tenantUser.Uid)
|
||||
if identityID := authsvc.FindIdentityIDByAccount(tenant.ID, account); identityID > 0 {
|
||||
userID = int(identityID)
|
||||
}
|
||||
token, err := jwtutil.GenerateToken(userID, account, tenantID, userType)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
+220
-220
@@ -1,220 +1,220 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/smtp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SMTPConfig 发送邮件所需参数(与 yz_system_email 字段对应)
|
||||
type SMTPConfig struct {
|
||||
FromAddress string
|
||||
FromName string
|
||||
Host string
|
||||
Port uint
|
||||
Password string
|
||||
Encryption string // ssl / tls / none
|
||||
Timeout uint // 秒
|
||||
}
|
||||
|
||||
// SendTestEmailSMTP 发送一封简单测试邮件(纯文本 UTF-8)
|
||||
func SendTestEmailSMTP(cfg SMTPConfig, to string) error {
|
||||
to = strings.TrimSpace(to)
|
||||
if to == "" {
|
||||
return fmt.Errorf("收件人不能为空")
|
||||
}
|
||||
if cfg.Host == "" || cfg.FromAddress == "" {
|
||||
return fmt.Errorf("SMTP 主机或发件人不能为空")
|
||||
}
|
||||
if cfg.Port == 0 {
|
||||
cfg.Port = 465
|
||||
}
|
||||
timeout := cfg.Timeout
|
||||
if timeout == 0 {
|
||||
timeout = 30
|
||||
}
|
||||
d := net.Dialer{Timeout: time.Duration(timeout) * time.Second}
|
||||
addr := net.JoinHostPort(cfg.Host, strconv.FormatUint(uint64(cfg.Port), 10))
|
||||
enc := strings.ToLower(strings.TrimSpace(cfg.Encryption))
|
||||
if enc == "" {
|
||||
enc = "ssl"
|
||||
}
|
||||
|
||||
var client *smtp.Client
|
||||
var err error
|
||||
|
||||
switch enc {
|
||||
case "ssl":
|
||||
conn, derr := tls.DialWithDialer(&d, "tcp", addr, &tls.Config{ServerName: cfg.Host, MinVersion: tls.VersionTLS12})
|
||||
if derr != nil {
|
||||
return fmt.Errorf("连接 SMTP 失败: %w", derr)
|
||||
}
|
||||
defer conn.Close()
|
||||
client, err = smtp.NewClient(conn, cfg.Host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("SMTP 握手失败: %w", err)
|
||||
}
|
||||
case "tls":
|
||||
conn, derr := d.Dial("tcp", addr)
|
||||
if derr != nil {
|
||||
return fmt.Errorf("连接 SMTP 失败: %w", derr)
|
||||
}
|
||||
defer conn.Close()
|
||||
client, err = smtp.NewClient(conn, cfg.Host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("SMTP 握手失败: %w", err)
|
||||
}
|
||||
if ok, _ := client.Extension("STARTTLS"); ok {
|
||||
if err = client.StartTLS(&tls.Config{ServerName: cfg.Host, MinVersion: tls.VersionTLS12}); err != nil {
|
||||
_ = client.Close()
|
||||
return fmt.Errorf("STARTTLS 失败: %w", err)
|
||||
}
|
||||
}
|
||||
case "none":
|
||||
conn, derr := d.Dial("tcp", addr)
|
||||
if derr != nil {
|
||||
return fmt.Errorf("连接 SMTP 失败: %w", derr)
|
||||
}
|
||||
defer conn.Close()
|
||||
client, err = smtp.NewClient(conn, cfg.Host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("SMTP 握手失败: %w", err)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("不支持的加密方式: %s", cfg.Encryption)
|
||||
}
|
||||
defer func() { _ = client.Close() }()
|
||||
|
||||
auth := smtp.PlainAuth("", cfg.FromAddress, cfg.Password, cfg.Host)
|
||||
if err = client.Auth(auth); err != nil {
|
||||
return fmt.Errorf("SMTP 认证失败: %w", err)
|
||||
}
|
||||
if err = client.Mail(cfg.FromAddress); err != nil {
|
||||
return fmt.Errorf("MAIL FROM 失败: %w", err)
|
||||
}
|
||||
if err = client.Rcpt(to); err != nil {
|
||||
return fmt.Errorf("RCPT TO 失败: %w", err)
|
||||
}
|
||||
wc, err := client.Data()
|
||||
if err != nil {
|
||||
return fmt.Errorf("DATA 失败: %w", err)
|
||||
}
|
||||
fromName := strings.TrimSpace(cfg.FromName)
|
||||
subject := "平台邮箱测试"
|
||||
body := "这是一封来自管理后台「邮箱管理」的测试邮件。\r\nThis is a test email from the platform email settings.\r\n"
|
||||
headers := fmt.Sprintf("From: %s\r\nTo: %s\r\nSubject: %s\r\nMIME-Version: 1.0\r\nContent-Type: text/plain; charset=UTF-8\r\nContent-Transfer-Encoding: 8bit\r\n\r\n",
|
||||
formatFromHeader(fromName, cfg.FromAddress), to, subject)
|
||||
if _, err = wc.Write([]byte(headers + body)); err != nil {
|
||||
return fmt.Errorf("写入邮件内容失败: %w", err)
|
||||
}
|
||||
if err = wc.Close(); err != nil {
|
||||
return fmt.Errorf("结束 DATA 失败: %w", err)
|
||||
}
|
||||
return client.Quit()
|
||||
}
|
||||
|
||||
// SendHTMLEmailSMTP 发送一封 HTML 格式邮件
|
||||
func SendHTMLEmailSMTP(cfg SMTPConfig, to string, subject string, htmlBody string) error {
|
||||
to = strings.TrimSpace(to)
|
||||
if to == "" {
|
||||
return fmt.Errorf("收件人不能为空")
|
||||
}
|
||||
if cfg.Host == "" || cfg.FromAddress == "" {
|
||||
return fmt.Errorf("SMTP 主机或发件人不能为空")
|
||||
}
|
||||
if cfg.Port == 0 {
|
||||
cfg.Port = 465
|
||||
}
|
||||
timeout := cfg.Timeout
|
||||
if timeout == 0 {
|
||||
timeout = 30
|
||||
}
|
||||
d := net.Dialer{Timeout: time.Duration(timeout) * time.Second}
|
||||
addr := net.JoinHostPort(cfg.Host, strconv.FormatUint(uint64(cfg.Port), 10))
|
||||
enc := strings.ToLower(strings.TrimSpace(cfg.Encryption))
|
||||
if enc == "" {
|
||||
enc = "ssl"
|
||||
}
|
||||
|
||||
var client *smtp.Client
|
||||
var err error
|
||||
|
||||
switch enc {
|
||||
case "ssl":
|
||||
conn, derr := tls.DialWithDialer(&d, "tcp", addr, &tls.Config{ServerName: cfg.Host, MinVersion: tls.VersionTLS12})
|
||||
if derr != nil {
|
||||
return fmt.Errorf("连接 SMTP 失败: %w", derr)
|
||||
}
|
||||
defer conn.Close()
|
||||
client, err = smtp.NewClient(conn, cfg.Host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("SMTP 握手失败: %w", err)
|
||||
}
|
||||
case "tls":
|
||||
conn, derr := d.Dial("tcp", addr)
|
||||
if derr != nil {
|
||||
return fmt.Errorf("连接 SMTP 失败: %w", derr)
|
||||
}
|
||||
defer conn.Close()
|
||||
client, err = smtp.NewClient(conn, cfg.Host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("SMTP 握手失败: %w", err)
|
||||
}
|
||||
if ok, _ := client.Extension("STARTTLS"); ok {
|
||||
if err = client.StartTLS(&tls.Config{ServerName: cfg.Host, MinVersion: tls.VersionTLS12}); err != nil {
|
||||
_ = client.Close()
|
||||
return fmt.Errorf("STARTTLS 失败: %w", err)
|
||||
}
|
||||
}
|
||||
case "none":
|
||||
conn, derr := d.Dial("tcp", addr)
|
||||
if derr != nil {
|
||||
return fmt.Errorf("连接 SMTP 失败: %w", derr)
|
||||
}
|
||||
defer conn.Close()
|
||||
client, err = smtp.NewClient(conn, cfg.Host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("SMTP 握手失败: %w", err)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("不支持的加密方式: %s", cfg.Encryption)
|
||||
}
|
||||
defer func() { _ = client.Close() }()
|
||||
|
||||
auth := smtp.PlainAuth("", cfg.FromAddress, cfg.Password, cfg.Host)
|
||||
if err = client.Auth(auth); err != nil {
|
||||
return fmt.Errorf("SMTP 认证失败: %w", err)
|
||||
}
|
||||
if err = client.Mail(cfg.FromAddress); err != nil {
|
||||
return fmt.Errorf("MAIL FROM 失败: %w", err)
|
||||
}
|
||||
if err = client.Rcpt(to); err != nil {
|
||||
return fmt.Errorf("RCPT TO 失败: %w", err)
|
||||
}
|
||||
wc, err := client.Data()
|
||||
if err != nil {
|
||||
return fmt.Errorf("DATA 失败: %w", err)
|
||||
}
|
||||
fromName := strings.TrimSpace(cfg.FromName)
|
||||
headers := fmt.Sprintf("From: %s\r\nTo: %s\r\nSubject: %s\r\nMIME-Version: 1.0\r\nContent-Type: text/html; charset=UTF-8\r\nContent-Transfer-Encoding: 8bit\r\n\r\n",
|
||||
formatFromHeader(fromName, cfg.FromAddress), to, subject)
|
||||
if _, err = wc.Write([]byte(headers + htmlBody)); err != nil {
|
||||
return fmt.Errorf("写入邮件内容失败: %w", err)
|
||||
}
|
||||
if err = wc.Close(); err != nil {
|
||||
return fmt.Errorf("结束 DATA 失败: %w", err)
|
||||
}
|
||||
return client.Quit()
|
||||
}
|
||||
|
||||
func formatFromHeader(name, addr string) string {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
return addr
|
||||
}
|
||||
return fmt.Sprintf("%s <%s>", name, addr)
|
||||
}
|
||||
package services
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/smtp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SMTPConfig 发送邮件所需参数(与 yz_system_email 字段对应)
|
||||
type SMTPConfig struct {
|
||||
FromAddress string
|
||||
FromName string
|
||||
Host string
|
||||
Port uint
|
||||
Password string
|
||||
Encryption string // ssl / tls / none
|
||||
Timeout uint // 秒
|
||||
}
|
||||
|
||||
// SendTestEmailSMTP 发送一封简单测试邮件(纯文本 UTF-8)
|
||||
func SendTestEmailSMTP(cfg SMTPConfig, to string) error {
|
||||
to = strings.TrimSpace(to)
|
||||
if to == "" {
|
||||
return fmt.Errorf("收件人不能为空")
|
||||
}
|
||||
if cfg.Host == "" || cfg.FromAddress == "" {
|
||||
return fmt.Errorf("SMTP 主机或发件人不能为空")
|
||||
}
|
||||
if cfg.Port == 0 {
|
||||
cfg.Port = 465
|
||||
}
|
||||
timeout := cfg.Timeout
|
||||
if timeout == 0 {
|
||||
timeout = 30
|
||||
}
|
||||
d := net.Dialer{Timeout: time.Duration(timeout) * time.Second}
|
||||
addr := net.JoinHostPort(cfg.Host, strconv.FormatUint(uint64(cfg.Port), 10))
|
||||
enc := strings.ToLower(strings.TrimSpace(cfg.Encryption))
|
||||
if enc == "" {
|
||||
enc = "ssl"
|
||||
}
|
||||
|
||||
var client *smtp.Client
|
||||
var err error
|
||||
|
||||
switch enc {
|
||||
case "ssl":
|
||||
conn, derr := tls.DialWithDialer(&d, "tcp", addr, &tls.Config{ServerName: cfg.Host, MinVersion: tls.VersionTLS12})
|
||||
if derr != nil {
|
||||
return fmt.Errorf("连接 SMTP 失败: %w", derr)
|
||||
}
|
||||
defer conn.Close()
|
||||
client, err = smtp.NewClient(conn, cfg.Host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("SMTP 握手失败: %w", err)
|
||||
}
|
||||
case "tls":
|
||||
conn, derr := d.Dial("tcp", addr)
|
||||
if derr != nil {
|
||||
return fmt.Errorf("连接 SMTP 失败: %w", derr)
|
||||
}
|
||||
defer conn.Close()
|
||||
client, err = smtp.NewClient(conn, cfg.Host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("SMTP 握手失败: %w", err)
|
||||
}
|
||||
if ok, _ := client.Extension("STARTTLS"); ok {
|
||||
if err = client.StartTLS(&tls.Config{ServerName: cfg.Host, MinVersion: tls.VersionTLS12}); err != nil {
|
||||
_ = client.Close()
|
||||
return fmt.Errorf("STARTTLS 失败: %w", err)
|
||||
}
|
||||
}
|
||||
case "none":
|
||||
conn, derr := d.Dial("tcp", addr)
|
||||
if derr != nil {
|
||||
return fmt.Errorf("连接 SMTP 失败: %w", derr)
|
||||
}
|
||||
defer conn.Close()
|
||||
client, err = smtp.NewClient(conn, cfg.Host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("SMTP 握手失败: %w", err)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("不支持的加密方式: %s", cfg.Encryption)
|
||||
}
|
||||
defer func() { _ = client.Close() }()
|
||||
|
||||
auth := smtp.PlainAuth("", cfg.FromAddress, cfg.Password, cfg.Host)
|
||||
if err = client.Auth(auth); err != nil {
|
||||
return fmt.Errorf("SMTP 认证失败: %w", err)
|
||||
}
|
||||
if err = client.Mail(cfg.FromAddress); err != nil {
|
||||
return fmt.Errorf("MAIL FROM 失败: %w", err)
|
||||
}
|
||||
if err = client.Rcpt(to); err != nil {
|
||||
return fmt.Errorf("RCPT TO 失败: %w", err)
|
||||
}
|
||||
wc, err := client.Data()
|
||||
if err != nil {
|
||||
return fmt.Errorf("DATA 失败: %w", err)
|
||||
}
|
||||
fromName := strings.TrimSpace(cfg.FromName)
|
||||
subject := "平台邮箱测试"
|
||||
body := "这是一封来自管理后台「邮箱管理」的测试邮件。\r\nThis is a test email from the platform email settings.\r\n"
|
||||
headers := fmt.Sprintf("From: %s\r\nTo: %s\r\nSubject: %s\r\nMIME-Version: 1.0\r\nContent-Type: text/plain; charset=UTF-8\r\nContent-Transfer-Encoding: 8bit\r\n\r\n",
|
||||
formatFromHeader(fromName, cfg.FromAddress), to, subject)
|
||||
if _, err = wc.Write([]byte(headers + body)); err != nil {
|
||||
return fmt.Errorf("写入邮件内容失败: %w", err)
|
||||
}
|
||||
if err = wc.Close(); err != nil {
|
||||
return fmt.Errorf("结束 DATA 失败: %w", err)
|
||||
}
|
||||
return client.Quit()
|
||||
}
|
||||
|
||||
// SendHTMLEmailSMTP 发送一封 HTML 格式邮件
|
||||
func SendHTMLEmailSMTP(cfg SMTPConfig, to string, subject string, htmlBody string) error {
|
||||
to = strings.TrimSpace(to)
|
||||
if to == "" {
|
||||
return fmt.Errorf("收件人不能为空")
|
||||
}
|
||||
if cfg.Host == "" || cfg.FromAddress == "" {
|
||||
return fmt.Errorf("SMTP 主机或发件人不能为空")
|
||||
}
|
||||
if cfg.Port == 0 {
|
||||
cfg.Port = 465
|
||||
}
|
||||
timeout := cfg.Timeout
|
||||
if timeout == 0 {
|
||||
timeout = 30
|
||||
}
|
||||
d := net.Dialer{Timeout: time.Duration(timeout) * time.Second}
|
||||
addr := net.JoinHostPort(cfg.Host, strconv.FormatUint(uint64(cfg.Port), 10))
|
||||
enc := strings.ToLower(strings.TrimSpace(cfg.Encryption))
|
||||
if enc == "" {
|
||||
enc = "ssl"
|
||||
}
|
||||
|
||||
var client *smtp.Client
|
||||
var err error
|
||||
|
||||
switch enc {
|
||||
case "ssl":
|
||||
conn, derr := tls.DialWithDialer(&d, "tcp", addr, &tls.Config{ServerName: cfg.Host, MinVersion: tls.VersionTLS12})
|
||||
if derr != nil {
|
||||
return fmt.Errorf("连接 SMTP 失败: %w", derr)
|
||||
}
|
||||
defer conn.Close()
|
||||
client, err = smtp.NewClient(conn, cfg.Host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("SMTP 握手失败: %w", err)
|
||||
}
|
||||
case "tls":
|
||||
conn, derr := d.Dial("tcp", addr)
|
||||
if derr != nil {
|
||||
return fmt.Errorf("连接 SMTP 失败: %w", derr)
|
||||
}
|
||||
defer conn.Close()
|
||||
client, err = smtp.NewClient(conn, cfg.Host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("SMTP 握手失败: %w", err)
|
||||
}
|
||||
if ok, _ := client.Extension("STARTTLS"); ok {
|
||||
if err = client.StartTLS(&tls.Config{ServerName: cfg.Host, MinVersion: tls.VersionTLS12}); err != nil {
|
||||
_ = client.Close()
|
||||
return fmt.Errorf("STARTTLS 失败: %w", err)
|
||||
}
|
||||
}
|
||||
case "none":
|
||||
conn, derr := d.Dial("tcp", addr)
|
||||
if derr != nil {
|
||||
return fmt.Errorf("连接 SMTP 失败: %w", derr)
|
||||
}
|
||||
defer conn.Close()
|
||||
client, err = smtp.NewClient(conn, cfg.Host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("SMTP 握手失败: %w", err)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("不支持的加密方式: %s", cfg.Encryption)
|
||||
}
|
||||
defer func() { _ = client.Close() }()
|
||||
|
||||
auth := smtp.PlainAuth("", cfg.FromAddress, cfg.Password, cfg.Host)
|
||||
if err = client.Auth(auth); err != nil {
|
||||
return fmt.Errorf("SMTP 认证失败: %w", err)
|
||||
}
|
||||
if err = client.Mail(cfg.FromAddress); err != nil {
|
||||
return fmt.Errorf("MAIL FROM 失败: %w", err)
|
||||
}
|
||||
if err = client.Rcpt(to); err != nil {
|
||||
return fmt.Errorf("RCPT TO 失败: %w", err)
|
||||
}
|
||||
wc, err := client.Data()
|
||||
if err != nil {
|
||||
return fmt.Errorf("DATA 失败: %w", err)
|
||||
}
|
||||
fromName := strings.TrimSpace(cfg.FromName)
|
||||
headers := fmt.Sprintf("From: %s\r\nTo: %s\r\nSubject: %s\r\nMIME-Version: 1.0\r\nContent-Type: text/html; charset=UTF-8\r\nContent-Transfer-Encoding: 8bit\r\n\r\n",
|
||||
formatFromHeader(fromName, cfg.FromAddress), to, subject)
|
||||
if _, err = wc.Write([]byte(headers + htmlBody)); err != nil {
|
||||
return fmt.Errorf("写入邮件内容失败: %w", err)
|
||||
}
|
||||
if err = wc.Close(); err != nil {
|
||||
return fmt.Errorf("结束 DATA 失败: %w", err)
|
||||
}
|
||||
return client.Quit()
|
||||
}
|
||||
|
||||
func formatFromHeader(name, addr string) string {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
return addr
|
||||
}
|
||||
return fmt.Sprintf("%s <%s>", name, addr)
|
||||
}
|
||||
|
||||
+135
-135
@@ -1,135 +1,135 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
)
|
||||
|
||||
// ListSystemEmails 返回从 yz_platform_normal_setting 组装的邮箱配置(切片,通常仅一条)
|
||||
func ListSystemEmails() ([]models.SystemEmail, error) {
|
||||
enabledStr := models.GetPlatformSettingValue("email_enabled", "0")
|
||||
fromAddress := models.GetPlatformSettingValue("email_from_address", "")
|
||||
fromName := models.GetPlatformSettingValue("email_from_name", "")
|
||||
host := models.GetPlatformSettingValue("email_host", "")
|
||||
portStr := models.GetPlatformSettingValue("email_port", "465")
|
||||
password := models.GetPlatformSettingValue("email_password", "")
|
||||
encryption := models.GetPlatformSettingValue("email_encryption", "ssl")
|
||||
timeoutStr := models.GetPlatformSettingValue("email_timeout", "30")
|
||||
|
||||
status := int8(0)
|
||||
if enabledStr == "1" {
|
||||
status = 1
|
||||
}
|
||||
portVal, _ := strconv.ParseUint(portStr, 10, 32)
|
||||
timeoutVal, _ := strconv.ParseUint(timeoutStr, 10, 32)
|
||||
|
||||
row := models.SystemEmail{
|
||||
ID: 1,
|
||||
FromAddress: fromAddress,
|
||||
Host: host,
|
||||
Port: uint(portVal),
|
||||
Password: password,
|
||||
Encryption: encryption,
|
||||
Timeout: uint(timeoutVal),
|
||||
Status: status,
|
||||
CreateTime: time.Now(),
|
||||
UpdateTime: time.Now(),
|
||||
}
|
||||
if fromName != "" {
|
||||
row.FromName = &fromName
|
||||
}
|
||||
|
||||
return []models.SystemEmail{row}, nil
|
||||
}
|
||||
|
||||
// UpsertFirstSystemEmail 将邮箱配置保存到 yz_platform_normal_setting 表中
|
||||
func UpsertFirstSystemEmail(fromAddress string, fromName *string, host string, port uint, password string, encryption string, timeout uint, status int8, remark *string) error {
|
||||
if encryption == "" {
|
||||
encryption = "ssl"
|
||||
}
|
||||
if port == 0 {
|
||||
port = 465
|
||||
}
|
||||
if timeout == 0 {
|
||||
timeout = 30
|
||||
}
|
||||
fromAddress = strings.TrimSpace(fromAddress)
|
||||
host = strings.TrimSpace(host)
|
||||
|
||||
fn := ""
|
||||
if fromName != nil {
|
||||
fn = *fromName
|
||||
}
|
||||
|
||||
statusStr := "0"
|
||||
if status == 1 {
|
||||
statusStr = "1"
|
||||
}
|
||||
|
||||
settings := []struct {
|
||||
code string
|
||||
name string
|
||||
value string
|
||||
remark string
|
||||
}{
|
||||
{"email_enabled", "邮件服务启用状态", statusStr, "0为关闭,1为开启"},
|
||||
{"email_from_address", "发件人邮箱", fromAddress, ""},
|
||||
{"email_from_name", "发件人名称", fn, ""},
|
||||
{"email_host", "SMTP 服务器地址", host, ""},
|
||||
{"email_port", "SMTP 端口", strconv.FormatUint(uint64(port), 10), ""},
|
||||
{"email_encryption", "邮件加密方式", encryption, "支持 ssl/tls/none"},
|
||||
{"email_timeout", "邮件发送超时时间", strconv.FormatUint(uint64(timeout), 10), ""},
|
||||
}
|
||||
|
||||
// 如果传入了新密码,或者目前还没有保存过密码,才更新密码
|
||||
if strings.TrimSpace(password) != "" {
|
||||
settings = append(settings, struct {
|
||||
code string
|
||||
name string
|
||||
value string
|
||||
remark string
|
||||
}{"email_password", "邮件授权码/密码", strings.TrimSpace(password), ""})
|
||||
} else {
|
||||
// 校验:如果完全没有配置过密码,必须填写密码
|
||||
existingPass := models.GetPlatformSettingValue("email_password", "")
|
||||
if existingPass == "" {
|
||||
return fmt.Errorf("首次保存必须填写授权码/密码")
|
||||
}
|
||||
}
|
||||
|
||||
for _, item := range settings {
|
||||
var setting models.PlatformNormalSetting
|
||||
err := models.Orm.QueryTable(new(models.PlatformNormalSetting)).
|
||||
Filter("code", item.code).
|
||||
Filter("delete_time__isnull", true).
|
||||
One(&setting)
|
||||
if err == nil {
|
||||
setting.Value = item.value
|
||||
setting.Name = item.name
|
||||
setting.Remark = item.remark
|
||||
now := time.Now()
|
||||
setting.UpdateTime = &now
|
||||
_, err = models.Orm.Update(&setting, "Value", "Name", "Remark", "UpdateTime")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
newSetting := models.PlatformNormalSetting{
|
||||
Name: item.name,
|
||||
Code: item.code,
|
||||
Value: item.value,
|
||||
Remark: item.remark,
|
||||
CreateTime: time.Now(),
|
||||
}
|
||||
_, err = models.Orm.Insert(&newSetting)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
)
|
||||
|
||||
// ListSystemEmails 返回从 yz_platform_normal_setting 组装的邮箱配置(切片,通常仅一条)
|
||||
func ListSystemEmails() ([]models.SystemEmail, error) {
|
||||
enabledStr := models.GetPlatformSettingValue("email_enabled", "0")
|
||||
fromAddress := models.GetPlatformSettingValue("email_from_address", "")
|
||||
fromName := models.GetPlatformSettingValue("email_from_name", "")
|
||||
host := models.GetPlatformSettingValue("email_host", "")
|
||||
portStr := models.GetPlatformSettingValue("email_port", "465")
|
||||
password := models.GetPlatformSettingValue("email_password", "")
|
||||
encryption := models.GetPlatformSettingValue("email_encryption", "ssl")
|
||||
timeoutStr := models.GetPlatformSettingValue("email_timeout", "30")
|
||||
|
||||
status := int8(0)
|
||||
if enabledStr == "1" {
|
||||
status = 1
|
||||
}
|
||||
portVal, _ := strconv.ParseUint(portStr, 10, 32)
|
||||
timeoutVal, _ := strconv.ParseUint(timeoutStr, 10, 32)
|
||||
|
||||
row := models.SystemEmail{
|
||||
ID: 1,
|
||||
FromAddress: fromAddress,
|
||||
Host: host,
|
||||
Port: uint(portVal),
|
||||
Password: password,
|
||||
Encryption: encryption,
|
||||
Timeout: uint(timeoutVal),
|
||||
Status: status,
|
||||
CreateTime: time.Now(),
|
||||
UpdateTime: time.Now(),
|
||||
}
|
||||
if fromName != "" {
|
||||
row.FromName = &fromName
|
||||
}
|
||||
|
||||
return []models.SystemEmail{row}, nil
|
||||
}
|
||||
|
||||
// UpsertFirstSystemEmail 将邮箱配置保存到 yz_platform_normal_setting 表中
|
||||
func UpsertFirstSystemEmail(fromAddress string, fromName *string, host string, port uint, password string, encryption string, timeout uint, status int8, remark *string) error {
|
||||
if encryption == "" {
|
||||
encryption = "ssl"
|
||||
}
|
||||
if port == 0 {
|
||||
port = 465
|
||||
}
|
||||
if timeout == 0 {
|
||||
timeout = 30
|
||||
}
|
||||
fromAddress = strings.TrimSpace(fromAddress)
|
||||
host = strings.TrimSpace(host)
|
||||
|
||||
fn := ""
|
||||
if fromName != nil {
|
||||
fn = *fromName
|
||||
}
|
||||
|
||||
statusStr := "0"
|
||||
if status == 1 {
|
||||
statusStr = "1"
|
||||
}
|
||||
|
||||
settings := []struct {
|
||||
code string
|
||||
name string
|
||||
value string
|
||||
remark string
|
||||
}{
|
||||
{"email_enabled", "邮件服务启用状态", statusStr, "0为关闭,1为开启"},
|
||||
{"email_from_address", "发件人邮箱", fromAddress, ""},
|
||||
{"email_from_name", "发件人名称", fn, ""},
|
||||
{"email_host", "SMTP 服务器地址", host, ""},
|
||||
{"email_port", "SMTP 端口", strconv.FormatUint(uint64(port), 10), ""},
|
||||
{"email_encryption", "邮件加密方式", encryption, "支持 ssl/tls/none"},
|
||||
{"email_timeout", "邮件发送超时时间", strconv.FormatUint(uint64(timeout), 10), ""},
|
||||
}
|
||||
|
||||
// 如果传入了新密码,或者目前还没有保存过密码,才更新密码
|
||||
if strings.TrimSpace(password) != "" {
|
||||
settings = append(settings, struct {
|
||||
code string
|
||||
name string
|
||||
value string
|
||||
remark string
|
||||
}{"email_password", "邮件授权码/密码", strings.TrimSpace(password), ""})
|
||||
} else {
|
||||
// 校验:如果完全没有配置过密码,必须填写密码
|
||||
existingPass := models.GetPlatformSettingValue("email_password", "")
|
||||
if existingPass == "" {
|
||||
return fmt.Errorf("首次保存必须填写授权码/密码")
|
||||
}
|
||||
}
|
||||
|
||||
for _, item := range settings {
|
||||
var setting models.PlatformNormalSetting
|
||||
err := models.Orm.QueryTable(new(models.PlatformNormalSetting)).
|
||||
Filter("code", item.code).
|
||||
Filter("delete_time__isnull", true).
|
||||
One(&setting)
|
||||
if err == nil {
|
||||
setting.Value = item.value
|
||||
setting.Name = item.name
|
||||
setting.Remark = item.remark
|
||||
now := time.Now()
|
||||
setting.UpdateTime = &now
|
||||
_, err = models.Orm.Update(&setting, "Value", "Name", "Remark", "UpdateTime")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
newSetting := models.PlatformNormalSetting{
|
||||
Name: item.name,
|
||||
Code: item.code,
|
||||
Value: item.value,
|
||||
Remark: item.remark,
|
||||
CreateTime: time.Now(),
|
||||
}
|
||||
_, err = models.Orm.Insert(&newSetting)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
+532
-532
File diff suppressed because it is too large
Load Diff
+144
-144
@@ -1,144 +1,144 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"server/models"
|
||||
)
|
||||
|
||||
// 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
|
||||
err := models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("tid", tid).
|
||||
Filter("uid", uid).
|
||||
One(&existed)
|
||||
if err == nil {
|
||||
update := map[string]interface{}{
|
||||
"account": account,
|
||||
"name": name,
|
||||
"phone": phone,
|
||||
"email": email,
|
||||
"password": password,
|
||||
"status": status,
|
||||
"is_default": isDefault,
|
||||
"remark": remark,
|
||||
}
|
||||
if sex != nil {
|
||||
update["sex"] = *sex
|
||||
}
|
||||
if birth != nil {
|
||||
trimmedBirth := strings.TrimSpace(*birth)
|
||||
if trimmedBirth == "" {
|
||||
update["birth"] = nil
|
||||
} else {
|
||||
update["birth"] = trimmedBirth
|
||||
}
|
||||
}
|
||||
_, uErr := models.Orm.QueryTable(new(models.SystemTenantUser)).Filter("id", existed.ID).Update(update)
|
||||
return existed.ID, uErr
|
||||
}
|
||||
|
||||
m := &models.SystemTenantUser{
|
||||
Tid: tid,
|
||||
Uid: uid,
|
||||
Account: account,
|
||||
Name: name,
|
||||
Phone: phone,
|
||||
Email: email,
|
||||
Password: password,
|
||||
IsDefault: isDefault,
|
||||
Status: status,
|
||||
Remark: remark,
|
||||
}
|
||||
if sex != nil {
|
||||
m.Sex = *sex
|
||||
}
|
||||
if birth != nil {
|
||||
trimmedBirth := strings.TrimSpace(*birth)
|
||||
if trimmedBirth != "" {
|
||||
m.Birth = &trimmedBirth
|
||||
}
|
||||
}
|
||||
id, iErr := models.Orm.Insert(m)
|
||||
return uint64(id), iErr
|
||||
}
|
||||
|
||||
// UnbindTenantUser 删除绑定关系
|
||||
func UnbindTenantUser(id uint64) error {
|
||||
_, err := models.Orm.QueryTable(new(models.SystemTenantUser)).Filter("id", id).Delete()
|
||||
return err
|
||||
}
|
||||
|
||||
// ListTenantUsersByTid 根据租户ID查询绑定关系
|
||||
func ListTenantUsersByTid(tid uint64) ([]models.SystemTenantUser, error) {
|
||||
var rows []models.SystemTenantUser
|
||||
_, err := models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("tid", tid).
|
||||
OrderBy("-is_default", "-id").
|
||||
All(&rows)
|
||||
return rows, err
|
||||
}
|
||||
|
||||
// ListTenantBindingsByUid 根据用户ID查询绑定关系
|
||||
func ListTenantBindingsByUid(uid uint64) ([]models.SystemTenantUser, error) {
|
||||
var rows []models.SystemTenantUser
|
||||
_, err := models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("uid", uid).
|
||||
OrderBy("-is_default", "-id").
|
||||
All(&rows)
|
||||
return rows, err
|
||||
}
|
||||
|
||||
// GetTenantUserByUidAndTid 根据用户ID和租户ID查询租户用户绑定关系
|
||||
func GetTenantUserByUidAndTid(uid, tid uint64) (*models.SystemTenantUser, error) {
|
||||
var row models.SystemTenantUser
|
||||
err := models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("uid", uid).
|
||||
Filter("tid", tid).
|
||||
One(&row)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &row, nil
|
||||
}
|
||||
|
||||
// GetTenantUserByUid 根据用户ID查询默认/最新租户用户绑定关系
|
||||
func GetTenantUserByUid(uid uint64) (*models.SystemTenantUser, error) {
|
||||
var row models.SystemTenantUser
|
||||
err := models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("uid", uid).
|
||||
OrderBy("-is_default", "-id").
|
||||
One(&row)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &row, nil
|
||||
}
|
||||
|
||||
// GetTenantByID 根据租户ID查询租户信息
|
||||
func GetTenantByID(id uint64) (*models.SystemTenant, error) {
|
||||
var row models.SystemTenant
|
||||
err := models.Orm.QueryTable(new(models.SystemTenant)).
|
||||
Filter("id", id).
|
||||
One(&row)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &row, nil
|
||||
}
|
||||
|
||||
// SetDefaultTenant 设置用户默认租户(同一用户仅一个默认)
|
||||
func SetDefaultTenant(uid, tid uint64) error {
|
||||
_, err := models.Orm.QueryTable(new(models.SystemTenantUser)).Filter("uid", uid).Update(map[string]interface{}{
|
||||
"is_default": 0,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("uid", uid).
|
||||
Filter("tid", tid).
|
||||
Update(map[string]interface{}{"is_default": 1})
|
||||
return err
|
||||
}
|
||||
package services
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"server/models"
|
||||
)
|
||||
|
||||
// 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
|
||||
err := models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("tid", tid).
|
||||
Filter("uid", uid).
|
||||
One(&existed)
|
||||
if err == nil {
|
||||
update := map[string]interface{}{
|
||||
"account": account,
|
||||
"name": name,
|
||||
"phone": phone,
|
||||
"email": email,
|
||||
"password": password,
|
||||
"status": status,
|
||||
"is_default": isDefault,
|
||||
"remark": remark,
|
||||
}
|
||||
if sex != nil {
|
||||
update["sex"] = *sex
|
||||
}
|
||||
if birth != nil {
|
||||
trimmedBirth := strings.TrimSpace(*birth)
|
||||
if trimmedBirth == "" {
|
||||
update["birth"] = nil
|
||||
} else {
|
||||
update["birth"] = trimmedBirth
|
||||
}
|
||||
}
|
||||
_, uErr := models.Orm.QueryTable(new(models.SystemTenantUser)).Filter("id", existed.ID).Update(update)
|
||||
return existed.ID, uErr
|
||||
}
|
||||
|
||||
m := &models.SystemTenantUser{
|
||||
Tid: tid,
|
||||
Uid: uid,
|
||||
Account: account,
|
||||
Name: name,
|
||||
Phone: phone,
|
||||
Email: email,
|
||||
Password: password,
|
||||
IsDefault: isDefault,
|
||||
Status: status,
|
||||
Remark: remark,
|
||||
}
|
||||
if sex != nil {
|
||||
m.Sex = *sex
|
||||
}
|
||||
if birth != nil {
|
||||
trimmedBirth := strings.TrimSpace(*birth)
|
||||
if trimmedBirth != "" {
|
||||
m.Birth = &trimmedBirth
|
||||
}
|
||||
}
|
||||
id, iErr := models.Orm.Insert(m)
|
||||
return uint64(id), iErr
|
||||
}
|
||||
|
||||
// UnbindTenantUser 删除绑定关系
|
||||
func UnbindTenantUser(id uint64) error {
|
||||
_, err := models.Orm.QueryTable(new(models.SystemTenantUser)).Filter("id", id).Delete()
|
||||
return err
|
||||
}
|
||||
|
||||
// ListTenantUsersByTid 根据租户ID查询绑定关系
|
||||
func ListTenantUsersByTid(tid uint64) ([]models.SystemTenantUser, error) {
|
||||
var rows []models.SystemTenantUser
|
||||
_, err := models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("tid", tid).
|
||||
OrderBy("-is_default", "-id").
|
||||
All(&rows)
|
||||
return rows, err
|
||||
}
|
||||
|
||||
// ListTenantBindingsByUid 根据用户ID查询绑定关系
|
||||
func ListTenantBindingsByUid(uid uint64) ([]models.SystemTenantUser, error) {
|
||||
var rows []models.SystemTenantUser
|
||||
_, err := models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("uid", uid).
|
||||
OrderBy("-is_default", "-id").
|
||||
All(&rows)
|
||||
return rows, err
|
||||
}
|
||||
|
||||
// GetTenantUserByUidAndTid 根据用户ID和租户ID查询租户用户绑定关系
|
||||
func GetTenantUserByUidAndTid(uid, tid uint64) (*models.SystemTenantUser, error) {
|
||||
var row models.SystemTenantUser
|
||||
err := models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("uid", uid).
|
||||
Filter("tid", tid).
|
||||
One(&row)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &row, nil
|
||||
}
|
||||
|
||||
// GetTenantUserByUid 根据用户ID查询默认/最新租户用户绑定关系
|
||||
func GetTenantUserByUid(uid uint64) (*models.SystemTenantUser, error) {
|
||||
var row models.SystemTenantUser
|
||||
err := models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("uid", uid).
|
||||
OrderBy("-is_default", "-id").
|
||||
One(&row)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &row, nil
|
||||
}
|
||||
|
||||
// GetTenantByID 根据租户ID查询租户信息
|
||||
func GetTenantByID(id uint64) (*models.SystemTenant, error) {
|
||||
var row models.SystemTenant
|
||||
err := models.Orm.QueryTable(new(models.SystemTenant)).
|
||||
Filter("id", id).
|
||||
One(&row)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &row, nil
|
||||
}
|
||||
|
||||
// SetDefaultTenant 设置用户默认租户(同一用户仅一个默认)
|
||||
func SetDefaultTenant(uid, tid uint64) error {
|
||||
_, err := models.Orm.QueryTable(new(models.SystemTenantUser)).Filter("uid", uid).Update(map[string]interface{}{
|
||||
"is_default": 0,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("uid", uid).
|
||||
Filter("tid", tid).
|
||||
Update(map[string]interface{}{"is_default": 1})
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -175,10 +175,10 @@ func ConfirmVerifyCodeByCode(bindType string, bindID, bindTid uint64, code strin
|
||||
Filter("id", row.ID).
|
||||
Filter("status", models.WechatVerifyStatusScanned).
|
||||
Update(map[string]interface{}{
|
||||
"bind_type": bindType,
|
||||
"bind_id": bindID,
|
||||
"bind_tid": bindTid,
|
||||
"status": models.WechatVerifyStatusUsed,
|
||||
"bind_type": bindType,
|
||||
"bind_id": bindID,
|
||||
"bind_tid": bindTid,
|
||||
"status": models.WechatVerifyStatusUsed,
|
||||
"update_time": now,
|
||||
})
|
||||
if uerr != nil {
|
||||
|
||||
Reference in New Issue
Block a user