1.新增短信模版相关接口;

This commit is contained in:
chenlin 2025-04-23 18:00:14 +08:00
parent 5a618d8c95
commit fd3414f7c7
8 changed files with 7221 additions and 54 deletions

File diff suppressed because it is too large Load Diff

View File

@ -10,6 +10,7 @@ const (
ShowCount = 100 // 前端展示手机号数量
)
// SmsTask 短信下行记录
type SmsTask struct {
models.Model
@ -27,6 +28,7 @@ type SmsTask struct {
ScheduleTime *time.Time `gorm:"schedule_time"` // 计划发送时间(定时时间)
}
// SmsTaskBatch 短信批量任务记录
type SmsTaskBatch struct {
models.Model
@ -45,6 +47,7 @@ type SmsTaskBatch struct {
ScheduleTime *time.Time `gorm:"schedule_time"` // 计划发送时间(定时时间)
}
// SmsSendRecord 短信发送明细
type SmsSendRecord struct {
models.Model
@ -70,6 +73,7 @@ type SensitiveWord struct {
IsEnabled bool `json:"is_enabled" gorm:"default:1;column:is_enabled;comment:是否启用"`
}
// SmsUplinkLog 短信上行记录
type SmsUplinkLog struct {
models.Model
@ -78,6 +82,7 @@ type SmsUplinkLog struct {
BatchID string `gorm:"column:batch_id;type:varchar(64);not null" json:"batch_id"` // 下行短信批次 ID
}
// SmsSignatureRealname 签名实名制
type SmsSignatureRealname struct {
models.Model
@ -100,12 +105,21 @@ type SmsSignatureRealname struct {
IsActive int `json:"is_active"` // 是否有效0 无效1 有效)
}
// SmsContactCategory 通讯录分类
type SmsContactCategory struct {
models.Model
Name string `json:"name" gorm:"type:varchar(100);not null"`
ParentID uint64 `json:"parent_id" gorm:"default:0"`
Children []SmsContactCategory `json:"children,omitempty" gorm:"-"`
}
// SmsContact 通讯录
type SmsContact struct {
models.Model
CooperativeNumber string `gorm:"column:cooperative_number"` // 合作商编号
CooperativeName string `gorm:"column:cooperative_name"` // 合作商名称
CategoryID uint64 `json:"category_id" gorm:"default:0"`
CooperativeNumber string `json:"cooperative_number"` // 合作商编号
CooperativeName string `json:"cooperative_name"` // 合作商名称
Name string `json:"name"`
PhoneNumber string `json:"phone_number"`
Gender string `json:"gender"`
@ -115,6 +129,75 @@ type SmsContact struct {
Remark string `json:"remark"`
}
// SmsPhraseCategory 短语分类
type SmsPhraseCategory struct {
models.Model
Name string `json:"name" gorm:"type:varchar(100);not null"`
ParentID uint64 `json:"parent_id" gorm:"default:0"`
Children []SmsPhraseCategory `json:"children,omitempty" gorm:"-"`
}
// SmsPhrase 通讯录-常用短语
type SmsPhrase struct {
models.Model
Content string `json:"content" gorm:"type:varchar(255);not null"`
CategoryID uint `json:"category_id" gorm:"default:0"`
}
// SmsCommonNumber 常用号码列表
type SmsCommonNumber struct {
models.Model
Name string `json:"name"`
PhoneNumbers string `json:"phone_numbers"` // 如:"12345678901,13322223333"
PhoneCount int `json:"phone_count" gorm:"-"` // 号码数量(从 phone_numbers 计算)
}
// SmsBlackList 黑名单列表
type SmsBlackList struct {
models.Model
PhoneNumber string `json:"phone_number"`
Remark string `json:"remark"`
}
// SmsTemplate 短信模版表
type SmsTemplate struct {
models.Model
CooperativeNumber string `gorm:"column:cooperative_number"` // 合作商编号
CooperativeName string `gorm:"column:cooperative_name"` // 合作商名称
Content string `json:"content"` // 模版内容(必填)
ExpireAt *time.Time `json:"expire_at"` // 到期时间(有效期)
Remark string `json:"remark"` // 备注
Status int `json:"status"` // 状态0=审核中 1=正常 2=拒绝 3=过期
}
type ContactInput struct {
CategoryID []uint64 `json:"category_id" binding:"required"`
ID uint64 `json:"id"` // 联系人ID
Name string `json:"name"`
PhoneNumber string `json:"phone_number"`
Gender string `json:"gender"`
BirthdayStr string `json:"birthday"` // 先用 string 接收
Company string `json:"company"`
Address string `json:"address"`
Remark string `json:"remark"`
}
type EditContactInput struct {
CategoryID uint64 `json:"category_id" binding:"required"`
ID uint64 `json:"id"` // 联系人ID
Name string `json:"name"`
PhoneNumber string `json:"phone_number"`
Gender string `json:"gender"`
BirthdayStr string `json:"birthday"` // 先用 string 接收
Company string `json:"company"`
Address string `json:"address"`
Remark string `json:"remark"`
}
type MassImportPhoneResp struct {
List []string `json:"list"` // 加密后的数据
ImportSerialNumber string `json:"import_serial_number"` // 导入excel返回的编号
@ -272,12 +355,40 @@ type SignatureRealnameQueryResp struct {
TotalPage int64 `json:"total_page"`
}
type AddContactCategoryReq struct {
Name string `json:"name" binding:"required"`
ParentID uint `json:"parent_id"` // 默认 0 表示顶级
}
type EditContactCategoryReq struct {
ID uint `json:"id" binding:"required"`
Name string `json:"name" binding:"required"`
}
// DeleteContactCategoryRequest 删除通讯录分类请求
type DeleteContactCategoryRequest struct {
IDs []uint `json:"ids" binding:"required"` // 要删除的分类ID列表
}
// ListContactCategoryRequest 查询通讯录分类树的请求
type ListContactCategoryRequest struct {
CategoryID uint64 `json:"category_id"` // 为空表示查询整个树
}
type SmsContactCategoryTree struct {
ID uint64 `json:"id"`
Name string `json:"name"`
ParentID uint64 `json:"parent_id"`
Children []SmsContactCategoryTree `json:"children,omitempty"`
}
// ContactQuery 请求结构体
type ContactQuery struct {
Name string `form:"name"` // 模糊搜索
PhoneNumber string `form:"phone_number"` // 模糊搜索
Page int `form:"page"`
PageSize int `form:"page_size"`
CategoryID []uint64 `json:"category_id" binding:"required"`
Name string `form:"name"` // 模糊搜索
PhoneNumber string `form:"phone_number"` // 模糊搜索
Page int `form:"page"`
PageSize int `form:"page_size"`
}
// ContactQueryResp 响应结构体
@ -292,3 +403,205 @@ type ContactQueryResp struct {
type ContactDeleteRequest struct {
ContactIDs []uint `json:"contact_ids" binding:"required"` // 要删除的记录ID列表
}
// ExportContactsRequest 导出联系人请求参数
type ExportContactsRequest struct {
IDs []uint `json:"ids"` // 联系人ID列表优先
All bool `json:"all"` // 是否导出全部
}
type ExportContactsResp struct {
ExportUrl string `json:"export_url"` // 下载链接
}
type AddPhraseCategoryReq struct {
Name string `json:"name" binding:"required"`
ParentID uint `json:"parent_id"` // 默认 0 表示顶级
}
type EditPhraseCategoryReq struct {
ID uint `json:"id" binding:"required"`
Name string `json:"name" binding:"required"`
}
// DeletePhraseCategoryRequest 删除短语分类请求
type DeletePhraseCategoryRequest struct {
IDs []uint `json:"ids" binding:"required"` // 要删除的分类ID列表
}
// SmsPhraseQuery 查询参数
type SmsPhraseQuery struct {
Content string `form:"content" binding:"required"` // 模糊搜索内容
CategoryID uint `form:"category_id"` // 所属分类ID
Page int `form:"page"`
PageSize int `form:"page_size"`
}
type SmsPhraseListResp struct {
List []SmsPhrase `json:"list"` // 短语列表
Total int64 `json:"total"` // 总条数
Page int `json:"page"` // 当前页
PageSize int `json:"page_size"` // 每页大小
TotalPage int64 `json:"total_page"` // 总页数
}
// SmsPhraseAddOrEdit 新增或编辑参数
type SmsPhraseAddOrEdit struct {
ID uint `json:"id"` // 编辑时用
Content string `json:"content" binding:"required"`
CategoryID uint `json:"category_id" binding:"required"`
}
// SmsPhraseBatchDeleteReq 批量删除
type SmsPhraseBatchDeleteReq struct {
IDs []uint `json:"ids" binding:"required"`
}
// ListPhraseCategoryRequest 查询短语分类树的请求
type ListPhraseCategoryRequest struct {
CategoryID uint64 `json:"category_id"` // 为空表示查询整个树
}
type SmsPhraseCategoryTree struct {
ID uint64 `json:"id"`
Name string `json:"name"`
ParentID uint64 `json:"parent_id"`
Children []SmsPhraseCategoryTree `json:"children,omitempty"`
}
// SmsCommonNumberQuery 查询
type SmsCommonNumberQuery struct {
Name string `json:"name"`
Page int `json:"page"`
PageSize int `json:"pageSize"`
}
// SmsCommonNumberAddReq 添加常用号码请求结构体
type SmsCommonNumberAddReq struct {
Name string `json:"name" binding:"required"` // 名称
PhoneList []string `json:"phone_list" binding:"required"` // 号码列表
}
// SmsCommonNumberAppendReq 追加常用号码请求结构体
type SmsCommonNumberAppendReq struct {
ID int64 `json:"id" binding:"required"` // 记录ID
PhoneList []string `json:"phone_list" binding:"required"` // 需要追加的号码列表
}
// SmsCommonNumberDetailReq 常用号码详情
type SmsCommonNumberDetailReq struct {
ID int64 `json:"id" binding:"required"` // 记录ID
}
// SmsCommonNumberDeleteReq 删除
type SmsCommonNumberDeleteReq struct {
Ids []uint64 `json:"ids"`
}
// SmsCommonNumberExportReq 导出
type SmsCommonNumberExportReq struct {
All bool `json:"all"` // 是否导出全部
Ids []uint64 `json:"ids"`
}
type SmsCommonNumberExportResp struct {
ExportUrl string `json:"export_url"` // 下载链接
}
type SmsCommonNumberListResp struct {
List []SmsCommonNumber `json:"list"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"pageSize"`
TotalPage int64 `json:"totalPage"`
}
type BlacklistAddReq struct {
PhoneList []string `json:"phone_list" binding:"required"`
Remark string `json:"remark"`
}
// ExportBlacklistRequest 导出黑名单请求
type ExportBlacklistRequest struct {
All bool `json:"all"` // 是否导出全部
Ids []string `json:"ids"` // 指定导出的手机号ID
}
// ExportBlacklistResp 导出响应
type ExportBlacklistResp struct {
ExportUrl string `json:"export_url"`
}
type BlacklistQuery struct {
PhoneNumber string `json:"phone_number"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
type BlacklistListResp struct {
List []SmsBlackList `json:"list"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
TotalPage int64 `json:"total_page"`
}
// SmsBlacklistBatchDeleteReq 批量删除黑名单请求
type SmsBlacklistBatchDeleteReq struct {
IDs []uint `json:"ids"` // 黑名单记录ID
}
// SmsTemplateQuery 短信模版查询参数
type SmsTemplateQuery struct {
Content string `json:"content"` // 模版内容模糊查询
Status int `json:"status"` // 模版状态0=待审核1=正常2=审核拒绝3=已过期)
CreateStart time.Time `json:"create_start"` // 创建时间起
CreateEnd time.Time `json:"create_end"` // 创建时间止
Page int `json:"page"`
PageSize int `json:"page_size"`
}
// SmsTemplateListResp 返回模版列表结构
type SmsTemplateListResp struct {
List []SmsTemplate `json:"list"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
TotalPage int64 `json:"total_page"`
}
// SmsTemplateExportReq 短信模版导出请求
type SmsTemplateExportReq struct {
Ids []uint `json:"ids"` // 选择的模版 ID 列表
All bool `json:"all"` // 是否导出全部
}
// ExportTemplateResp 导出结果
type ExportTemplateResp struct {
ExportUrl string `json:"export_url"` // 下载地址
}
type ApproveTemplateRequest struct {
ID uint `json:"id"`
Status int `json:"status"` // 1 正常, 2 审核不通过
}
// SmsTemplateUpdateRequest 修改短信模版请求
type SmsTemplateUpdateRequest struct {
ID uint `json:"id" binding:"required"`
Content string `json:"content" binding:"required"`
ExpireAt *time.Time `json:"expire_at"`
Remark string `json:"remark"`
}
// DeleteIdsRequest 批量删除模版时使用
type DeleteIdsRequest struct {
IDs []uint `json:"ids" binding:"required"`
}
// SmsTemplateCreateRequest 创建短信模版请求
type SmsTemplateCreateRequest struct {
Content string `json:"content" binding:"required"` // 模版内容
ExpireAt *time.Time `json:"expire_at"` // 到期时间
Remark string `json:"remark"` // 备注
}

View File

@ -39,9 +39,43 @@ func registerSmsManageRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMidd
sms.POST("/signature_realname/update", api.UpdateSignatureRealname) // 编辑实名签名
sms.POST("/signature_realname/delete", api.DeleteSignatureRealname) // 批量删除签名实名记录
sms.POST("/contacts/list", api.ListContacts) // 查询通讯录列表
sms.POST("/contacts/add", api.AddContact) // 新增联系人
sms.POST("/contacts/edit", api.EditContact) // 编辑联系人
sms.POST("/contacts/batch_delete", api.BulkDeleteContacts) // 批量删除联系人
sms.POST("/contacts_category/add", api.AddContactsCategory) // 新增通讯录分类节点
sms.POST("/contacts_category/edit", api.EditContactsCategory) // 编辑通讯录分类节点
sms.POST("/contacts_category/delete", api.DeleteContactsCategories) // 删除通讯录分类
sms.POST("/contacts_category/list", api.ListContactsCategories) // 查询通讯录分类列表(树形结构)
sms.POST("/contacts/list", api.ListContacts) // 查询通讯录列表
sms.POST("/contacts/add", api.AddContact) // 新增联系人
sms.POST("/contacts/edit", api.EditContact) // 编辑联系人
sms.POST("/contacts/batch_delete", api.BulkDeleteContacts) // 批量删除联系人
sms.POST("/contacts/import", api.BulkImportContacts) // 批量导入通讯录
sms.POST("/contacts/export", api.BulkExportContacts) // 批量导出通讯录
sms.POST("/phrase_category/add", api.AddPhraseCategory) // 新增短语分类节点
sms.POST("/phrase_category/edit", api.EditPhraseCategory) // 编辑短语分类节点
sms.POST("/phrase_category/delete", api.DeletePhraseCategories) // 删除常用短语分类
sms.POST("/phrase_category/list", api.ListPhraseCategories) // 查询常用短语分类列表(树形结构)
sms.POST("/phrase/add", api.AddPhrase) // 新增常用短语
sms.POST("/phrase/edit", api.EditPhrase) // 编辑常用短语
sms.POST("/phrase/delete", api.DeletePhrases) // 批量删除短语
sms.POST("/phrase/list", api.ListPhrases) // 查询常用短语列表
sms.POST("/common_number/list", api.ListCommonNumbers) // 常用号码列表
sms.POST("/common_number/add", api.AddCommonNumber) // 添加常用号码
sms.POST("/common_number/append", api.AppendCommonNumber) // 追加号码
sms.POST("/common_number/detail", api.CommonNumberDetail) // 常用号码详情
sms.POST("/common_number/delete", api.DeleteCommonNumbers) // 批量删除常用号码
sms.POST("/common_number/export", api.ExportCommonNumber) // 导出常用号码
sms.POST("/black_list/add", api.AddBlacklistNumber) // 添加黑名单
sms.POST("/black_list/list", api.ListBlacklist) // 黑名单列表
sms.POST("/black_list/export", api.ExportBlacklist) // 导出黑名单号码
sms.POST("/black_list/delete", api.DeleteBlacklist) // 批量删除黑名单
sms.POST("/template/create", api.CreateSmsTemplate) // 新增短信模版
sms.POST("/template/delete", api.DeleteSmsTemplates) // 批量删除短信模版
sms.POST("/template/update", api.UpdateSmsTemplate) // 修改短信模版
sms.POST("/template/approve", api.ApproveSmsTemplate) // 审核短信模版
sms.POST("/template/list", api.ListSmsTemplates) // 获取短信模版列表
sms.POST("/template/export", api.ExportSmsTemplate) // 导出短信模版
}
}

View File

@ -2,6 +2,7 @@ package bus_service
import (
"bytes"
"encoding/csv"
"errors"
"fmt"
"github.com/go-admin-team/go-admin-core/logger"
@ -11,7 +12,11 @@ import (
"go-admin/common/redisx"
"golang.org/x/net/context"
"gorm.io/gorm"
"io"
"math/rand"
"os"
"regexp"
"sort"
"strconv"
"strings"
"time"
@ -811,6 +816,9 @@ func (s *SmsService) ListContacts(req bus_models.ContactQuery, db *gorm.DB) (bus
query := db.Model(&bus_models.SmsContact{})
if len(req.CategoryID) != 0 {
query = query.Where("category_id IN ?", req.CategoryID)
}
if req.Name != "" {
query = query.Where("name LIKE ?", "%"+req.Name+"%")
}
@ -862,7 +870,7 @@ func (s *SmsService) AddContact(contact bus_models.SmsContact, db *gorm.DB) erro
}
// EditContact 编辑联系人
func (s *SmsService) EditContact(id string, contact bus_models.SmsContact, db *gorm.DB) error {
func (s *SmsService) EditContact(id uint64, contact bus_models.SmsContact, db *gorm.DB) error {
var existingContact bus_models.SmsContact
if err := db.Where("id = ?", id).First(&existingContact).Error; err != nil {
return err
@ -886,3 +894,847 @@ func (s *SmsService) EditContact(id string, contact bus_models.SmsContact, db *g
func (s *SmsService) BulkDeleteContacts(req bus_models.ContactDeleteRequest, db *gorm.DB) error {
return db.Where("id IN (?)", req.ContactIDs).Delete(&bus_models.SmsContact{}).Error
}
// ImportContactsFromExcel 从Excel导入联系人含格式校验
func (s *SmsService) ImportContactsFromExcel(r io.Reader, db *gorm.DB, coopNum, coopName string, categoryId uint64) error {
excelFile, err := excelize.OpenReader(r)
if err != nil {
return errors.New("解析Excel失败")
}
rows, err := excelFile.GetRows("Sheet1")
if err != nil || len(rows) < 2 {
return errors.New("excel格式错误或无有效数据")
}
var contacts []bus_models.SmsContact
for i, row := range rows {
if i == 0 {
continue
}
if len(row) < 7 {
// 如果一行少于7个字段补充缺失字段为空字符串或默认值
for len(row) < 7 {
row = append(row, "")
}
}
phone := strings.TrimSpace(row[3])
if !isValidPhoneNumber(phone) {
return fmt.Errorf("第 %d 行手机号码格式不正确: %s", i+1, phone)
}
birthday, err := parseExcelDate(row[4])
if err != nil {
return fmt.Errorf("第 %d 行生日格式不正确应为YYYY-MM-DD: %s", i+1, row[4])
}
contacts = append(contacts, bus_models.SmsContact{
CategoryID: categoryId,
CooperativeNumber: coopNum,
CooperativeName: coopName,
Name: strings.TrimSpace(row[0]),
Company: strings.TrimSpace(row[1]),
Gender: strings.TrimSpace(row[2]),
PhoneNumber: phone,
Birthday: birthday,
Address: strings.TrimSpace(row[5]),
Remark: strings.TrimSpace(row[6]),
})
}
if len(contacts) == 0 {
return errors.New("无有效联系人记录")
}
err = db.Create(&contacts).Error
if err != nil {
return errors.New("导入失败:" + err.Error())
}
return nil
}
func isValidPhoneNumber(phone string) bool {
match, _ := regexp.MatchString(`^1\d{10}$`, phone)
return match
}
func parseExcelDate(dateStr string) (*time.Time, error) {
if dateStr == "" {
return nil, nil
}
layouts := []string{"2006-01-02", "2006/01/02"}
for _, layout := range layouts {
if t, err := time.Parse(layout, dateStr); err == nil {
return &t, nil
}
}
return nil, fmt.Errorf("日期格式不正确")
}
func (s *SmsService) ExportContactsToExcel(db *gorm.DB, req bus_models.ExportContactsRequest) (string, error) {
var contacts []bus_models.SmsContact
query := db.Order("id desc")
if !req.All {
if len(req.IDs) == 0 {
return "", fmt.Errorf("未传入ID且未选择导出全部")
}
query = query.Where("id IN ?", req.IDs)
}
if err := query.Find(&contacts).Error; err != nil {
return "", err
}
if len(contacts) == 0 {
return "", fmt.Errorf("没有可导出的联系人")
}
file := excelize.NewFile()
sheet := "Sheet1"
headers := []string{"姓名", "公司", "性别", "手机号码", "生日", "地址", "备注"}
// 表头
for i, h := range headers {
col := string('A' + i)
file.SetCellValue(sheet, col+"1", h)
}
// 内容
for i, c := range contacts {
row := i + 2
file.SetCellValue(sheet, "A"+strconv.Itoa(row), c.Name)
file.SetCellValue(sheet, "B"+strconv.Itoa(row), c.Company)
file.SetCellValue(sheet, "C"+strconv.Itoa(row), c.Gender)
file.SetCellValue(sheet, "D"+strconv.Itoa(row), c.PhoneNumber)
if c.Birthday != nil {
file.SetCellValue(sheet, "E"+strconv.Itoa(row), c.Birthday.Format("2006-01-02"))
}
file.SetCellValue(sheet, "F"+strconv.Itoa(row), c.Address)
file.SetCellValue(sheet, "G"+strconv.Itoa(row), c.Remark)
}
style, _ := file.NewStyle(&excelize.Style{
Alignment: &excelize.Alignment{
Horizontal: "center",
Vertical: "center",
},
})
file.SetColWidth(sheet, "A", "G", 20)
file.SetCellStyle(sheet, "A1", fmt.Sprintf("G%d", len(contacts)+1), style)
// 保存文件
fileName := time.Now().Format("20060102150405") + "_导出通讯录.xlsx"
filePath := ExportFile + fileName
fileUrl := MiGuExportUrl + fileName
if err := file.SaveAs(filePath); err != nil {
logger.Errorf("导出通讯录失败: %v", err)
return "", err
}
return fileUrl, nil
}
func (s *SmsService) AddPhraseCategory(req bus_models.AddPhraseCategoryReq, db *gorm.DB) error {
return db.Create(&bus_models.SmsPhraseCategory{
Name: req.Name,
ParentID: uint64(req.ParentID),
}).Error
}
func (s *SmsService) EditPhraseCategory(req bus_models.EditPhraseCategoryReq, db *gorm.DB) error {
return db.Model(&bus_models.SmsPhraseCategory{}).
Where("id = ?", req.ID).
Update("name", req.Name).
Error
}
func (s *SmsService) DeletePhraseCategories(ids []uint, db *gorm.DB) error {
if len(ids) == 0 {
return errors.New("未指定需要删除的分类")
}
var total int64
if err := db.Model(&bus_models.SmsPhraseCategory{}).Count(&total).Error; err != nil {
return err
}
// 递归找出所有要删除的 ID含子分类
var allIDs []uint
var walk func(uint)
walk = func(parentID uint) {
var children []bus_models.SmsPhraseCategory
db.Where("parent_id = ?", parentID).Find(&children)
for _, child := range children {
allIDs = append(allIDs, uint(child.ID))
walk(uint(child.ID))
}
}
for _, id := range ids {
allIDs = append(allIDs, id)
walk(id)
}
// 保证删除后至少保留一个分类节点
if len(allIDs) >= int(total) {
return errors.New("无法删除所有分类节点,至少保留一个")
}
// 删除短语
if err := db.Where("category_id IN ?", allIDs).Delete(&bus_models.SmsPhrase{}).Error; err != nil {
return err
}
// 删除分类
return db.Where("id IN ?", allIDs).Delete(&bus_models.SmsPhraseCategory{}).Error
}
func (s *SmsService) ListPhrases(req bus_models.SmsPhraseQuery, db *gorm.DB) (bus_models.SmsPhraseListResp, error) {
var list []bus_models.SmsPhrase
var total int64
query := db.Model(&bus_models.SmsPhrase{})
if req.Content != "" {
query = query.Where("content LIKE ?", "%"+req.Content+"%")
}
if req.CategoryID != 0 {
query = query.Where("category_id = ?", req.CategoryID)
}
err := query.Count(&total).Error
if err != nil {
return bus_models.SmsPhraseListResp{}, err
}
// 处理分页
page := req.Page
if page < 1 {
page = 1
}
pageSize := req.PageSize
if pageSize <= 0 {
pageSize = 10
}
err = query.Order("id desc").
Offset((page - 1) * pageSize).
Limit(pageSize).
Find(&list).Error
if err != nil {
return bus_models.SmsPhraseListResp{}, err
}
return bus_models.SmsPhraseListResp{
List: list,
Total: total,
Page: page,
PageSize: pageSize,
TotalPage: (total + int64(pageSize) - 1) / int64(pageSize),
}, nil
}
// AddPhrase 新增
func (s *SmsService) AddPhrase(req bus_models.SmsPhraseAddOrEdit, db *gorm.DB) error {
phrase := bus_models.SmsPhrase{
Content: req.Content,
CategoryID: req.CategoryID,
}
return db.Create(&phrase).Error
}
// EditPhrase 编辑
func (s *SmsService) EditPhrase(req bus_models.SmsPhraseAddOrEdit, db *gorm.DB) error {
return db.Model(&bus_models.SmsPhrase{}).
Where("id = ?", req.ID).
Updates(map[string]interface{}{
"content": req.Content,
"category_id": req.CategoryID,
}).Error
}
// DeletePhrases 批量删除
func (s *SmsService) DeletePhrases(ids []uint, db *gorm.DB) error {
return db.Where("id IN (?)", ids).Delete(&bus_models.SmsPhrase{}).Error
}
func (s *SmsService) GetPhraseCategoryTree(db *gorm.DB, parentID uint64) ([]bus_models.SmsPhraseCategoryTree, error) {
var categories []bus_models.SmsPhraseCategory
if err := db.Find(&categories).Error; err != nil {
return nil, err
}
// 构建 map[id]category
idMap := make(map[uint64]*bus_models.SmsPhraseCategoryTree)
for _, cat := range categories {
node := &bus_models.SmsPhraseCategoryTree{
ID: cat.ID,
Name: cat.Name,
ParentID: cat.ParentID,
}
idMap[cat.ID] = node
}
// 构建树结构
for _, node := range idMap {
if parent, ok := idMap[node.ParentID]; ok {
parent.Children = append(parent.Children, *node)
}
}
// 返回指定节点下的树
if parentID != 0 {
if root, ok := idMap[parentID]; ok {
return []bus_models.SmsPhraseCategoryTree{*root}, nil
}
return []bus_models.SmsPhraseCategoryTree{}, nil // 指定节点不存在
}
// 否则返回整棵树
var roots []bus_models.SmsPhraseCategoryTree
for _, node := range idMap {
if node.ParentID == 0 {
roots = append(roots, *node)
}
}
return roots, nil
}
func (s *SmsService) AddContactsCategory(req bus_models.AddContactCategoryReq, db *gorm.DB) error {
return db.Create(&bus_models.SmsContactCategory{
Name: req.Name,
ParentID: uint64(req.ParentID),
}).Error
}
func (s *SmsService) EditContactsCategory(req bus_models.EditContactCategoryReq, db *gorm.DB) error {
return db.Model(&bus_models.SmsContactCategory{}).
Where("id = ?", req.ID).
Update("name", req.Name).
Error
}
func (s *SmsService) DeleteContactsCategories(ids []uint, db *gorm.DB) error {
if len(ids) == 0 {
return errors.New("未指定需要删除的分类")
}
var total int64
if err := db.Model(&bus_models.SmsContactCategory{}).Count(&total).Error; err != nil {
return err
}
// 递归找出所有要删除的 ID含子分类
var allIDs []uint
var walk func(uint)
walk = func(parentID uint) {
var children []bus_models.SmsContactCategory
db.Where("parent_id = ?", parentID).Find(&children)
for _, child := range children {
allIDs = append(allIDs, uint(child.ID))
walk(uint(child.ID))
}
}
for _, id := range ids {
allIDs = append(allIDs, id)
walk(id)
}
// 保证删除后至少保留一个分类节点
if len(allIDs) >= int(total) {
return errors.New("无法删除所有分类节点,至少保留一个")
}
// 删除短语
if err := db.Where("category_id IN ?", allIDs).Delete(&bus_models.SmsContact{}).Error; err != nil {
return err
}
// 删除分类
return db.Where("id IN ?", allIDs).Delete(&bus_models.SmsContactCategory{}).Error
}
func (s *SmsService) GetContactsCategoryTree(db *gorm.DB, parentID uint64) ([]bus_models.SmsContactCategoryTree, error) {
var categories []bus_models.SmsContactCategory
if err := db.Find(&categories).Error; err != nil {
return nil, err
}
// 构建 map[id]category
idMap := make(map[uint64]*bus_models.SmsContactCategoryTree)
for _, cat := range categories {
node := &bus_models.SmsContactCategoryTree{
ID: cat.ID,
Name: cat.Name,
ParentID: cat.ParentID,
}
idMap[cat.ID] = node
}
// 构建树结构
for _, node := range idMap {
if parent, ok := idMap[node.ParentID]; ok {
parent.Children = append(parent.Children, *node)
}
}
// 返回指定节点下的树
if parentID != 0 {
if root, ok := idMap[parentID]; ok {
return []bus_models.SmsContactCategoryTree{*root}, nil
}
return []bus_models.SmsContactCategoryTree{}, nil // 指定节点不存在
}
// 否则返回整棵树
var roots []bus_models.SmsContactCategoryTree
for _, node := range idMap {
if node.ParentID == 0 {
roots = append(roots, *node)
}
}
return roots, nil
}
func (s *SmsService) ListCommonNumbers(req bus_models.SmsCommonNumberQuery, db *gorm.DB) (bus_models.SmsCommonNumberListResp, error) {
var dbList []bus_models.SmsCommonNumber
var total int64
query := db.Model(&bus_models.SmsCommonNumber{})
if req.Name != "" {
query = query.Where("name LIKE ?", "%"+req.Name+"%")
}
err := query.Count(&total).Error
if err != nil {
return bus_models.SmsCommonNumberListResp{}, err
}
// 处理分页
page := req.Page
if page < 1 {
page = 1
}
pageSize := req.PageSize
if pageSize <= 0 {
pageSize = 10
}
err = query.Order("id desc").
Offset((page - 1) * pageSize).
Limit(pageSize).
Find(&dbList).Error
if err != nil {
return bus_models.SmsCommonNumberListResp{}, err
}
// 转换为返回结构体
var respList []bus_models.SmsCommonNumber
for _, item := range dbList {
respList = append(respList, bus_models.SmsCommonNumber{
Name: item.Name,
PhoneNumbers: item.PhoneNumbers,
PhoneCount: len(strings.Split(item.PhoneNumbers, ",")),
})
}
totalPage := (total + int64(page) - 1) / int64(pageSize)
if totalPage < 1 {
totalPage = 1
}
return bus_models.SmsCommonNumberListResp{
List: respList,
Total: total,
Page: page,
PageSize: pageSize,
TotalPage: totalPage,
}, nil
}
func (s *SmsService) AddCommonNumber(req bus_models.SmsCommonNumberAddReq, db *gorm.DB) error {
if req.Name == "" {
return errors.New("名称不能为空")
}
return db.Create(&bus_models.SmsCommonNumber{
Name: req.Name,
PhoneNumbers: strings.Join(req.PhoneList, ","),
}).Error
}
// AppendCommonNumber 向已有常用号码记录中追加号码(自动去重)
// 逻辑:获取原始号码列表 + 新号码列表 => 合并去重 => 更新保存
func (s *SmsService) AppendCommonNumber(req bus_models.SmsCommonNumberAppendReq, db *gorm.DB) error {
var record bus_models.SmsCommonNumber
err := db.Where("id = ?", req.ID).First(&record).Error
if err != nil {
return err
}
// 拆分原始号码
originalNumbers := strings.Split(record.PhoneNumbers, ",")
numberSet := make(map[string]struct{})
// 原号码去重填入 map
for _, num := range originalNumbers {
num = strings.TrimSpace(num)
if num != "" {
numberSet[num] = struct{}{}
}
}
// 新号码去重合并
for _, num := range req.PhoneList {
num = strings.TrimSpace(num)
if num != "" {
numberSet[num] = struct{}{}
}
}
var deduplicated []string
for num := range numberSet {
deduplicated = append(deduplicated, num)
}
sort.Strings(deduplicated) // 方便前端对比、稳定输出顺序
record.PhoneNumbers = strings.Join(deduplicated, ",")
return db.Save(&record).Error
}
func (s *SmsService) GetCommonNumberDetail(id int64, db *gorm.DB) (bus_models.SmsCommonNumber, error) {
var record bus_models.SmsCommonNumber
err := db.First(&record, id).Error
return record, err
}
func (s *SmsService) DeleteCommonNumbers(ids []uint64, db *gorm.DB) error {
return db.Where("id IN (?)", ids).Delete(&bus_models.SmsCommonNumber{}).Error
}
// ExportCommonNumbers 导出多个常用号码名称下的所有号码到一个文件
func (s *SmsService) ExportCommonNumbers(req bus_models.SmsCommonNumberExportReq, db *gorm.DB) (string, error) {
var records []bus_models.SmsCommonNumber
query := db.Model(&bus_models.SmsCommonNumber{}).Order("id desc")
if !req.All {
if len(req.Ids) == 0 {
return "", fmt.Errorf("未传入号码且未选择导出全部")
}
query = query.Where("id IN ?", req.Ids)
}
err := query.Find(&records).Error
if err != nil {
return "", err
}
if len(records) == 0 {
return "", errors.New("未找到对应的常用号码数据")
}
// 创建文件
fileName := time.Now().Format("20060102150405") + "_常用号码.xlsx"
filePath := ExportFile + fileName
fileUrl := MiGuExportUrl + fileName
file, err := os.Create(filePath)
if err != nil {
return "", err
}
defer file.Close()
writer := csv.NewWriter(file)
defer writer.Flush()
// 写数据
for _, record := range records {
numbers := strings.Split(record.PhoneNumbers, ",")
for _, number := range numbers {
trimmed := strings.TrimSpace(number)
if trimmed != "" {
writer.Write([]string{trimmed})
}
}
}
return fileUrl, nil
}
func (s *SmsService) AddBlacklistNumber(req bus_models.BlacklistAddReq, db *gorm.DB) error {
if len(req.PhoneList) == 0 {
return fmt.Errorf("手机号列表不能为空")
}
var records []bus_models.SmsBlackList
for _, phone := range req.PhoneList {
records = append(records, bus_models.SmsBlackList{
PhoneNumber: phone,
Remark: req.Remark,
})
}
return db.Create(&records).Error
}
func (s *SmsService) ListBlacklist(req bus_models.BlacklistQuery, db *gorm.DB) (bus_models.BlacklistListResp, error) {
var list []bus_models.SmsBlackList
var total int64
query := db.Model(&bus_models.SmsBlackList{})
if req.PhoneNumber != "" {
query = query.Where("phone_number LIKE ?", "%"+req.PhoneNumber+"%")
}
err := query.Count(&total).Error
if err != nil {
return bus_models.BlacklistListResp{}, err
}
page := req.Page
if page < 1 {
page = 1
}
pageSize := req.PageSize
if pageSize <= 0 {
pageSize = 10
}
err = query.Order("id desc").
Offset((page - 1) * pageSize).
Limit(pageSize).
Find(&list).Error
if err != nil {
return bus_models.BlacklistListResp{}, err
}
totalPage := (total + int64(pageSize) - 1) / int64(pageSize)
if totalPage < 1 {
totalPage = 1
}
return bus_models.BlacklistListResp{
List: list,
Total: total,
Page: page,
PageSize: pageSize,
TotalPage: totalPage,
}, nil
}
func (s *SmsService) ExportBlacklistToExcel(db *gorm.DB, req bus_models.ExportBlacklistRequest) (string, error) {
var blacklists []bus_models.SmsBlackList
query := db.Model(&bus_models.SmsBlackList{}).Order("id desc")
if !req.All {
if len(req.Ids) == 0 {
return "", fmt.Errorf("未传入号码且未选择导出全部")
}
query = query.Where("id IN ?", req.Ids)
}
if err := query.Find(&blacklists).Error; err != nil {
return "", err
}
if len(blacklists) == 0 {
return "", fmt.Errorf("没有可导出的黑名单记录")
}
file := excelize.NewFile()
sheet := "Sheet1"
headers := []string{"手机号", "备注", "创建时间"}
for i, h := range headers {
col := string('A' + i)
file.SetCellValue(sheet, col+"1", h)
}
for i, b := range blacklists {
row := strconv.Itoa(i + 2)
file.SetCellValue(sheet, "A"+row, b.PhoneNumber)
file.SetCellValue(sheet, "B"+row, b.Remark)
file.SetCellValue(sheet, "C"+row, b.CreatedAt.Format("2006-01-02 15:04:05"))
}
style, _ := file.NewStyle(&excelize.Style{
Alignment: &excelize.Alignment{
Horizontal: "center",
Vertical: "center",
},
})
file.SetColWidth(sheet, "A", "C", 20)
file.SetCellStyle(sheet, "A1", fmt.Sprintf("C%d", len(blacklists)+1), style)
fileName := time.Now().Format("20060102150405") + "_导出黑名单.xlsx"
filePath := ExportFile + fileName
fileUrl := MiGuExportUrl + fileName
if err := file.SaveAs(filePath); err != nil {
logger.Errorf("导出黑名单失败: %v", err)
return "", err
}
return fileUrl, nil
}
// DeleteBlacklist 批量删除黑名单记录
func (s *SmsService) DeleteBlacklist(ids []uint, db *gorm.DB) error {
return db.Where("id IN ?", ids).Delete(&bus_models.SmsBlackList{}).Error
}
// CreateSmsTemplate 创建短信模版
func (s *SmsService) CreateSmsTemplate(data *bus_models.SmsTemplate, db *gorm.DB) error {
return db.Create(data).Error
}
// DeleteSmsTemplates 批量删除短信模版
func (s *SmsService) DeleteSmsTemplates(ids []uint, db *gorm.DB) error {
return db.Delete(&bus_models.SmsTemplate{}, ids).Error
}
// UpdateSmsTemplate 更新短信模版
func (s *SmsService) UpdateSmsTemplate(req *bus_models.SmsTemplateUpdateRequest, db *gorm.DB) error {
var tmpl bus_models.SmsTemplate
// 查找原始数据
if err := db.First(&tmpl, req.ID).Error; err != nil {
return fmt.Errorf("未找到指定的短信模版")
}
// 更新模版内容
tmpl.Content = req.Content
// 更新到期时间(如果传入了新的时间)
if !req.ExpireAt.IsZero() {
tmpl.ExpireAt = req.ExpireAt
}
// 更新更新时间
tmpl.UpdatedAt = time.Now()
// 保存更新后的模版
return db.Save(&tmpl).Error
}
// ApproveSmsTemplate 审核短信模版
func (s *SmsService) ApproveSmsTemplate(id uint, status int, db *gorm.DB) error {
return db.Model(&bus_models.SmsTemplate{}).
Where("id = ?", id).
Update("status", status).Error
}
func (s *SmsService) ListSmsTemplates(req bus_models.SmsTemplateQuery, db *gorm.DB) (bus_models.SmsTemplateListResp, error) {
var list []bus_models.SmsTemplate
var total int64
query := db.Model(&bus_models.SmsTemplate{})
if req.Content != "" {
query = query.Where("content LIKE ?", "%"+req.Content+"%")
}
if req.Status != 0 {
query = query.Where("status = ?", req.Status)
}
if !req.CreateStart.IsZero() && !req.CreateEnd.IsZero() {
query = query.Where("created_at BETWEEN ? AND ?", req.CreateStart, req.CreateEnd)
}
err := query.Count(&total).Error
if err != nil {
return bus_models.SmsTemplateListResp{}, err
}
page := req.Page
if page < 1 {
page = 1
}
pageSize := req.PageSize
if pageSize <= 0 {
pageSize = 10
}
err = query.Order("id desc").
Offset((page - 1) * pageSize).
Limit(pageSize).
Find(&list).Error
if err != nil {
return bus_models.SmsTemplateListResp{}, err
}
totalPage := (total + int64(pageSize) - 1) / int64(pageSize)
if totalPage < 1 {
totalPage = 1
}
return bus_models.SmsTemplateListResp{
List: list,
Total: total,
Page: page,
PageSize: pageSize,
TotalPage: totalPage,
}, nil
}
func (s *SmsService) ExportSmsTemplates(req bus_models.SmsTemplateExportReq, db *gorm.DB) (string, error) {
var templates []bus_models.SmsTemplate
query := db.Model(&bus_models.SmsTemplate{}).Order("id desc")
if !req.All {
if len(req.Ids) == 0 {
return "", fmt.Errorf("未传入模版且未选择导出全部")
}
query = query.Where("id IN ?", req.Ids)
}
err := query.Find(&templates).Error
if err != nil {
return "", err
}
if len(templates) == 0 {
return "", errors.New("未找到对应的短信模版数据")
}
// 生成文件
fileName := time.Now().Format("20060102150405") + "_短信模版导出.csv"
filePath := ExportFile + fileName
fileUrl := MiGuExportUrl + fileName
file, err := os.Create(filePath)
if err != nil {
return "", err
}
defer file.Close()
writer := csv.NewWriter(file)
defer writer.Flush()
// 写表头
writer.Write([]string{"模版ID", "模版内容", "备注", "状态", "创建时间"})
// 写内容
for _, tmpl := range templates {
status := map[int]string{
0: "待审核",
1: "正常",
2: "审核拒绝",
3: "已过期",
}[tmpl.Status]
writer.Write([]string{
strconv.Itoa(int(tmpl.ID)),
tmpl.Content,
tmpl.Remark,
status,
tmpl.CreatedAt.Format("2006-01-02 15:04:05"),
})
}
return fileUrl, nil
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

9
tools/utils/utils.go Normal file
View File

@ -0,0 +1,9 @@
package utils
import "regexp"
func IsValidPhone(phone string) bool {
// 简单验证以1开头后面10位数字
reg := regexp.MustCompile(`^1[0-9]{10}$`)
return reg.MatchString(phone)
}