支付,OSS 服务重构完成

This commit is contained in:
GeekMaster
2025-08-24 19:32:45 +08:00
parent 7fb0aad3c7
commit 536b4b8056
57 changed files with 1663 additions and 1358 deletions

View File

@@ -20,9 +20,9 @@ type CaptchaService struct {
client *req.Client
}
func NewCaptchaService(config types.CaptchaConfig) *CaptchaService {
func NewCaptchaService(captchaConfig types.CaptchaConfig) *CaptchaService {
return &CaptchaService{
config: config,
config: captchaConfig,
client: req.C().SetTimeout(10 * time.Second),
}
}

View File

@@ -65,12 +65,6 @@ func (s *ConfigMigrationService) MigrateFromConfig(config *types.AppConfig) erro
return err
}
// 迁移API配置
if err := s.migrateApiConfig(config); err != nil {
logger.Errorf("迁移API配置失败: %v", err)
return err
}
// 标记迁移完成
if err := s.markMigrationCompleted(); err != nil {
logger.Errorf("标记迁移完成失败: %v", err)
@@ -101,58 +95,13 @@ func (s *ConfigMigrationService) markMigrationCompleted() error {
// 迁移支付配置
func (s *ConfigMigrationService) migratePaymentConfig(config *types.AppConfig) error {
// 支付宝配置
alipayConfig := map[string]any{
"enabled": config.AlipayConfig.Enabled,
"sand_box": config.AlipayConfig.SandBox,
"app_id": config.AlipayConfig.AppId,
"private_key": config.AlipayConfig.PrivateKey,
"alipay_public_key": config.AlipayConfig.AlipayPublicKey,
"notify_url": config.AlipayConfig.NotifyURL,
"return_url": config.AlipayConfig.ReturnURL,
}
if err := s.saveConfig("alipay", alipayConfig); err != nil {
return err
}
// 微信支付配置
wechatConfig := map[string]any{
"enabled": config.WechatPayConfig.Enabled,
"app_id": config.WechatPayConfig.AppId,
"mch_id": config.WechatPayConfig.MchId,
"serial_no": config.WechatPayConfig.SerialNo,
"private_key": config.WechatPayConfig.PrivateKey,
"api_v3_key": config.WechatPayConfig.ApiV3Key,
"notify_url": config.WechatPayConfig.NotifyURL,
paymentConfig := types.PaymentConfig{
Alipay: config.AlipayConfig,
Epay: config.GeekPayConfig,
WxPay: config.WechatPayConfig,
}
if err := s.saveConfig("wechat", wechatConfig); err != nil {
return err
}
// 虎皮椒配置
hupiConfig := map[string]any{
"enabled": config.HuPiPayConfig.Enabled,
"app_id": config.HuPiPayConfig.AppId,
"app_secret": config.HuPiPayConfig.AppSecret,
"api_url": config.HuPiPayConfig.ApiURL,
"notify_url": config.HuPiPayConfig.NotifyURL,
"return_url": config.HuPiPayConfig.ReturnURL,
}
if err := s.saveConfig("hupi", hupiConfig); err != nil {
return err
}
// GeekPay配置
geekpayConfig := map[string]any{
"enabled": config.GeekPayConfig.Enabled,
"app_id": config.GeekPayConfig.AppId,
"private_key": config.GeekPayConfig.PrivateKey,
"api_url": config.GeekPayConfig.ApiURL,
"notify_url": config.GeekPayConfig.NotifyURL,
"return_url": config.GeekPayConfig.ReturnURL,
"methods": config.GeekPayConfig.Methods,
}
if err := s.saveConfig("geekpay", geekpayConfig); err != nil {
if err := s.saveConfig(types.ConfigKeyPayment, paymentConfig); err != nil {
return err
}
@@ -161,37 +110,15 @@ func (s *ConfigMigrationService) migratePaymentConfig(config *types.AppConfig) e
// 迁移存储配置
func (s *ConfigMigrationService) migrateStorageConfig(config *types.AppConfig) error {
ossConfig := map[string]any{
"active": config.OSS.Active,
"local": map[string]any{
"base_path": config.OSS.Local.BasePath,
"base_url": config.OSS.Local.BaseURL,
},
"minio": map[string]any{
"endpoint": config.OSS.Minio.Endpoint,
"access_key": config.OSS.Minio.AccessKey,
"access_secret": config.OSS.Minio.AccessSecret,
"bucket": config.OSS.Minio.Bucket,
"use_ssl": config.OSS.Minio.UseSSL,
"domain": config.OSS.Minio.Domain,
},
"qiniu": map[string]any{
"zone": config.OSS.QiNiu.Zone,
"access_key": config.OSS.QiNiu.AccessKey,
"access_secret": config.OSS.QiNiu.AccessSecret,
"bucket": config.OSS.QiNiu.Bucket,
"domain": config.OSS.QiNiu.Domain,
},
"aliyun": map[string]any{
"endpoint": config.OSS.AliYun.Endpoint,
"access_key": config.OSS.AliYun.AccessKey,
"access_secret": config.OSS.AliYun.AccessSecret,
"bucket": config.OSS.AliYun.Bucket,
"sub_dir": config.OSS.AliYun.SubDir,
"domain": config.OSS.AliYun.Domain,
},
ossConfig := types.OSSConfig{
Active: config.OSS.Active,
Local: config.OSS.Local,
Minio: config.OSS.Minio,
QiNiu: config.OSS.QiNiu,
AliYun: config.OSS.AliYun,
}
return s.saveConfig("oss", ossConfig)
return s.saveConfig(types.ConfigKeyOss, ossConfig)
}
// 迁移通信配置
@@ -205,7 +132,7 @@ func (s *ConfigMigrationService) migrateCommunicationConfig(config *types.AppCon
"from": config.SmtpConfig.From,
"password": config.SmtpConfig.Password,
}
if err := s.saveConfig("smtp", smtpConfig); err != nil {
if err := s.saveConfig(types.ConfigKeySmtp, smtpConfig); err != nil {
return err
}
@@ -215,34 +142,17 @@ func (s *ConfigMigrationService) migrateCommunicationConfig(config *types.AppCon
"ali": map[string]any{
"access_key": config.SMS.Ali.AccessKey,
"access_secret": config.SMS.Ali.AccessSecret,
"product": config.SMS.Ali.Product,
"domain": config.SMS.Ali.Domain,
"sign": config.SMS.Ali.Sign,
"code_temp_id": config.SMS.Ali.CodeTempId,
},
"bao": map[string]any{
"username": config.SMS.Bao.Username,
"password": config.SMS.Bao.Password,
"domain": config.SMS.Bao.Domain,
"sign": config.SMS.Bao.Sign,
"code_template": config.SMS.Bao.CodeTemplate,
},
}
return s.saveConfig("sms", smsConfig)
}
// 迁移API配置
func (s *ConfigMigrationService) migrateApiConfig(config *types.AppConfig) error {
apiConfig := map[string]any{
"api_url": config.ApiConfig.ApiURL,
"app_id": config.ApiConfig.AppId,
"token": config.ApiConfig.Token,
"jimeng_config": map[string]any{
"access_key": config.ApiConfig.JimengConfig.AccessKey,
"secret_key": config.ApiConfig.JimengConfig.SecretKey,
},
}
return s.saveConfig("api", apiConfig)
return s.saveConfig(types.ConfigKeySms, smsConfig)
}
// 保存配置到数据库

View File

@@ -0,0 +1,66 @@
package service
import (
"geekai/store/model"
"github.com/go-redis/redis/v8"
"gorm.io/gorm"
)
type DataFixService struct {
db *gorm.DB
redis *redis.Client
}
func NewDataFixService(db *gorm.DB, redis *redis.Client) *DataFixService {
return &DataFixService{db: db, redis: redis}
}
func (s *DataFixService) FixData() {
s.FixColumn()
}
// 字段修正
func (s *DataFixService) FixColumn() {
// 订单字段整理
if s.db.Migrator().HasColumn(&model.Order{}, "pay_type") {
s.db.Migrator().RenameColumn(&model.Order{}, "pay_type", "channel")
}
if !s.db.Migrator().HasColumn(&model.Order{}, "check") {
s.db.Migrator().AddColumn(&model.Order{}, "checked")
}
// 重命名 config 表字段
if s.db.Migrator().HasColumn(&model.Config{}, "config_json") {
s.db.Migrator().RenameColumn(&model.Config{}, "config_json", "value")
}
if s.db.Migrator().HasColumn(&model.Config{}, "marker") {
s.db.Migrator().RenameColumn(&model.Config{}, "marker", "name")
}
if s.db.Migrator().HasIndex(&model.Config{}, "idx_chatgpt_configs_key") {
s.db.Migrator().DropIndex(&model.Config{}, "idx_chatgpt_configs_key")
}
if s.db.Migrator().HasIndex(&model.Config{}, "marker") {
s.db.Migrator().DropIndex(&model.Config{}, "marker")
}
// 手动删除字段
if s.db.Migrator().HasColumn(&model.Order{}, "deleted_at") {
s.db.Migrator().DropColumn(&model.Order{}, "deleted_at")
}
if s.db.Migrator().HasColumn(&model.ChatItem{}, "deleted_at") {
s.db.Migrator().DropColumn(&model.ChatItem{}, "deleted_at")
}
if s.db.Migrator().HasColumn(&model.ChatMessage{}, "deleted_at") {
s.db.Migrator().DropColumn(&model.ChatMessage{}, "deleted_at")
}
if s.db.Migrator().HasColumn(&model.User{}, "chat_config") {
s.db.Migrator().DropColumn(&model.User{}, "chat_config")
}
if s.db.Migrator().HasColumn(&model.ChatModel{}, "category") {
s.db.Migrator().DropColumn(&model.ChatModel{}, "category")
}
if s.db.Migrator().HasColumn(&model.ChatModel{}, "description") {
s.db.Migrator().DropColumn(&model.ChatModel{}, "description")
}
}

View File

@@ -21,7 +21,6 @@ import (
)
type LicenseService struct {
config types.GeekServiceConfig
levelDB *store.LevelDB
license *types.License
urlWhiteList []string
@@ -39,7 +38,6 @@ func NewLicenseService(server *core.AppServer, levelDB *store.LevelDB) *LicenseS
}
logger.Infof("License: %+v", license)
return &LicenseService{
config: server.Config.ApiConfig,
levelDB: levelDB,
license: &license,
machineId: machineId,
@@ -63,7 +61,7 @@ func (s *LicenseService) ActiveLicense(license string, machineId string) error {
Message string `json:"message"`
Data License `json:"data"`
}
apiURL := fmt.Sprintf("%s/%s", s.config.ApiURL, "api/license/active")
apiURL := fmt.Sprintf("%s/%s", types.GeekAPIURL, "api/license/active")
response, err := req.C().R().
SetBody(map[string]string{"license": license, "machine_id": machineId}).
SetSuccessResult(&res).Post(apiURL)
@@ -129,7 +127,7 @@ func (s *LicenseService) fetchLicense() (*types.License, error) {
Message string `json:"message"`
Data License `json:"data"`
}
apiURL := fmt.Sprintf("%s/%s", s.config.ApiURL, "api/license/check")
apiURL := fmt.Sprintf("%s/%s", types.GeekAPIURL, "api/license/check")
response, err := req.C().R().
SetBody(map[string]string{"license": s.license.Key, "machine_id": s.machineId}).
SetSuccessResult(&res).Post(apiURL)
@@ -158,7 +156,7 @@ func (s *LicenseService) fetchUrlWhiteList() ([]string, error) {
Message string `json:"message"`
Data []string `json:"data"`
}
apiURL := fmt.Sprintf("%s/%s", s.config.ApiURL, "api/license/urls")
apiURL := fmt.Sprintf("%s/%s", types.GeekAPIURL, "api/license/urls")
response, err := req.C().R().SetSuccessResult(&res).Get(apiURL)
if err != nil {
return nil, fmt.Errorf("发送请求失败: %v", err)

View File

@@ -28,30 +28,32 @@ type AliYunOss struct {
proxyURL string
}
func NewAliYunOss(appConfig *types.AppConfig) (*AliYunOss, error) {
config := &appConfig.OSS.AliYun
// 创建 OSS 客户端
func NewAliYunOss(sysConfig *types.SystemConfig, appConfig *types.AppConfig) (*AliYunOss, error) {
s := &AliYunOss{
proxyURL: appConfig.ProxyURL,
}
if sysConfig.OSS.Active == AliYun {
err := s.UpdateConfig(&sysConfig.OSS.AliYun)
if err != nil {
logger.Errorf("阿里云OSS初始化失败: %v", err)
}
}
return s, nil
}
func (s *AliYunOss) UpdateConfig(config *types.AliYunOssConfig) error {
client, err := oss.New(config.Endpoint, config.AccessKey, config.AccessSecret)
if err != nil {
return nil, err
return err
}
// 获取存储空间
bucket, err := client.Bucket(config.Bucket)
if err != nil {
return nil, err
return err
}
if config.SubDir == "" {
config.SubDir = "gpt"
}
return &AliYunOss{
config: config,
bucket: bucket,
proxyURL: appConfig.ProxyURL,
}, nil
s.bucket = bucket
s.config = config
return nil
}
func (s AliYunOss) PutFile(ctx *gin.Context, name string) (File, error) {

View File

@@ -25,13 +25,17 @@ type LocalStorage struct {
proxyURL string
}
func NewLocalStorage(config *types.AppConfig) LocalStorage {
return LocalStorage{
config: &config.OSS.Local,
proxyURL: config.ProxyURL,
func NewLocalStorage(sysConfig *types.SystemConfig, appConfig *types.AppConfig) *LocalStorage {
return &LocalStorage{
config: &sysConfig.OSS.Local,
proxyURL: appConfig.ProxyURL,
}
}
func (s *LocalStorage) UpdateConfig(config *types.LocalStorageConfig) {
s.config = config
}
func (s LocalStorage) PutFile(ctx *gin.Context, name string) (File, error) {
file, err := ctx.FormFile(name)
if err != nil {

View File

@@ -29,19 +29,29 @@ type MiniOss struct {
proxyURL string
}
func NewMiniOss(appConfig *types.AppConfig) (MiniOss, error) {
config := &appConfig.OSS.Minio
func NewMiniOss(sysConfig *types.SystemConfig, appConfig *types.AppConfig) (*MiniOss, error) {
s := &MiniOss{proxyURL: appConfig.ProxyURL}
if sysConfig.OSS.Active == Minio {
err := s.UpdateConfig(&sysConfig.OSS.Minio)
if err != nil {
logger.Errorf("MinioOSS初始化失败: %v", err)
}
}
return s, nil
}
func (s *MiniOss) UpdateConfig(config *types.MiniOssConfig) error {
minioClient, err := minio.New(config.Endpoint, &minio.Options{
Creds: credentials.NewStaticV4(config.AccessKey, config.AccessSecret, ""),
Secure: config.UseSSL,
})
if err != nil {
return MiniOss{}, err
return err
}
if config.SubDir == "" {
config.SubDir = "gpt"
}
return MiniOss{config: config, client: minioClient, proxyURL: appConfig.ProxyURL}, nil
s.config = config
s.client = minioClient
return nil
}
func (s MiniOss) PutUrlFile(fileURL string, ext string, useProxy bool) (string, error) {

View File

@@ -29,13 +29,21 @@ type QinNiuOss struct {
mac *qbox.Mac
putPolicy storage.PutPolicy
uploader *storage.FormUploader
manager *storage.BucketManager
bucket *storage.BucketManager
proxyURL string
}
func NewQiNiuOss(appConfig *types.AppConfig) QinNiuOss {
config := &appConfig.OSS.QiNiu
// build storage uploader
func NewQiNiuOss(sysConfig *types.SystemConfig, appConfig *types.AppConfig) *QinNiuOss {
s := &QinNiuOss{
proxyURL: appConfig.ProxyURL,
}
if sysConfig.OSS.Active == QiNiu {
s.UpdateConfig(&sysConfig.OSS.QiNiu)
}
return s
}
func (s *QinNiuOss) UpdateConfig(config *types.QiNiuOssConfig) {
zone, ok := storage.GetRegionByID(storage.RegionID(config.Zone))
if !ok {
zone = storage.ZoneHuanan
@@ -47,19 +55,12 @@ func NewQiNiuOss(appConfig *types.AppConfig) QinNiuOss {
putPolicy := storage.PutPolicy{
Scope: config.Bucket,
}
if config.SubDir == "" {
config.SubDir = "gpt"
}
return QinNiuOss{
config: config,
mac: mac,
putPolicy: putPolicy,
uploader: formUploader,
manager: storage.NewBucketManager(mac, &storeConfig),
proxyURL: appConfig.ProxyURL,
}
s.config = config
s.mac = mac
s.putPolicy = putPolicy
s.uploader = formUploader
s.bucket = storage.NewBucketManager(mac, &storeConfig)
}
func (s QinNiuOss) PutFile(ctx *gin.Context, name string) (File, error) {
// 解析表单
file, err := ctx.FormFile(name)
@@ -147,7 +148,7 @@ func (s QinNiuOss) Delete(fileURL string) error {
objectKey = fileURL
}
return s.manager.Delete(s.config.Bucket, objectKey)
return s.bucket.Delete(s.config.Bucket, objectKey)
}
var _ Uploader = QinNiuOss{}

View File

@@ -10,44 +10,45 @@ package oss
import (
"geekai/core/types"
"strings"
logger2 "geekai/logger"
)
var logger = logger2.GetLogger()
type UploaderManager struct {
handler Uploader
local *LocalStorage
aliyun *AliYunOss
mini *MiniOss
qiniu *QinNiuOss
config *types.OSSConfig
}
func NewUploaderManager(config *types.AppConfig) (*UploaderManager, error) {
active := Local
if config.OSS.Active != "" {
active = strings.ToUpper(config.OSS.Active)
}
var handler Uploader
switch active {
case Local:
handler = NewLocalStorage(config)
break
case Minio:
client, err := NewMiniOss(config)
if err != nil {
return nil, err
}
handler = client
break
case QiNiu:
handler = NewQiNiuOss(config)
break
case AliYun:
client, err := NewAliYunOss(config)
if err != nil {
return nil, err
}
handler = client
break
func NewUploaderManager(sysConfig *types.SystemConfig, local *LocalStorage, aliyun *AliYunOss, mini *MiniOss, qiniu *QinNiuOss) (*UploaderManager, error) {
if sysConfig.OSS.Active == "" {
sysConfig.OSS.Active = Local
}
sysConfig.OSS.Active = strings.ToLower(sysConfig.OSS.Active)
return &UploaderManager{handler: handler}, nil
return &UploaderManager{
config: &sysConfig.OSS,
local: local,
aliyun: aliyun,
mini: mini,
qiniu: qiniu,
}, nil
}
func (m *UploaderManager) GetUploadHandler() Uploader {
return m.handler
switch m.config.Active {
case Local:
return m.local
case AliYun:
return m.aliyun
case Minio:
return m.mini
case QiNiu:
return m.qiniu
}
return m.local
}

View File

@@ -20,109 +20,90 @@ import (
)
type AlipayService struct {
config *types.AlipayConfig
client *alipay.Client
config *types.AlipayConfig
}
var logger = logger2.GetLogger()
func NewAlipayService(appConfig *types.AppConfig) (*AlipayService, error) {
config := appConfig.AlipayConfig
func NewAlipayService(sysConfig *types.SystemConfig) (*AlipayService, error) {
config := sysConfig.Payment.Alipay
if !config.Enabled {
logger.Info("Disabled Alipay service")
return nil, nil
logger.Debug("Disabled Alipay service")
}
service := &AlipayService{config: &config}
if config.Enabled {
err := service.UpdateConfig(&config)
if err != nil {
logger.Errorf("支付宝服务初始化失败: %v", err)
}
}
return service, nil
}
func (s *AlipayService) UpdateConfig(config *types.AlipayConfig) error {
client, err := alipay.NewClient(config.AppId, config.PrivateKey, !config.SandBox)
if err != nil {
return nil, fmt.Errorf("error with initialize alipay service: %v", err)
return fmt.Errorf("error with initialize alipay service: %v", err)
}
return &AlipayService{config: &config, client: client}, nil
s.client = client
s.config = config
if os.Getenv("GEEKAI_DEBUG") == "true" {
logger.Info("Alipay Debug mode is enabled")
client.DebugSwitch = gopay.DebugOn
}
return nil
}
type AlipayParams struct {
OutTradeNo string `json:"out_trade_no"`
Subject string `json:"subject"`
TotalFee string `json:"total_fee"`
ReturnURL string `json:"return_url"`
NotifyURL string `json:"notify_url"`
}
func (s *AlipayService) PayMobile(params AlipayParams) (string, error) {
bm := make(gopay.BodyMap)
bm.Set("subject", params.Subject)
bm.Set("out_trade_no", params.OutTradeNo)
bm.Set("quit_url", params.ReturnURL)
bm.Set("total_amount", params.TotalFee)
bm.Set("product_code", "QUICK_WAP_WAY")
return s.client.SetNotifyUrl(params.NotifyURL).SetReturnUrl(params.ReturnURL).TradeWapPay(context.Background(), bm)
}
func (s *AlipayService) PayPC(params AlipayParams) (string, error) {
func (s *AlipayService) Pay(params PayRequest) (string, error) {
bm := make(gopay.BodyMap)
bm.Set("subject", params.Subject)
bm.Set("out_trade_no", params.OutTradeNo)
bm.Set("total_amount", params.TotalFee)
bm.Set("product_code", "FAST_INSTANT_TRADE_PAY")
return s.client.SetNotifyUrl(params.NotifyURL).SetReturnUrl(params.ReturnURL).TradePagePay(context.Background(), bm)
return s.client.TradeWapPay(context.Background(), bm)
}
func (s *AlipayService) Query(outTradeNo string) (OrderInfo, error) {
bm := make(gopay.BodyMap)
bm.Set("out_trade_no", outTradeNo)
rsp, err := s.client.TradeQuery(context.Background(), bm)
if err != nil {
return OrderInfo{}, fmt.Errorf("error with trade query: %v", err)
}
switch rsp.Response.TradeStatus {
case "TRADE_SUCCESS":
logger.Debugf("支付宝查询订单成功:%+v", rsp.Response)
return OrderInfo{
OutTradeNo: rsp.Response.OutTradeNo,
TradeId: rsp.Response.TradeNo,
Amount: rsp.Response.TotalAmount,
Status: Success,
PayTime: rsp.Response.SendPayDate,
}, nil
case "TRADE_CLOSED":
return OrderInfo{Status: Closed}, nil
default:
return OrderInfo{}, fmt.Errorf("error with trade query: %v", rsp.Response.TradeStatus)
}
}
// TradeVerify 交易验证
func (s *AlipayService) TradeVerify(request *http.Request) NotifyVo {
func (s *AlipayService) TradeVerify(request *http.Request) (OrderInfo, error) {
notifyReq, err := alipay.ParseNotifyToBodyMap(request) // c.Request 是 gin 框架的写法
if err != nil {
return NotifyVo{
Status: Failure,
Message: "error with parse notify request: " + err.Error(),
}
return OrderInfo{}, fmt.Errorf("error with parse notify request: %v", err)
}
_, err = alipay.VerifySignWithCert(s.config.AlipayPublicKey, notifyReq)
if err != nil {
return NotifyVo{
Status: Failure,
Message: "error with verify sign: " + err.Error(),
}
return OrderInfo{}, fmt.Errorf("error with verify sign: %v", err)
}
return s.TradeQuery(request.Form.Get("out_trade_no"))
return s.Query(request.Form.Get("out_trade_no"))
}
func (s *AlipayService) TradeQuery(outTradeNo string) NotifyVo {
bm := make(gopay.BodyMap)
bm.Set("out_trade_no", outTradeNo)
//查询订单
rsp, err := s.client.TradeQuery(context.Background(), bm)
if err != nil {
return NotifyVo{
Status: Failure,
Message: "异步查询验证订单信息发生错误" + outTradeNo + err.Error(),
}
}
if rsp.Response.TradeStatus == "TRADE_SUCCESS" {
return NotifyVo{
Status: Success,
OutTradeNo: rsp.Response.OutTradeNo,
TradeId: rsp.Response.TradeNo,
Amount: rsp.Response.TotalAmount,
Subject: rsp.Response.Subject,
Message: "OK",
}
} else {
return NotifyVo{
Status: Failure,
Message: "异步查询验证订单信息发生错误" + outTradeNo,
}
}
}
func readKey(filename string) (string, error) {
data, err := os.ReadFile(filename)
if err != nil {
return "", err
}
return string(data), nil
}
var _ PayService = (*AlipayService)(nil)

View File

@@ -22,41 +22,30 @@ import (
"time"
)
// GeekPayService Geek 支付服务
type GeekPayService struct {
config *types.GeekPayConfig
// EPayService 支付服务
type EPayService struct {
config *types.EpayConfig
}
func NewJPayService(appConfig *types.AppConfig) *GeekPayService {
return &GeekPayService{
config: &appConfig.GeekPayConfig,
func NewEPayService(sysConfig *types.SystemConfig) *EPayService {
return &EPayService{
config: &sysConfig.Payment.Epay,
}
}
type GeekPayParams struct {
Method string `json:"method"` // 接口类型
Device string `json:"device"` // 设备类型
Type string `json:"type"` // 支付方式
OutTradeNo string `json:"out_trade_no"` // 商户订单号
Name string `json:"name"` // 商品名称
Money string `json:"money"` // 商品金额
ClientIP string `json:"clientip"` //用户IP地址
SubOpenId string `json:"sub_openid"` // 微信用户 openid仅小程序支付需要
SubAppId string `json:"sub_appid"` // 小程序 AppId仅小程序支付需要
NotifyURL string `json:"notify_url"`
ReturnURL string `json:"return_url"`
func (s *EPayService) UpdateConfig(config *types.EpayConfig) {
s.config = config
}
// Pay 支付订单
func (s *GeekPayService) Pay(params GeekPayParams) (*GeekPayResp, error) {
func (s *EPayService) Pay(params PayRequest) (string, error) {
p := map[string]string{
"pid": s.config.AppId,
//"method": params.Method,
"pid": s.config.AppId,
"device": params.Device,
"type": params.Type,
"type": params.PayWay,
"out_trade_no": params.OutTradeNo,
"name": params.Name,
"money": params.Money,
"name": params.Subject,
"money": params.TotalFee,
"clientip": params.ClientIP,
"notify_url": params.NotifyURL,
"return_url": params.ReturnURL,
@@ -64,10 +53,21 @@ func (s *GeekPayService) Pay(params GeekPayParams) (*GeekPayResp, error) {
}
p["sign"] = s.Sign(p)
p["sign_type"] = "MD5"
return s.sendRequest(s.config.ApiURL, p)
resp, err := s.sendRequest(s.config.ApiURL, p)
if err != nil {
return "", err
}
if resp.Code != 1 {
return "", errors.New(resp.Msg)
}
if resp.PayURL != "" {
return resp.PayURL, nil
} else {
return resp.QrCode, nil
}
}
func (s *GeekPayService) Sign(params map[string]string) string {
func (s *EPayService) Sign(params map[string]string) string {
// 按字母顺序排序参数
var keys []string
for k := range params {
@@ -100,7 +100,7 @@ type GeekPayResp struct {
UrlScheme string `json:"urlscheme"` // 小程序跳转支付链接
}
func (s *GeekPayService) sendRequest(endpoint string, params map[string]string) (*GeekPayResp, error) {
func (s *EPayService) sendRequest(endpoint string, params map[string]string) (*GeekPayResp, error) {
form := url.Values{}
for k, v := range params {
form.Add(k, v)
@@ -137,3 +137,61 @@ func (s *GeekPayService) sendRequest(endpoint string, params map[string]string)
}
return &r, nil
}
func (s *EPayService) Query(outTradeNo string) (OrderInfo, error) {
params := url.Values{}
params.Set("act", "order")
params.Set("pid", s.config.AppId)
params.Set("key", s.config.PrivateKey)
params.Set("out_trade_no", outTradeNo)
apiURL := fmt.Sprintf("%s/api.php?%s", s.config.ApiURL, params.Encode())
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client := &http.Client{Transport: tr}
resp, err := client.Get(apiURL)
if err != nil {
return OrderInfo{}, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return OrderInfo{}, err
}
logger.Debugf(string(body))
var result struct {
Code int `json:"code"`
Msg string `json:"msg"`
Status string `json:"status"`
Name string `json:"name"`
Money string `json:"money"`
EndTime string `json:"endtime"`
TradeNo string `json:"trade_no"`
}
if err := json.Unmarshal(body, &result); err != nil {
return OrderInfo{}, errors.New("订单查询响应解析失败")
}
if result.Code != 1 {
return OrderInfo{}, errors.New(result.Msg)
}
logger.Debugf("订单信息:%+v", result)
orderInfo := OrderInfo{
OutTradeNo: outTradeNo,
TradeId: result.TradeNo,
Amount: result.Money,
PayTime: result.EndTime,
}
if result.Status == "1" {
orderInfo.Status = Success
} else {
orderInfo.Status = Failure
}
return orderInfo, nil
}
var _ PayService = (*EPayService)(nil)

View File

@@ -1,171 +0,0 @@
package payment
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
// * Use of this source code is governed by a Apache-2.0 license
// * that can be found in the LICENSE file.
// * @Author yangjian102621@163.com
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import (
"crypto/md5"
"encoding/hex"
"errors"
"fmt"
"geekai/core/types"
"geekai/utils"
"io"
"net/http"
"net/url"
"sort"
"strconv"
"strings"
"time"
)
type HuPiPayService struct {
appId string
appSecret string
apiURL string
}
func NewHuPiPay(config *types.AppConfig) *HuPiPayService {
return &HuPiPayService{
appId: config.HuPiPayConfig.AppId,
appSecret: config.HuPiPayConfig.AppSecret,
apiURL: config.HuPiPayConfig.ApiURL,
}
}
type HuPiPayParams struct {
AppId string `json:"appid"`
Version string `json:"version"`
TradeOrderId string `json:"trade_order_id"`
TotalFee string `json:"total_fee"`
Title string `json:"title"`
NotifyURL string `json:"notify_url"`
ReturnURL string `json:"return_url"`
WapName string `json:"wap_name"`
CallbackURL string `json:"callback_url"`
Time string `json:"time"`
NonceStr string `json:"nonce_str"`
Type string `json:"type"`
WapUrl string `json:"wap_url"`
}
type HuPiPayResp struct {
Openid interface{} `json:"openid"`
UrlQrcode string `json:"url_qrcode"`
URL string `json:"url"`
ErrCode int `json:"errcode"`
ErrMsg string `json:"errmsg,omitempty"`
}
// Pay 执行支付请求操作
func (s *HuPiPayService) Pay(params HuPiPayParams) (HuPiPayResp, error) {
data := url.Values{}
simple := strconv.FormatInt(time.Now().Unix(), 10)
params.AppId = s.appId
params.Time = simple
params.NonceStr = simple
encode := utils.JsonEncode(params)
m := make(map[string]string)
_ = utils.JsonDecode(encode, &m)
for k, v := range m {
data.Add(k, fmt.Sprintf("%v", v))
}
// 生成签名
data.Add("hash", s.Sign(data))
// 发送支付请求
apiURL := fmt.Sprintf("%s/payment/do.html", s.apiURL)
resp, err := http.PostForm(apiURL, data)
if err != nil {
return HuPiPayResp{}, fmt.Errorf("error with requst api: %v", err)
}
defer resp.Body.Close()
all, err := io.ReadAll(resp.Body)
if err != nil {
return HuPiPayResp{}, fmt.Errorf("error with reading response: %v", err)
}
var res HuPiPayResp
err = utils.JsonDecode(string(all), &res)
if err != nil {
return HuPiPayResp{}, fmt.Errorf("error with decode payment result: %v", err)
}
if res.ErrCode != 0 {
return HuPiPayResp{}, fmt.Errorf("error with generate pay url: %s", res.ErrMsg)
}
return res, nil
}
// Sign 签名方法
func (s *HuPiPayService) Sign(params url.Values) string {
params.Del(`Sign`)
var keys = make([]string, 0, 0)
for key := range params {
if params.Get(key) != `` {
keys = append(keys, key)
}
}
sort.Strings(keys)
var pList = make([]string, 0, 0)
for _, key := range keys {
var value = strings.TrimSpace(params.Get(key))
if len(value) > 0 {
pList = append(pList, key+"="+value)
}
}
var src = strings.Join(pList, "&")
src += s.appSecret
md5bs := md5.Sum([]byte(src))
return hex.EncodeToString(md5bs[:])
}
// Check 校验订单状态
func (s *HuPiPayService) Check(outTradeNo string) error {
data := url.Values{}
data.Add("appid", s.appId)
data.Add("out_trade_order", outTradeNo)
stamp := strconv.FormatInt(time.Now().Unix(), 10)
data.Add("time", stamp)
data.Add("nonce_str", stamp)
data.Add("hash", s.Sign(data))
apiURL := fmt.Sprintf("%s/payment/query.html", s.apiURL)
resp, err := http.PostForm(apiURL, data)
if err != nil {
return fmt.Errorf("error with http reqeust: %v", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("error with reading response: %v", err)
}
var r struct {
ErrCode int `json:"errcode"`
Data struct {
Status string `json:"status"`
OpenOrderId string `json:"open_order_id"`
} `json:"data,omitempty"`
ErrMsg string `json:"errmsg"`
Hash string `json:"hash"`
}
err = utils.JsonDecode(string(body), &r)
if err != nil {
return fmt.Errorf("error with decode response: %v", err)
}
if r.ErrCode == 0 && r.Data.Status == "OD" {
return nil
} else {
logger.Debugf("%+v", r)
return errors.New("order not paid" + r.ErrMsg)
}
}

View File

@@ -0,0 +1,54 @@
package payment
// 支付渠道定义
const PayChannelAL = "alipay" // 支付宝
const PayChannelWX = "wxpay" // 微信支付
const PayChannelEpay = "epay" // 易支付
// 支付方式
const PayWayAL = "alipay"
const PayWayWX = "wxpay"
const (
Success = 0
Failure = 1
Closed = 2
)
type PayRequest struct {
OutTradeNo string // 商户订单号
Subject string // 商品名称
TotalFee string // 商品金额
ReturnURL string // 回调地址
NotifyURL string // 回调地址
// 易支付专有参数
Method string // 接口类型
Device string // 设备类型
PayWay string // 支付方式
ClientIP string //用户IP地址
OpenID string // 用户openid
}
type OrderInfo struct {
Mchid string // 商户号
OutTradeNo string // 商户订单号
TradeId string // 交易号
Amount string // 金额
Status int // 状态 0: 未支付 1: 已支付 2: 已关闭
PayTime string // 完成支付时间
}
func (o OrderInfo) Closed() bool {
return o.Status == Closed
}
func (o OrderInfo) Success() bool {
return o.Status == Success
}
type PayService interface {
Pay(params PayRequest) (string, error) // 生成支付链接
Query(outTradeNo string) (OrderInfo, error) // 查询订单
}

View File

@@ -1,19 +0,0 @@
package payment
type NotifyVo struct {
Status int
OutTradeNo string // 商户订单号
TradeId string // 交易ID
Amount string // 交易金额
Message string
Subject string
}
func (v NotifyVo) Success() bool {
return v.Status == Success
}
const (
Success = 0
Failure = 1
)

View File

@@ -1,141 +0,0 @@
package payment
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
// * Use of this source code is governed by a Apache-2.0 license
// * that can be found in the LICENSE file.
// * @Author yangjian102621@163.com
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import (
"context"
"fmt"
"geekai/core/types"
"net/http"
"time"
"github.com/go-pay/gopay"
"github.com/go-pay/gopay/wechat/v3"
)
type WechatPayService struct {
config *types.WechatPayConfig
client *wechat.ClientV3
}
func NewWechatService(appConfig *types.AppConfig) (*WechatPayService, error) {
config := appConfig.WechatPayConfig
if !config.Enabled {
logger.Info("Disabled WechatPay service")
return nil, nil
}
client, err := wechat.NewClientV3(config.MchId, config.SerialNo, config.ApiV3Key, config.PrivateKey)
if err != nil {
return nil, fmt.Errorf("error with initialize WechatPay service: %v", err)
}
err = client.AutoVerifySign()
if err != nil {
return nil, fmt.Errorf("error with autoVerifySign: %v", err)
}
//client.DebugSwitch = gopay.DebugOn
return &WechatPayService{config: &config, client: client}, nil
}
type WechatPayParams struct {
OutTradeNo string `json:"out_trade_no"`
TotalFee int `json:"total_fee"`
Subject string `json:"subject"`
ClientIP string `json:"client_ip"`
ReturnURL string `json:"return_url"`
NotifyURL string `json:"notify_url"`
}
func (s *WechatPayService) PayUrlNative(params WechatPayParams) (string, error) {
expire := time.Now().Add(10 * time.Minute).Format(time.RFC3339)
// 初始化 BodyMap
bm := make(gopay.BodyMap)
bm.Set("appid", s.config.AppId).
Set("mchid", s.config.MchId).
Set("description", params.Subject).
Set("out_trade_no", params.OutTradeNo).
Set("time_expire", expire).
Set("notify_url", params.NotifyURL).
SetBodyMap("amount", func(bm gopay.BodyMap) {
bm.Set("total", params.TotalFee).
Set("currency", "CNY")
})
wxRsp, err := s.client.V3TransactionNative(context.Background(), bm)
if err != nil {
return "", fmt.Errorf("error with client v3 transaction Native: %v", err)
}
if wxRsp.Code != wechat.Success {
return "", fmt.Errorf("error status with generating pay url: %v", wxRsp.Error)
}
return wxRsp.Response.CodeUrl, nil
}
func (s *WechatPayService) PayUrlH5(params WechatPayParams) (string, error) {
expire := time.Now().Add(10 * time.Minute).Format(time.RFC3339)
// 初始化 BodyMap
bm := make(gopay.BodyMap)
bm.Set("appid", s.config.AppId).
Set("mchid", s.config.MchId).
Set("description", params.Subject).
Set("out_trade_no", params.OutTradeNo).
Set("time_expire", expire).
Set("notify_url", params.NotifyURL).
SetBodyMap("amount", func(bm gopay.BodyMap) {
bm.Set("total", params.TotalFee).
Set("currency", "CNY")
}).
SetBodyMap("scene_info", func(bm gopay.BodyMap) {
bm.Set("payer_client_ip", params.ClientIP).
SetBodyMap("h5_info", func(bm gopay.BodyMap) {
bm.Set("type", "Wap")
})
})
wxRsp, err := s.client.V3TransactionH5(context.Background(), bm)
if err != nil {
return "", fmt.Errorf("error with client v3 transaction H5: %v", err)
}
if wxRsp.Code != wechat.Success {
return "", fmt.Errorf("error with generating pay url: %v", wxRsp.Error)
}
return wxRsp.Response.H5Url, nil
}
type NotifyResponse struct {
Code string `json:"code"`
Message string `xml:"message"`
}
// TradeVerify 交易验证
func (s *WechatPayService) TradeVerify(request *http.Request) NotifyVo {
notifyReq, err := wechat.V3ParseNotify(request)
if err != nil {
return NotifyVo{Status: 1, Message: fmt.Sprintf("error with client v3 parse notify: %v", err)}
}
// TODO: 这里验签程序有 Bug一直报错crypto/rsa: verification error先暂时取消验签
//err = notifyReq.VerifySignByPK(s.client.WxPublicKey())
//if err != nil {
// return fmt.Errorf("error with client v3 verify sign: %v", err)
//}
// 解密支付密文,验证订单信息
result, err := notifyReq.DecryptPayCipherText(s.config.ApiV3Key)
if err != nil {
return NotifyVo{Status: Failure, Message: fmt.Sprintf("error with client v3 decrypt: %v", err)}
}
return NotifyVo{
Status: Success,
OutTradeNo: result.OutTradeNo,
TradeId: result.TransactionId,
Amount: fmt.Sprintf("%.2f", float64(result.Amount.Total)/100),
}
}

View File

@@ -0,0 +1,216 @@
package payment
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
// * Use of this source code is governed by a Apache-2.0 license
// * that can be found in the LICENSE file.
// * @Author yangjian102621@163.com
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import (
"context"
"fmt"
"geekai/core/types"
"geekai/utils"
"net/http"
"os"
"time"
"github.com/go-pay/gopay"
"github.com/go-pay/gopay/wechat/v3"
)
type WxPayService struct {
config *types.WxPayConfig
client *wechat.ClientV3
}
func NewWxpayService(sysConfig *types.SystemConfig) (*WxPayService, error) {
config := sysConfig.Payment.WxPay
if !config.Enabled {
logger.Debug("Disabled WechatPay service")
}
service := &WxPayService{config: &config}
if config.Enabled {
err := service.UpdateConfig(&config)
if err != nil {
logger.Errorf("微信支付服务初始化失败: %v", err)
}
}
return service, nil
}
func (s *WxPayService) UpdateConfig(config *types.WxPayConfig) error {
client, err := wechat.NewClientV3(config.MchId, config.SerialNo, config.ApiV3Key, config.PrivateKey)
if err != nil {
return fmt.Errorf("error with initialize WechatPay service: %v", err)
}
err = client.AutoVerifySign()
if err != nil {
return fmt.Errorf("error with autoVerifySign: %v", err)
}
s.client = client
if os.Getenv("GEEKAI_DEBUG") == "true" {
logger.Info("WechatPay Debug mode is enabled")
client.DebugSwitch = gopay.DebugOn
}
s.config = config
return nil
}
func (s *WxPayService) Pay(params PayRequest) (string, error) {
expire := time.Now().Add(10 * time.Minute).Format(time.RFC3339)
// 初始化 BodyMap
bm := make(gopay.BodyMap)
bm.Set("appid", s.config.AppId).
Set("mchid", s.config.MchId).
Set("description", params.Subject).
Set("out_trade_no", params.OutTradeNo).
Set("time_expire", expire).
Set("notify_url", params.NotifyURL).
SetBodyMap("amount", func(bm gopay.BodyMap) {
bm.Set("total", utils.IntValue(params.TotalFee, 0)).
Set("currency", "CNY")
})
if params.Device == "mobile" {
bm.SetBodyMap("scene_info", func(bm gopay.BodyMap) {
bm.Set("payer_client_ip", params.ClientIP)
}).SetBodyMap("payer", func(bm gopay.BodyMap) {
bm.Set("openid", params.OpenID)
})
wxRsp, err := s.client.V3TransactionJsapi(context.Background(), bm)
if err != nil {
return "", fmt.Errorf("error with client v3 transaction Jsapi: %v", err)
}
if wxRsp.Code != wechat.Success {
return "", fmt.Errorf("error status with generating pay url: %v", wxRsp.Error)
}
return wxRsp.Response.PrepayId, nil
} else if params.Device == "pc" {
wxRsp, err := s.client.V3TransactionNative(context.Background(), bm)
if err != nil {
return "", fmt.Errorf("error with client v3 transaction Native: %v", err)
}
if wxRsp.Code != wechat.Success {
return "", fmt.Errorf("error status with generating pay url: %v", wxRsp.Error)
}
return wxRsp.Response.CodeUrl, nil
}
return "", nil
}
func (s *WxPayService) Query(outTradeNo string) (OrderInfo, error) {
wxRsp, err := s.client.V3TransactionQueryOrder(context.Background(), wechat.OutTradeNo, outTradeNo)
if err != nil {
return OrderInfo{}, fmt.Errorf("error with client v3 transaction query: %v", err)
}
if wxRsp.Code != wechat.Success {
return OrderInfo{}, fmt.Errorf("error status with querying order: %v", wxRsp.Error)
}
if wxRsp.Response.TradeState == "CLOSED" {
return OrderInfo{Status: Closed}, nil
}
orderInfo := OrderInfo{
OutTradeNo: wxRsp.Response.OutTradeNo,
TradeId: wxRsp.Response.TransactionId,
Amount: fmt.Sprintf("%d", wxRsp.Response.Amount.Total/100),
PayTime: wxRsp.Response.SuccessTime,
}
if wxRsp.Response.TradeState == "SUCCESS" {
orderInfo.Status = Success
} else {
orderInfo.Status = Failure
}
return orderInfo, nil
}
// TradeVerify 交易验证
func (s *WxPayService) TradeVerify(request *http.Request) (OrderInfo, error) {
notifyReq, err := wechat.V3ParseNotify(request)
if err != nil {
return OrderInfo{}, fmt.Errorf("error with client v3 parse notify: %v", err)
}
// 解密支付密文,验证订单信息
result, err := notifyReq.DecryptPayCipherText(s.config.ApiV3Key)
if err != nil {
return OrderInfo{}, fmt.Errorf("error with client v3 decrypt: %v", err)
}
return OrderInfo{
Status: Success,
OutTradeNo: result.OutTradeNo,
TradeId: result.TransactionId,
Amount: fmt.Sprintf("%.2f", float64(result.Amount.Total)/100),
PayTime: result.SuccessTime,
}, nil
}
// func (s *WechatPayService) PayUrlNative(params WechatPayParams) (string, error) {
// expire := time.Now().Add(10 * time.Minute).Format(time.RFC3339)
// // 初始化 BodyMap
// bm := make(gopay.BodyMap)
// bm.Set("appid", s.config.AppId).
// Set("mchid", s.config.MchId).
// Set("description", params.Subject).
// Set("out_trade_no", params.OutTradeNo).
// Set("time_expire", expire).
// Set("notify_url", params.NotifyURL).
// SetBodyMap("amount", func(bm gopay.BodyMap) {
// bm.Set("total", params.TotalFee).
// Set("currency", "CNY")
// })
// wxRsp, err := s.client.V3TransactionNative(context.Background(), bm)
// if err != nil {
// return "", fmt.Errorf("error with client v3 transaction Native: %v", err)
// }
// if wxRsp.Code != wechat.Success {
// return "", fmt.Errorf("error status with generating pay url: %v", wxRsp.Error)
// }
// return wxRsp.Response.CodeUrl, nil
// }
// func (s *WechatPayService) PayUrlH5(params WechatPayParams) (string, error) {
// expire := time.Now().Add(10 * time.Minute).Format(time.RFC3339)
// // 初始化 BodyMap
// bm := make(gopay.BodyMap)
// bm.Set("appid", s.config.AppId).
// Set("mchid", s.config.MchId).
// Set("description", params.Subject).
// Set("out_trade_no", params.OutTradeNo).
// Set("time_expire", expire).
// Set("notify_url", params.NotifyURL).
// SetBodyMap("amount", func(bm gopay.BodyMap) {
// bm.Set("total", params.TotalFee).
// Set("currency", "CNY")
// }).
// SetBodyMap("scene_info", func(bm gopay.BodyMap) {
// bm.Set("payer_client_ip", params.ClientIP).
// SetBodyMap("h5_info", func(bm gopay.BodyMap) {
// bm.Set("type", "Wap")
// })
// })
// wxRsp, err := s.client.V3TransactionH5(context.Background(), bm)
// if err != nil {
// return "", fmt.Errorf("error with client v3 transaction H5: %v", err)
// }
// if wxRsp.Code != wechat.Success {
// return "", fmt.Errorf("error with generating pay url: %v", wxRsp.Error)
// }
// return wxRsp.Response.H5Url, nil
// }
// type NotifyResponse struct {
// Code string `json:"code"`
// Message string `xml:"message"`
// }
var _ PayService = (*WxPayService)(nil)

View File

@@ -10,36 +10,57 @@ package sms
import (
"fmt"
"geekai/core/types"
"github.com/aliyun/alibaba-cloud-sdk-go/services/dysmsapi"
)
type AliYunSmsService struct {
config *types.SmsConfigAli
client *dysmsapi.Client
domain string
zoneId string
}
func NewAliYunSmsService(appConfig *types.AppConfig) (*AliYunSmsService, error) {
config := &appConfig.SMS.Ali
// 创建阿里云短信客户端
func NewAliYunSmsService(sysConfig *types.SystemConfig) (*AliYunSmsService, error) {
config := &sysConfig.SMS.Ali
domain := "dysmsapi.aliyuncs.com"
zoneId := "cn-hangzhou"
s := AliYunSmsService{
config: config,
domain: domain,
zoneId: zoneId,
}
if sysConfig.SMS.Active == Ali {
err := s.UpdateConfig(config)
if err != nil {
logger.Errorf("阿里云短信初始化失败: %v", err)
}
}
return &s, nil
}
func (s *AliYunSmsService) UpdateConfig(config *types.SmsConfigAli) error {
client, err := dysmsapi.NewClientWithAccessKey(
"cn-hangzhou",
s.zoneId,
config.AccessKey,
config.AccessSecret)
if err != nil {
return nil, fmt.Errorf("failed to create client: %v", err)
return fmt.Errorf("failed to create client: %v", err)
}
return &AliYunSmsService{
config: config,
client: client,
}, nil
s.client = client
s.config = config
return nil
}
func (s *AliYunSmsService) SendVerifyCode(mobile string, code int) error {
if s.client == nil {
return fmt.Errorf("阿里云短信服务未初始化")
}
// 创建短信请求并设置参数
request := dysmsapi.CreateSendSmsRequest()
request.Scheme = "https"
request.Domain = s.config.Domain
request.Domain = s.domain
request.PhoneNumbers = mobile
request.SignName = s.config.Sign
request.TemplateCode = s.config.CodeTempId

View File

@@ -20,19 +20,20 @@ import (
type BaoSmsService struct {
config *types.SmsConfigBao
domain string
}
func NewSmsBaoSmsService(appConfig *types.AppConfig) *BaoSmsService {
config := appConfig.SMS.Bao
if config.Domain == "" { // use default domain
config.Domain = "api.smsbao.com"
logger.Infof("Using default domain for SMS-BAO: %s", config.Domain)
}
func NewBaoSmsService(sysConfig *types.SystemConfig) *BaoSmsService {
return &BaoSmsService{
config: &config,
config: &sysConfig.SMS.Bao,
domain: "api.smsbao.com",
}
}
func (s *BaoSmsService) UpdateConfig(config *types.SmsConfigBao) {
s.config = config
}
var errMsg = map[string]string{
"0": "短信发送成功",
"-1": "参数不全",
@@ -56,7 +57,7 @@ func (s *BaoSmsService) SendVerifyCode(mobile string, code int) error {
params.Set("m", mobile)
params.Set("c", content)
apiURL := fmt.Sprintf("https://%s/sms?%s", s.config.Domain, params.Encode())
apiURL := fmt.Sprintf("https://%s/sms?%s", s.domain, params.Encode())
response, err := http.Get(apiURL)
if err != nil {
return err

View File

@@ -10,37 +10,32 @@ package sms
import (
"geekai/core/types"
logger2 "geekai/logger"
"strings"
)
type ServiceManager struct {
handler Service
type SmsManager struct {
aliyun *AliYunSmsService
bao *BaoSmsService
active string
}
var logger = logger2.GetLogger()
func NewSendServiceManager(config *types.AppConfig) (*ServiceManager, error) {
active := Ali
if config.SMS.Active != "" {
active = strings.ToUpper(config.SMS.Active)
}
var handler Service
switch active {
case Ali:
client, err := NewAliYunSmsService(config)
if err != nil {
return nil, err
}
handler = client
break
case Bao:
handler = NewSmsBaoSmsService(config)
break
}
func NewSmsManager(sysConfig *types.SystemConfig, aliyun *AliYunSmsService, bao *BaoSmsService) (*SmsManager, error) {
return &ServiceManager{handler: handler}, nil
return &SmsManager{
active: sysConfig.SMS.Active,
aliyun: aliyun,
bao: bao,
}, nil
}
func (m *ServiceManager) GetService() Service {
return m.handler
func (m *SmsManager) GetService() Service {
if m.active == Ali {
return m.aliyun
}
return m.bao
}
func (m *SmsManager) SetActive(active string) {
m.active = active
}

View File

@@ -0,0 +1,109 @@
package service
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
// * Use of this source code is governed by a Apache-2.0 license
// * that can be found in the LICENSE file.
// * @Author yangjian102621@163.com
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import (
"context"
"errors"
"fmt"
"geekai/core/types"
"geekai/utils"
"time"
"github.com/go-redis/redis/v8"
"github.com/imroc/req/v3"
)
type WxLoginService struct {
config types.WxLoginConfig
client *req.Client
redisClient *redis.Client
}
const loginStateKeyPrefix = "wx_login_state/"
type LoginStatus struct {
Status string `json:"status"`
OpenID string `json:"openid"`
Token string `json:"token"`
}
const (
LoginStatusPending = "pending"
LoginStatusSuccess = "success"
LoginStatusExpired = "expired" // 登录失效,需要重新登录
)
func NewWxLoginService(config types.WxLoginConfig, redisClient *redis.Client) *WxLoginService {
return &WxLoginService{
config: config,
client: req.C().SetTimeout(10 * time.Second),
redisClient: redisClient,
}
}
func (s *WxLoginService) UpdateConfig(config types.WxLoginConfig) {
s.config = config
}
func (s *WxLoginService) GetLoginQrCodeUrl(state string) (string, error) {
if s.config.ApiKey == "" {
return "", errors.New("无效的 API Key")
}
url := fmt.Sprintf("%s/api/auth/wechat/login", types.GeekAPIURL)
var res struct {
Code types.BizCode `json:"code"`
Message string `json:"message"`
Data struct {
Ticket string `json:"ticket"`
Url string `json:"url"`
} `json:"data"`
}
r, err := s.client.R().
SetHeader("Authorization", s.config.ApiKey).
SetBody(map[string]string{
"notify_url": s.config.NotifyURL,
"state": state,
}).
SetSuccessResult(&res).Post(url)
if err != nil || r.IsErrorState() {
return "", fmt.Errorf("请求 API 失败:%v", err)
}
if res.Code != types.Success {
return "", fmt.Errorf("请求 API 失败:%s", res.Message)
}
status := LoginStatus{
Status: LoginStatusPending,
OpenID: "",
}
s.redisClient.Set(context.Background(), loginStateKeyPrefix+state, utils.JsonEncode(status), time.Hour)
return res.Data.Url, nil
}
func (s *WxLoginService) GetLoginStatus(state string) (*LoginStatus, error) {
result, err := s.redisClient.Get(context.Background(), loginStateKeyPrefix+state).Result()
if err != nil {
return nil, errors.New("登录失败")
}
var status LoginStatus
err = utils.JsonDecode(result, &status)
if err != nil {
return nil, errors.New("登录失败")
}
return &status, nil
}
func (s *WxLoginService) SetLoginStatus(state string, status LoginStatus) {
s.redisClient.Set(context.Background(), loginStateKeyPrefix+state, utils.JsonEncode(status), time.Hour)
}