This commit is contained in:
2026-06-24 10:04:03 +08:00
parent b103192fac
commit 0f961789dc
538 changed files with 128210 additions and 128008 deletions
+73 -73
View File
@@ -1,73 +1,73 @@
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
}
+232 -232
View File
@@ -1,232 +1,232 @@
package services
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"math/rand"
"net/http"
"strings"
"sync"
"time"
"server/models"
)
type loginCodeItem struct {
Code string
Channel string
ExpiredAt time.Time
}
var loginCodeStore sync.Map
func codeKey(account, channel string) string {
return strings.ToLower(strings.TrimSpace(account)) + "|" + strings.TrimSpace(channel)
}
func SendPlatformLoginCode(account, channel string) error {
account = strings.TrimSpace(account)
channel = strings.TrimSpace(channel)
if account == "" {
return errors.New("账号不能为空")
}
if channel != "sms" && channel != "email" {
return errors.New("仅支持短信或邮箱验证码")
}
var u models.AdminUser
if err := models.Orm.QueryTable(new(models.AdminUser)).Filter("account", account).One(&u); err != nil {
return errors.New("用户不存在")
}
if u.Status == 0 {
return errors.New("账号已禁用")
}
if channel == "sms" && (u.Phone == nil || strings.TrimSpace(*u.Phone) == "") {
return errors.New("该账号未绑定手机号")
}
if channel == "email" && (u.Email == nil || strings.TrimSpace(*u.Email) == "") {
return errors.New("该账号未绑定邮箱")
}
rand.Seed(time.Now().UnixNano())
code := fmt.Sprintf("%06d", rand.Intn(1000000))
loginCodeStore.Store(codeKey(account, channel), loginCodeItem{
Code: code,
Channel: channel,
ExpiredAt: time.Now().Add(5 * time.Minute),
})
return nil
}
func VerifyPlatformLoginCode(account, channel, code string) error {
account = strings.TrimSpace(account)
channel = strings.TrimSpace(channel)
code = strings.TrimSpace(code)
if account == "" || code == "" {
return errors.New("验证码不能为空")
}
val, ok := loginCodeStore.Load(codeKey(account, channel))
if !ok {
return errors.New("验证码不存在或已失效")
}
item, ok := val.(loginCodeItem)
if !ok {
return errors.New("验证码状态异常")
}
if time.Now().After(item.ExpiredAt) {
loginCodeStore.Delete(codeKey(account, channel))
return errors.New("验证码已过期")
}
if item.Code != code {
return errors.New("验证码错误")
}
loginCodeStore.Delete(codeKey(account, channel))
return nil
}
func SendBackendLoginCode(tenantName, account, channel string) error {
tenantName = strings.TrimSpace(tenantName)
account = strings.TrimSpace(account)
channel = strings.TrimSpace(channel)
if tenantName == "" || account == "" {
return errors.New("租户名称和账号不能为空")
}
if channel != "sms" && channel != "email" {
return errors.New("仅支持短信或邮箱验证码")
}
var tenant models.SystemTenant
if err := models.Orm.QueryTable(new(models.SystemTenant)).Filter("tenant_name", tenantName).One(&tenant); err != nil {
return errors.New("租户不存在")
}
rand.Seed(time.Now().UnixNano())
code := fmt.Sprintf("%06d", rand.Intn(1000000))
switch channel {
case "sms":
phone := account
var user models.SystemTenantUser
if err := models.Orm.QueryTable(new(models.SystemTenantUser)).
Filter("tid", tenant.ID).
Filter("phone", phone).
One(&user); err != nil {
return errors.New("该手机号非当前企业绑定号码,请重试")
}
if user.Status == 0 {
return errors.New("账号已禁用")
}
if user.Phone == nil || strings.TrimSpace(*user.Phone) == "" {
return errors.New("该手机号非当前企业绑定号码,请重试")
}
content := "短信验证码:" + code
if err := enqueueSMSTaskForLogin(tenant.ID, phone, content, code); err != nil {
return errors.New("短信发送失败,请重试")
}
case "email":
email := account
var user models.SystemTenantUser
if err := models.Orm.QueryTable(new(models.SystemTenantUser)).
Filter("tid", tenant.ID).
Filter("email", email).
One(&user); err != nil {
return errors.New("该账号未绑定邮箱")
}
if user.Status == 0 {
return errors.New("账号已禁用")
}
if user.Email == nil || strings.TrimSpace(*user.Email) == "" {
return errors.New("该账号未绑定邮箱")
}
}
loginCodeStore.Store(codeKey(tenantName+"#"+account, channel), loginCodeItem{
Code: code,
Channel: channel,
ExpiredAt: time.Now().Add(5 * time.Minute),
})
return nil
}
func VerifyBackendLoginCode(tenantName, account, channel, code string) error {
return VerifyPlatformLoginCode(tenantName+"#"+account, channel, code)
}
func getDefaultSystemSMSConfig() (backendURL string, apiKey string, err error) {
backendURL = models.GetPlatformSettingValue("sms_custom_url", "")
apiKey = models.GetPlatformSettingValue("sms_custom_key", "")
if backendURL == "" || apiKey == "" {
return "", "", fmt.Errorf("短信网关未配置")
}
return backendURL, apiKey, nil
}
// enqueueSMSTaskForLogin 入队短信任务到网关,并写入 yz_system_sms_tasks
func enqueueSMSTaskForLogin(tid uint64, phone, content, code string) error {
backendURL, apiKey, err := getDefaultSystemSMSConfig()
if err != nil {
return err
}
if backendURL == "" || apiKey == "" {
return errors.New("短信网关未配置")
}
enqueueURL := strings.TrimRight(backendURL, "/") + "/api/v1/business/outbound-tasks"
payload := map[string]interface{}{
"phone": phone,
"content": content,
}
bs, _ := json.Marshal(payload)
client := &http.Client{Timeout: 10 * time.Second}
req, err := http.NewRequest("POST", enqueueURL, bytes.NewReader(bs))
if err != nil {
return err
}
req.Header.Set("X-Api-Key", apiKey)
req.Header.Set("Content-Type", "application/json; charset=utf-8")
req.Header.Set("Accept", "application/json")
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
bodyBytes, _ := io.ReadAll(resp.Body)
bodyStr := strings.TrimSpace(string(bodyBytes))
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("gateway http status: %d, body: %s", resp.StatusCode, bodyStr)
}
now := time.Now()
tidCopy := tid
contentPtr := content
var reportPtr *string
if bodyStr != "" {
reportPtr = &bodyStr
}
task := &models.SystemSMSTask{
Tid: &tidCopy,
ApiKey: apiKey,
Phone: phone,
Content: &contentPtr,
Status: 3,
Code: code,
ReportRaw: reportPtr,
CreateTime: &now,
UpdateTime: &now,
}
_, insertErr := models.Orm.Insert(task)
if insertErr != nil {
return nil
}
return nil
}
package services
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"math/rand"
"net/http"
"strings"
"sync"
"time"
"server/models"
)
type loginCodeItem struct {
Code string
Channel string
ExpiredAt time.Time
}
var loginCodeStore sync.Map
func codeKey(account, channel string) string {
return strings.ToLower(strings.TrimSpace(account)) + "|" + strings.TrimSpace(channel)
}
func SendPlatformLoginCode(account, channel string) error {
account = strings.TrimSpace(account)
channel = strings.TrimSpace(channel)
if account == "" {
return errors.New("账号不能为空")
}
if channel != "sms" && channel != "email" {
return errors.New("仅支持短信或邮箱验证码")
}
var u models.AdminUser
if err := models.Orm.QueryTable(new(models.AdminUser)).Filter("account", account).One(&u); err != nil {
return errors.New("用户不存在")
}
if u.Status == 0 {
return errors.New("账号已禁用")
}
if channel == "sms" && (u.Phone == nil || strings.TrimSpace(*u.Phone) == "") {
return errors.New("该账号未绑定手机号")
}
if channel == "email" && (u.Email == nil || strings.TrimSpace(*u.Email) == "") {
return errors.New("该账号未绑定邮箱")
}
rand.Seed(time.Now().UnixNano())
code := fmt.Sprintf("%06d", rand.Intn(1000000))
loginCodeStore.Store(codeKey(account, channel), loginCodeItem{
Code: code,
Channel: channel,
ExpiredAt: time.Now().Add(5 * time.Minute),
})
return nil
}
func VerifyPlatformLoginCode(account, channel, code string) error {
account = strings.TrimSpace(account)
channel = strings.TrimSpace(channel)
code = strings.TrimSpace(code)
if account == "" || code == "" {
return errors.New("验证码不能为空")
}
val, ok := loginCodeStore.Load(codeKey(account, channel))
if !ok {
return errors.New("验证码不存在或已失效")
}
item, ok := val.(loginCodeItem)
if !ok {
return errors.New("验证码状态异常")
}
if time.Now().After(item.ExpiredAt) {
loginCodeStore.Delete(codeKey(account, channel))
return errors.New("验证码已过期")
}
if item.Code != code {
return errors.New("验证码错误")
}
loginCodeStore.Delete(codeKey(account, channel))
return nil
}
func SendBackendLoginCode(tenantName, account, channel string) error {
tenantName = strings.TrimSpace(tenantName)
account = strings.TrimSpace(account)
channel = strings.TrimSpace(channel)
if tenantName == "" || account == "" {
return errors.New("租户名称和账号不能为空")
}
if channel != "sms" && channel != "email" {
return errors.New("仅支持短信或邮箱验证码")
}
var tenant models.SystemTenant
if err := models.Orm.QueryTable(new(models.SystemTenant)).Filter("tenant_name", tenantName).One(&tenant); err != nil {
return errors.New("租户不存在")
}
rand.Seed(time.Now().UnixNano())
code := fmt.Sprintf("%06d", rand.Intn(1000000))
switch channel {
case "sms":
phone := account
var user models.SystemTenantUser
if err := models.Orm.QueryTable(new(models.SystemTenantUser)).
Filter("tid", tenant.ID).
Filter("phone", phone).
One(&user); err != nil {
return errors.New("该手机号非当前企业绑定号码,请重试")
}
if user.Status == 0 {
return errors.New("账号已禁用")
}
if user.Phone == nil || strings.TrimSpace(*user.Phone) == "" {
return errors.New("该手机号非当前企业绑定号码,请重试")
}
content := "短信验证码:" + code
if err := enqueueSMSTaskForLogin(tenant.ID, phone, content, code); err != nil {
return errors.New("短信发送失败,请重试")
}
case "email":
email := account
var user models.SystemTenantUser
if err := models.Orm.QueryTable(new(models.SystemTenantUser)).
Filter("tid", tenant.ID).
Filter("email", email).
One(&user); err != nil {
return errors.New("该账号未绑定邮箱")
}
if user.Status == 0 {
return errors.New("账号已禁用")
}
if user.Email == nil || strings.TrimSpace(*user.Email) == "" {
return errors.New("该账号未绑定邮箱")
}
}
loginCodeStore.Store(codeKey(tenantName+"#"+account, channel), loginCodeItem{
Code: code,
Channel: channel,
ExpiredAt: time.Now().Add(5 * time.Minute),
})
return nil
}
func VerifyBackendLoginCode(tenantName, account, channel, code string) error {
return VerifyPlatformLoginCode(tenantName+"#"+account, channel, code)
}
func getDefaultSystemSMSConfig() (backendURL string, apiKey string, err error) {
backendURL = models.GetPlatformSettingValue("sms_custom_url", "")
apiKey = models.GetPlatformSettingValue("sms_custom_key", "")
if backendURL == "" || apiKey == "" {
return "", "", fmt.Errorf("短信网关未配置")
}
return backendURL, apiKey, nil
}
// enqueueSMSTaskForLogin 入队短信任务到网关,并写入 yz_system_sms_tasks
func enqueueSMSTaskForLogin(tid uint64, phone, content, code string) error {
backendURL, apiKey, err := getDefaultSystemSMSConfig()
if err != nil {
return err
}
if backendURL == "" || apiKey == "" {
return errors.New("短信网关未配置")
}
enqueueURL := strings.TrimRight(backendURL, "/") + "/api/v1/business/outbound-tasks"
payload := map[string]interface{}{
"phone": phone,
"content": content,
}
bs, _ := json.Marshal(payload)
client := &http.Client{Timeout: 10 * time.Second}
req, err := http.NewRequest("POST", enqueueURL, bytes.NewReader(bs))
if err != nil {
return err
}
req.Header.Set("X-Api-Key", apiKey)
req.Header.Set("Content-Type", "application/json; charset=utf-8")
req.Header.Set("Accept", "application/json")
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
bodyBytes, _ := io.ReadAll(resp.Body)
bodyStr := strings.TrimSpace(string(bodyBytes))
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("gateway http status: %d, body: %s", resp.StatusCode, bodyStr)
}
now := time.Now()
tidCopy := tid
contentPtr := content
var reportPtr *string
if bodyStr != "" {
reportPtr = &bodyStr
}
task := &models.SystemSMSTask{
Tid: &tidCopy,
ApiKey: apiKey,
Phone: phone,
Content: &contentPtr,
Status: 3,
Code: code,
ReportRaw: reportPtr,
CreateTime: &now,
UpdateTime: &now,
}
_, insertErr := models.Orm.Insert(task)
if insertErr != nil {
return nil
}
return nil
}
+51 -51
View File
@@ -1,51 +1,51 @@
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
}
+158 -158
View File
@@ -1,158 +1,158 @@
package services
import (
"errors"
"strings"
"server/models"
"server/pkg/jwtutil"
"server/pkg/passwordutil"
)
type PlatformLoginUser struct {
ID uint64
Account string
Name string
Tid uint64
Rid uint64
Avatar string
RoleName string
}
func adminRoleNameByID(roleID uint64) string {
if roleID == 0 {
return ""
}
var role models.AdminRole
err := models.Orm.QueryTable(new(models.AdminRole)).Filter("id", roleID).One(&role)
if err != nil {
return ""
}
return role.Name
}
func toPlatformLoginUser(user *models.AdminUser) *PlatformLoginUser {
name := ""
if user.Name != nil {
name = *user.Name
}
avatar := ""
if user.Avatar != nil {
avatar = *user.Avatar
}
return &PlatformLoginUser{
ID: user.ID,
Account: user.Account,
Name: name,
Tid: 0,
Rid: user.RoleID,
Avatar: avatar,
RoleName: adminRoleNameByID(user.RoleID),
}
}
// PlatformAdminLogin 平台端登录:仅校验 yz_system_admin_user,不需要租户。
func PlatformAdminLogin(account, password string) (string, *PlatformLoginUser, error) {
account = strings.TrimSpace(account)
password = strings.TrimSpace(password)
if account == "" || password == "" {
return "", nil, errors.New("用户名或密码不能为空")
}
var user models.AdminUser
err := models.Orm.QueryTable(new(models.AdminUser)).
Filter("account", account).
One(&user)
if err != nil {
return "", nil, errors.New("用户名或密码错误")
}
if user.Status == 0 {
return "", nil, errors.New("账号已禁用")
}
if !passwordutil.Verify(user.Password, password) {
return "", nil, errors.New("用户名或密码错误")
}
const tenantID = 0
const userType = "platform"
token, err := jwtutil.GenerateToken(int(user.ID), user.Account, tenantID, userType)
if err != nil {
return "", nil, err
}
loginUser := toPlatformLoginUser(&user)
return token, loginUser, nil
}
// BackendLogin backend 登录:先校验租户,再校验租户下用户账号和密码。
func BackendLogin(tenantName, account, password string) (string, *PlatformLoginUser, error) {
tenantName = strings.TrimSpace(tenantName)
account = strings.TrimSpace(account)
password = strings.TrimSpace(password)
if tenantName == "" || account == "" || password == "" {
return "", nil, errors.New("租户名称、用户名或密码不能为空")
}
var tenant models.SystemTenant
err := models.Orm.QueryTable(new(models.SystemTenant)).
Filter("tenant_name", tenantName).
One(&tenant)
if err != nil {
return "", nil, errors.New("租户不存在")
}
if tenant.Status != 1 {
return "", nil, errors.New("租户已停用")
}
var tenantUser models.SystemTenantUser
err = models.Orm.QueryTable(new(models.SystemTenantUser)).
Filter("tid", tenant.ID).
Filter("account", account).
One(&tenantUser)
if err != nil {
return "", nil, errors.New("用户名或密码错误")
}
if tenantUser.Status == 0 {
return "", nil, errors.New("账号已禁用")
}
if tenantUser.Password == nil || !passwordutil.Verify(*tenantUser.Password, password) {
return "", nil, errors.New("用户名或密码错误")
}
tenantID := int(tenant.ID)
const userType = "backend"
token, err := jwtutil.GenerateToken(int(tenantUser.Uid), account, tenantID, userType)
if err != nil {
return "", nil, err
}
loginUser := &PlatformLoginUser{
ID: tenantUser.Uid,
Account: account,
Name: "",
Tid: tenant.ID,
Rid: 0,
Avatar: "",
RoleName: "",
}
if tenantUser.Account != nil && strings.TrimSpace(*tenantUser.Account) != "" {
loginUser.Account = strings.TrimSpace(*tenantUser.Account)
}
if tenantUser.Name != nil {
loginUser.Name = strings.TrimSpace(*tenantUser.Name)
}
return token, loginUser, nil
}
// PlatformGetCurrentUser 根据平台管理员用户 ID 返回登录用户信息(含角色名称)。
func PlatformGetCurrentUser(uid uint64) (*PlatformLoginUser, error) {
u, err := GetAdminUserByID(uid)
if err != nil {
return nil, errors.New("用户不存在")
}
if u.Status == 0 {
return nil, errors.New("账号已禁用")
}
return toPlatformLoginUser(u), nil
}
package services
import (
"errors"
"strings"
"server/models"
"server/pkg/jwtutil"
"server/pkg/passwordutil"
)
type PlatformLoginUser struct {
ID uint64
Account string
Name string
Tid uint64
Rid uint64
Avatar string
RoleName string
}
func adminRoleNameByID(roleID uint64) string {
if roleID == 0 {
return ""
}
var role models.AdminRole
err := models.Orm.QueryTable(new(models.AdminRole)).Filter("id", roleID).One(&role)
if err != nil {
return ""
}
return role.Name
}
func toPlatformLoginUser(user *models.AdminUser) *PlatformLoginUser {
name := ""
if user.Name != nil {
name = *user.Name
}
avatar := ""
if user.Avatar != nil {
avatar = *user.Avatar
}
return &PlatformLoginUser{
ID: user.ID,
Account: user.Account,
Name: name,
Tid: 0,
Rid: user.RoleID,
Avatar: avatar,
RoleName: adminRoleNameByID(user.RoleID),
}
}
// PlatformAdminLogin 平台端登录:仅校验 yz_system_admin_user,不需要租户。
func PlatformAdminLogin(account, password string) (string, *PlatformLoginUser, error) {
account = strings.TrimSpace(account)
password = strings.TrimSpace(password)
if account == "" || password == "" {
return "", nil, errors.New("用户名或密码不能为空")
}
var user models.AdminUser
err := models.Orm.QueryTable(new(models.AdminUser)).
Filter("account", account).
One(&user)
if err != nil {
return "", nil, errors.New("用户名或密码错误")
}
if user.Status == 0 {
return "", nil, errors.New("账号已禁用")
}
if !passwordutil.Verify(user.Password, password) {
return "", nil, errors.New("用户名或密码错误")
}
const tenantID = 0
const userType = "platform"
token, err := jwtutil.GenerateToken(int(user.ID), user.Account, tenantID, userType)
if err != nil {
return "", nil, err
}
loginUser := toPlatformLoginUser(&user)
return token, loginUser, nil
}
// BackendLogin backend 登录:先校验租户,再校验租户下用户账号和密码。
func BackendLogin(tenantName, account, password string) (string, *PlatformLoginUser, error) {
tenantName = strings.TrimSpace(tenantName)
account = strings.TrimSpace(account)
password = strings.TrimSpace(password)
if tenantName == "" || account == "" || password == "" {
return "", nil, errors.New("租户名称、用户名或密码不能为空")
}
var tenant models.SystemTenant
err := models.Orm.QueryTable(new(models.SystemTenant)).
Filter("tenant_name", tenantName).
One(&tenant)
if err != nil {
return "", nil, errors.New("租户不存在")
}
if tenant.Status != 1 {
return "", nil, errors.New("租户已停用")
}
var tenantUser models.SystemTenantUser
err = models.Orm.QueryTable(new(models.SystemTenantUser)).
Filter("tid", tenant.ID).
Filter("account", account).
One(&tenantUser)
if err != nil {
return "", nil, errors.New("用户名或密码错误")
}
if tenantUser.Status == 0 {
return "", nil, errors.New("账号已禁用")
}
if tenantUser.Password == nil || !passwordutil.Verify(*tenantUser.Password, password) {
return "", nil, errors.New("用户名或密码错误")
}
tenantID := int(tenant.ID)
const userType = "backend"
token, err := jwtutil.GenerateToken(int(tenantUser.Uid), account, tenantID, userType)
if err != nil {
return "", nil, err
}
loginUser := &PlatformLoginUser{
ID: tenantUser.Uid,
Account: account,
Name: "",
Tid: tenant.ID,
Rid: 0,
Avatar: "",
RoleName: "",
}
if tenantUser.Account != nil && strings.TrimSpace(*tenantUser.Account) != "" {
loginUser.Account = strings.TrimSpace(*tenantUser.Account)
}
if tenantUser.Name != nil {
loginUser.Name = strings.TrimSpace(*tenantUser.Name)
}
return token, loginUser, nil
}
// PlatformGetCurrentUser 根据平台管理员用户 ID 返回登录用户信息(含角色名称)。
func PlatformGetCurrentUser(uid uint64) (*PlatformLoginUser, error) {
u, err := GetAdminUserByID(uid)
if err != nil {
return nil, errors.New("用户不存在")
}
if u.Status == 0 {
return nil, errors.New("账号已禁用")
}
return toPlatformLoginUser(u), nil
}
+371 -371
View File
@@ -1,371 +1,371 @@
package services
import (
"bytes"
"context"
"crypto/rand"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"server/models"
)
// ReminderSender 提醒发送接口
type ReminderSender interface {
Send(ctx context.Context, reminder *models.PlatformScheduleReminder, title, content string) (success bool, err error)
}
// SMSSender 短信发送实现
type SMSSender struct{}
func (s *SMSSender) Send(ctx context.Context, reminder *models.PlatformScheduleReminder, title, content string) (bool, error) {
backendURL, apiKey, err := getDefaultSystemSMSConfig()
if err != nil {
return false, err
}
phone := ""
if reminder.ReceiverTarget != nil && *reminder.ReceiverTarget != "" {
phone = *reminder.ReceiverTarget
} else {
var user models.AdminUser
if err := models.Orm.QueryTable(new(models.AdminUser)).Filter("id", reminder.ReceiverUserID).One(&user); err == nil && user.Phone != nil {
phone = *user.Phone
}
}
if phone == "" {
return false, fmt.Errorf("未配置手机号")
}
enqueueURL := strings.TrimRight(backendURL, "/") + "/api/v1/business/outbound-tasks"
payload := map[string]interface{}{
"phone": phone,
"content": title + ": " + content,
}
bs, _ := json.Marshal(payload)
client := &http.Client{Timeout: 10 * time.Second}
req, err := http.NewRequestWithContext(ctx, "POST", enqueueURL, bytes.NewReader(bs))
if err != nil {
return false, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Api-Key", apiKey)
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
bodyBytes, _ := io.ReadAll(resp.Body)
return false, fmt.Errorf("网关返回HTTP状态码: %d, 返回内容: %s", resp.StatusCode, string(bodyBytes))
}
return true, nil
}
// EmailSender 邮件发送实现
type EmailSender struct{}
func (s *EmailSender) Send(ctx context.Context, reminder *models.PlatformScheduleReminder, title, content string) (bool, error) {
emails, err := ListSystemEmails()
if err != nil || len(emails) == 0 {
return false, fmt.Errorf("未配置系统邮箱")
}
emailCfg := emails[0]
if emailCfg.FromAddress == "" || emailCfg.Host == "" {
return false, fmt.Errorf("未配置系统邮箱")
}
toEmail := ""
if reminder.ReceiverTarget != nil && *reminder.ReceiverTarget != "" {
toEmail = *reminder.ReceiverTarget
} else {
var user models.AdminUser
if err := models.Orm.QueryTable(new(models.AdminUser)).Filter("id", reminder.ReceiverUserID).One(&user); err == nil && user.Email != nil {
toEmail = *user.Email
}
}
if toEmail == "" {
return false, fmt.Errorf("未配置收件邮箱")
}
sysDomain := models.GetPlatformSettingValue("system_domain", "https://api.yunzer.cn")
ackToken := ""
if reminder.AckToken != nil {
ackToken = *reminder.AckToken
}
// 构造 HTML 邮件
htmlBody := fmt.Sprintf(`
<div style="font-family: Arial, sans-serif; padding: 20px; border: 1px solid #eee; border-radius: 5px; max-width: 600px; margin: 0 auto;">
<h2 style="color: #409EFF; margin-bottom: 20px;">日程提醒:%s</h2>
<p style="font-size: 16px; line-height: 1.6; color: #333;">%s</p>
<hr style="border: 0; border-top: 1px solid #eee; margin: 20px 0;" />
`, title, content)
if ackToken != "" {
ackURL := fmt.Sprintf("%s/api/schedule/reminder/ack?token=%s", strings.TrimRight(sysDomain, "/"), ackToken)
htmlBody += fmt.Sprintf(`
<div style="text-align: center; margin-top: 30px;">
<a href="%s" target="_blank" style="background-color: #409EFF; color: #fff; padding: 12px 24px; text-decoration: none; border-radius: 4px; font-weight: bold; display: inline-block;">
收到,确认此提醒
</a>
</div>
<p style="font-size: 12px; color: #999; text-align: center; margin-top: 15px;">确认收到后,系统将不再向您发送该日程的重复提醒。</p>
`, ackURL)
}
htmlBody += "</div>"
cfg := SMTPConfig{
FromAddress: emailCfg.FromAddress,
Host: emailCfg.Host,
Port: emailCfg.Port,
Password: emailCfg.Password,
Encryption: emailCfg.Encryption,
Timeout: emailCfg.Timeout,
}
if emailCfg.FromName != nil {
cfg.FromName = *emailCfg.FromName
}
err = SendHTMLEmailSMTP(cfg, toEmail, title, htmlBody)
if err != nil {
return false, err
}
return true, nil
}
// BarkSender Bark 推送实现
type BarkSender struct{}
func (s *BarkSender) Send(ctx context.Context, reminder *models.PlatformScheduleReminder, title, content string) (bool, error) {
deviceKey := ""
if reminder.ReceiverTarget != nil && *reminder.ReceiverTarget != "" {
deviceKey = *reminder.ReceiverTarget
} else {
deviceKey = models.GetPlatformSettingValue("bark_device_key", "")
}
if deviceKey == "" {
return false, fmt.Errorf("Bark 设备 Key 未配置")
}
serverURL := models.GetPlatformSettingValue("bark_server_url", "https://api.day.app")
sysDomain := models.GetPlatformSettingValue("system_domain", "https://api.yunzer.cn")
ackToken := ""
if reminder.AckToken != nil {
ackToken = *reminder.AckToken
}
baseURL := strings.TrimRight(serverURL, "/")
escapedTitle := url.PathEscape(title)
pushContent := content
if ackToken != "" {
pushContent += "\n确认收到请点击→"
}
escapedContent := url.PathEscape(pushContent)
barkURL := fmt.Sprintf("%s/%s/%s/%s", baseURL, deviceKey, escapedTitle, escapedContent)
if ackToken != "" {
ackURL := fmt.Sprintf("%s/api/schedule/reminder/ack?token=%s", strings.TrimRight(sysDomain, "/"), ackToken)
// Bark 官方推送支持 url 参数
barkURL += "?url=" + url.QueryEscape(ackURL)
}
client := &http.Client{Timeout: 10 * time.Second}
req, err := http.NewRequestWithContext(ctx, "GET", barkURL, nil)
if err != nil {
return false, err
}
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
bodyBytes, _ := io.ReadAll(resp.Body)
return false, fmt.Errorf("Bark返回HTTP状态码: %d, 返回内容: %s", resp.StatusCode, string(bodyBytes))
}
return true, nil
}
// SiteMsgSender 站内信发送实现
type SiteMsgSender struct{}
func (s *SiteMsgSender) Send(ctx context.Context, reminder *models.PlatformScheduleReminder, title, content string) (bool, error) {
now := time.Now()
msg := &models.SystemReminderList{
Title: title,
Content: content,
SenderID: 0,
SenderType: "system",
ReceiverID: reminder.ReceiverUserID,
ReceiverType: "platform", // 平台端用户
IsRead: 0,
CreateTime: &now,
}
_, err := models.Orm.Insert(msg)
if err != nil {
return false, err
}
return true, nil
}
// generateUUID 生成一个安全的随机 UUID 字符
func generateUUID() string {
b := make([]byte, 16)
_, _ = rand.Read(b)
b[6] = (b[6] & 0x0f) | 0x40
b[8] = (b[8] & 0x3f) | 0x80
return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
}
// StartReminderScheduler 启动定时提醒调度器 (1分钟一次的 Ticker)
func StartReminderScheduler(stopChan chan struct{}) {
ticker := time.NewTicker(1 * time.Minute)
go func() {
for {
select {
case <-ticker.C:
scanAndSendReminders()
case <-stopChan:
ticker.Stop()
return
}
}
}()
}
func scanAndSendReminders() {
// 1. 生成唯一扫描批次号用于抢占锁定
scanBatch := generateUUID()
now := time.Now()
// 2. 抢占待处理的数据(乐观锁防并发重复发送)
_, err := models.Orm.Raw(`
UPDATE yz_platform_schedule_reminder
SET scan_lock = ?, update_time = NOW()
WHERE next_remind_time <= ?
AND remind_status IN (0, 1)
AND is_deleted = 0
AND (scan_lock = '' OR scan_lock IS NULL)
`, scanBatch, now).Exec()
if err != nil {
return
}
// 3. 查询自己锁定成功的数据
var list []models.PlatformScheduleReminder
_, err = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
Filter("scan_lock", scanBatch).
Filter("remind_status__in", 0, 1).
Filter("is_deleted", 0).
All(&list)
if err != nil || len(list) == 0 {
return
}
// 实例分发发送
senders := map[string]ReminderSender{
"SMS": &SMSSender{},
"EMAIL": &EmailSender{},
"BARK": &BarkSender{},
"SITE_MSG": &SiteMsgSender{},
}
for i := range list {
reminder := &list[i]
// 3.1 获取日程信息(主要拿 Content,Title 统一为 "日程提醒"
var schedule models.PlatformSchedule
err := models.Orm.QueryTable(new(models.PlatformSchedule)).
Filter("id", reminder.ScheduleID).
One(&schedule)
title := "日程提醒"
content := "您有一个待处理的日程时间已到,请注意查收。"
if err == nil {
content = schedule.Content
}
sender, ok := senders[reminder.RemindChannel]
if !ok {
// 未知渠道,直接强制置为结束
_, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
Filter("id", reminder.ID).
Update(map[string]interface{}{
"remind_status": 2,
"scan_lock": "",
"update_time": time.Now(),
})
continue
}
// 执行发送
ctx := context.Background()
success, sendErr := sender.Send(ctx, reminder, title, content)
// 3.2 记录发送流水日志
sendResult := int8(0)
var failReason *string
if success {
sendResult = 1
} else if sendErr != nil {
errStr := sendErr.Error()
if len(errStr) > 255 {
errStr = errStr[:255]
}
failReason = &errStr
}
logRow := &models.PlatformScheduleReminderSendLog{
ReminderID: reminder.ID,
SendTime: time.Now(),
SendResult: sendResult,
FailReason: failReason,
}
_, _ = models.Orm.Insert(logRow)
// 3.3 根据发送渠道分类更新提醒状态和下一次发送时间
newSendCount := reminder.SendCount + 1
newStatus := reminder.RemindStatus
if reminder.RemindChannel == "SMS" || reminder.RemindChannel == "SITE_MSG" {
// 一次性发送:发送后直接置为结束
newStatus = 2
} else {
// 重复发送渠道 EMAIL / BARK
// 如果还没被 Ack,且没有达到 max_send_count,继续提醒
if reminder.AckStatus == 0 && newSendCount < reminder.MaxSendCount {
newStatus = 1 // 提醒中
// 更新下次发送时间
reminder.NextRemindTime = time.Now().Add(time.Duration(reminder.RepeatIntervalMinutes) * time.Minute)
} else {
// 达到最大上限或者已 Ack
newStatus = 2
}
}
// 3.4 回写主表记录
_, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
Filter("id", reminder.ID).
Update(map[string]interface{}{
"SendCount": newSendCount,
"NextRemindTime": reminder.NextRemindTime,
"RemindStatus": newStatus,
"ScanLock": "", // 释放扫描锁
"UpdateTime": time.Now(),
})
}
}
package services
import (
"bytes"
"context"
"crypto/rand"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"server/models"
)
// ReminderSender 提醒发送接口
type ReminderSender interface {
Send(ctx context.Context, reminder *models.PlatformScheduleReminder, title, content string) (success bool, err error)
}
// SMSSender 短信发送实现
type SMSSender struct{}
func (s *SMSSender) Send(ctx context.Context, reminder *models.PlatformScheduleReminder, title, content string) (bool, error) {
backendURL, apiKey, err := getDefaultSystemSMSConfig()
if err != nil {
return false, err
}
phone := ""
if reminder.ReceiverTarget != nil && *reminder.ReceiverTarget != "" {
phone = *reminder.ReceiverTarget
} else {
var user models.AdminUser
if err := models.Orm.QueryTable(new(models.AdminUser)).Filter("id", reminder.ReceiverUserID).One(&user); err == nil && user.Phone != nil {
phone = *user.Phone
}
}
if phone == "" {
return false, fmt.Errorf("未配置手机号")
}
enqueueURL := strings.TrimRight(backendURL, "/") + "/api/v1/business/outbound-tasks"
payload := map[string]interface{}{
"phone": phone,
"content": title + ": " + content,
}
bs, _ := json.Marshal(payload)
client := &http.Client{Timeout: 10 * time.Second}
req, err := http.NewRequestWithContext(ctx, "POST", enqueueURL, bytes.NewReader(bs))
if err != nil {
return false, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Api-Key", apiKey)
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
bodyBytes, _ := io.ReadAll(resp.Body)
return false, fmt.Errorf("网关返回HTTP状态码: %d, 返回内容: %s", resp.StatusCode, string(bodyBytes))
}
return true, nil
}
// EmailSender 邮件发送实现
type EmailSender struct{}
func (s *EmailSender) Send(ctx context.Context, reminder *models.PlatformScheduleReminder, title, content string) (bool, error) {
emails, err := ListSystemEmails()
if err != nil || len(emails) == 0 {
return false, fmt.Errorf("未配置系统邮箱")
}
emailCfg := emails[0]
if emailCfg.FromAddress == "" || emailCfg.Host == "" {
return false, fmt.Errorf("未配置系统邮箱")
}
toEmail := ""
if reminder.ReceiverTarget != nil && *reminder.ReceiverTarget != "" {
toEmail = *reminder.ReceiverTarget
} else {
var user models.AdminUser
if err := models.Orm.QueryTable(new(models.AdminUser)).Filter("id", reminder.ReceiverUserID).One(&user); err == nil && user.Email != nil {
toEmail = *user.Email
}
}
if toEmail == "" {
return false, fmt.Errorf("未配置收件邮箱")
}
sysDomain := models.GetPlatformSettingValue("system_domain", "https://api.yunzer.cn")
ackToken := ""
if reminder.AckToken != nil {
ackToken = *reminder.AckToken
}
// 构造 HTML 邮件
htmlBody := fmt.Sprintf(`
<div style="font-family: Arial, sans-serif; padding: 20px; border: 1px solid #eee; border-radius: 5px; max-width: 600px; margin: 0 auto;">
<h2 style="color: #409EFF; margin-bottom: 20px;">日程提醒:%s</h2>
<p style="font-size: 16px; line-height: 1.6; color: #333;">%s</p>
<hr style="border: 0; border-top: 1px solid #eee; margin: 20px 0;" />
`, title, content)
if ackToken != "" {
ackURL := fmt.Sprintf("%s/api/schedule/reminder/ack?token=%s", strings.TrimRight(sysDomain, "/"), ackToken)
htmlBody += fmt.Sprintf(`
<div style="text-align: center; margin-top: 30px;">
<a href="%s" target="_blank" style="background-color: #409EFF; color: #fff; padding: 12px 24px; text-decoration: none; border-radius: 4px; font-weight: bold; display: inline-block;">
收到,确认此提醒
</a>
</div>
<p style="font-size: 12px; color: #999; text-align: center; margin-top: 15px;">确认收到后,系统将不再向您发送该日程的重复提醒。</p>
`, ackURL)
}
htmlBody += "</div>"
cfg := SMTPConfig{
FromAddress: emailCfg.FromAddress,
Host: emailCfg.Host,
Port: emailCfg.Port,
Password: emailCfg.Password,
Encryption: emailCfg.Encryption,
Timeout: emailCfg.Timeout,
}
if emailCfg.FromName != nil {
cfg.FromName = *emailCfg.FromName
}
err = SendHTMLEmailSMTP(cfg, toEmail, title, htmlBody)
if err != nil {
return false, err
}
return true, nil
}
// BarkSender Bark 推送实现
type BarkSender struct{}
func (s *BarkSender) Send(ctx context.Context, reminder *models.PlatformScheduleReminder, title, content string) (bool, error) {
deviceKey := ""
if reminder.ReceiverTarget != nil && *reminder.ReceiverTarget != "" {
deviceKey = *reminder.ReceiverTarget
} else {
deviceKey = models.GetPlatformSettingValue("bark_device_key", "")
}
if deviceKey == "" {
return false, fmt.Errorf("Bark 设备 Key 未配置")
}
serverURL := models.GetPlatformSettingValue("bark_server_url", "https://api.day.app")
sysDomain := models.GetPlatformSettingValue("system_domain", "https://api.yunzer.cn")
ackToken := ""
if reminder.AckToken != nil {
ackToken = *reminder.AckToken
}
baseURL := strings.TrimRight(serverURL, "/")
escapedTitle := url.PathEscape(title)
pushContent := content
if ackToken != "" {
pushContent += "\n确认收到请点击→"
}
escapedContent := url.PathEscape(pushContent)
barkURL := fmt.Sprintf("%s/%s/%s/%s", baseURL, deviceKey, escapedTitle, escapedContent)
if ackToken != "" {
ackURL := fmt.Sprintf("%s/api/schedule/reminder/ack?token=%s", strings.TrimRight(sysDomain, "/"), ackToken)
// Bark 官方推送支持 url 参数
barkURL += "?url=" + url.QueryEscape(ackURL)
}
client := &http.Client{Timeout: 10 * time.Second}
req, err := http.NewRequestWithContext(ctx, "GET", barkURL, nil)
if err != nil {
return false, err
}
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
bodyBytes, _ := io.ReadAll(resp.Body)
return false, fmt.Errorf("Bark返回HTTP状态码: %d, 返回内容: %s", resp.StatusCode, string(bodyBytes))
}
return true, nil
}
// SiteMsgSender 站内信发送实现
type SiteMsgSender struct{}
func (s *SiteMsgSender) Send(ctx context.Context, reminder *models.PlatformScheduleReminder, title, content string) (bool, error) {
now := time.Now()
msg := &models.SystemReminderList{
Title: title,
Content: content,
SenderID: 0,
SenderType: "system",
ReceiverID: reminder.ReceiverUserID,
ReceiverType: "platform", // 平台端用户
IsRead: 0,
CreateTime: &now,
}
_, err := models.Orm.Insert(msg)
if err != nil {
return false, err
}
return true, nil
}
// generateUUID 生成一个安全的随机 UUID 字符
func generateUUID() string {
b := make([]byte, 16)
_, _ = rand.Read(b)
b[6] = (b[6] & 0x0f) | 0x40
b[8] = (b[8] & 0x3f) | 0x80
return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
}
// StartReminderScheduler 启动定时提醒调度器 (1分钟一次的 Ticker)
func StartReminderScheduler(stopChan chan struct{}) {
ticker := time.NewTicker(1 * time.Minute)
go func() {
for {
select {
case <-ticker.C:
scanAndSendReminders()
case <-stopChan:
ticker.Stop()
return
}
}
}()
}
func scanAndSendReminders() {
// 1. 生成唯一扫描批次号用于抢占锁定
scanBatch := generateUUID()
now := time.Now()
// 2. 抢占待处理的数据(乐观锁防并发重复发送)
_, err := models.Orm.Raw(`
UPDATE yz_platform_schedule_reminder
SET scan_lock = ?, update_time = NOW()
WHERE next_remind_time <= ?
AND remind_status IN (0, 1)
AND is_deleted = 0
AND (scan_lock = '' OR scan_lock IS NULL)
`, scanBatch, now).Exec()
if err != nil {
return
}
// 3. 查询自己锁定成功的数据
var list []models.PlatformScheduleReminder
_, err = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
Filter("scan_lock", scanBatch).
Filter("remind_status__in", 0, 1).
Filter("is_deleted", 0).
All(&list)
if err != nil || len(list) == 0 {
return
}
// 实例分发发送
senders := map[string]ReminderSender{
"SMS": &SMSSender{},
"EMAIL": &EmailSender{},
"BARK": &BarkSender{},
"SITE_MSG": &SiteMsgSender{},
}
for i := range list {
reminder := &list[i]
// 3.1 获取日程信息(主要拿 Content,Title 统一为 "日程提醒"
var schedule models.PlatformSchedule
err := models.Orm.QueryTable(new(models.PlatformSchedule)).
Filter("id", reminder.ScheduleID).
One(&schedule)
title := "日程提醒"
content := "您有一个待处理的日程时间已到,请注意查收。"
if err == nil {
content = schedule.Content
}
sender, ok := senders[reminder.RemindChannel]
if !ok {
// 未知渠道,直接强制置为结束
_, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
Filter("id", reminder.ID).
Update(map[string]interface{}{
"remind_status": 2,
"scan_lock": "",
"update_time": time.Now(),
})
continue
}
// 执行发送
ctx := context.Background()
success, sendErr := sender.Send(ctx, reminder, title, content)
// 3.2 记录发送流水日志
sendResult := int8(0)
var failReason *string
if success {
sendResult = 1
} else if sendErr != nil {
errStr := sendErr.Error()
if len(errStr) > 255 {
errStr = errStr[:255]
}
failReason = &errStr
}
logRow := &models.PlatformScheduleReminderSendLog{
ReminderID: reminder.ID,
SendTime: time.Now(),
SendResult: sendResult,
FailReason: failReason,
}
_, _ = models.Orm.Insert(logRow)
// 3.3 根据发送渠道分类更新提醒状态和下一次发送时间
newSendCount := reminder.SendCount + 1
newStatus := reminder.RemindStatus
if reminder.RemindChannel == "SMS" || reminder.RemindChannel == "SITE_MSG" {
// 一次性发送:发送后直接置为结束
newStatus = 2
} else {
// 重复发送渠道 EMAIL / BARK
// 如果还没被 Ack,且没有达到 max_send_count,继续提醒
if reminder.AckStatus == 0 && newSendCount < reminder.MaxSendCount {
newStatus = 1 // 提醒中
// 更新下次发送时间
reminder.NextRemindTime = time.Now().Add(time.Duration(reminder.RepeatIntervalMinutes) * time.Minute)
} else {
// 达到最大上限或者已 Ack
newStatus = 2
}
}
// 3.4 回写主表记录
_, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
Filter("id", reminder.ID).
Update(map[string]interface{}{
"SendCount": newSendCount,
"NextRemindTime": reminder.NextRemindTime,
"RemindStatus": newStatus,
"ScanLock": "", // 释放扫描锁
"UpdateTime": time.Now(),
})
}
}
+45 -17
View File
@@ -1,6 +1,7 @@
package services
import (
"encoding/json"
"strings"
"server/models"
@@ -21,6 +22,49 @@ func PublicRequestBaseURL(c *beego.Controller) (scheme, host string) {
return scheme, host
}
// ResolveSoftwareDownloadURL 优先使用自定义 download_url;否则根据 file_id 读附件 src 拼完整 URL
func ResolveSoftwareDownloadURLs(scheme, host string, downloadURLs *string) map[string]string {
result := map[string]string{}
if downloadURLs == nil || strings.TrimSpace(*downloadURLs) == "" {
return result
}
raw := strings.TrimSpace(*downloadURLs)
var values map[string]string
if err := json.Unmarshal([]byte(raw), &values); err != nil {
return result
}
for k, v := range values {
platform := strings.ToLower(strings.TrimSpace(k))
url := strings.TrimSpace(v)
if platform == "" || url == "" {
continue
}
result[platform] = ResolvePublicURL(scheme, host, url)
}
return result
}
// ResolvePublicURL 把相对路径拼成公开 URL;http(s) 地址原样返回。
func ResolvePublicURL(scheme, host, src string) string {
src = strings.TrimSpace(src)
if src == "" {
return ""
}
if strings.HasPrefix(strings.ToLower(src), "http://") || strings.HasPrefix(strings.ToLower(src), "https://") {
return src
}
if host == "" {
return src
}
if !strings.HasPrefix(src, "/") {
src = "/" + src
}
if scheme == "" {
scheme = "http"
}
return scheme + "://" + host + src
}
// ResolveSoftwareDownloadURL 优先使用自定义 download_url;否则根据 file_id 读附件 src 拼完整 URL
func ResolveSoftwareDownloadURL(scheme, host string, downloadURL *string, fileID *uint64) string {
if downloadURL != nil {
@@ -40,21 +84,5 @@ func ResolveSoftwareDownloadURL(scheme, host string, downloadURL *string, fileID
if err != nil {
return ""
}
src := strings.TrimSpace(f.Src)
if src == "" {
return ""
}
if strings.HasPrefix(strings.ToLower(src), "http://") || strings.HasPrefix(strings.ToLower(src), "https://") {
return src
}
if host == "" {
return src
}
if !strings.HasPrefix(src, "/") {
src = "/" + src
}
if scheme == "" {
scheme = "http"
}
return scheme + "://" + host + src
return ResolvePublicURL(scheme, host, f.Src)
}
+191 -191
View File
@@ -1,191 +1,191 @@
package services
import (
"fmt"
"mime/multipart"
"os"
"path/filepath"
"strings"
"sync"
"server/models"
)
// MigrationProgress 迁移进度
type MigrationProgress struct {
Total int
Success int
Failed int
Current string
Errors []string
mu sync.Mutex
}
// AddSuccess 增加成功计数
func (p *MigrationProgress) AddSuccess() {
p.mu.Lock()
defer p.mu.Unlock()
p.Success++
}
// AddFailed 增加失败计数
func (p *MigrationProgress) AddFailed(err string) {
p.mu.Lock()
defer p.mu.Unlock()
p.Failed++
p.Errors = append(p.Errors, err)
}
// SetCurrent 设置当前处理的文件
func (p *MigrationProgress) SetCurrent(filename string) {
p.mu.Lock()
defer p.mu.Unlock()
p.Current = filename
}
// GetProgress 获取进度信息
func (p *MigrationProgress) GetProgress() (int, int, int, string) {
p.mu.Lock()
defer p.mu.Unlock()
return p.Total, p.Success, p.Failed, p.Current
}
// StorageMigration 存储迁移服务
type StorageMigration struct {
fromService StorageService
toService StorageService
progress *MigrationProgress
}
// NewStorageMigration 创建存储迁移服务
func NewStorageMigration(from, to StorageService) *StorageMigration {
return &StorageMigration{
fromService: from,
toService: to,
progress: &MigrationProgress{
Errors: make([]string, 0),
},
}
}
// MigrateFile 迁移单个文件
func (m *StorageMigration) MigrateFile(file *models.SystemFile) error {
m.progress.SetCurrent(file.Name)
// 如果是本地存储,从本地读取文件
if localFrom, ok := m.fromService.(*LocalStorage); ok {
// 从本地文件系统读取
localPath := strings.TrimPrefix(file.Src, "/")
filePath := filepath.Join(localFrom.BaseDir, localPath)
f, err := os.Open(filePath)
if err != nil {
return fmt.Errorf("打开本地文件失败: %w", err)
}
defer f.Close()
// 获取文件信息
stat, err := f.Stat()
if err != nil {
return fmt.Errorf("获取文件信息失败: %w", err)
}
// 创建 multipart.FileHeader
header := &multipart.FileHeader{
Filename: file.Name,
Size: stat.Size(),
}
// 上传到目标存储
result, err := m.toService.Upload(f, header)
if err != nil {
return fmt.Errorf("上传到目标存储失败: %w", err)
}
// 更新数据库记录
_, err = models.Orm.QueryTable(new(models.SystemFile)).
Filter("id", file.ID).
Update(map[string]interface{}{
"src": result.URL,
})
if err != nil {
// 上传成功但更新数据库失败,尝试删除已上传的文件
_ = m.toService.Delete(result.Key)
return fmt.Errorf("更新数据库失败: %w", err)
}
m.progress.AddSuccess()
return nil
}
// 如果是七牛云存储,需要先下载再上传(这里简化处理)
return fmt.Errorf("暂不支持从七牛云迁移到本地")
}
// MigrateAll 迁移所有文件
func (m *StorageMigration) MigrateAll(tid uint64) error {
// 获取所有文件
var files []models.SystemFile
_, err := models.Orm.QueryTable(new(models.SystemFile)).
Filter("tid", tid).
Filter("delete_time__isnull", true).
All(&files)
if err != nil {
return fmt.Errorf("获取文件列表失败: %w", err)
}
m.progress.Total = len(files)
// 并发迁移(限制并发数)
concurrency := 5
sem := make(chan struct{}, concurrency)
var wg sync.WaitGroup
for i := range files {
wg.Add(1)
go func(file *models.SystemFile) {
defer wg.Done()
sem <- struct{}{} // 获取信号量
defer func() { <-sem }() // 释放信号量
if err := m.MigrateFile(file); err != nil {
m.progress.AddFailed(fmt.Sprintf("%s: %v", file.Name, err))
}
}(&files[i])
}
wg.Wait()
return nil
}
// GetProgress 获取迁移进度
func (m *StorageMigration) GetProgress() *MigrationProgress {
return m.progress
}
// MigrateLocalToQiniu 从本地存储迁移到七牛云
func MigrateLocalToQiniu(tid uint64) (*MigrationProgress, error) {
// 获取存储配置
cfg, err := models.GetStorageConfig()
if err != nil {
return nil, fmt.Errorf("获取存储配置失败: %w", err)
}
if cfg.StorageType != "qiniu" {
return nil, fmt.Errorf("当前存储类型不是七牛云")
}
// 创建存储服务
localStorage := NewLocalStorage()
qiniuStorage := NewQiniuStorage(cfg)
// 创建迁移服务
migration := NewStorageMigration(localStorage, qiniuStorage)
// 执行迁移
if err := migration.MigrateAll(tid); err != nil {
return migration.GetProgress(), err
}
return migration.GetProgress(), nil
}
package services
import (
"fmt"
"mime/multipart"
"os"
"path/filepath"
"strings"
"sync"
"server/models"
)
// MigrationProgress 迁移进度
type MigrationProgress struct {
Total int
Success int
Failed int
Current string
Errors []string
mu sync.Mutex
}
// AddSuccess 增加成功计数
func (p *MigrationProgress) AddSuccess() {
p.mu.Lock()
defer p.mu.Unlock()
p.Success++
}
// AddFailed 增加失败计数
func (p *MigrationProgress) AddFailed(err string) {
p.mu.Lock()
defer p.mu.Unlock()
p.Failed++
p.Errors = append(p.Errors, err)
}
// SetCurrent 设置当前处理的文件
func (p *MigrationProgress) SetCurrent(filename string) {
p.mu.Lock()
defer p.mu.Unlock()
p.Current = filename
}
// GetProgress 获取进度信息
func (p *MigrationProgress) GetProgress() (int, int, int, string) {
p.mu.Lock()
defer p.mu.Unlock()
return p.Total, p.Success, p.Failed, p.Current
}
// StorageMigration 存储迁移服务
type StorageMigration struct {
fromService StorageService
toService StorageService
progress *MigrationProgress
}
// NewStorageMigration 创建存储迁移服务
func NewStorageMigration(from, to StorageService) *StorageMigration {
return &StorageMigration{
fromService: from,
toService: to,
progress: &MigrationProgress{
Errors: make([]string, 0),
},
}
}
// MigrateFile 迁移单个文件
func (m *StorageMigration) MigrateFile(file *models.SystemFile) error {
m.progress.SetCurrent(file.Name)
// 如果是本地存储,从本地读取文件
if localFrom, ok := m.fromService.(*LocalStorage); ok {
// 从本地文件系统读取
localPath := strings.TrimPrefix(file.Src, "/")
filePath := filepath.Join(localFrom.BaseDir, localPath)
f, err := os.Open(filePath)
if err != nil {
return fmt.Errorf("打开本地文件失败: %w", err)
}
defer f.Close()
// 获取文件信息
stat, err := f.Stat()
if err != nil {
return fmt.Errorf("获取文件信息失败: %w", err)
}
// 创建 multipart.FileHeader
header := &multipart.FileHeader{
Filename: file.Name,
Size: stat.Size(),
}
// 上传到目标存储
result, err := m.toService.Upload(f, header)
if err != nil {
return fmt.Errorf("上传到目标存储失败: %w", err)
}
// 更新数据库记录
_, err = models.Orm.QueryTable(new(models.SystemFile)).
Filter("id", file.ID).
Update(map[string]interface{}{
"src": result.URL,
})
if err != nil {
// 上传成功但更新数据库失败,尝试删除已上传的文件
_ = m.toService.Delete(result.Key)
return fmt.Errorf("更新数据库失败: %w", err)
}
m.progress.AddSuccess()
return nil
}
// 如果是七牛云存储,需要先下载再上传(这里简化处理)
return fmt.Errorf("暂不支持从七牛云迁移到本地")
}
// MigrateAll 迁移所有文件
func (m *StorageMigration) MigrateAll(tid uint64) error {
// 获取所有文件
var files []models.SystemFile
_, err := models.Orm.QueryTable(new(models.SystemFile)).
Filter("tid", tid).
Filter("delete_time__isnull", true).
All(&files)
if err != nil {
return fmt.Errorf("获取文件列表失败: %w", err)
}
m.progress.Total = len(files)
// 并发迁移(限制并发数)
concurrency := 5
sem := make(chan struct{}, concurrency)
var wg sync.WaitGroup
for i := range files {
wg.Add(1)
go func(file *models.SystemFile) {
defer wg.Done()
sem <- struct{}{} // 获取信号量
defer func() { <-sem }() // 释放信号量
if err := m.MigrateFile(file); err != nil {
m.progress.AddFailed(fmt.Sprintf("%s: %v", file.Name, err))
}
}(&files[i])
}
wg.Wait()
return nil
}
// GetProgress 获取迁移进度
func (m *StorageMigration) GetProgress() *MigrationProgress {
return m.progress
}
// MigrateLocalToQiniu 从本地存储迁移到七牛云
func MigrateLocalToQiniu(tid uint64) (*MigrationProgress, error) {
// 获取存储配置
cfg, err := models.GetStorageConfig()
if err != nil {
return nil, fmt.Errorf("获取存储配置失败: %w", err)
}
if cfg.StorageType != "qiniu" {
return nil, fmt.Errorf("当前存储类型不是七牛云")
}
// 创建存储服务
localStorage := NewLocalStorage()
qiniuStorage := NewQiniuStorage(cfg)
// 创建迁移服务
migration := NewStorageMigration(localStorage, qiniuStorage)
// 执行迁移
if err := migration.MigrateAll(tid); err != nil {
return migration.GetProgress(), err
}
return migration.GetProgress(), nil
}
+252 -252
View File
@@ -1,252 +1,252 @@
package services
import (
"context"
"crypto/md5"
"encoding/hex"
"fmt"
"io"
"mime/multipart"
"os"
"path/filepath"
"strings"
"time"
"server/models"
"github.com/qiniu/go-sdk/v7/auth/qbox"
"github.com/qiniu/go-sdk/v7/storage"
)
// StorageService 存储服务接口
type StorageService interface {
Upload(file multipart.File, header *multipart.FileHeader) (*UploadResult, error)
GetPublicURL(key string) string
Delete(key string) error
}
// UploadResult 上传结果
type UploadResult struct {
URL string // 完整访问URL
Key string // 存储key/路径
Size int64 // 文件大小
MD5 string // 文件MD5
MimeType string // 文件类型
}
// LocalStorage 本地存储实现
type LocalStorage struct {
BaseDir string // 基础目录,默认 "uploads"
BaseURL string // 基础URL,默认 "/"
}
// NewLocalStorage 创建本地存储服务
func NewLocalStorage() *LocalStorage {
return &LocalStorage{
BaseDir: "uploads",
BaseURL: "/",
}
}
// Upload 上传文件到本地
func (s *LocalStorage) Upload(file multipart.File, header *multipart.FileHeader) (*UploadResult, error) {
// 生成存储路径
ext := filepath.Ext(header.Filename)
datePath := time.Now().Format("2006/01/02")
fileName := fmt.Sprintf("%d%s", time.Now().UnixNano(), ext)
savePath := filepath.Join(datePath, fileName)
// 创建目录
destDir := filepath.Join(s.BaseDir, filepath.FromSlash(datePath))
if err := os.MkdirAll(destDir, 0755); err != nil {
return nil, fmt.Errorf("创建目录失败: %w", err)
}
// 保存文件
destPath := filepath.Join(s.BaseDir, filepath.FromSlash(savePath))
dst, err := os.Create(destPath)
if err != nil {
return nil, fmt.Errorf("创建文件失败: %w", err)
}
defer dst.Close()
// 计算MD5并复制文件
hash := md5.New()
size, err := io.Copy(io.MultiWriter(dst, hash), file)
if err != nil {
_ = os.Remove(destPath)
return nil, fmt.Errorf("保存文件失败: %w", err)
}
md5Sum := hex.EncodeToString(hash.Sum(nil))
webURL := s.BaseURL + strings.ReplaceAll(filepath.ToSlash(destPath), "\\", "/")
return &UploadResult{
URL: webURL,
Key: savePath,
Size: size,
MD5: md5Sum,
MimeType: header.Header.Get("Content-Type"),
}, nil
}
// GetPublicURL 获取公开访问URL
func (s *LocalStorage) GetPublicURL(key string) string {
return s.BaseURL + filepath.ToSlash(filepath.Join(s.BaseDir, key))
}
// Delete 删除本地文件
func (s *LocalStorage) Delete(key string) error {
filePath := filepath.Join(s.BaseDir, filepath.FromSlash(key))
return os.Remove(filePath)
}
// QiniuStorage 七牛云存储实现
type QiniuStorage struct {
AccessKey string
SecretKey string
Bucket string
Domain string
Region string
}
// NewQiniuStorage 创建七牛云存储服务
func NewQiniuStorage(cfg *models.StorageConfig) *QiniuStorage {
return &QiniuStorage{
AccessKey: cfg.QiniuAccessKey,
SecretKey: cfg.QiniuSecretKey,
Bucket: cfg.QiniuBucket,
Domain: cfg.QiniuDomain,
Region: cfg.QiniuRegion,
}
}
// getZone 根据区域代码获取存储区域
func (s *QiniuStorage) getZone() *storage.Region {
switch s.Region {
case "z0":
return &storage.ZoneHuadong
case "z1":
return &storage.ZoneHuabei
case "z2":
return &storage.ZoneHuanan
case "na0":
return &storage.ZoneBeimei
case "as0":
return &storage.ZoneXinjiapo
case "cn-east-2":
return &storage.ZoneHuadongZheJiang2
default:
return &storage.ZoneHuadong // 默认华东
}
}
// Upload 上传文件到七牛云
func (s *QiniuStorage) Upload(file multipart.File, header *multipart.FileHeader) (*UploadResult, error) {
// 生成存储key
ext := filepath.Ext(header.Filename)
datePath := time.Now().Format("2006/01/02")
fileName := fmt.Sprintf("%d%s", time.Now().UnixNano(), ext)
key := filepath.ToSlash(filepath.Join(datePath, fileName))
// 创建上传凭证
mac := qbox.NewMac(s.AccessKey, s.SecretKey)
putPolicy := storage.PutPolicy{
Scope: s.Bucket,
}
upToken := putPolicy.UploadToken(mac)
// 配置上传参数
cfg := storage.Config{
Region: s.getZone(),
UseHTTPS: true,
UseCdnDomains: false,
}
// 创建表单上传器
formUploader := storage.NewFormUploader(&cfg)
ret := storage.PutRet{}
putExtra := storage.PutExtra{}
// 计算文件大小和MD5
tmpFile, err := os.CreateTemp("", "qiniu_upload_*")
if err != nil {
return nil, fmt.Errorf("创建临时文件失败: %w", err)
}
defer os.Remove(tmpFile.Name())
defer tmpFile.Close()
hash := md5.New()
size, err := io.Copy(io.MultiWriter(tmpFile, hash), file)
if err != nil {
return nil, fmt.Errorf("读取文件失败: %w", err)
}
md5Sum := hex.EncodeToString(hash.Sum(nil))
// 重置文件指针
if _, err := tmpFile.Seek(0, 0); err != nil {
return nil, fmt.Errorf("重置文件指针失败: %w", err)
}
// 执行上传
err = formUploader.Put(context.Background(), &ret, upToken, key, tmpFile, size, &putExtra)
if err != nil {
return nil, fmt.Errorf("上传到七牛云失败: %w", err)
}
// 构建完整URL
domain := strings.TrimRight(s.Domain, "/")
url := fmt.Sprintf("%s/%s", domain, ret.Key)
return &UploadResult{
URL: url,
Key: ret.Key,
Size: size,
MD5: md5Sum,
MimeType: header.Header.Get("Content-Type"),
}, nil
}
// GetPublicURL 获取七牛云公开访问URL
func (s *QiniuStorage) GetPublicURL(key string) string {
domain := strings.TrimRight(s.Domain, "/")
return fmt.Sprintf("%s/%s", domain, key)
}
// Delete 删除七牛云文件
func (s *QiniuStorage) Delete(key string) error {
mac := qbox.NewMac(s.AccessKey, s.SecretKey)
cfg := storage.Config{
Region: s.getZone(),
UseHTTPS: true,
}
bucketManager := storage.NewBucketManager(mac, &cfg)
err := bucketManager.Delete(s.Bucket, key)
if err != nil {
return fmt.Errorf("删除七牛云文件失败: %w", err)
}
return nil
}
// GetStorageService 根据配置获取存储服务
func GetStorageService() (StorageService, error) {
cfg, err := models.GetStorageConfig()
if err != nil {
// 默认使用本地存储
return NewLocalStorage(), nil
}
switch cfg.StorageType {
case "qiniu":
if cfg.QiniuAccessKey == "" || cfg.QiniuSecretKey == "" ||
cfg.QiniuBucket == "" || cfg.QiniuDomain == "" {
return nil, fmt.Errorf("七牛云配置不完整")
}
return NewQiniuStorage(cfg), nil
case "local":
return NewLocalStorage(), nil
default:
return NewLocalStorage(), nil
}
}
package services
import (
"context"
"crypto/md5"
"encoding/hex"
"fmt"
"io"
"mime/multipart"
"os"
"path/filepath"
"strings"
"time"
"server/models"
"github.com/qiniu/go-sdk/v7/auth/qbox"
"github.com/qiniu/go-sdk/v7/storage"
)
// StorageService 存储服务接口
type StorageService interface {
Upload(file multipart.File, header *multipart.FileHeader) (*UploadResult, error)
GetPublicURL(key string) string
Delete(key string) error
}
// UploadResult 上传结果
type UploadResult struct {
URL string // 完整访问URL
Key string // 存储key/路径
Size int64 // 文件大小
MD5 string // 文件MD5
MimeType string // 文件类型
}
// LocalStorage 本地存储实现
type LocalStorage struct {
BaseDir string // 基础目录,默认 "uploads"
BaseURL string // 基础URL,默认 "/"
}
// NewLocalStorage 创建本地存储服务
func NewLocalStorage() *LocalStorage {
return &LocalStorage{
BaseDir: "uploads",
BaseURL: "/",
}
}
// Upload 上传文件到本地
func (s *LocalStorage) Upload(file multipart.File, header *multipart.FileHeader) (*UploadResult, error) {
// 生成存储路径
ext := filepath.Ext(header.Filename)
datePath := time.Now().Format("2006/01/02")
fileName := fmt.Sprintf("%d%s", time.Now().UnixNano(), ext)
savePath := filepath.Join(datePath, fileName)
// 创建目录
destDir := filepath.Join(s.BaseDir, filepath.FromSlash(datePath))
if err := os.MkdirAll(destDir, 0755); err != nil {
return nil, fmt.Errorf("创建目录失败: %w", err)
}
// 保存文件
destPath := filepath.Join(s.BaseDir, filepath.FromSlash(savePath))
dst, err := os.Create(destPath)
if err != nil {
return nil, fmt.Errorf("创建文件失败: %w", err)
}
defer dst.Close()
// 计算MD5并复制文件
hash := md5.New()
size, err := io.Copy(io.MultiWriter(dst, hash), file)
if err != nil {
_ = os.Remove(destPath)
return nil, fmt.Errorf("保存文件失败: %w", err)
}
md5Sum := hex.EncodeToString(hash.Sum(nil))
webURL := s.BaseURL + strings.ReplaceAll(filepath.ToSlash(destPath), "\\", "/")
return &UploadResult{
URL: webURL,
Key: savePath,
Size: size,
MD5: md5Sum,
MimeType: header.Header.Get("Content-Type"),
}, nil
}
// GetPublicURL 获取公开访问URL
func (s *LocalStorage) GetPublicURL(key string) string {
return s.BaseURL + filepath.ToSlash(filepath.Join(s.BaseDir, key))
}
// Delete 删除本地文件
func (s *LocalStorage) Delete(key string) error {
filePath := filepath.Join(s.BaseDir, filepath.FromSlash(key))
return os.Remove(filePath)
}
// QiniuStorage 七牛云存储实现
type QiniuStorage struct {
AccessKey string
SecretKey string
Bucket string
Domain string
Region string
}
// NewQiniuStorage 创建七牛云存储服务
func NewQiniuStorage(cfg *models.StorageConfig) *QiniuStorage {
return &QiniuStorage{
AccessKey: cfg.QiniuAccessKey,
SecretKey: cfg.QiniuSecretKey,
Bucket: cfg.QiniuBucket,
Domain: cfg.QiniuDomain,
Region: cfg.QiniuRegion,
}
}
// getZone 根据区域代码获取存储区域
func (s *QiniuStorage) getZone() *storage.Region {
switch s.Region {
case "z0":
return &storage.ZoneHuadong
case "z1":
return &storage.ZoneHuabei
case "z2":
return &storage.ZoneHuanan
case "na0":
return &storage.ZoneBeimei
case "as0":
return &storage.ZoneXinjiapo
case "cn-east-2":
return &storage.ZoneHuadongZheJiang2
default:
return &storage.ZoneHuadong // 默认华东
}
}
// Upload 上传文件到七牛云
func (s *QiniuStorage) Upload(file multipart.File, header *multipart.FileHeader) (*UploadResult, error) {
// 生成存储key
ext := filepath.Ext(header.Filename)
datePath := time.Now().Format("2006/01/02")
fileName := fmt.Sprintf("%d%s", time.Now().UnixNano(), ext)
key := filepath.ToSlash(filepath.Join(datePath, fileName))
// 创建上传凭证
mac := qbox.NewMac(s.AccessKey, s.SecretKey)
putPolicy := storage.PutPolicy{
Scope: s.Bucket,
}
upToken := putPolicy.UploadToken(mac)
// 配置上传参数
cfg := storage.Config{
Region: s.getZone(),
UseHTTPS: true,
UseCdnDomains: false,
}
// 创建表单上传器
formUploader := storage.NewFormUploader(&cfg)
ret := storage.PutRet{}
putExtra := storage.PutExtra{}
// 计算文件大小和MD5
tmpFile, err := os.CreateTemp("", "qiniu_upload_*")
if err != nil {
return nil, fmt.Errorf("创建临时文件失败: %w", err)
}
defer os.Remove(tmpFile.Name())
defer tmpFile.Close()
hash := md5.New()
size, err := io.Copy(io.MultiWriter(tmpFile, hash), file)
if err != nil {
return nil, fmt.Errorf("读取文件失败: %w", err)
}
md5Sum := hex.EncodeToString(hash.Sum(nil))
// 重置文件指针
if _, err := tmpFile.Seek(0, 0); err != nil {
return nil, fmt.Errorf("重置文件指针失败: %w", err)
}
// 执行上传
err = formUploader.Put(context.Background(), &ret, upToken, key, tmpFile, size, &putExtra)
if err != nil {
return nil, fmt.Errorf("上传到七牛云失败: %w", err)
}
// 构建完整URL
domain := strings.TrimRight(s.Domain, "/")
url := fmt.Sprintf("%s/%s", domain, ret.Key)
return &UploadResult{
URL: url,
Key: ret.Key,
Size: size,
MD5: md5Sum,
MimeType: header.Header.Get("Content-Type"),
}, nil
}
// GetPublicURL 获取七牛云公开访问URL
func (s *QiniuStorage) GetPublicURL(key string) string {
domain := strings.TrimRight(s.Domain, "/")
return fmt.Sprintf("%s/%s", domain, key)
}
// Delete 删除七牛云文件
func (s *QiniuStorage) Delete(key string) error {
mac := qbox.NewMac(s.AccessKey, s.SecretKey)
cfg := storage.Config{
Region: s.getZone(),
UseHTTPS: true,
}
bucketManager := storage.NewBucketManager(mac, &cfg)
err := bucketManager.Delete(s.Bucket, key)
if err != nil {
return fmt.Errorf("删除七牛云文件失败: %w", err)
}
return nil
}
// GetStorageService 根据配置获取存储服务
func GetStorageService() (StorageService, error) {
cfg, err := models.GetStorageConfig()
if err != nil {
// 默认使用本地存储
return NewLocalStorage(), nil
}
switch cfg.StorageType {
case "qiniu":
if cfg.QiniuAccessKey == "" || cfg.QiniuSecretKey == "" ||
cfg.QiniuBucket == "" || cfg.QiniuDomain == "" {
return nil, fmt.Errorf("七牛云配置不完整")
}
return NewQiniuStorage(cfg), nil
case "local":
return NewLocalStorage(), nil
default:
return NewLocalStorage(), nil
}
}
+220 -220
View File
@@ -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
View File
@@ -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
}
+439 -439
View File
@@ -1,439 +1,439 @@
package services
import (
"context"
"fmt"
"strconv"
"time"
"github.com/beego/beego/v2/client/orm"
"server/models"
)
// GetSiteReminderConfig 获取站内信配置(从 yz_platform_normal_setting 读取)
func GetSiteReminderConfig() (models.SystemSiteReminder, error) {
retentionDaysStr := models.GetPlatformSettingValue("sitemsg_retention_days", "30")
autoReadStr := models.GetPlatformSettingValue("sitemsg_auto_read", "0")
retentionDays, _ := strconv.Atoi(retentionDaysStr)
if retentionDays <= 0 {
retentionDays = 30
}
autoRead := int8(0)
if autoReadStr == "1" {
autoRead = 1
}
now := time.Now()
row := models.SystemSiteReminder{
ID: 1,
RetentionDays: retentionDays,
AutoRead: autoRead,
CreateTime: &now,
UpdateTime: &now,
}
return row, nil
}
// SaveSiteReminderConfig 保存/更新配置
func SaveSiteReminderConfig(retentionDays int, autoRead int8) error {
if retentionDays <= 0 {
retentionDays = 30
}
autoReadStr := "0"
if autoRead == 1 {
autoReadStr = "1"
}
settings := []struct {
code string
name string
value string
remark string
}{
{"sitemsg_retention_days", "站内信消息保留天数", strconv.Itoa(retentionDays), ""},
{"sitemsg_auto_read", "自动标记已读状态", autoReadStr, "0为关闭,1为开启"},
}
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
}
// SendSiteReminder 发送站内信
// targetType: platform (平台端), tenant_all (管理端所有用户), role (平台角色), tenant (特定租户)
func SendSiteReminder(title, content string, senderID uint64, senderType string, targetType string, targetRoleID uint64, targetTenantID uint64) error {
var receiverIDs []uint64
var receiverType string
switch targetType {
case "platform":
receiverType = "platform"
var list []models.AdminUser
_, err := models.Orm.QueryTable(new(models.AdminUser)).Filter("status", 1).Filter("delete_time__isnull", true).All(&list, "id")
if err != nil {
return fmt.Errorf("查询平台用户失败: %w", err)
}
for _, u := range list {
receiverIDs = append(receiverIDs, u.ID)
}
case "tenant_all":
receiverType = "tenant"
var list []models.SystemTenantUser
_, err := models.Orm.QueryTable(new(models.SystemTenantUser)).Filter("status", 1).Filter("delete_time__isnull", true).All(&list, "id")
if err != nil {
return fmt.Errorf("查询租户用户失败: %w", err)
}
for _, u := range list {
receiverIDs = append(receiverIDs, u.ID)
}
case "role":
receiverType = "platform"
var list []models.AdminUser
_, err := models.Orm.QueryTable(new(models.AdminUser)).Filter("status", 1).Filter("role_id", targetRoleID).Filter("delete_time__isnull", true).All(&list, "id")
if err != nil {
return fmt.Errorf("根据角色查询用户失败: %w", err)
}
for _, u := range list {
receiverIDs = append(receiverIDs, u.ID)
}
case "tenant":
receiverType = "tenant"
var list []models.SystemTenantUser
_, err := models.Orm.QueryTable(new(models.SystemTenantUser)).Filter("status", 1).Filter("tid", targetTenantID).Filter("delete_time__isnull", true).All(&list, "id")
if err != nil {
return fmt.Errorf("根据租户查询用户失败: %w", err)
}
for _, u := range list {
receiverIDs = append(receiverIDs, u.ID)
}
default:
return fmt.Errorf("未知的发送目标类型: %s", targetType)
}
if len(receiverIDs) == 0 {
return nil
}
now := time.Now()
batchID := fmt.Sprintf("%d_%d", now.UnixNano(), senderID)
var reminders []models.SystemReminderList
for _, rid := range receiverIDs {
reminders = append(reminders, models.SystemReminderList{
Title: title,
Content: content,
SenderID: senderID,
SenderType: senderType,
ReceiverID: rid,
ReceiverType: receiverType,
IsRead: 0,
CreateTime: &now,
BatchID: batchID,
TargetType: targetType,
TargetRoleID: targetRoleID,
TargetTenantID: targetTenantID,
})
}
// 批量插入
_, err := models.Orm.InsertMulti(100, reminders)
return err
}
// ListReminders 列表查询
func ListReminders(receiverID uint64, receiverType string, page, pageSize int, isRead *int8) ([]models.SystemReminderList, int64, error) {
if page <= 0 {
page = 1
}
if pageSize <= 0 {
pageSize = 10
}
var list []models.SystemReminderList
qs := models.Orm.QueryTable(new(models.SystemReminderList)).
Filter("receiver_id", receiverID).
Filter("receiver_type", receiverType).
Filter("delete_time__isnull", true)
if isRead != nil {
qs = qs.Filter("is_read", *isRead)
}
total, err := qs.Count()
if err != nil {
return nil, 0, err
}
offset := (page - 1) * pageSize
_, err = qs.OrderBy("-create_time", "-id").Limit(pageSize, offset).All(&list)
return list, total, err
}
// MarkReminderRead 标记单条已读
func MarkReminderRead(id uint64, receiverID uint64, receiverType string) error {
now := time.Now()
_, err := models.Orm.QueryTable(new(models.SystemReminderList)).
Filter("id", id).
Filter("receiver_id", receiverID).
Filter("receiver_type", receiverType).
Update(map[string]interface{}{
"is_read": 1,
"read_time": &now,
})
return err
}
// MarkAllRemindersRead 一键全部已读
func MarkAllRemindersRead(receiverID uint64, receiverType string) error {
now := time.Now()
_, err := models.Orm.QueryTable(new(models.SystemReminderList)).
Filter("receiver_id", receiverID).
Filter("receiver_type", receiverType).
Filter("is_read", 0).
Update(map[string]interface{}{
"is_read": 1,
"read_time": &now,
})
return err
}
// DeleteReminder 删除消息
func DeleteReminder(id uint64, receiverID uint64, receiverType string) error {
now := time.Now()
_, err := models.Orm.QueryTable(new(models.SystemReminderList)).
Filter("id", id).
Filter("receiver_id", receiverID).
Filter("receiver_type", receiverType).
Update(map[string]interface{}{
"delete_time": &now,
})
return err
}
// AutoCleanExpiredReminders 自动清理过期站内信
func AutoCleanExpiredReminders() error {
cfg, err := GetSiteReminderConfig()
if err != nil {
return err
}
if cfg.RetentionDays <= 0 {
return nil
}
expireTime := time.Now().AddDate(0, 0, -cfg.RetentionDays)
_, err = models.Orm.QueryTable(new(models.SystemReminderList)).
Filter("create_time__lt", expireTime).
Delete()
return err
}
// ListSentReminders 获取已发送的消息列表(按 batch_id 分组)
func ListSentReminders(senderID uint64, page, pageSize int) ([]models.SystemReminderList, int64, error) {
if page <= 0 {
page = 1
}
if pageSize <= 0 {
pageSize = 10
}
offset := (page - 1) * pageSize
var total int64
err := models.Orm.Raw("SELECT COUNT(DISTINCT batch_id) FROM yz_system_reminderlist WHERE sender_id = ? AND delete_time IS NULL", senderID).QueryRow(&total)
if err != nil {
return nil, 0, err
}
var list []models.SystemReminderList
_, err = models.Orm.Raw("SELECT * FROM yz_system_reminderlist WHERE id IN (SELECT MIN(id) FROM yz_system_reminderlist WHERE sender_id = ? AND delete_time IS NULL GROUP BY batch_id) ORDER BY id DESC LIMIT ? OFFSET ?", senderID, pageSize, offset).QueryRows(&list)
if err != nil {
return nil, 0, err
}
return list, total, nil
}
// UpdateSentReminder 更新已发出的消息(更新该批次下所有接收者的消息,支持修改目标接收群体)
func UpdateSentReminder(batchID string, title, content, targetType string, targetRoleID, targetTenantID uint64) error {
// 1. 获取当前发送者ID (从该批次中任意一条记录中获取)
var firstRecord models.SystemReminderList
err := models.Orm.QueryTable(new(models.SystemReminderList)).Filter("batch_id", batchID).Limit(1).One(&firstRecord)
if err != nil {
return fmt.Errorf("找不到该批次的站内信记录: %w", err)
}
senderID := firstRecord.SenderID
senderType := firstRecord.SenderType
// 2. 根据新的目标接收群体获取接收人列表
var receiverIDs []uint64
var receiverType string
switch targetType {
case "platform":
receiverType = "platform"
var list []models.AdminUser
_, err := models.Orm.QueryTable(new(models.AdminUser)).Filter("status", 1).Filter("delete_time__isnull", true).All(&list, "id")
if err != nil {
return fmt.Errorf("查询平台用户失败: %w", err)
}
for _, u := range list {
receiverIDs = append(receiverIDs, u.ID)
}
case "tenant_all":
receiverType = "tenant"
var list []models.SystemTenantUser
_, err := models.Orm.QueryTable(new(models.SystemTenantUser)).Filter("status", 1).Filter("delete_time__isnull", true).All(&list, "id")
if err != nil {
return fmt.Errorf("查询租户用户失败: %w", err)
}
for _, u := range list {
receiverIDs = append(receiverIDs, u.ID)
}
case "role":
receiverType = "platform"
var list []models.AdminUser
_, err := models.Orm.QueryTable(new(models.AdminUser)).Filter("status", 1).Filter("role_id", targetRoleID).Filter("delete_time__isnull", true).All(&list, "id")
if err != nil {
return fmt.Errorf("根据角色查询用户失败: %w", err)
}
for _, u := range list {
receiverIDs = append(receiverIDs, u.ID)
}
case "tenant":
receiverType = "tenant"
var list []models.SystemTenantUser
_, err := models.Orm.QueryTable(new(models.SystemTenantUser)).Filter("status", 1).Filter("tid", targetTenantID).Filter("delete_time__isnull", true).All(&list, "id")
if err != nil {
return fmt.Errorf("根据租户查询用户失败: %w", err)
}
for _, u := range list {
receiverIDs = append(receiverIDs, u.ID)
}
default:
return fmt.Errorf("未知的发送目标类型: %s", targetType)
}
// 3. 获取该批次中现有的所有记录 (包括已删除的)
var existingRecords []models.SystemReminderList
_, err = models.Orm.QueryTable(new(models.SystemReminderList)).Filter("batch_id", batchID).All(&existingRecords)
if err != nil {
return fmt.Errorf("获取现有记录失败: %w", err)
}
// 建立 map 快速查找,Key 为 "receiverType_receiverID"
existingMap := make(map[string]*models.SystemReminderList)
for i := range existingRecords {
key := fmt.Sprintf("%s_%d", existingRecords[i].ReceiverType, existingRecords[i].ReceiverID)
existingMap[key] = &existingRecords[i]
}
newReceiverMap := make(map[string]bool)
for _, rid := range receiverIDs {
key := fmt.Sprintf("%s_%d", receiverType, rid)
newReceiverMap[key] = true
}
now := time.Now()
// 事务处理
err = models.Orm.DoTx(func(c context.Context, txOrm orm.TxOrmer) error {
// A. 对于已经不在新接收者列表中的用户,软删除
for key, rec := range existingMap {
if !newReceiverMap[key] {
if rec.DeleteTime == nil {
rec.DeleteTime = &now
if _, e := txOrm.Update(rec, "DeleteTime"); e != nil {
return e
}
}
}
}
// B. 对于仍然在新接收者列表中的用户,更新标题、内容、以及 target 信息;如果原来被删除了,清除 delete_time
var newInserts []models.SystemReminderList
for _, rid := range receiverIDs {
key := fmt.Sprintf("%s_%d", receiverType, rid)
if rec, exists := existingMap[key]; exists {
rec.Title = title
rec.Content = content
rec.TargetType = targetType
rec.TargetRoleID = targetRoleID
rec.TargetTenantID = targetTenantID
cols := []string{"Title", "Content", "TargetType", "TargetRoleID", "TargetTenantID"}
if rec.DeleteTime != nil {
rec.DeleteTime = nil
rec.IsRead = 0
rec.ReadTime = nil
cols = append(cols, "DeleteTime", "IsRead", "ReadTime")
}
if _, e := txOrm.Update(rec, cols...); e != nil {
return e
}
} else {
// C. 对于新增加的接收者,插入新记录
newInserts = append(newInserts, models.SystemReminderList{
Title: title,
Content: content,
SenderID: senderID,
SenderType: senderType,
ReceiverID: rid,
ReceiverType: receiverType,
IsRead: 0,
CreateTime: &now,
BatchID: batchID,
TargetType: targetType,
TargetRoleID: targetRoleID,
TargetTenantID: targetTenantID,
})
}
}
if len(newInserts) > 0 {
if _, e := txOrm.InsertMulti(100, newInserts); e != nil {
return e
}
}
return nil
})
return err
}
// DeleteSentReminderBatch 删除已发送消息(删除该批次下所有记录)
func DeleteSentReminderBatch(batchID string) error {
now := time.Now()
_, err := models.Orm.QueryTable(new(models.SystemReminderList)).
Filter("batch_id", batchID).
Update(map[string]interface{}{
"delete_time": &now,
})
return err
}
package services
import (
"context"
"fmt"
"strconv"
"time"
"github.com/beego/beego/v2/client/orm"
"server/models"
)
// GetSiteReminderConfig 获取站内信配置(从 yz_platform_normal_setting 读取)
func GetSiteReminderConfig() (models.SystemSiteReminder, error) {
retentionDaysStr := models.GetPlatformSettingValue("sitemsg_retention_days", "30")
autoReadStr := models.GetPlatformSettingValue("sitemsg_auto_read", "0")
retentionDays, _ := strconv.Atoi(retentionDaysStr)
if retentionDays <= 0 {
retentionDays = 30
}
autoRead := int8(0)
if autoReadStr == "1" {
autoRead = 1
}
now := time.Now()
row := models.SystemSiteReminder{
ID: 1,
RetentionDays: retentionDays,
AutoRead: autoRead,
CreateTime: &now,
UpdateTime: &now,
}
return row, nil
}
// SaveSiteReminderConfig 保存/更新配置
func SaveSiteReminderConfig(retentionDays int, autoRead int8) error {
if retentionDays <= 0 {
retentionDays = 30
}
autoReadStr := "0"
if autoRead == 1 {
autoReadStr = "1"
}
settings := []struct {
code string
name string
value string
remark string
}{
{"sitemsg_retention_days", "站内信消息保留天数", strconv.Itoa(retentionDays), ""},
{"sitemsg_auto_read", "自动标记已读状态", autoReadStr, "0为关闭,1为开启"},
}
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
}
// SendSiteReminder 发送站内信
// targetType: platform (平台端), tenant_all (管理端所有用户), role (平台角色), tenant (特定租户)
func SendSiteReminder(title, content string, senderID uint64, senderType string, targetType string, targetRoleID uint64, targetTenantID uint64) error {
var receiverIDs []uint64
var receiverType string
switch targetType {
case "platform":
receiverType = "platform"
var list []models.AdminUser
_, err := models.Orm.QueryTable(new(models.AdminUser)).Filter("status", 1).Filter("delete_time__isnull", true).All(&list, "id")
if err != nil {
return fmt.Errorf("查询平台用户失败: %w", err)
}
for _, u := range list {
receiverIDs = append(receiverIDs, u.ID)
}
case "tenant_all":
receiverType = "tenant"
var list []models.SystemTenantUser
_, err := models.Orm.QueryTable(new(models.SystemTenantUser)).Filter("status", 1).Filter("delete_time__isnull", true).All(&list, "id")
if err != nil {
return fmt.Errorf("查询租户用户失败: %w", err)
}
for _, u := range list {
receiverIDs = append(receiverIDs, u.ID)
}
case "role":
receiverType = "platform"
var list []models.AdminUser
_, err := models.Orm.QueryTable(new(models.AdminUser)).Filter("status", 1).Filter("role_id", targetRoleID).Filter("delete_time__isnull", true).All(&list, "id")
if err != nil {
return fmt.Errorf("根据角色查询用户失败: %w", err)
}
for _, u := range list {
receiverIDs = append(receiverIDs, u.ID)
}
case "tenant":
receiverType = "tenant"
var list []models.SystemTenantUser
_, err := models.Orm.QueryTable(new(models.SystemTenantUser)).Filter("status", 1).Filter("tid", targetTenantID).Filter("delete_time__isnull", true).All(&list, "id")
if err != nil {
return fmt.Errorf("根据租户查询用户失败: %w", err)
}
for _, u := range list {
receiverIDs = append(receiverIDs, u.ID)
}
default:
return fmt.Errorf("未知的发送目标类型: %s", targetType)
}
if len(receiverIDs) == 0 {
return nil
}
now := time.Now()
batchID := fmt.Sprintf("%d_%d", now.UnixNano(), senderID)
var reminders []models.SystemReminderList
for _, rid := range receiverIDs {
reminders = append(reminders, models.SystemReminderList{
Title: title,
Content: content,
SenderID: senderID,
SenderType: senderType,
ReceiverID: rid,
ReceiverType: receiverType,
IsRead: 0,
CreateTime: &now,
BatchID: batchID,
TargetType: targetType,
TargetRoleID: targetRoleID,
TargetTenantID: targetTenantID,
})
}
// 批量插入
_, err := models.Orm.InsertMulti(100, reminders)
return err
}
// ListReminders 列表查询
func ListReminders(receiverID uint64, receiverType string, page, pageSize int, isRead *int8) ([]models.SystemReminderList, int64, error) {
if page <= 0 {
page = 1
}
if pageSize <= 0 {
pageSize = 10
}
var list []models.SystemReminderList
qs := models.Orm.QueryTable(new(models.SystemReminderList)).
Filter("receiver_id", receiverID).
Filter("receiver_type", receiverType).
Filter("delete_time__isnull", true)
if isRead != nil {
qs = qs.Filter("is_read", *isRead)
}
total, err := qs.Count()
if err != nil {
return nil, 0, err
}
offset := (page - 1) * pageSize
_, err = qs.OrderBy("-create_time", "-id").Limit(pageSize, offset).All(&list)
return list, total, err
}
// MarkReminderRead 标记单条已读
func MarkReminderRead(id uint64, receiverID uint64, receiverType string) error {
now := time.Now()
_, err := models.Orm.QueryTable(new(models.SystemReminderList)).
Filter("id", id).
Filter("receiver_id", receiverID).
Filter("receiver_type", receiverType).
Update(map[string]interface{}{
"is_read": 1,
"read_time": &now,
})
return err
}
// MarkAllRemindersRead 一键全部已读
func MarkAllRemindersRead(receiverID uint64, receiverType string) error {
now := time.Now()
_, err := models.Orm.QueryTable(new(models.SystemReminderList)).
Filter("receiver_id", receiverID).
Filter("receiver_type", receiverType).
Filter("is_read", 0).
Update(map[string]interface{}{
"is_read": 1,
"read_time": &now,
})
return err
}
// DeleteReminder 删除消息
func DeleteReminder(id uint64, receiverID uint64, receiverType string) error {
now := time.Now()
_, err := models.Orm.QueryTable(new(models.SystemReminderList)).
Filter("id", id).
Filter("receiver_id", receiverID).
Filter("receiver_type", receiverType).
Update(map[string]interface{}{
"delete_time": &now,
})
return err
}
// AutoCleanExpiredReminders 自动清理过期站内信
func AutoCleanExpiredReminders() error {
cfg, err := GetSiteReminderConfig()
if err != nil {
return err
}
if cfg.RetentionDays <= 0 {
return nil
}
expireTime := time.Now().AddDate(0, 0, -cfg.RetentionDays)
_, err = models.Orm.QueryTable(new(models.SystemReminderList)).
Filter("create_time__lt", expireTime).
Delete()
return err
}
// ListSentReminders 获取已发送的消息列表(按 batch_id 分组)
func ListSentReminders(senderID uint64, page, pageSize int) ([]models.SystemReminderList, int64, error) {
if page <= 0 {
page = 1
}
if pageSize <= 0 {
pageSize = 10
}
offset := (page - 1) * pageSize
var total int64
err := models.Orm.Raw("SELECT COUNT(DISTINCT batch_id) FROM yz_system_reminderlist WHERE sender_id = ? AND delete_time IS NULL", senderID).QueryRow(&total)
if err != nil {
return nil, 0, err
}
var list []models.SystemReminderList
_, err = models.Orm.Raw("SELECT * FROM yz_system_reminderlist WHERE id IN (SELECT MIN(id) FROM yz_system_reminderlist WHERE sender_id = ? AND delete_time IS NULL GROUP BY batch_id) ORDER BY id DESC LIMIT ? OFFSET ?", senderID, pageSize, offset).QueryRows(&list)
if err != nil {
return nil, 0, err
}
return list, total, nil
}
// UpdateSentReminder 更新已发出的消息(更新该批次下所有接收者的消息,支持修改目标接收群体)
func UpdateSentReminder(batchID string, title, content, targetType string, targetRoleID, targetTenantID uint64) error {
// 1. 获取当前发送者ID (从该批次中任意一条记录中获取)
var firstRecord models.SystemReminderList
err := models.Orm.QueryTable(new(models.SystemReminderList)).Filter("batch_id", batchID).Limit(1).One(&firstRecord)
if err != nil {
return fmt.Errorf("找不到该批次的站内信记录: %w", err)
}
senderID := firstRecord.SenderID
senderType := firstRecord.SenderType
// 2. 根据新的目标接收群体获取接收人列表
var receiverIDs []uint64
var receiverType string
switch targetType {
case "platform":
receiverType = "platform"
var list []models.AdminUser
_, err := models.Orm.QueryTable(new(models.AdminUser)).Filter("status", 1).Filter("delete_time__isnull", true).All(&list, "id")
if err != nil {
return fmt.Errorf("查询平台用户失败: %w", err)
}
for _, u := range list {
receiverIDs = append(receiverIDs, u.ID)
}
case "tenant_all":
receiverType = "tenant"
var list []models.SystemTenantUser
_, err := models.Orm.QueryTable(new(models.SystemTenantUser)).Filter("status", 1).Filter("delete_time__isnull", true).All(&list, "id")
if err != nil {
return fmt.Errorf("查询租户用户失败: %w", err)
}
for _, u := range list {
receiverIDs = append(receiverIDs, u.ID)
}
case "role":
receiverType = "platform"
var list []models.AdminUser
_, err := models.Orm.QueryTable(new(models.AdminUser)).Filter("status", 1).Filter("role_id", targetRoleID).Filter("delete_time__isnull", true).All(&list, "id")
if err != nil {
return fmt.Errorf("根据角色查询用户失败: %w", err)
}
for _, u := range list {
receiverIDs = append(receiverIDs, u.ID)
}
case "tenant":
receiverType = "tenant"
var list []models.SystemTenantUser
_, err := models.Orm.QueryTable(new(models.SystemTenantUser)).Filter("status", 1).Filter("tid", targetTenantID).Filter("delete_time__isnull", true).All(&list, "id")
if err != nil {
return fmt.Errorf("根据租户查询用户失败: %w", err)
}
for _, u := range list {
receiverIDs = append(receiverIDs, u.ID)
}
default:
return fmt.Errorf("未知的发送目标类型: %s", targetType)
}
// 3. 获取该批次中现有的所有记录 (包括已删除的)
var existingRecords []models.SystemReminderList
_, err = models.Orm.QueryTable(new(models.SystemReminderList)).Filter("batch_id", batchID).All(&existingRecords)
if err != nil {
return fmt.Errorf("获取现有记录失败: %w", err)
}
// 建立 map 快速查找,Key 为 "receiverType_receiverID"
existingMap := make(map[string]*models.SystemReminderList)
for i := range existingRecords {
key := fmt.Sprintf("%s_%d", existingRecords[i].ReceiverType, existingRecords[i].ReceiverID)
existingMap[key] = &existingRecords[i]
}
newReceiverMap := make(map[string]bool)
for _, rid := range receiverIDs {
key := fmt.Sprintf("%s_%d", receiverType, rid)
newReceiverMap[key] = true
}
now := time.Now()
// 事务处理
err = models.Orm.DoTx(func(c context.Context, txOrm orm.TxOrmer) error {
// A. 对于已经不在新接收者列表中的用户,软删除
for key, rec := range existingMap {
if !newReceiverMap[key] {
if rec.DeleteTime == nil {
rec.DeleteTime = &now
if _, e := txOrm.Update(rec, "DeleteTime"); e != nil {
return e
}
}
}
}
// B. 对于仍然在新接收者列表中的用户,更新标题、内容、以及 target 信息;如果原来被删除了,清除 delete_time
var newInserts []models.SystemReminderList
for _, rid := range receiverIDs {
key := fmt.Sprintf("%s_%d", receiverType, rid)
if rec, exists := existingMap[key]; exists {
rec.Title = title
rec.Content = content
rec.TargetType = targetType
rec.TargetRoleID = targetRoleID
rec.TargetTenantID = targetTenantID
cols := []string{"Title", "Content", "TargetType", "TargetRoleID", "TargetTenantID"}
if rec.DeleteTime != nil {
rec.DeleteTime = nil
rec.IsRead = 0
rec.ReadTime = nil
cols = append(cols, "DeleteTime", "IsRead", "ReadTime")
}
if _, e := txOrm.Update(rec, cols...); e != nil {
return e
}
} else {
// C. 对于新增加的接收者,插入新记录
newInserts = append(newInserts, models.SystemReminderList{
Title: title,
Content: content,
SenderID: senderID,
SenderType: senderType,
ReceiverID: rid,
ReceiverType: receiverType,
IsRead: 0,
CreateTime: &now,
BatchID: batchID,
TargetType: targetType,
TargetRoleID: targetRoleID,
TargetTenantID: targetTenantID,
})
}
}
if len(newInserts) > 0 {
if _, e := txOrm.InsertMulti(100, newInserts); e != nil {
return e
}
}
return nil
})
return err
}
// DeleteSentReminderBatch 删除已发送消息(删除该批次下所有记录)
func DeleteSentReminderBatch(batchID string) error {
now := time.Now()
_, err := models.Orm.QueryTable(new(models.SystemReminderList)).
Filter("batch_id", batchID).
Update(map[string]interface{}{
"delete_time": &now,
})
return err
}
+144 -144
View File
@@ -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
}