package controllers import ( "bytes" "crypto/rand" "encoding/json" "fmt" "io" "net/http" "strconv" "strings" "time" "server/models" "server/pkg/jwtutil" beego "github.com/beego/beego/v2/server/web" ) // PlatformSMSController 短信配置(yz_system_sms),兼容旧前端 /platform/sms/* 接口 type PlatformSMSController struct { beego.Controller } func (c *PlatformSMSController) platformClaims() (*jwtutil.Claims, error) { auth := c.Ctx.Request.Header.Get("Authorization") if auth == "" { return nil, fmt.Errorf("未登录") } parts := strings.SplitN(auth, " ", 2) if len(parts) != 2 || parts[0] != "Bearer" { return nil, fmt.Errorf("认证信息格式错误") } claims, err := jwtutil.ParseToken(parts[1]) if err != nil { return nil, fmt.Errorf("无效的token") } if claims.UserType != "platform" { return nil, fmt.Errorf("无权访问") } return claims, nil } func (c *PlatformSMSController) jsonErr(httpStatus, bizCode int, msg string) { c.Ctx.Output.SetStatus(httpStatus) c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg} _ = c.ServeJSON() } // GetSmsInfo GET /platform/sms/info // 返回 data[0],字段兼容 backend_url/api_key 与 backendUrl/apiKey(沿用旧前端) func (c *PlatformSMSController) GetSmsInfo() { if _, err := c.platformClaims(); err != nil { c.jsonErr(401, 401, err.Error()) return } var row models.SystemSMS // 优先默认通道,其次 custom err := models.Orm.QueryTable(new(models.SystemSMS)). Filter("is_default", 1). Filter("status", 1). OrderBy("-weight", "-id"). Limit(1). One(&row) if err != nil { _ = models.Orm.QueryTable(new(models.SystemSMS)). Filter("config_code", "custom"). OrderBy("-id"). Limit(1). One(&row) } backendURL := strings.TrimSpace(row.ApiURL) apiKey := strings.TrimSpace(row.ApiKey) data := []map[string]interface{}{{ "backend_url": backendURL, "api_key": apiKey, "backendUrl": backendURL, "apiKey": apiKey, }} c.Data["json"] = map[string]interface{}{"code": 200, "msg": "获取成功", "data": data} _ = c.ServeJSON() } type smsEditPayload struct { BackendUrl string `json:"backendUrl"` BackendURL string `json:"backend_url"` ApiKey string `json:"apiKey"` APIKey string `json:"api_key"` } // EditSmsInfo POST /platform/sms/editinfo // 将旧前端的 backendUrl/apiKey 落到 yz_system_sms 的 api_url/api_key(写入 config_code=custom) func (c *PlatformSMSController) EditSmsInfo() { if _, err := c.platformClaims(); err != nil { c.jsonErr(401, 401, err.Error()) return } raw, err := io.ReadAll(c.Ctx.Request.Body) if err != nil { c.jsonErr(400, 400, "参数错误") return } var p smsEditPayload if err := json.Unmarshal(raw, &p); err != nil { c.jsonErr(400, 400, "参数错误") return } backendURL := strings.TrimSpace(p.BackendUrl) if backendURL == "" { backendURL = strings.TrimSpace(p.BackendURL) } apiKey := strings.TrimSpace(p.ApiKey) if apiKey == "" { apiKey = strings.TrimSpace(p.APIKey) } if backendURL == "" { c.jsonErr(400, 400, "请输入短信网关地址") return } if apiKey == "" { c.jsonErr(400, 400, "请输入API KEY") return } // 确保只有一个默认:先清空默认,再 upsert custom 为默认 _, _ = models.Orm.QueryTable(new(models.SystemSMS)).Update(map[string]interface{}{"is_default": 0}) var existed models.SystemSMS e := models.Orm.QueryTable(new(models.SystemSMS)).Filter("config_code", "custom").Limit(1).One(&existed) if e == nil && existed.ID > 0 { _, err = models.Orm.QueryTable(new(models.SystemSMS)).Filter("id", existed.ID).Update(map[string]interface{}{ "config_name": "自定义网关", "channel_type": 2, "api_url": backendURL, "api_key": apiKey, "weight": 10, "is_default": 1, "status": 1, }) if err != nil { c.jsonErr(500, 500, "更新失败: "+err.Error()) return } } else { row := &models.SystemSMS{ ConfigCode: "custom", ConfigName: "自定义网关", ChannelType: 2, ApiURL: backendURL, ApiKey: apiKey, ApiSecret: "", SignName: "", TemplateID: "", TestPhone: "", Weight: 10, IsDefault: 1, Status: 1, Remark: "", } if _, err := models.Orm.Insert(row); err != nil { c.jsonErr(500, 500, "更新失败: "+err.Error()) return } } updated := []map[string]interface{}{{ "backend_url": backendURL, "api_key": apiKey, }} c.Data["json"] = map[string]interface{}{"code": 200, "msg": "更新成功", "data": updated} _ = c.ServeJSON() } type smsTestPayload struct { BackendUrl string `json:"backendUrl"` BackendURL string `json:"backend_url"` ApiKey string `json:"apiKey"` APIKey string `json:"api_key"` Tid *uint64 `json:"tid"` Phone string `json:"phone"` Content string `json:"content"` } // SendTestSms POST /platform/sms/sendtest // 调用短信网关入队接口:{backendUrl}/api/v1/business/outbound-tasks,header: X-Api-Key func (c *PlatformSMSController) SendTestSms() { if _, err := c.platformClaims(); err != nil { c.jsonErr(401, 401, err.Error()) return } raw, err := io.ReadAll(c.Ctx.Request.Body) if err != nil { c.jsonErr(400, 400, "参数错误") return } var p smsTestPayload if err := json.Unmarshal(raw, &p); err != nil { c.jsonErr(400, 400, "参数错误") return } phone := strings.TrimSpace(p.Phone) if phone == "" { c.jsonErr(400, 400, "缺少测试手机号") return } if !strings.HasPrefix(phone, "+") { c.jsonErr(400, 400, "请使用国际格式手机号(以 + 开头,后为数字)") return } for _, ch := range phone[1:] { if ch < '0' || ch > '9' { c.jsonErr(400, 400, "请使用国际格式手机号(以 + 开头,后为数字)") return } } backendURL := strings.TrimSpace(p.BackendUrl) if backendURL == "" { backendURL = strings.TrimSpace(p.BackendURL) } apiKey := strings.TrimSpace(p.ApiKey) if apiKey == "" { apiKey = strings.TrimSpace(p.APIKey) } // 兜底:body 未带时从默认配置取 if backendURL == "" || apiKey == "" { var row models.SystemSMS _ = models.Orm.QueryTable(new(models.SystemSMS)). Filter("is_default", 1). Filter("status", 1). OrderBy("-weight", "-id"). Limit(1). One(&row) if backendURL == "" { backendURL = strings.TrimSpace(row.ApiURL) } if apiKey == "" { apiKey = strings.TrimSpace(row.ApiKey) } } if backendURL == "" { c.jsonErr(400, 400, "请先配置短信网关地址 backendUrl") return } if apiKey == "" { c.jsonErr(400, 400, "请先配置短信网关 API KEY") return } content := strings.TrimSpace(p.Content) code := randomDigits6() if content == "" { content = "短信测试验证码:" + code } enqueueURL := strings.TrimRight(backendURL, "/") + "/api/v1/business/outbound-tasks" payload := map[string]interface{}{ "phone": phone, "content": content, } bs, _ := json.Marshal(payload) client := &http.Client{Timeout: 10 * time.Second} req, err := http.NewRequest("POST", enqueueURL, bytes.NewReader(bs)) if err != nil { c.jsonErr(500, 500, "创建请求失败: "+err.Error()) return } req.Header.Set("X-Api-Key", apiKey) req.Header.Set("Content-Type", "application/json; charset=utf-8") req.Header.Set("Accept", "application/json") resp, err := client.Do(req) if err != nil { c.jsonErr(500, 500, "短信网关入队失败: "+err.Error()) return } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) if resp.StatusCode < 200 || resp.StatusCode >= 300 { msg := strings.TrimSpace(string(body)) if msg == "" { msg = resp.Status } c.jsonErr(500, 500, "短信网关入队失败: "+msg) return } bodyStr := string(body) report := strings.TrimSpace(bodyStr) var reportPtr *string if report != "" { reportPtr = &bodyStr } // 网关 HTTP 2xx:平台侧视为「已受理并成功提交」;与前端 tasklist 中 status=3「发送成功」对齐 taskStatus := 3 // 若网关返回 JSON 且含通用状态字段,则优先映射(便于以后网关回传异步状态) var gw map[string]interface{} if json.Unmarshal(body, &gw) == nil { if v, ok := gw["status"]; ok { switch x := v.(type) { case float64: taskStatus = mapGatewayStatus(int(x)) case string: if n, e := strconv.Atoi(strings.TrimSpace(x)); e == nil { taskStatus = mapGatewayStatus(n) } } } } // 写入本地任务表(用于前端列表/对账) now := time.Now() task := &models.SystemSMSTask{ Tid: p.Tid, // 测试可为空 ApiKey: apiKey, Phone: phone, Content: &content, Status: taskStatus, Code: code, ReportRaw: reportPtr, CreateTime: &now, UpdateTime: &now, } taskID, terr := models.Orm.Insert(task) if terr != nil { // 入队已成功,任务写库失败也不影响短信发送,只返回提示 c.Data["json"] = map[string]interface{}{ "code": 200, "msg": "短信测试任务入队成功(任务写库失败)", "data": map[string]interface{}{ "taskId": nil, "code": code, "gatewayResp": json.RawMessage(body), }, } _ = c.ServeJSON() return } c.Data["json"] = map[string]interface{}{ "code": 200, "msg": "短信测试任务入队成功", "data": map[string]interface{}{ "taskId": uint64(taskID), "code": code, "gatewayResp": json.RawMessage(body), }, } _ = c.ServeJSON() } // GetSmsTaskList GET /platform/sms/taskList func (c *PlatformSMSController) GetSmsTaskList() { if _, err := c.platformClaims(); err != nil { c.jsonErr(401, 401, err.Error()) return } statusStr := strings.TrimSpace(c.GetString("status")) phoneKw := strings.TrimSpace(c.GetString("phone")) tidStr := strings.TrimSpace(c.GetString("tid")) qs := models.Orm.QueryTable(new(models.SystemSMSTask)).Filter("delete_time__isnull", true) if statusStr != "" { if st, err := strconv.Atoi(statusStr); err == nil { qs = qs.Filter("status", st) } } if phoneKw != "" { qs = qs.Filter("phone__icontains", phoneKw) } if tidStr != "" { if tid, err := strconv.ParseUint(tidStr, 10, 64); err == nil && tid > 0 { qs = qs.Filter("tid", tid) } } var rows []models.SystemSMSTask _, err := qs.OrderBy("-id").All(&rows) if err != nil { c.jsonErr(500, 500, "获取短信任务列表失败: "+err.Error()) return } list := make([]map[string]interface{}, 0, len(rows)) for i := range rows { item := map[string]interface{}{ "id": rows[i].ID, "api_key": rows[i].ApiKey, "phone": rows[i].Phone, "content": "", "status": rows[i].Status, "code": rows[i].Code, "report_raw": rows[i].ReportRaw, "create_time": "", "update_time": "", } if rows[i].Tid != nil { item["tid"] = *rows[i].Tid } if rows[i].Content != nil { item["content"] = *rows[i].Content } if rows[i].CreateTime != nil { item["create_time"] = rows[i].CreateTime.Format("2006-01-02 15:04:05") } if rows[i].UpdateTime != nil { item["update_time"] = rows[i].UpdateTime.Format("2006-01-02 15:04:05") } list = append(list, item) } c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "list": list} _ = c.ServeJSON() } // EditSmsTask POST /platform/sms/taskEdit/:id func (c *PlatformSMSController) EditSmsTask() { if _, err := c.platformClaims(); err != nil { c.jsonErr(401, 401, err.Error()) return } idStr := c.Ctx.Input.Param(":id") id, err := strconv.ParseUint(idStr, 10, 64) if err != nil || id == 0 { c.jsonErr(400, 400, "无效ID") return } raw, err := io.ReadAll(c.Ctx.Request.Body) if err != nil { c.jsonErr(400, 400, "参数错误") return } var p map[string]interface{} _ = json.Unmarshal(raw, &p) up := map[string]interface{}{} if v, ok := p["status"]; ok { switch x := v.(type) { case float64: up["status"] = int(x) case string: if n, e := strconv.Atoi(strings.TrimSpace(x)); e == nil { up["status"] = n } } } if v, ok := p["report_raw"]; ok { if s, ok := v.(string); ok { up["report_raw"] = s } } if v, ok := p["content"]; ok { if s, ok := v.(string); ok { up["content"] = s } } if len(up) == 0 { c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success"} _ = c.ServeJSON() return } now := time.Now() up["update_time"] = now n, err := models.Orm.QueryTable(new(models.SystemSMSTask)).Filter("id", id).Update(up) if err != nil { c.jsonErr(500, 500, "更新失败: "+err.Error()) return } if n == 0 { c.jsonErr(404, 404, "记录不存在") return } c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success"} _ = c.ServeJSON() } func randomDigits6() string { // 生成 6 位数字字符串 b := make([]byte, 4) if _, err := rand.Read(b); err != nil { return "123456" } n := int(b[0])<<24 | int(b[1])<<16 | int(b[2])<<8 | int(b[3]) if n < 0 { n = -n } code := n%900000 + 100000 return strconv.Itoa(code) } // mapGatewayStatus 将网关侧 status 粗略映射到前端列表:0待发送 1发送中 2失败 3成功 func mapGatewayStatus(st int) int { switch st { case 0: return 0 case 1, 4, 5: return 1 case 2, 6: return 2 case 3: return 3 default: // 网关枚举未约定时:HTTP 已 2xx,按「已成功提交」显示为发送成功 return 3 } }