go-platform/controllers/platform_sms.go
2026-04-01 15:15:21 +08:00

520 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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-tasksheader: 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
}
}