From d289f71b243a0adcea660d99b24e01a0a702c2ad Mon Sep 17 00:00:00 2001 From: chenlin Date: Fri, 11 Apr 2025 20:15:45 +0800 Subject: [PATCH] =?UTF-8?q?1.=E6=8F=90=E4=BA=A4=E7=9F=AD=E4=BF=A1=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E7=9B=B8=E5=85=B3=E6=8E=A5=E5=8F=A3=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/apis/bus_apis/a_contract.go | 2 +- app/admin/apis/bus_apis/a_sms_manage.go | 242 ++++++++++++++++++ app/admin/models/bus_models/m_sms_manage.go | 66 +++++ app/admin/router/bus_sms_manage.go | 24 ++ app/admin/router/router.go | 3 + app/admin/service/bus_service/s_sms_manage.go | 218 ++++++++++++++++ cmd/api/server.go | 6 + common/redisx/redisx.go | 15 ++ common/storage/initialize.go | 10 +- config/settings.dev.yml | 7 + docs/admin/admin_docs.go | 44 +++- docs/admin/admin_swagger.json | 44 +++- docs/admin/admin_swagger.yaml | 29 ++- go.mod | 6 + test/gen_test.go | 98 ++++++- tools/crypto/aes.go | 102 ++++++++ tools/crypto/rsa.go | 47 ++++ 17 files changed, 952 insertions(+), 11 deletions(-) create mode 100644 app/admin/apis/bus_apis/a_sms_manage.go create mode 100644 app/admin/models/bus_models/m_sms_manage.go create mode 100644 app/admin/router/bus_sms_manage.go create mode 100644 app/admin/service/bus_service/s_sms_manage.go create mode 100644 common/redisx/redisx.go create mode 100644 tools/crypto/aes.go create mode 100644 tools/crypto/rsa.go diff --git a/app/admin/apis/bus_apis/a_contract.go b/app/admin/apis/bus_apis/a_contract.go index 16243bf..339f255 100644 --- a/app/admin/apis/bus_apis/a_contract.go +++ b/app/admin/apis/bus_apis/a_contract.go @@ -49,7 +49,7 @@ func (e *ContractApi) CreateContract(c *gin.Context) { // EditContract 编辑合同信息 // @Summary 编辑合同信息 -// @Tags 合同管理 +// @Tags 合同管理-V1.0.0 // @Produce json // @Accept json // @Param request body bus_models.EditContractReq true "编辑合同信息" diff --git a/app/admin/apis/bus_apis/a_sms_manage.go b/app/admin/apis/bus_apis/a_sms_manage.go new file mode 100644 index 0000000..c6d6d4a --- /dev/null +++ b/app/admin/apis/bus_apis/a_sms_manage.go @@ -0,0 +1,242 @@ +package bus_apis + +import ( + "encoding/json" + "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" + "github.com/go-admin-team/go-admin-core/sdk/api" + "github.com/google/uuid" + "go-admin/app/admin/models/bus_models" + "go-admin/app/admin/service/bus_service" + "go-admin/tools/app" + "go-admin/tools/crypto" + "io" +) + +type SmsApi struct { + api.Api +} + +// MassImportPhone 导入号码(群发短信) +// @Summary 导入号码(群发短信) +// @Tags 短信管理-V1.0.0 +// @Produce json +// @Accept json +// @Param file body string true "上传excel文件" +// @Success 200 {object} bus_models.MassImportPhoneResp +// @Router /api/v1/sms/mass_import_phone [post] +func (e SmsApi) MassImportPhone(c *gin.Context) { + var smsService bus_service.SmsService + + // 绑定 JSON 数据,获取 secret_key + err := e.MakeContext(c). + MakeOrm(). + MakeService(&smsService.Service). + Errors + if err != nil { + e.Logger.Error(err) + e.Error(500, err, err.Error()) + return + } + + // 获取上传的文件 + file, _, err := c.Request.FormFile("file") + if err != nil { + e.Logger.Error("获取文件失败:", err) + e.Error(400, err, "请上传有效的文件") + return + } + defer file.Close() + + // 读取文件内容并处理 + fileBytes, err := io.ReadAll(file) + if err != nil { + e.Logger.Error("读取文件内容失败:", err) + e.Error(500, err, "读取文件失败") + return + } + + // 使用 SmsService 读取 Excel 文件 + phones, err := smsService.ReadExcelFile(fileBytes) + if err != nil { + e.Error(500, err, "读取文件失败") + return + } + + if len(phones) == 0 { + e.Error(400, nil, "导入的手机号为空") + return + } + + importSerial := uuid.New().String() + + err = smsService.CachePhonesByShard(importSerial, phones) + if err != nil { + e.Error(500, err, "手机号缓存失败") + return + } + + // 返回加密后的数据 + resp := bus_models.MassImportPhoneResp{ + ImportSerialNumber: uuid.New().String(), + } + if len(phones) > bus_models.ShowCount { + resp.List = phones[0:200] + } else { + resp.List = phones + } + + // 将JSON数据转换为字符串 + plainText, err := json.Marshal(resp) + if err != nil { + e.Logger.Error("JSON 序列化失败:", err) + e.Error(500, err, "数据转换失败") + return + } + + // 使用AES加密手机号 + encryptedData, err := crypto.AESEncryptJson(bus_models.AESKey, string(plainText)) + if err != nil { + e.Logger.Error("AES 加密失败:", err) + e.Error(500, err, "加密失败") + return + } + + app.OK(c, encryptedData, "导入成功") + return +} + +func (e SmsApi) SelfImportPhone(c *gin.Context) { + s := bus_service.ProductService{} + var req bus_models.ProductListReq + + err := e.MakeContext(c). + MakeOrm(). + Bind(&req, binding.JSON). + MakeService(&s.Service). + Errors + if err != nil { + e.Logger.Error(err) + e.Error(500, err, err.Error()) + return + } + +} + +func (e SmsApi) FileImportPhone(c *gin.Context) { + s := bus_service.ProductService{} + var req bus_models.ProductListReq + + err := e.MakeContext(c). + MakeOrm(). + Bind(&req, binding.JSON). + MakeService(&s.Service). + Errors + if err != nil { + e.Logger.Error(err) + e.Error(500, err, err.Error()) + return + } + +} + +func (e SmsApi) ExcelImportPhone(c *gin.Context) { + s := bus_service.ProductService{} + var req bus_models.ProductListReq + + err := e.MakeContext(c). + MakeOrm(). + Bind(&req, binding.JSON). + MakeService(&s.Service). + Errors + if err != nil { + e.Logger.Error(err) + e.Error(500, err, err.Error()) + return + } + +} + +func (e SmsApi) ExportMessPhone(c *gin.Context) { + s := bus_service.ProductService{} + var req bus_models.ProductListReq + + err := e.MakeContext(c). + MakeOrm(). + Bind(&req, binding.JSON). + MakeService(&s.Service). + Errors + if err != nil { + e.Logger.Error(err) + e.Error(500, err, err.Error()) + return + } + +} + +func (e SmsApi) SendPreCheck(c *gin.Context) { + s := bus_service.ProductService{} + var req bus_models.ProductListReq + + err := e.MakeContext(c). + MakeOrm(). + Bind(&req, binding.JSON). + MakeService(&s.Service). + Errors + if err != nil { + e.Logger.Error(err) + e.Error(500, err, err.Error()) + return + } + +} + +// SendSms 提交发送任务 +// @Summary 提交发送任务 +// @Tags 短信管理-V1.0.0 +// @Produce json +// @Accept json +// @Param request body bus_models.SendSmsReq true "提交发送任务模型" +// @Success 200 {object} app.Response +// @Router /api/v1/sms/send_sms [post] +func (e SmsApi) SendSms(c *gin.Context) { + smsService := bus_service.SmsService{} + var req bus_models.SendSmsReq + + err := e.MakeContext(c). + MakeOrm(). + Bind(&req, binding.JSON). + MakeService(&smsService.Service). + Errors + if err != nil { + e.Logger.Error(err) + e.Error(500, err, err.Error()) + return + } + + if req.ImportSerialNumber == "" { + e.Error(400, nil, "import_serial_number 不能为空") + return + } + + // 如果还有额外手机号,直接追加到 Redis 缓存列表 + if len(req.PhoneList) > 0 { + err := smsService.AppendPhonesToRedis(req.ImportSerialNumber, req.PhoneList) + if err != nil { + e.Logger.Error("手机号追加失败:", err) + e.Error(500, err, "手机号追加失败") + return + } + } + + // 创建发送任务,使用 redis 缓存中的数据分片处理 + err = smsService.SubmitSmsTaskFromRedis(c.Request.Context(), req.ImportSerialNumber, req.SmsContent) + if err != nil { + e.Logger.Error("任务提交失败:", err) + e.Error(500, err, "短信任务提交失败") + return + } + + app.OK(c, nil, "短信发送任务提交成功") + return +} diff --git a/app/admin/models/bus_models/m_sms_manage.go b/app/admin/models/bus_models/m_sms_manage.go new file mode 100644 index 0000000..53767c4 --- /dev/null +++ b/app/admin/models/bus_models/m_sms_manage.go @@ -0,0 +1,66 @@ +package bus_models + +import ( + "go-admin/app/admin/models" + "time" +) + +const ( + AESKey = "3ca176c2d9d0273695f48c55c3170e32d204e125ac06c050c94e0fcec01679ed" + ShowCount = 100 // 前端展示手机号数量 +) + +type SmsTask struct { + models.Model + + CooperativeNumber string `gorm:"column:cooperative_number"` + CooperativeName string `gorm:"column:cooperative_name"` + BatchID string `gorm:"column:batch_id"` + ImportID string `gorm:"column:import_id"` + SmsContent string `gorm:"column:sms_content"` + SmsContentCost int `gorm:"column:sms_content_cost"` + TotalPhoneCount int `gorm:"column:total_phone_count"` + TotalSmsCount int `gorm:"column:total_sms_count"` + Status int `gorm:"column:status"` + InterceptFailCount int `gorm:"column:intercept_fail_count"` + ChannelFailCount int `gorm:"column:channel_fail_count"` +} + +type SmsTaskBatch struct { + models.Model + + TaskID uint64 `gorm:"column:task_id"` + BatchID string `gorm:"column:batch_id"` + ImportID string `gorm:"column:import_id"` + Num int `gorm:"column:num"` + PhoneCount int `gorm:"column:phone_count"` + SmsCount int `gorm:"column:sms_count"` + Status int `gorm:"column:status"` + InterceptFailCount int `gorm:"column:intercept_fail_count"` + ChannelFailCount int `gorm:"column:channel_fail_count"` +} + +type SmsSendRecord struct { + models.Model + + TaskID uint64 `gorm:"column:task_id"` + TaskBatchID uint64 `gorm:"column:task_batch_id"` + BatchID string `gorm:"column:batch_id"` + CooperativeNumber string `gorm:"column:cooperative_number"` + CooperativeName string `gorm:"column:cooperative_name"` + Phone string `gorm:"column:phone"` + SmsContent string `gorm:"column:sms_content"` + ReceiveTime *time.Time `gorm:"column:receive_time"` + SmsCode string `gorm:"column:sms_code"` +} + +type MassImportPhoneResp struct { + List []string `json:"list"` // 加密后的数据 + ImportSerialNumber string `json:"import_serial_number"` // 导入excel返回的编号 +} + +type SendSmsReq struct { + PhoneList []string `json:"phone_list"` // 手机号码列表 + ImportSerialNumber string `json:"import_serial_number"` // 导入excel返回的编号 + SmsContent string `json:"sms_content"` // 短信内容 +} diff --git a/app/admin/router/bus_sms_manage.go b/app/admin/router/bus_sms_manage.go new file mode 100644 index 0000000..41f7bce --- /dev/null +++ b/app/admin/router/bus_sms_manage.go @@ -0,0 +1,24 @@ +package router + +import ( + "github.com/gin-gonic/gin" + jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth" + "go-admin/app/admin/apis/bus_apis" + "go-admin/common/middleware" +) + +// 需认证的路由代码 +func registerSmsManageRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) { + api := bus_apis.SmsApi{} + + sms := v1.Group("/sms").Use(authMiddleware.MiddlewareFunc()).Use(middleware.AuthCheckRole()) + { + sms.POST("/mass_import_phone", api.MassImportPhone) // 导入号码(群发短信) + sms.POST("/self_import_phone", api.SelfImportPhone) // 导入号码(个性短信) + sms.POST("/file_import_phone", api.FileImportPhone) // 导入号码(文件短信) + sms.POST("/excel_import_phone", api.ExcelImportPhone) // 导入号码(EXCEL短信) + sms.POST("/export_mess_phone", api.ExportMessPhone) // 导出号码(群发短信) + sms.POST("/send_pre_check", api.SendPreCheck) // 短信内容审核 + sms.POST("/send_sms", api.SendSms) // 提交发送任务 + } +} diff --git a/app/admin/router/router.go b/app/admin/router/router.go index 5551a5c..55c1966 100644 --- a/app/admin/router/router.go +++ b/app/admin/router/router.go @@ -50,4 +50,7 @@ func businessCheckRoleRouter(r *gin.Engine, authMiddleware *jwtauth.GinJWTMiddle // 合同管理 registerContractManageRouter(v1, authMiddleware) + + // 短信管理 + registerSmsManageRouter(v1, authMiddleware) } diff --git a/app/admin/service/bus_service/s_sms_manage.go b/app/admin/service/bus_service/s_sms_manage.go new file mode 100644 index 0000000..8eb5511 --- /dev/null +++ b/app/admin/service/bus_service/s_sms_manage.go @@ -0,0 +1,218 @@ +package bus_service + +import ( + "bytes" + "fmt" + "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" + "math/rand" + "strconv" + "time" +) + +const ( + maxPhonesPerShard = 10000 + cacheExpire = time.Hour +) + +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 string, content 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("手机号为空,不能创建任务") + } + + // 计算短信条数(按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: "", // 可根据需要填充 + CooperativeName: "", + BatchID: batchID, + ImportID: serial, + SmsContent: content, + SmsContentCost: contentCost, + TotalPhoneCount: totalPhones, + TotalSmsCount: totalSmsCount, + Status: 0, + InterceptFailCount: 0, + ChannelFailCount: 0, + } + if err := tx.Create(task).Error; err != nil { + return fmt.Errorf("创建短信任务失败: %v", err) + } + + // 插入批次记录 + for i := 1; i <= total; i++ { + batch := &bus_models.SmsTaskBatch{ + TaskID: task.ID, + BatchID: batchID, + ImportID: serial, + Num: i, + PhoneCount: phoneCounts[i-1], + SmsCount: phoneCounts[i-1] * contentCost, + Status: 0, + } + 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 +} diff --git a/cmd/api/server.go b/cmd/api/server.go index bf36f41..4ab918a 100644 --- a/cmd/api/server.go +++ b/cmd/api/server.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "github.com/pkg/errors" + "go-admin/common/redisx" "log" "net/http" "os" @@ -65,6 +66,11 @@ func setup() { database.Setup, storage.Setup, ) + redisx.Init( + config.CacheConfig.Redis.Addr, + config.CacheConfig.Redis.Password, + config.CacheConfig.Redis.DB, + ) //注册监听函数 queue := sdk.Runtime.GetMemoryQueue("") queue.Register(global.LoginLog, models.SaveLoginLog) diff --git a/common/redisx/redisx.go b/common/redisx/redisx.go new file mode 100644 index 0000000..8ef9d67 --- /dev/null +++ b/common/redisx/redisx.go @@ -0,0 +1,15 @@ +package redisx + +import ( + "github.com/redis/go-redis/v9" +) + +var Client *redis.Client + +func Init(addr string, password string, db int) { + Client = redis.NewClient(&redis.Options{ + Addr: addr, + Password: password, + DB: db, + }) +} diff --git a/common/storage/initialize.go b/common/storage/initialize.go index 6fa323d..37bf150 100644 --- a/common/storage/initialize.go +++ b/common/storage/initialize.go @@ -8,6 +8,7 @@ package storage import ( + "github.com/go-admin-team/go-admin-core/storage" "log" "github.com/go-admin-team/go-admin-core/sdk" @@ -15,16 +16,19 @@ import ( "github.com/go-admin-team/go-admin-core/sdk/pkg/captcha" ) +var Redis storage.AdapterCache + // Setup 配置storage组件 func Setup() { //4. 设置缓存 - cacheAdapter, err := config.CacheConfig.Setup() + var err error + Redis, err = config.CacheConfig.Setup() if err != nil { log.Fatalf("cache setup error, %s\n", err.Error()) } - sdk.Runtime.SetCacheAdapter(cacheAdapter) + sdk.Runtime.SetCacheAdapter(Redis) //5. 设置验证码store - captcha.SetStore(captcha.NewCacheStore(cacheAdapter, 600)) + captcha.SetStore(captcha.NewCacheStore(Redis, 600)) //6. 设置队列 if !config.QueueConfig.Empty() { diff --git a/config/settings.dev.yml b/config/settings.dev.yml index 65d450c..3087b09 100644 --- a/config/settings.dev.yml +++ b/config/settings.dev.yml @@ -51,3 +51,10 @@ settings: extend: # 扩展项使用说明 demo: name: data + cache: + redis: + addr: 127.0.0.1:6379 + password: yy@2025 + db: 1 + # key存在即可 + memory: '' diff --git a/docs/admin/admin_docs.go b/docs/admin/admin_docs.go index 80586ad..3c0696a 100644 --- a/docs/admin/admin_docs.go +++ b/docs/admin/admin_docs.go @@ -216,7 +216,7 @@ const docTemplateadmin = `{ "application/json" ], "tags": [ - "合同管理" + "合同管理-V1.0.0" ], "summary": "获取合同下载链接", "parameters": [ @@ -2350,6 +2350,39 @@ const docTemplateadmin = `{ } } }, + "/api/v1/sms/mass_import_phone": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "短信管理-V1.0.0" + ], + "summary": "导入号码(群发短信)", + "parameters": [ + { + "description": "上传excel文件", + "name": "file", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/bus_models.MassImportPhoneResp" + } + } + } + } + }, "/api/v1/sys-api": { "get": { "security": [ @@ -4467,6 +4500,15 @@ const docTemplateadmin = `{ } } }, + "bus_models.MassImportPhoneResp": { + "type": "object", + "properties": { + "list": { + "description": "加密后的数据", + "type": "string" + } + } + }, "bus_models.ProductDetail": { "type": "object", "properties": { diff --git a/docs/admin/admin_swagger.json b/docs/admin/admin_swagger.json index 4b85e15..bab1c0e 100644 --- a/docs/admin/admin_swagger.json +++ b/docs/admin/admin_swagger.json @@ -208,7 +208,7 @@ "application/json" ], "tags": [ - "合同管理" + "合同管理-V1.0.0" ], "summary": "获取合同下载链接", "parameters": [ @@ -2342,6 +2342,39 @@ } } }, + "/api/v1/sms/mass_import_phone": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "短信管理-V1.0.0" + ], + "summary": "导入号码(群发短信)", + "parameters": [ + { + "description": "上传excel文件", + "name": "file", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/bus_models.MassImportPhoneResp" + } + } + } + } + }, "/api/v1/sys-api": { "get": { "security": [ @@ -4459,6 +4492,15 @@ } } }, + "bus_models.MassImportPhoneResp": { + "type": "object", + "properties": { + "list": { + "description": "加密后的数据", + "type": "string" + } + } + }, "bus_models.ProductDetail": { "type": "object", "properties": { diff --git a/docs/admin/admin_swagger.yaml b/docs/admin/admin_swagger.yaml index c6f6feb..27f9c37 100644 --- a/docs/admin/admin_swagger.yaml +++ b/docs/admin/admin_swagger.yaml @@ -675,6 +675,12 @@ definitions: description: 总记录数 type: integer type: object + bus_models.MassImportPhoneResp: + properties: + list: + description: 加密后的数据 + type: string + type: object bus_models.ProductDetail: properties: discount: @@ -2051,7 +2057,7 @@ paths: type: object summary: 获取合同下载链接 tags: - - 合同管理 + - 合同管理-V1.0.0 /api/v1/contract/edit: post: consumes: @@ -3364,6 +3370,27 @@ paths: summary: 设置配置 tags: - 配置管理 + /api/v1/sms/mass_import_phone: + post: + consumes: + - application/json + parameters: + - description: 上传excel文件 + in: body + name: file + required: true + schema: + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/bus_models.MassImportPhoneResp' + summary: 导入号码(群发短信) + tags: + - 短信管理-V1.0.0 /api/v1/sys-api: delete: description: 删除接口管理 diff --git a/go.mod b/go.mod index f58e2f1..1b5df54 100644 --- a/go.mod +++ b/go.mod @@ -29,6 +29,7 @@ require ( github.com/swaggo/gin-swagger v1.6.0 github.com/swaggo/swag v1.16.4 github.com/unrolled/secure v1.17.0 + github.com/xuri/excelize/v2 v2.8.0 golang.org/x/crypto v0.36.0 golang.org/x/net v0.33.0 gorm.io/driver/mysql v1.5.7 @@ -129,6 +130,7 @@ require ( github.com/mitchellh/reflectwalk v1.0.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/mojocn/base64Captcha v1.3.6 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/nsqio/go-nsq v1.1.0 // indirect @@ -139,6 +141,8 @@ require ( github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/redis/go-redis/v9 v9.3.1 // indirect + github.com/richardlehane/mscfb v1.0.4 // indirect + github.com/richardlehane/msoleps v1.0.3 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/satori/go.uuid v1.2.0 // indirect github.com/shamsher31/goimgext v1.0.0 // indirect @@ -153,6 +157,8 @@ require ( github.com/ugorji/go/codec v1.2.12 // indirect github.com/urfave/cli/v2 v2.24.3 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect + github.com/xuri/efp v0.0.0-20230802181842-ad255f2331ca // indirect + github.com/xuri/nfp v0.0.0-20230819163627-dc951e3ffe1a // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.26.0 // indirect diff --git a/test/gen_test.go b/test/gen_test.go index 99a5c74..2421f43 100644 --- a/test/gen_test.go +++ b/test/gen_test.go @@ -1,8 +1,17 @@ package test import ( - //"go-admin/models/tools" - //"os" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "fmt" + "go-admin/app/admin/models/bus_models" + "go-admin/tools/crypto" + "io" + "os" + "testing" //"text/template" ) @@ -25,7 +34,16 @@ func TestGoModelTemplate(t *testing.T) { //defer file.Close() // //_ = t1.Execute(file, tab) - t.Log("") + //t.Log("") + + encryptedText := "Uo++o250R//CB0qbuTH2/DpbIEPDccCzAcXQPB+Yg86KuXil5/BJRioIFNBJkfD0O7CJoV6s/vHp8MoTazEteWmBb8AS31tYxfEfYCvWl6sLTUXhvEQLBOZRjWSXRMlHTF4EXnrio7Ga3UQJ7C9B62lhvLWCG42AK8niTswx0DbgHHCXw1gS7vXq/bhs1K6JlNA1fLKW23SqwaIKJ7dSUdEyUkpv649RXAQK6T1kKoKwh6fqY6+J4H17z1KTDHaVHX11NPmCtZYVWEg8Q10uRM0FqxHd7jTKfMfDlpc/xBacUVhA0QC/VJEkkVs+Bm4ZXi5HghKgRjBiRv1bpvInj+TvkpR83iB7Y5gMwS+1RfdfkZ8pqjMzOuQEDLLRmDIvLyCwYTjZWXcMsO/C1POWg/JyRKAK3kGKkXe1LyLBTSzEDfa8c28LkCNMDgHYW4g9r1bZ5m4H/27/RcQEkT1TRiIHYS0fFqiXt7jcr3GWqT8ES5k/Y6MRheXB2SauiQYueauS6e487cDlzMf245Tw1lkJmn+Yg0Byr2O2IIvx9TGroqstDYWwWbc6NtyLL744fsZW+RZ9N1e41+T3kJUFP++RJWIXvIvZeeVUD+OEgEI=" + // 解密 + decryptedText, err := crypto.AESDecryptJson(bus_models.AESKey, encryptedText) + if err != nil { + fmt.Println("解密失败:", err) + return + } + fmt.Println("解密后的文本:", decryptedText) } func TestGoApiTemplate(t *testing.T) { @@ -43,5 +61,77 @@ func TestGoApiTemplate(t *testing.T) { //defer file.Close() // //_ = t1.Execute(file, tab) - t.Log("") + //t.Log("") + + dir, err := os.Getwd() + if err != nil { + fmt.Println("Error getting current directory:", err) + } + fmt.Println("Current working directory:", dir) + + // 生成 AES 密钥 + aesKey, err := GenerateAESKey() + if err != nil { + fmt.Printf("生成 AES 密钥失败: %v", err) + } + + // 打印 AES 密钥 + fmt.Printf("生成的 AES 密钥: %x\n", aesKey) + + publicKeyPath := "/Users/max/Documents/code/deovo/telco_server/config/sms/public.pem" + + // 使用公钥加密 AES 密钥 + encryptedKey, err := EncryptWithRSA(publicKeyPath, aesKey) + if err != nil { + fmt.Printf("加密 AES 密钥失败: %v", err) + } + + // 打印加密后的 AES 密钥(Base64 编码) + fmt.Printf("加密后的 AES 密钥(Base64 编码): %s\n", encryptedKey) +} + +// GenerateAESKey 生成随机的 AES 密钥 +func GenerateAESKey() ([]byte, error) { + // 生成 256 位(32 字节)的 AES 密钥 + key := make([]byte, 32) + _, err := rand.Read(key) + if err != nil { + return nil, err + } + return key, nil +} + +// EncryptWithRSA 公钥加密 AES 密钥 +func EncryptWithRSA(publicKeyPath string, aesKey []byte) (string, error) { + // 读取公钥文件 + pubFile, err := os.Open(publicKeyPath) + if err != nil { + return "", err + } + defer pubFile.Close() + + // 解析公钥 + pubBytes, err := io.ReadAll(pubFile) + if err != nil { + return "", err + } + + block, _ := pem.Decode(pubBytes) + if block == nil { + return "", fmt.Errorf("failed to parse PEM block containing the public key") + } + + pubKey, err := x509.ParsePKCS1PublicKey(block.Bytes) + if err != nil { + return "", err + } + + // 使用公钥加密 AES 密钥 + encryptedKey, err := rsa.EncryptPKCS1v15(rand.Reader, pubKey, aesKey) + if err != nil { + return "", err + } + + // 返回加密后的密钥(base64 编码) + return base64.StdEncoding.EncodeToString(encryptedKey), nil } diff --git a/tools/crypto/aes.go b/tools/crypto/aes.go new file mode 100644 index 0000000..91aa3cc --- /dev/null +++ b/tools/crypto/aes.go @@ -0,0 +1,102 @@ +package crypto + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "encoding/hex" + "fmt" + "io" +) + +// AESEncryptJson AES加密json函数 +func AESEncryptJson(fixedAESKey string, plainText string) (string, error) { + // 将十六进制字符串转换为字节数组 + key, err := hex.DecodeString(fixedAESKey) + if err != nil { + fmt.Printf("解码 AES 密钥失败: %v", err) + return "", err + } + + // 密钥长度检查,确保是 16、24 或 32 字节 + if len(key) != 16 && len(key) != 24 && len(key) != 32 { + return "", fmt.Errorf("无效的AES密钥长度: %v", len(key)) + } + + // 创建AES块 + block, err := aes.NewCipher(key) + if err != nil { + return "", fmt.Errorf("创建AES块失败: %v", err) + } + + // 使用AES GCM加密 + gcm, err := cipher.NewGCM(block) + if err != nil { + return "", fmt.Errorf("创建GCM模式失败: %v", err) + } + + // 创建随机的Nonce + nonce := make([]byte, gcm.NonceSize()) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return "", fmt.Errorf("生成随机数失败: %v", err) + } + + // 加密数据 + ciphertext := gcm.Seal(nonce, nonce, []byte(plainText), nil) + + // 返回Base64编码的密文 + return base64.StdEncoding.EncodeToString(ciphertext), nil +} + +// AESDecryptJson 解密JSON数据 +func AESDecryptJson(fixedAESKey string, encryptedText string) (string, error) { + // 将十六进制字符串转换为字节数组 + key, err := hex.DecodeString(fixedAESKey) + if err != nil { + fmt.Printf("解码 AES 密钥失败: %v", err) + return "", err + } + + // 密钥长度检查,确保是 16、24 或 32 字节 + if len(key) != 16 && len(key) != 24 && len(key) != 32 { + return "", fmt.Errorf("无效的AES密钥长度: %v", len(key)) + } + + // 创建AES块 + block, err := aes.NewCipher(key) + if err != nil { + return "", fmt.Errorf("创建AES块失败: %v", err) + } + + // 解码Base64加密的文本 + ciphertext, err := base64.StdEncoding.DecodeString(encryptedText) + if err != nil { + return "", fmt.Errorf("解码Base64密文失败: %v", err) + } + + // 获取nonce大小 + gcm, err := cipher.NewGCM(block) + if err != nil { + return "", fmt.Errorf("创建GCM模式失败: %v", err) + } + + nonceSize := gcm.NonceSize() + + // 检查密文长度,确保包含nonce + if len(ciphertext) < nonceSize { + return "", fmt.Errorf("密文过短") + } + + // 提取nonce和密文 + nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:] + + // 解密 + plainText, err := gcm.Open(nil, nonce, ciphertext, nil) + if err != nil { + return "", fmt.Errorf("解密失败: %v", err) + } + + // 将解密后的字节数组转换为字符串并返回 + return string(plainText), nil +} diff --git a/tools/crypto/rsa.go b/tools/crypto/rsa.go new file mode 100644 index 0000000..d348086 --- /dev/null +++ b/tools/crypto/rsa.go @@ -0,0 +1,47 @@ +package crypto + +import ( + "crypto/rsa" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "errors" + "fmt" + "io/ioutil" +) + +// LoadRSAPrivateKeyFromFile 加载私钥 +func LoadRSAPrivateKeyFromFile(filename string) (*rsa.PrivateKey, error) { + privBytes, err := ioutil.ReadFile(filename) + if err != nil { + return nil, fmt.Errorf("无法读取私钥文件: %v", err) + } + + block, _ := pem.Decode(privBytes) + if block == nil || block.Type != "RSA PRIVATE KEY" { + return nil, errors.New("无效的私钥格式") + } + + privKey, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return nil, fmt.Errorf("解析私钥失败: %v", err) + } + + return privKey, nil +} + +// RSADecrypt 使用私钥解密AES密钥 +func RSADecrypt(privKey *rsa.PrivateKey, encryptedKeyBase64 string) ([]byte, error) { + // Base64 解码 + encryptedKey, err := base64.StdEncoding.DecodeString(encryptedKeyBase64) + if err != nil { + return nil, fmt.Errorf("Base64解码失败: %v", err) + } + + // RSA 解密 + decryptedKey, err := rsa.DecryptPKCS1v15(nil, privKey, encryptedKey) + if err != nil { + return nil, fmt.Errorf("RSA解密失败: %v", err) + } + return decryptedKey, nil +}