1.提交短信管理相关接口;
This commit is contained in:
parent
15c6dda42a
commit
d289f71b24
|
@ -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 "编辑合同信息"
|
||||
|
|
242
app/admin/apis/bus_apis/a_sms_manage.go
Normal file
242
app/admin/apis/bus_apis/a_sms_manage.go
Normal file
|
@ -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
|
||||
}
|
66
app/admin/models/bus_models/m_sms_manage.go
Normal file
66
app/admin/models/bus_models/m_sms_manage.go
Normal file
|
@ -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"` // 短信内容
|
||||
}
|
24
app/admin/router/bus_sms_manage.go
Normal file
24
app/admin/router/bus_sms_manage.go
Normal file
|
@ -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) // 提交发送任务
|
||||
}
|
||||
}
|
|
@ -50,4 +50,7 @@ func businessCheckRoleRouter(r *gin.Engine, authMiddleware *jwtauth.GinJWTMiddle
|
|||
|
||||
// 合同管理
|
||||
registerContractManageRouter(v1, authMiddleware)
|
||||
|
||||
// 短信管理
|
||||
registerSmsManageRouter(v1, authMiddleware)
|
||||
}
|
||||
|
|
218
app/admin/service/bus_service/s_sms_manage.go
Normal file
218
app/admin/service/bus_service/s_sms_manage.go
Normal file
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
|
|
15
common/redisx/redisx.go
Normal file
15
common/redisx/redisx.go
Normal file
|
@ -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,
|
||||
})
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -51,3 +51,10 @@ settings:
|
|||
extend: # 扩展项使用说明
|
||||
demo:
|
||||
name: data
|
||||
cache:
|
||||
redis:
|
||||
addr: 127.0.0.1:6379
|
||||
password: yy@2025
|
||||
db: 1
|
||||
# key存在即可
|
||||
memory: ''
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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: 删除接口管理
|
||||
|
|
6
go.mod
6
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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
102
tools/crypto/aes.go
Normal file
102
tools/crypto/aes.go
Normal file
|
@ -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
|
||||
}
|
47
tools/crypto/rsa.go
Normal file
47
tools/crypto/rsa.go
Normal file
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue
Block a user