package bus_service import ( "bytes" "encoding/csv" "errors" "fmt" "github.com/go-admin-team/go-admin-core/logger" "github.com/go-admin-team/go-admin-core/sdk/service" "github.com/xuri/excelize/v2" "go-admin/app/admin/models/bus_models" "go-admin/common/redisx" "golang.org/x/net/context" "gorm.io/gorm" "io" "math/rand" "os" "regexp" "sort" "strconv" "strings" "time" "unicode/utf8" ) const ( maxPhonesPerShard = 10000 cacheExpire = time.Hour TimeFormat = "2006-01-02T15:04:05+08:00" ExportFile = "/www/server/images/export/" MiGuExportUrl = "https://telecom.deovo.com/load/export/" ) type SmsService struct { service.Service } // ReadExcelFile 读取 Excel 文件并提取第一列的手机号 func (s *SmsService) ReadExcelFile(file []byte) ([]string, error) { // 使用 excelize.OpenReader 直接从文件流读取内容 f, err := excelize.OpenReader(bytes.NewReader(file)) if err != nil { return nil, fmt.Errorf("无法打开 Excel 文件: %v", err) } var phones []string // 获取所有工作表列表 sheetList := f.GetSheetList() if len(sheetList) == 0 { return nil, fmt.Errorf("excel 文件中没有工作表") } // 选择第一个工作表 sheet := sheetList[0] rows, err := f.GetRows(sheet) if err != nil { return nil, fmt.Errorf("读取 Excel 行失败: %v", err) } // 假设第一列是手机号 for _, row := range rows { if len(row) > 0 { phone := row[0] phones = append(phones, phone) } } return phones, nil } func (s *SmsService) CachePhonesByShard(importSerial string, phones []string) error { ctx := context.Background() total := 0 for i := 0; i < len(phones); i += maxPhonesPerShard { end := i + maxPhonesPerShard if end > len(phones) { end = len(phones) } shard := phones[i:end] total++ key := fmt.Sprintf("sms:import:%s:%d", importSerial, total) if err := redisx.Client.RPush(ctx, key, shard).Err(); err != nil { return err } redisx.Client.Expire(ctx, key, cacheExpire) } // 保存总分片数量 totalKey := fmt.Sprintf("sms:import:%s:total", importSerial) err := redisx.Client.Set(ctx, totalKey, total, cacheExpire).Err() return err } func (s *SmsService) AppendPhonesToRedis(serial string, phones []string) error { ctx := context.Background() // 拿当前最大分片号 totalKey := fmt.Sprintf("sms:import:%s:total", serial) totalStr, err := redisx.Client.Get(ctx, totalKey).Result() if err != nil { return fmt.Errorf("无法读取缓存分片: %v", err) } total, _ := strconv.Atoi(totalStr) if total == 0 { return fmt.Errorf("缓存分片不存在") } // 直接往最后一片中添加(你也可以按量分片再扩展) lastKey := fmt.Sprintf("sms:import:%s:%d", serial, total) values := make([]interface{}, len(phones)) for i, p := range phones { values[i] = p } err = redisx.Client.RPush(ctx, lastKey, values...).Err() return err } func (s *SmsService) SubmitSmsTaskFromRedis(ctx context.Context, serial, content, sendTime, coopNum, coopName string) error { // 读取分片数量 totalKey := fmt.Sprintf("sms:import:%s:total", serial) totalStr, err := redisx.Client.Get(ctx, totalKey).Result() if err != nil { return fmt.Errorf("获取缓存分片数失败: %v", err) } total, _ := strconv.Atoi(totalStr) if total == 0 { return fmt.Errorf("分片数为 0,无法创建任务") } // 统计总手机号数量 var totalPhones int phoneCounts := make([]int, total) for i := 1; i <= total; i++ { redisKey := fmt.Sprintf("sms:import:%s:%d", serial, i) count, err := redisx.Client.LLen(ctx, redisKey).Result() if err != nil { return fmt.Errorf("读取缓存分片 %d 失败: %v", i, err) } totalPhones += int(count) phoneCounts[i-1] = int(count) } if totalPhones == 0 { return fmt.Errorf("手机号为空,不能创建任务") } var planTime time.Time // 判断是否设置了定时发送 if sendTime != "" { loc, _ := time.LoadLocation("Asia/Shanghai") planTime, err = time.ParseInLocation(TimeFormat, sendTime, loc) if err != nil { return fmt.Errorf("解析时间出错:%s", err.Error()) } } // 计算短信条数(按70字分割) // 短信条数计算:70字以内算1条,超过则每67字拆1条(长短信按67字分段) contentCost := (len([]rune(content)) + 66) / 67 totalSmsCount := totalPhones * contentCost // 使用数据库事务 return s.Orm.WithContext(ctx).Transaction(func(tx *gorm.DB) error { // 生成批次ID batchID, _ := GenerateBatchID(s.Orm) // 插入任务 task := &bus_models.SmsTask{ CooperativeNumber: coopNum, // 可根据需要填充 CooperativeName: coopName, BatchID: batchID, ImportID: serial, SmsContent: content, SmsContentCost: contentCost, TotalPhoneCount: totalPhones, TotalSmsCount: totalSmsCount, Status: 0, InterceptFailCount: 0, ChannelFailCount: 0, ScheduleTime: &planTime, } if err := tx.Create(task).Error; err != nil { return fmt.Errorf("创建短信任务失败: %v", err) } // 插入批次记录 for i := 1; i <= total; i++ { batch := &bus_models.SmsTaskBatch{ CooperativeNumber: coopNum, // 可根据需要填充 CooperativeName: coopName, TaskID: task.ID, BatchID: batchID, ImportID: serial, Num: i, PhoneCount: phoneCounts[i-1], SmsCount: phoneCounts[i-1] * contentCost, SmsContent: content, Status: 0, ScheduleTime: &planTime, } if err := tx.Create(batch).Error; err != nil { return fmt.Errorf("创建批次失败: %v", err) } } // ✅ 提示:不在这里生成 SmsSendRecord,后续通过消费者从 Redis 中读取批次生成 // 否则数据量大时会影响事务和响应速度 return nil }) } // GenerateBatchID 生成唯一的批次ID:日期(YYYYMMDD)+ 8位随机数(共16位) func GenerateBatchID(db *gorm.DB) (string, error) { // 获取当前日期(年月日) dateStr := time.Now().Format("20060102") // 例如:20250411 // 生成一个8位随机数(范围:00000000 ~ 99999999) rand.Seed(time.Now().UnixNano()) randomNum := rand.Int63n(100000000) // 8位最大是1亿,即10^8 // 格式化为8位字符串(左侧补0) randomNumStr := fmt.Sprintf("%08d", randomNum) // 拼接成 batch_id batchID := fmt.Sprintf("%s%s", dateStr, randomNumStr) // 检查 sms_task 表中是否已存在该 batch_id var count int64 err := db.Table("sms_task").Where("batch_id = ?", batchID).Count(&count).Error if err != nil { return "", err } // 如果已存在,则递归重试 if count > 0 { return GenerateBatchID(db) } return batchID, nil } func (s *SmsService) ExportPhoneListToExcel(importSerial string, directPhones []string) (string, error) { ctx := context.Background() var allPhones []string // 从 Redis 获取导入的号码 if importSerial != "" { totalKey := fmt.Sprintf("sms:import:%s:total", importSerial) totalStr, err := redisx.Client.Get(ctx, totalKey).Result() if err == nil { total, _ := strconv.Atoi(totalStr) for i := 1; i <= total; i++ { key := fmt.Sprintf("sms:import:%s:%d", importSerial, i) shardPhones, err := redisx.Client.LRange(ctx, key, 0, -1).Result() if err == nil { allPhones = append(allPhones, shardPhones...) } } } } // 追加直接输入的号码 if len(directPhones) > 0 { allPhones = append(allPhones, directPhones...) } if len(allPhones) == 0 { return "", fmt.Errorf("没有可导出的号码") } // 创建 Excel 文件 file := excelize.NewFile() sheet := "Sheet1" for i, phone := range allPhones { cell := fmt.Sprintf("A%d", i+1) file.SetCellValue(sheet, cell, phone) } // 设置样式 style, _ := file.NewStyle(&excelize.Style{ Alignment: &excelize.Alignment{ Horizontal: "center", Vertical: "center", }, }) _ = file.SetCellStyle(sheet, "A1", fmt.Sprintf("A%d", len(allPhones)), style) file.SetColWidth(sheet, "A", "A", 20) // 保存文件 fileName := time.Now().Format("20060102150405") + "_号码导出.xlsx" url := MiGuExportUrl + fileName if err := file.SaveAs(ExportFile + fileName); err != nil { logger.Errorf("导出Excel失败: %v", err) return "", err } return url, nil } // CheckSensitiveWords 检查短信内容是否包含敏感词 func (s *SmsService) CheckSensitiveWords(content string) ([]string, error) { var sensitiveWords []bus_models.SensitiveWord err := s.Orm.Table("sensitive_words").Where("is_enabled = ?", 1). Find(&sensitiveWords).Error if err != nil { return nil, err } var hits []string for _, sw := range sensitiveWords { if strings.Contains(content, sw.Word) { hits = append(hits, sw.Word) } } return hits, nil } // QuerySmsTaskList 查询短信下行记录 func (s *SmsService) QuerySmsTaskList(req bus_models.SmsTaskQueryRequest, db *gorm.DB) (*bus_models.SmsTaskQueryResponse, error) { var tasks []bus_models.SmsTask var total int64 query := db.Model(&bus_models.SmsTask{}).Where("status = 2") if req.BatchID != "" { query = query.Where("batch_id = ?", req.BatchID) } if req.MinTotalSms > 0 { query = query.Where("total_sms_count >= ?", req.MinTotalSms) } if req.MinPhoneCount > 0 { query = query.Where("total_phone_count >= ?", req.MinPhoneCount) } if req.Status != nil { query = query.Where("status = ?", *req.Status) } if req.StartTime != "" && req.EndTime != "" { query = query.Where("created_at BETWEEN ? AND ?", req.StartTime, req.EndTime) } // 统计总数 if err := query.Count(&total).Error; err != nil { return nil, err } // 分页处理 page := req.Page if page < 1 { page = 1 } pageSize := req.PageSize if pageSize <= 0 { pageSize = 10 } offset := (page - 1) * pageSize // 查询分页数据 if err := query. Order("created_at DESC"). Limit(pageSize). Offset(offset). Find(&tasks).Error; err != nil { return nil, err } totalPage := int((total + int64(pageSize) - 1) / int64(pageSize)) if totalPage < 1 { totalPage = 1 } return &bus_models.SmsTaskQueryResponse{ List: tasks, Total: total, Page: page, PageSize: pageSize, TotalPage: totalPage, }, nil } func (s *SmsService) QuerySmsSendRecords(req bus_models.SmsSendRecordQueryReq) (bus_models.SmsSendRecordQueryResp, error) { var resp bus_models.SmsSendRecordQueryResp db := s.Orm.Model(&bus_models.SmsSendRecord{}) if req.BatchID != "" { db = db.Where("batch_id = ?", req.BatchID) } if req.Phone != "" { db = db.Where("phone = ?", req.Phone) } if req.SmsCode != "" { db = db.Where("sms_code = ?", req.SmsCode) } if req.StartTime != "" { db = db.Where("receive_time >= ?", req.StartTime) } if req.EndTime != "" { db = db.Where("receive_time <= ?", req.EndTime) } if req.MinSegments > 0 { // 计算短信计费条数的粗略估算:每67字1条,注意实际可以存字段 db = db.Where("(length(sms_content) / 67) + 1 >= ?", req.MinSegments) } err := db.Count(&resp.Total).Error if err != nil { return resp, err } // 分页处理 page := req.Page if page < 1 { page = 1 } pageSize := req.PageSize if pageSize <= 0 { pageSize = 10 } offset := (page - 1) * pageSize // 查询分页数据 if err = db. Order("created_at DESC"). Limit(pageSize). Offset(offset). Find(&resp.List).Error; err != nil { return resp, err } totalPage := int((resp.Total + int64(pageSize) - 1) / int64(pageSize)) if totalPage < 1 { totalPage = 1 } resp.Page = page resp.PageSize = pageSize resp.TotalPage = totalPage return resp, nil } // GetPhonesFromCache 获取导入手机号前 limit 条 func (s *SmsService) GetPhonesFromCache(importSerial string, limit int) ([]string, error) { ctx := context.Background() totalKey := fmt.Sprintf("sms:import:%s:total", importSerial) totalStr, err := redisx.Client.Get(ctx, totalKey).Result() if err != nil { return nil, err } totalShards, err := strconv.Atoi(totalStr) if err != nil { return nil, err } result := make([]string, 0, limit) for i := 1; i <= totalShards; i++ { key := fmt.Sprintf("sms:import:%s:%d", importSerial, i) phones, err := redisx.Client.LRange(ctx, key, 0, -1).Result() if err != nil { return nil, err } result = append(result, phones...) if len(result) >= limit { return result[:limit], nil } } return result, nil } // GetSentPhonesByBatchID 从数据库查询已发送的手机号,最多返回 limit 条 func (s *SmsService) GetSentPhonesByBatchID(db *gorm.DB, batchID string, limit int) ([]string, error) { var records []bus_models.SmsSendRecord err := db. Where("batch_id = ?", batchID). Order("id DESC"). Limit(limit). Find(&records).Error if err != nil { return nil, err } var phones []string for _, record := range records { phones = append(phones, record.Phone) } return phones, nil } // QueryScheduledSmsTaskList 查询定时短信任务 func (s *SmsService) QueryScheduledSmsTaskList(req bus_models.SmsTaskScheduledQueryRequest, db *gorm.DB) (*bus_models.SmsTaskQueryResponse, error) { var tasks []bus_models.SmsTask var total int64 query := db.Model(&bus_models.SmsTask{}). Where("schedule_time IS NOT NULL"). Where("status = ?", 0) if req.StartTime != "" && req.EndTime != "" { query = query.Where("schedule_time BETWEEN ? AND ?", req.StartTime, req.EndTime) } // 统计总数 if err := query.Count(&total).Error; err != nil { return nil, err } // 分页处理 page := req.Page if page < 1 { page = 1 } pageSize := req.PageSize if pageSize <= 0 { pageSize = 10 } offset := (page - 1) * pageSize if err := query. Order("schedule_time ASC"). Limit(pageSize). Offset(offset). Find(&tasks).Error; err != nil { return nil, err } totalPage := int((total + int64(pageSize) - 1) / int64(pageSize)) if totalPage < 1 { totalPage = 1 } return &bus_models.SmsTaskQueryResponse{ List: tasks, Total: total, Page: page, PageSize: pageSize, TotalPage: totalPage, }, nil } func (s *SmsService) QuerySmsUplinkList(req bus_models.SmsUplinkQueryRequest, db *gorm.DB) (bus_models.SmsUplinkQueryResponse, error) { var resp bus_models.SmsUplinkQueryResponse var total int64 query := db.Table("sms_uplink_log AS uplink"). Select(` uplink.id AS uplink_id, uplink.phone_number, sr.id AS send_id, sr.sms_content, uplink.reply_content, uplink.created_at AS receive_time, sr.created_at AS send_time `). Joins("LEFT JOIN sms_send_record sr ON uplink.batch_id = sr.batch_id AND uplink.phone_number = sr.phone") // 条件拼接 if req.PhoneNumber != "" { query = query.Where("uplink.phone_number LIKE ?", "%"+req.PhoneNumber+"%") } if req.SendID > 0 { query = query.Where("sr.id = ?", req.SendID) } if req.UplinkID > 0 { query = query.Where("uplink.id = ?", req.UplinkID) } if req.ReplyContent != "" { query = query.Where("uplink.reply_content LIKE ?", "%"+req.ReplyContent+"%") } if req.StartTime != "" && req.EndTime != "" { query = query.Where("uplink.created_at BETWEEN ? AND ?", req.StartTime, req.EndTime) } // 统计总数 if err := query.Count(&total).Error; err != nil { return resp, err } // 分页处理 page := req.Page if page <= 0 { page = 1 } pageSize := req.PageSize if pageSize <= 0 { pageSize = 10 } offset := (page - 1) * pageSize var results []bus_models.SmsUplinkRecordResponse // 查询数据 err := query.Order("uplink.created_at DESC").Limit(pageSize).Offset(offset).Scan(&results).Error if err != nil { return resp, err } resp.List = results resp.Total = total resp.Page = page resp.PageSize = pageSize resp.TotalPage = (total + int64(page) - 1) / int64(pageSize) if resp.TotalPage < 1 { resp.TotalPage = 1 } return resp, nil } // BatchUpdateSmsContent 修改短信内容 func (s *SmsService) BatchUpdateSmsContent(req bus_models.BatchUpdateSmsContentRequest, db *gorm.DB) error { // 计算短信内容消耗(每70字符1条,超出部分算1条) runeCount := utf8.RuneCountInString(req.SmsContent) smsCost := runeCount / 70 if runeCount%70 != 0 { smsCost++ } // 批量更新 SmsTask if err := db.Model(&bus_models.SmsTask{}). Where("id IN ?", req.TaskIDs). Updates(map[string]interface{}{ "sms_content": req.SmsContent, "sms_content_cost": smsCost, }).Error; err != nil { return err } // 同步更新 SmsTaskBatch 中相应内容 if err := db.Model(&bus_models.SmsTaskBatch{}). Where("task_id IN ?", req.TaskIDs). Updates(map[string]interface{}{ "sms_content": req.SmsContent, }).Error; err != nil { return err } return nil } func (s *SmsService) BatchCancelSmsTasks(req bus_models.BatchUpdateRequest, db *gorm.DB) error { if len(req.TaskIDs) == 0 { return errors.New("任务ID列表不能为空") } // 取消任务状态为 4(取消) if err := db.Model(&bus_models.SmsTask{}). Where("id IN ?", req.TaskIDs). Updates(map[string]interface{}{ "status": 4, }).Error; err != nil { return err } // 同步取消子任务 if err := db.Model(&bus_models.SmsTaskBatch{}). Where("task_id IN ?", req.TaskIDs). Updates(map[string]interface{}{ "status": 4, }).Error; err != nil { return err } return nil } // BatchResetScheduleTime 批量重置定时时间 func (s *SmsService) BatchResetScheduleTime(req bus_models.BatchResetScheduleTimeRequest, db *gorm.DB) error { if len(req.TaskIDs) == 0 || req.ScheduleTime == "" { return errors.New("任务ID和定时时间不能为空") } var planTime time.Time var err error // 判断是否设置了定时发送 if req.ScheduleTime != "" { loc, _ := time.LoadLocation("Asia/Shanghai") planTime, err = time.ParseInLocation(TimeFormat, req.ScheduleTime, loc) if err != nil { return fmt.Errorf("解析时间出错:%s", err.Error()) } } // 更新 SmsTask if err := db.Model(&bus_models.SmsTask{}). Where("id IN ?", req.TaskIDs). Updates(map[string]interface{}{ "schedule_time": planTime, }).Error; err != nil { return err } // 同步更新 SmsTaskBatch if err := db.Model(&bus_models.SmsTaskBatch{}). Where("task_id IN ?", req.TaskIDs). Updates(map[string]interface{}{ "schedule_time": planTime, }).Error; err != nil { return err } return nil } // CreateSignatureRealname 创建签名实名制记录 func (s *SmsService) CreateSignatureRealname(data *bus_models.SmsSignatureRealname, db *gorm.DB) error { return db.Create(data).Error } // UpdateSignatureRealname 编辑签名实名制记录 func (s *SmsService) UpdateSignatureRealname(data *bus_models.SmsSignatureRealname, db *gorm.DB) error { if data.ID == 0 { return errors.New("ID不能为空") } return db.Model(&bus_models.SmsSignatureRealname{}). Where("id = ?", data.ID). Updates(data).Error } // BatchDeleteSignatureRealname 删除签名实名制记录 func (s *SmsService) BatchDeleteSignatureRealname(ids []uint, db *gorm.DB) error { if len(ids) == 0 { return errors.New("删除ID列表不能为空") } return db.Where("id IN ?", ids).Delete(&bus_models.SmsSignatureRealname{}).Error } func (s *SmsService) ListSignatureRealname(req bus_models.SignatureRealnameQuery, db *gorm.DB) (bus_models.SignatureRealnameQueryResp, error) { var resp bus_models.SignatureRealnameQueryResp var list []bus_models.SmsSignatureRealname var total int64 query := db.Model(&bus_models.SmsSignatureRealname{}) if req.Signature != "" { query = query.Where("signature LIKE ?", "%"+req.Signature+"%") } if req.CooperativeName != "" { query = query.Where("cooperative_name LIKE ?", "%"+req.CooperativeName+"%") } if req.CompanyName != "" { query = query.Where("company_name LIKE ?", "%"+req.CompanyName+"%") } if req.CompanyCreditCode != "" { query = query.Where("company_credit_code LIKE ?", "%"+req.CompanyCreditCode+"%") } if req.ResponsibleName != "" { query = query.Where("responsible_name LIKE ?", "%"+req.ResponsibleName+"%") } if req.UsageCategory != 0 { query = query.Where("usage_category = ?", req.UsageCategory) } if req.IsActive != 0 { query = query.Where("is_active = ?", req.IsActive) } // 获取总条数 err := query.Count(&total).Error if err != nil { return resp, err } // 分页处理 page := req.Page if page <= 0 { page = 1 } pageSize := req.PageSize if pageSize <= 0 { pageSize = 10 } offset := (page - 1) * pageSize // 分页查询 err = query.Order("id desc"). Offset(offset). Limit(pageSize). Find(&list).Error if err != nil { return resp, err } // 构建分页响应 totalPage := (total + int64(pageSize) - 1) / int64(pageSize) if totalPage < 1 { totalPage = 1 } resp = bus_models.SignatureRealnameQueryResp{ List: list, Total: total, Page: page, PageSize: pageSize, TotalPage: totalPage, } return resp, nil } func (s *SmsService) ListContacts(req bus_models.ContactQuery, db *gorm.DB) (bus_models.ContactQueryResp, error) { var resp bus_models.ContactQueryResp var list []bus_models.SmsContact var total int64 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+"%") } if req.PhoneNumber != "" { query = query.Where("phone_number LIKE ?", "%"+req.PhoneNumber+"%") } err := query.Count(&total).Error if err != nil { return resp, err } page := req.Page if page <= 0 { page = 1 } pageSize := req.PageSize if pageSize <= 0 { pageSize = 10 } offset := (page - 1) * pageSize err = query.Order("id desc"). Offset(offset). Limit(pageSize). Find(&list).Error if err != nil { return resp, err } totalPage := (total + int64(pageSize) - 1) / int64(pageSize) if totalPage < 1 { totalPage = 1 } resp = bus_models.ContactQueryResp{ List: list, Total: total, Page: page, PageSize: pageSize, TotalPage: totalPage, } return resp, nil } // AddContact 添加联系人 func (s *SmsService) AddContact(contact bus_models.SmsContact, db *gorm.DB) error { return db.Create(&contact).Error } // EditContact 编辑联系人 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 } // 更新联系人字段 existingContact.Name = contact.Name existingContact.PhoneNumber = contact.PhoneNumber existingContact.Gender = contact.Gender existingContact.Birthday = contact.Birthday existingContact.Company = contact.Company existingContact.Address = contact.Address existingContact.Remark = contact.Remark existingContact.CooperativeName = contact.CooperativeName existingContact.CooperativeNumber = contact.CooperativeNumber return db.Save(&existingContact).Error } // BulkDeleteContacts 批量删除联系人 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 }