Commit 85ce55c3 by zhengqiuyun86

微信支付

parent 5fe915ab
package dao
import (
"dc_golang_server_1/dao/db"
baseModel "dc_golang_server_1/model/base"
)
func FindWechatAppConfigByPrimaryKey(appId string) (baseModel.WechatAppConfig, error) {
var model baseModel.WechatAppConfig
result := db.PgDb.First(&model, appId)
return model, result.Error
}
package dao
import (
"dc_golang_server_1/dao/db"
baseModel "dc_golang_server_1/model/base"
)
func FindWechatMchConfigByPrimaryKey(mchId string) (baseModel.WechatMchConfig, error) {
var model baseModel.WechatMchConfig
result := db.PgDb.First(&model, mchId)
return model, result.Error
}
package dao
import (
"dc_golang_server_1/dao/db"
baseModel "dc_golang_server_1/model/base"
)
func FindWechatPayConfigByPrimaryKey(areaServiceId uint32) (baseModel.WechatPayConfig, error) {
var model baseModel.WechatPayConfig
result := db.PgDb.First(&model, areaServiceId)
return model, result.Error
}
package dao
package db
import (
"dc_golang_server_1/config"
dc_logger "dc_golang_server_1/logger"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"gorm.io/driver/postgres"
"log"
"os"
"time"
......@@ -13,22 +15,27 @@ import (
var PgDb *gorm.DB
var NewLogger logger.Interface
func InitPgDb() {
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer(日志输出的目标,前缀和日志包含的内容——译者注)
NewLogger = logger.New(
log.New(os.Stdout, " ", log.LstdFlags), // io writer(日志输出的目标,前缀和日志包含的内容——译者注)
logger.Config{
SlowThreshold: time.Second, // 慢 SQL 阈值
LogLevel: logger.Info, // 日志级别
IgnoreRecordNotFoundError: true, // 忽略ErrRecordNotFound(记录未找到)错误
Colorful: false, // 禁用彩色打印
SlowThreshold: time.Second, // 慢 SQL 阈值
LogLevel: logger.Error, // 日志级别
IgnoreRecordNotFoundError: true, // 忽略ErrRecordNotFound(记录未找到)错误
Colorful: true, // 禁用彩色打印
},
)
var err error
dc_logger.Log.Info("初始化连接")
PgDb, err = gorm.Open(postgres.Open(config.DbDSN), &gorm.Config{
Logger: newLogger,
db, err := gorm.Open(postgres.Open(config.DbDSN), &gorm.Config{
Logger: NewLogger,
})
if err != nil {
dc_logger.Log.Error(err.Error())
log.Printf(err.Error())
}
//if conf.ShowSql {
// PgDb = db.Debug()
//}else{
PgDb = db
//}
}
package model
import (
"time"
)
type BArea struct {
Id uint32 `gorm:"column:id;not null" json:"id"`
CountyId uint32 `gorm:"column:county_id json:"countyId"`
Name string `gorm:"column:name json:"name"`
Location string `gorm:"column:location json:"location"`
Status uint16 `gorm:"column:status json:"status"`
CreatorId uint32 `gorm:"column:creator_id json:"creatorId"`
UpdaterId uint32 `gorm:"column:updater_id json:"updaterId"`
CreateAt time.Time `gorm:"column:create_at json:"createAt"`
UpdateAt time.Time `gorm:"column:update_at json:"updateAt"`
}
package model
import (
"time"
)
// 区域服务
type BAreaService struct {
Id uint32 `gorm:"column:id;not null" json:"id"`
ServiceType uint16 `gorm:"column:service_type json:"serviceType"`
Name string `gorm:"column:name json:"name"`
AreaId uint32 `gorm:"column:area_id json:"areaId"`
OperatorId uint32 `gorm:"column:operator_id json:"operatorId"`
Manager string `gorm:"column:manager json:"manager"`
ManagerPhone string `gorm:"column:manager_phone json:"managerPhone"`
Status uint16 `gorm:"column:status json:"status"`
CreatorId uint32 `gorm:"column:creator_id json:"creatorId"`
UpdaterId uint32 `gorm:"column:updater_id json:"updaterId"`
CreateAt time.Time `gorm:"column:create_at json:"createAt"`
UpdateAt time.Time `gorm:"column:update_at json:"updateAt"`
}
package model
import (
"time"
)
// 区域收款信息
type BAreaServiceCollection struct {
AreaServiceId uint32 `gorm:"column:area_service_id;not null" json:"areaServiceId"`
ReceiverId uint32 `gorm:"column:receiver_id;not null" json:"receiverId"`
Status bool `gorm:"column:status json:"status"`
Rate uint16 `gorm:"column:rate json:"rate"`
CreatorId uint32 `gorm:"column:creator_id json:"creatorId"`
UpdaterId uint32 `gorm:"column:updater_id json:"updaterId"`
CreateAt time.Time `gorm:"column:create_at json:"createAt"`
UpdateAt time.Time `gorm:"column:update_at json:"updateAt"`
}
package model
// 省市区
type BCity struct {
Id uint32 `gorm:"column:id;not null" json:"id"`
Name string `gorm:"column:name json:"name"`
ParentId uint32 `gorm:"column:parent_id json:"parentId"`
Level uint16 `gorm:"column:level json:"level"`
Initial string `gorm:"column:initial json:"initial"` // 首字母
Sort uint32 `gorm:"column:sort json:"sort"` // 排序
}
package model
import (
"time"
)
// 服务商
type BOperator struct {
Id uint32 `gorm:"column:id;not null" json:"id"`
Name string `gorm:"column:name json:"name"`
ShortName string `gorm:"column:short_name json:"shortName"`
CountyId uint32 `gorm:"column:county_id json:"countyId"`
Address string `gorm:"column:address json:"address"`
LegalPerson string `gorm:"column:legal_person json:"legalPerson"`
LegalPhone string `gorm:"column:legal_phone json:"legalPhone"`
AptitudeUrl string `gorm:"column:aptitude_url json:"aptitudeUrl"`
CreatorId uint32 `gorm:"column:creator_id json:"creatorId"`
UpdaterId uint32 `gorm:"column:updater_id json:"updaterId"`
CreateAt time.Time `gorm:"column:create_at json:"createAt"`
UpdateAt time.Time `gorm:"column:update_at json:"updateAt"`
}
package model
import (
"time"
)
// 微信应用配置
type BWechatAppConfig struct {
Appid uint32 `gorm:"column:appid;not null" json:"appid"`
Name string `gorm:"column:name json:"name"`
Secret string `gorm:"column:secret json:"secret"`
CreatorId uint32 `gorm:"column:creator_id json:"creatorId"`
UpdaterId uint32 `gorm:"column:updater_id json:"updaterId"`
CreateAt time.Time `gorm:"column:create_at json:"createAt"`
UpdateAt time.Time `gorm:"column:update_at json:"updateAt"`
}
package model
import (
"time"
)
// 微信商户配置
type BWechatMchConfig struct {
Mchid string `gorm:"column:mchid;not null" json:"mchid"`
Name string `gorm:"column:name json:"name"`
ApiV3Key string `gorm:"column:api_v3_key json:"apiV3Key"`
MchSerialNo string `gorm:"column:mch_serial_no json:"mchSerialNo"`
PrivateKey string `gorm:"column:private_key json:"privateKey"`
CreatorId uint32 `gorm:"column:creator_id json:"creatorId"`
UpdaterId uint32 `gorm:"column:updater_id json:"updaterId"`
CreateAt time.Time `gorm:"column:create_at json:"createAt"`
UpdateAt time.Time `gorm:"column:update_at json:"updateAt"`
}
package model
import (
"time"
)
// 微信支付配置
type BWechatPayConfig struct {
Id uint32 `gorm:"column:id;not null" json:"id"`
AreaServiceId uint32 `gorm:"column:area_service_id json:"areaServiceId"`
SpAppid string `gorm:"column:sp_appid json:"spAppid"`
SpMchid string `gorm:"column:sp_mchid json:"spMchid"`
SubMchid string `gorm:"column:sub_mchid json:"subMchid"`
CreatorId uint32 `gorm:"column:creator_id json:"creatorId"`
UpdaterId uint32 `gorm:"column:updater_id json:"updaterId"`
CreateAt time.Time `gorm:"column:create_at json:"createAt"`
UpdateAt time.Time `gorm:"column:update_at json:"updateAt"`
}
package model
import (
"time"
)
// 微信分账接收方
type BWechatProfitSharingReceiver struct {
Id uint32 `gorm:"column:id;not null" json:"id"`
Type string `gorm:"column:type json:"type"`
Account string `gorm:"column:account json:"account"`
Name string `gorm:"column:name json:"name"`
CreatorId uint32 `gorm:"column:creator_id json:"creatorId"`
UpdaterId uint32 `gorm:"column:updater_id json:"updaterId"`
CreateAt time.Time `gorm:"column:create_at json:"createAt"`
UpdateAt time.Time `gorm:"column:update_at json:"updateAt"`
}
package model
import (
"time"
)
// 后台用户
type SAdmin struct {
Id uint32 `gorm:"column:id;not null" json:"id"`
LoginAccount string `gorm:"column:login_account json:"loginAccount"`
Password string `gorm:"column:password json:"password"`
Name string `gorm:"column:name json:"name"`
SysRoleId uint32 `gorm:"column:sys_role_id json:"sysRoleId"`
ParentId uint32 `gorm:"column:parent_id json:"parentId"`
CreatorId uint32 `gorm:"column:creator_id json:"creatorId"`
UpdaterId uint32 `gorm:"column:updater_id json:"updaterId"`
CreateAt time.Time `gorm:"column:create_at json:"createAt"`
UpdateAt time.Time `gorm:"column:update_at json:"updateAt"`
}
package model
import (
"time"
)
// 登录流水
type SAdminLogin struct {
AdminId uint32 `gorm:"column:admin_id json:"adminId"`
Ip string `gorm:"column:ip json:"ip"`
CreateAt time.Time `gorm:"column:create_at json:"createAt"`
}
package model
import (
"time"
)
// 用户区域角色
type SAdminRole struct {
AdminId uint32 `gorm:"column:admin_id;not null" json:"adminId"`
RoleId uint32 `gorm:"column:role_id;not null" json:"roleId"`
AreaServiceId uint32 `gorm:"column:area_service_id;not null" json:"areaServiceId"`
CreatorId uint32 `gorm:"column:creator_id json:"creatorId"`
UpdaterId uint32 `gorm:"column:updater_id json:"updaterId"`
CreateAt time.Time `gorm:"column:create_at json:"createAt"`
UpdateAt time.Time `gorm:"column:update_at json:"updateAt"`
}
package model
import (
"time"
)
// api接口
type SApi struct {
Id uint32 `gorm:"column:id;not null" json:"id"`
ApiPath string `gorm:"column:api_path json:"apiPath"`
ApiMethod string `gorm:"column:api_method json:"apiMethod"`
CreatorId uint32 `gorm:"column:creator_id json:"creatorId"`
UpdaterId uint32 `gorm:"column:updater_id json:"updaterId"`
CreateAt time.Time `gorm:"column:create_at json:"createAt"`
UpdateAt time.Time `gorm:"column:update_at json:"updateAt"`
}
package model
import (
"time"
)
// 菜单及按钮
type SMenuBtn struct {
Id uint32 `gorm:"column:id;not null" json:"id"`
Name string `gorm:"column:name json:"name"`
Status bool `gorm:"column:status json:"status"`
Explain string `gorm:"column:explain json:"explain"`
ParentId uint32 `gorm:"column:parent_id json:"parentId"`
CreatorId uint32 `gorm:"column:creator_id json:"creatorId"`
UpdaterId uint32 `gorm:"column:updater_id json:"updaterId"`
CreateAt time.Time `gorm:"column:create_at json:"createAt"`
UpdateAt time.Time `gorm:"column:update_at json:"updateAt"`
}
package model
import (
"time"
)
// 角色
type SRole struct {
Id uint32 `gorm:"column:id;not null" json:"id"`
Name string `gorm:"column:name json:"name"`
CreatorId uint32 `gorm:"column:creator_id json:"creatorId"`
UpdaterId uint32 `gorm:"column:updater_id json:"updaterId"`
CreateAt time.Time `gorm:"column:create_at json:"createAt"`
UpdateAt time.Time `gorm:"column:update_at json:"updateAt"`
}
package model
import (
"time"
)
// 角色接口权限
type SRoleApiAuth struct {
RoleId uint32 `gorm:"column:role_id;not null" json:"roleId"`
ApiId uint32 `gorm:"column:api_id;not null" json:"apiId"`
CreatorId uint32 `gorm:"column:creator_id json:"creatorId"`
UpdaterId uint32 `gorm:"column:updater_id json:"updaterId"`
CreateAt time.Time `gorm:"column:create_at json:"createAt"`
UpdateAt time.Time `gorm:"column:update_at json:"updateAt"`
}
package model
import (
"time"
)
// 角色菜单权限
type SRoleMenuAuth struct {
RoleId uint32 `gorm:"column:role_id;not null" json:"roleId"`
MenuId uint32 `gorm:"column:menu_id;not null" json:"menuId"`
CreatorId uint32 `gorm:"column:creator_id json:"creatorId"`
UpdaterId uint32 `gorm:"column:updater_id json:"updaterId"`
CreateAt time.Time `gorm:"column:create_at json:"createAt"`
UpdateAt time.Time `gorm:"column:update_at json:"updateAt"`
}
package exception
import (
"errors"
)
func ThrowsErr(err error) {
if err != nil {
panic(err)
}
}
func ThrowsErrS(err string) {
if &err != nil && err != "" {
panic(errors.New(err))
}
}
package catch
import (
"dc_golang_server_1/web/response"
"github.com/gin-gonic/gin"
"net/http"
)
func ExceptionCatch(c *gin.Context) {
err := recover() //获取异常
if err != nil {
e := (err).(error)
msg := ErrorMapping[e.Error()]
if &msg == nil || msg == "" {
c.JSON(http.StatusOK, response.FailAndMsg(e.Error()))
} else {
c.JSON(http.StatusOK, response.FailAndMsg(msg))
}
}
}
type ErrorMap map[string]string
var ErrorMapping = ErrorMap{
"": "信息不存在",
"unexpected EOF": "参数格式不正确",
}
......@@ -10,6 +10,7 @@ require (
github.com/natefinch/lumberjack v2.0.0+incompatible
github.com/swaggo/gin-swagger v1.3.2
github.com/swaggo/swag v1.7.3
github.com/wechatpay-apiv3/wechatpay-go v0.2.9
go.uber.org/zap v1.19.1
gopkg.in/ini.v1 v1.63.2
gorm.io/driver/postgres v1.1.2
......
......@@ -10,6 +10,8 @@ github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tN
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/agiledragon/gomonkey v2.0.2+incompatible h1:eXKi9/piiC3cjJD1658mEE2o3NjkJ5vDLgYjCQu0Xlw=
github.com/agiledragon/gomonkey v2.0.2+incompatible/go.mod h1:2NGfXu1a80LLr2cmWXGBDaHEjb1idR6+FVlX5T3D9hw=
github.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394 h1:OYA+5W64v3OgClL+IrOD63t4i/RW7RqrAVl9LTZ9UqQ=
github.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394/go.mod h1:Q8n74mJTIgjX4RBBcHnJ05h//6/k6foqmgE45jTQtxg=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
......@@ -226,6 +228,8 @@ github.com/ugorji/go/codec v1.2.6 h1:7kbGefxLoDBuYXOms4yD7223OpNMMPNPZxXk5TvFcyQ
github.com/ugorji/go/codec v1.2.6/go.mod h1:V6TCNZ4PHqoHGFZuSG1W8nrCzzdgA2DozYxWFFpvxTw=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/wechatpay-apiv3/wechatpay-go v0.2.9 h1:FnFdYLquHWEB0pBacOHC9BePgXcf26vZfn2X3uYbo0c=
github.com/wechatpay-apiv3/wechatpay-go v0.2.9/go.mod h1:W8ucVAOCKOii933cWROLaDLmRQ2cg/vHHVF4vGAVq9Q=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
......
......@@ -2,10 +2,10 @@ package main
import (
"dc_golang_server_1/config"
"dc_golang_server_1/dao/db"
"dc_golang_server_1/devproduct/dcphone20"
"dc_golang_server_1/logger"
"dc_golang_server_1/web"
"dc_golang_server_1/wechat/dao"
"fmt"
"runtime"
"time"
......@@ -18,7 +18,7 @@ import (
func initConfigAndLogger() {
config.ConfigInit()
logger.InitLogger()
dao.InitPgDb()
db.InitPgDb()
//突然发现这种方式瓜眉日眼的,以后不用了,直接在shell脚本里面重定向输出文件
//f, err := os.OpenFile(config.LogPath+"panic.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0755)
//if err != nil {
......
package model
import (
"dc_golang_server_1/model/conver"
)
func (WechatAppConfig) TableName() string {
return "b_wechat_app_config"
}
// WechatAppConfig 微信应用配置
type WechatAppConfig struct {
AppId *string `gorm:"primaryKey:app_id" json:"appId,omitempty"` // APPID
Name *string `gorm:"column:name" json:"name,omitempty"` // 应用名称
Secret *string `gorm:"column:secret" json:"secret,omitempty"` // 微信应用secret
CreatorId *uint32 `gorm:"column:creator_id" json:"creatorId,omitempty"` // 创建人
UpdaterId *uint32 `gorm:"column:updater_id" json:"updaterId,omitempty"` // 修改人
CreateAt *conver.Time `gorm:"column:create_at" json:"createAt,omitempty"` // 创建时间
UpdateAt *conver.Time `gorm:"column:update_at" json:"updateAt,omitempty"` // 修改时间
}
type WechatAppConfigColumnsStruct struct {
AppId string
Name string
Secret string
CreatorId string
UpdaterId string
CreateAt string
UpdateAt string
}
var WechatAppConfigColumns = WechatAppConfigColumnsStruct{
AppId: "app_id",
Name: "name",
Secret: "secret",
CreatorId: "creator_id",
UpdaterId: "updater_id",
CreateAt: "create_at",
UpdateAt: "update_at",
}
package model
import (
"dc_golang_server_1/model/conver"
)
func (WechatMchConfig) TableName() string {
return "b_wechat_mch_config"
}
// WechatMchConfig 微信商户配置
type WechatMchConfig struct {
MchId *string `gorm:"primaryKey:mch_id" json:"mchId,omitempty"` // 商户号
Name *string `gorm:"column:name" json:"name,omitempty"` // 商户名称
ApiV3Key *string `gorm:"column:api_v3_key" json:"apiV3Key,omitempty"` // ApiV3Key
MchSerialNo *string `gorm:"column:mch_serial_no" json:"mchSerialNo,omitempty"` // 商户证书序号
PrivateKey *string `gorm:"column:private_key" json:"privateKey,omitempty"` // 商户私钥
CreatorId *uint32 `gorm:"column:creator_id" json:"creatorId,omitempty"` // 创建人
UpdaterId *uint32 `gorm:"column:updater_id" json:"updaterId,omitempty"` // 修改人
CreateAt *conver.Time `gorm:"column:create_at" json:"createAt,omitempty"` // 创建时间
UpdateAt *conver.Time `gorm:"column:update_at" json:"updateAt,omitempty"` // 修改时间
}
type WechatMchConfigColumnsStruct struct {
MchId string
Name string
ApiV3Key string
MchSerialNo string
PrivateKey string
CreatorId string
UpdaterId string
CreateAt string
UpdateAt string
}
var WechatMchConfigColumns = WechatMchConfigColumnsStruct{
MchId: "mch_id",
Name: "name",
ApiV3Key: "api_v3_key",
MchSerialNo: "mch_serial_no",
PrivateKey: "private_key",
CreatorId: "creator_id",
UpdaterId: "updater_id",
CreateAt: "create_at",
UpdateAt: "update_at",
}
package model
import (
"dc_golang_server_1/model/conver"
)
func (WechatPayConfig) TableName() string {
return "b_wechat_pay_config"
}
// WechatPayConfig 微信支付配置
type WechatPayConfig struct {
AreaServiceId *uint32 `gorm:"primaryKey:area_service_id" json:"areaServiceId,omitempty"` // 区域服务ID
SpAppid *string `gorm:"column:sp_appid" json:"spAppid,omitempty"` // 服务商APPID
SpMchid *string `gorm:"column:sp_mchid" json:"spMchid,omitempty"` // 服务商商户号
SubMchid *string `gorm:"column:sub_mchid" json:"subMchid,omitempty"` // 特约商户号
CreatorId *uint32 `gorm:"column:creator_id" json:"creatorId,omitempty"` // 创建人
UpdaterId *uint32 `gorm:"column:updater_id" json:"updaterId,omitempty"` // 修改人
CreateAt *conver.Time `gorm:"column:create_at" json:"createAt,omitempty"` // 创建时间
UpdateAt *conver.Time `gorm:"column:update_at" json:"updateAt,omitempty"` // 修改时间
}
type WechatPayConfigColumnsStruct struct {
AreaServiceId string
SpAppid string
SpMchid string
SubMchid string
CreatorId string
UpdaterId string
CreateAt string
UpdateAt string
}
var WechatPayConfigColumns = WechatPayConfigColumnsStruct{
AreaServiceId: "area_service_id",
SpAppid: "sp_appid",
SpMchid: "sp_mchid",
SubMchid: "sub_mchid",
CreatorId: "creator_id",
UpdaterId: "updater_id",
CreateAt: "create_at",
UpdateAt: "update_at",
}
package conver
import (
"database/sql/driver"
"errors"
"fmt"
"strings"
"time"
)
//Time 自定义时间
type Time time.Time
func (t *Time) UnmarshalJSON(data []byte) error {
if string(data) == "null" {
return nil
}
var err error
//前端接收的时间字符串
str := string(data)
//去除接收的str收尾多余的"
timeStr := strings.Trim(str, "\"")
t1, err := time.Parse("2006-01-02 15:04:05", timeStr)
*t = Time(t1)
return err
}
func (t Time) MarshalJSON() ([]byte, error) {
formatted := fmt.Sprintf("\"%v\"", time.Time(t).Format("2006-01-02 15:04:05"))
return []byte(formatted), nil
}
func (t Time) Value() (driver.Value, error) {
// Time 转换成 time.Time 类型
tTime := time.Time(t)
return tTime.Format("2006-01-02 15:04:05"), nil
}
func (t *Time) Scan(v interface{}) error {
switch vt := v.(type) {
case time.Time:
// 字符串转成 time.Time 类型
*t = Time(vt)
default:
return errors.New("类型处理错误")
}
return nil
}
func (t *Time) String() string {
return fmt.Sprintf("hhh:%s", time.Time(*t).String())
}
func Now() Time {
return Time(time.Now())
}
package params
type WechatPartnerJsapiOrderParams struct {
AreaServiceId *uint32 `json:"areaServiceId,omitempty"` //区域服务id
Title *string `json:"title,omitempty"` //支付标题
OutTradeNo *string `json:"outTradeNo,omitempty"` //商户订单号
Amount *float64 `json:"amount,omitempty"` //订单金额,单位元,保留两位小数
OpenId *string `json:"openId,omitempty"` //微信小程序对应openId
}
type WechatPartnerRefundParams struct {
AreaServiceId *uint32 `json:"areaServiceId,omitempty"` //区域服务id
OutRefundNo *string `json:"outRefundNo,omitempty"` //退款单号
OutTradeNo *string `json:"outTradeNo,omitempty"` //原商户订单号
OrderAmount *float64 `json:"orderAmount,omitempty"` //原订单金额
RefundAmount *float64 `json:"refundAmount,omitempty"` //本次退款金额
}
package response
type WxMiniRequestPayment struct {
AppId string `json:"appId"`
TimeStamp string `json:"timeStamp"`
NonceStr string `json:"nonceStr"`
Package string `json:"package"`
SignType string `json:"signType"`
PaySign string `json:"paySign"`
}
type WxOAuth2ResponseData struct {
Appid string `json:"appId"`
Openid string `json:"openid"`
Unionid string `json:"unionid"`
Errcode string `json:"errcode"`
Errmsg string `json:"errmsg"`
}
package service
import (
baseDao "dc_golang_server_1/dao/base"
baseModel "dc_golang_server_1/model/base"
)
func FindWechatAppConfig(appId string) (baseModel.WechatAppConfig, error) {
return baseDao.FindWechatAppConfigByPrimaryKey(appId)
}
package service
import (
baseDao "dc_golang_server_1/dao/base"
baseModel "dc_golang_server_1/model/base"
)
func FindWechatMchConfig(mchId string) (baseModel.WechatMchConfig, error) {
return baseDao.FindWechatMchConfigByPrimaryKey(mchId)
}
package service
import (
baseDao "dc_golang_server_1/dao/base"
baseModel "dc_golang_server_1/model/base"
)
func FindWechatPayConfig(areaServiceId uint32) (baseModel.WechatPayConfig, error) {
return baseDao.FindWechatPayConfigByPrimaryKey(areaServiceId)
}
package wechat
import (
"bytes"
"context"
"crypto/rsa"
"dc_golang_server_1/exception"
baseModel "dc_golang_server_1/model/base"
baseService "dc_golang_server_1/service/base"
"github.com/wechatpay-apiv3/wechatpay-go/core"
"github.com/wechatpay-apiv3/wechatpay-go/core/auth/verifiers"
"github.com/wechatpay-apiv3/wechatpay-go/core/downloader"
"github.com/wechatpay-apiv3/wechatpay-go/core/notify"
"github.com/wechatpay-apiv3/wechatpay-go/core/option"
"github.com/wechatpay-apiv3/wechatpay-go/utils"
"log"
)
type WxClient struct {
Client *core.Client
MchConfig baseModel.WechatMchConfig
PrivateKey *rsa.PrivateKey
}
func GetHandler(mchID string) *notify.Handler {
wechatMchConfig, err := baseService.FindWechatMchConfig(mchID)
if err != nil {
exception.ThrowsErr(err)
}
if wechatMchConfig.MchId == nil {
exception.ThrowsErrS("[" + mchID + "]对应配置不存在,请检查")
}
// 获取平台证书访问器
certVisitor := downloader.MgrInstance().GetCertificateVisitor(mchID)
handler := notify.NewNotifyHandler(*wechatMchConfig.ApiV3Key, verifiers.NewSHA256WithRSAVerifier(certVisitor))
return handler
}
func BuildClient(mchID string) WxClient {
wechatMchConfig, err := baseService.FindWechatMchConfig(mchID)
if err != nil {
exception.ThrowsErr(err)
}
if wechatMchConfig.MchId == nil {
exception.ThrowsErrS("[" + mchID + "]对应配置不存在,请检查")
}
client, privateKey := initClientByMchID(mchID, *wechatMchConfig.MchSerialNo, *wechatMchConfig.ApiV3Key, *wechatMchConfig.PrivateKey)
return WxClient{client, wechatMchConfig, privateKey}
}
func initClientByMchID(mchID string, mchCertificateSerialNumber string, mchAPIv3Key string, mchPrivateKey string) (*core.Client, *rsa.PrivateKey) {
// 使用 utils 提供的函数从本地文件中加载商户私钥,商户私钥会用来生成请求的签名
//privateKey, err := utils.LoadPrivateKeyWithPath(PKeyPath)
privateKey := loadPrivateKey(mchPrivateKey)
ctx := context.Background()
// 使用商户私钥等初始化 client,并使它具有自动定时获取微信支付平台证书的能力
opts := []core.ClientOption{
option.WithWechatPayAutoAuthCipher(mchID, mchCertificateSerialNumber, privateKey, mchAPIv3Key),
}
wechatClient, err := core.NewClient(ctx, opts...)
if err != nil {
exception.ThrowsErr(err)
}
return wechatClient, privateKey
}
func loadPrivateKey(privateKeyStr string) *rsa.PrivateKey {
privateKey, err := utils.LoadPrivateKey(privateKeyStr)
if err != nil {
exception.ThrowsErr(err)
}
return privateKey
}
func SignPay(appId string, timeStamp string, nonceStr string, packageInfo string, privateKey *rsa.PrivateKey) string {
var data bytes.Buffer
data.WriteString(appId)
data.WriteString("\n")
data.WriteString(timeStamp)
data.WriteString("\n")
data.WriteString(nonceStr)
data.WriteString("\n")
data.WriteString(packageInfo)
data.WriteString("\n")
log.Println("需要签名的信息:" + data.String())
signStr, err := utils.SignSHA256WithRSA(data.String(), privateKey)
if err != nil {
exception.ThrowsErr(err)
}
return signStr
}
package wechat
import (
"context"
"encoding/json"
//"dc_golang_server_1/conf"
dao "dc_golang_server_1/dao/base"
"dc_golang_server_1/exception"
"dc_golang_server_1/model/wechat/params"
"dc_golang_server_1/model/wechat/response"
"dc_golang_server_1/util"
"github.com/wechatpay-apiv3/wechatpay-go/services/payments"
"github.com/wechatpay-apiv3/wechatpay-go/services/refunddomestic"
"io/ioutil"
"math"
"net/http"
"strconv"
"strings"
"time"
"github.com/wechatpay-apiv3/wechatpay-go/core"
"github.com/wechatpay-apiv3/wechatpay-go/services/partnerpayments/jsapi"
)
const MainHost = "" //回调通知的外网域名
func ToWxMoney(amount float64) *int64 {
return core.Int64(int64(math.Floor(amount * 100)))
}
//JsapiPartnerPrepayOrder 服务商模式小程序支付
func JsapiPartnerPrepayOrder(params params.WechatPartnerJsapiOrderParams) response.WxMiniRequestPayment {
wechatPayConfig, err := dao.FindWechatPayConfigByPrimaryKey(*params.AreaServiceId)
if wechatPayConfig.AreaServiceId == nil {
exception.ThrowsErrS("[" + strconv.Itoa(int(*params.AreaServiceId)) + "]对应微信支付配不存在")
}
wechatClient := BuildClient(*wechatPayConfig.SpMchid)
svc := jsapi.JsapiApiService{Client: wechatClient.Client}
// 得到prepay_id,以及调起支付所需的参数和签名
resp, _, err := svc.Prepay(context.Background(),
jsapi.PrepayRequest{
SpAppid: core.String(*wechatPayConfig.SpAppid),
SpMchid: core.String(*wechatPayConfig.SpMchid),
SubMchid: core.String(*wechatPayConfig.SubMchid),
Description: core.String(*params.Title),
OutTradeNo: core.String(*params.OutTradeNo),
NotifyUrl: core.String(MainHost + "/wechat/partner/pay/notify/" + *wechatPayConfig.SpMchid),
Amount: &jsapi.Amount{
Total: ToWxMoney(*params.Amount),
},
Payer: &jsapi.Payer{
SpOpenid: params.OpenId,
},
},
)
if err != nil {
exception.ThrowsErr(err)
}
return buildWxMiniPayInfo(*wechatPayConfig.SpAppid, *resp.PrepayId, wechatClient)
}
func buildWxMiniPayInfo(appId string, prepayId string, wechatClient WxClient) response.WxMiniRequestPayment {
var rd response.WxMiniRequestPayment
rd.AppId = appId
rd.TimeStamp = util.Strval(time.Now().Unix())
rd.NonceStr = util.FandomString(32)
rd.Package = "prepay_id=" + prepayId
rd.SignType = "RSA"
paySign := SignPay(rd.AppId, rd.TimeStamp, rd.NonceStr, rd.Package, wechatClient.PrivateKey)
rd.PaySign = paySign
return rd
}
func PayBack(mchID string, request *http.Request) bool {
handler := GetHandler(mchID)
transaction := new(payments.Transaction)
notifyReq, err := handler.ParseNotifyRequest(context.Background(), request, transaction)
if err != nil {
exception.ThrowsErr(err)
}
if notifyReq != nil {
if *transaction.TradeState == "SUCCESS" {
payBackBusiness(*transaction.OutTradeNo)
return true
}
}
return false
}
/**
支付回调处理业务
注意:
同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。 推荐的做法是,当商户系统收到通知进行处理时,先检查对应业务数据的状态,并判断该通知是否已经处理。如果未处理,则再进行处理;如果已处理,则直接返回结果成功。在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。
*/
func payBackBusiness(outTradeNo string) {
}
type RefundNotifyData struct {
SpMchId *string `json:"sp_mchid,omitempty"`
SubMchid *string `json:"sub_mchid,omitempty"`
TransactionId *string `json:"transaction_id,omitempty"`
OutTradeNo *string `json:"out_trade_no,omitempty"`
RefundId *string `json:"refund_id,omitempty"`
OutRefundNo *string `json:"out_refund_no,omitempty"`
RefundStatus *string `json:"refund_status,omitempty"`
SuccessTime *string `json:"success_time,omitempty"`
UserReceivedAccount string `json:"user_received_account,omitempty"`
Amount *RefundNotifyAmountData `json:"amount"`
}
type RefundNotifyAmountData struct {
Total *int64 `json:"total,omitempty"`
Refund *int64 `json:"refund,omitempty"`
PayerTotal *int64 `json:"payer_total,omitempty"`
PayerRefund *int64 `json:"payer_refund,omitempty"`
}
func RefundBack(mchID string, request *http.Request) bool {
handler := GetHandler(mchID)
refund := new(RefundNotifyData)
notifyReq, err := handler.ParseNotifyRequest(context.Background(), request, refund)
if err != nil {
exception.ThrowsErr(err)
return false
}
if notifyReq != nil {
if *refund.RefundStatus == "SUCCESS" {
refundBackBusiness(*refund.OutRefundNo)
}
}
return false
}
/**
退款回调处理业务
注意:
同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。 推荐的做法是,当商户系统收到通知进行处理时,先检查对应业务数据的状态,并判断该通知是否已经处理。如果未处理,则再进行处理;如果已处理,则直接返回结果成功。在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。
*/
func refundBackBusiness(outRefundNo string) {
}
/** Refund
退款
*/
func Refund(params params.WechatPartnerRefundParams) {
wechatPayConfig, err := dao.FindWechatPayConfigByPrimaryKey(*params.AreaServiceId)
if wechatPayConfig.AreaServiceId == nil {
exception.ThrowsErrS("[" + strconv.Itoa(int(*params.AreaServiceId)) + "]对应微信支付配不存在")
}
wechatClient := BuildClient(*wechatPayConfig.SpMchid)
svc := refunddomestic.RefundsApiService{Client: wechatClient.Client}
resp, _, err := svc.Create(context.Background(), refunddomestic.CreateRequest{
SubMchid: wechatPayConfig.SubMchid,
OutTradeNo: params.OutTradeNo,
OutRefundNo: params.OutRefundNo,
NotifyUrl: core.String(MainHost + "/wechat/partner/refund/notify/" + *wechatPayConfig.SpMchid),
Amount: &refunddomestic.AmountReq{
Refund: ToWxMoney(*params.RefundAmount),
Total: ToWxMoney(*params.OrderAmount),
Currency: core.String("CNY"),
},
})
if err != nil {
var apiErr = err.(*core.APIError)
exception.ThrowsErrS(apiErr.Message)
}
if *resp.Status != refunddomestic.STATUS_SUCCESS &&
*resp.Status != refunddomestic.STATUS_PROCESSING {
exception.ThrowsErrS("退款错误")
}
}
func JsCode2session(code string, appId string) (responseData response.WxOAuth2ResponseData, err error) {
//var NAME = "微信授权"
//var name = NAME + "-code2Session"
var url = "https://web.weixin.qq.com/sns/jscode2session?appid=${appId}&secret=${secret}&js_code=${code}&grant_type=authorization_code"
baseWechatPayConfig, err := dao.FindWechatAppConfigByPrimaryKey(appId)
url = strings.ReplaceAll(url, "${appId}", *baseWechatPayConfig.AppId)
url = strings.ReplaceAll(url, "${secret}", *baseWechatPayConfig.Secret)
url = strings.ReplaceAll(url, "${code}", code)
//logger.Log.Debug(name,
// zap.String("url", url))
request, err := http.NewRequest(http.MethodGet, url, nil)
client := http.DefaultClient
response, err := client.Do(request)
if err != nil {
return responseData, err
}
defer func() {
response.Body.Close()
}()
bodyBytes, err := ioutil.ReadAll(response.Body)
//logger.Log.Debug(name,
// zap.String("url", url),
// zap.String("response", string(bodyBytes)))
if err != nil {
return responseData, err
}
if err = json.Unmarshal(bodyBytes, &responseData); err != nil {
return responseData, err
}
if responseData.Errmsg != "" {
exception.ThrowsErrS(responseData.Errmsg)
}
return responseData, nil
}
package util
import (
"encoding/json"
"github.com/gin-gonic/gin"
"math/rand"
"strconv"
)
// 生成: 时间戳 + 设置前缀 + 随即字符串
const alphanum = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
func FandomString(strlen int) string {
result := make([]byte, strlen)
for i := 0; i < strlen; i++ {
result[i] = alphanum[rand.Intn(len(alphanum))]
}
return string(result)
}
func ParamUin32(c *gin.Context, key string) (uint32, error) {
v := c.Param(key)
i, err := strconv.Atoi(v)
return uint32(i), err
}
// Strval 获取变量的字符串值
// 浮点型 3.0将会转换成字符串3, "3"
// 非数值或字符类型的变量将会被转换成JSON格式字符串
func Strval(value interface{}) string {
// interface 转 string
var key string
if value == nil {
return key
}
switch value.(type) {
case float64:
ft := value.(float64)
key = strconv.FormatFloat(ft, 'f', -1, 64)
case float32:
ft := value.(float32)
key = strconv.FormatFloat(float64(ft), 'f', -1, 64)
case int:
it := value.(int)
key = strconv.Itoa(it)
case uint:
it := value.(uint)
key = strconv.Itoa(int(it))
case int8:
it := value.(int8)
key = strconv.Itoa(int(it))
case uint8:
it := value.(uint8)
key = strconv.Itoa(int(it))
case int16:
it := value.(int16)
key = strconv.Itoa(int(it))
case uint16:
it := value.(uint16)
key = strconv.Itoa(int(it))
case int32:
it := value.(int32)
key = strconv.Itoa(int(it))
case uint32:
it := value.(uint32)
key = strconv.Itoa(int(it))
case int64:
it := value.(int64)
key = strconv.FormatInt(it, 10)
case uint64:
it := value.(uint64)
key = strconv.FormatUint(it, 10)
case string:
key = value.(string)
case []byte:
key = string(value.([]byte))
default:
newValue, _ := json.Marshal(value)
key = string(newValue)
}
return key
}
package response
import (
responseState "dc_golang_server_1/web/response/state"
)
type ServerResponse struct {
Code responseState.State `json:"code,omitempty"` //1000为成功,其他都是失败
Msg string `json:"msg,omitempty"` //非1000情况下的错误描述
}
type ServerDataResponse struct {
ServerResponse
Data interface{} `json:"data,omitempty"`
}
func Success(v interface{}) ServerDataResponse {
return ServerDataResponse{ServerResponse: ServerResponse{Code: responseState.StateOK, Msg: responseState.StateMapping[responseState.StateOK]}, Data: v}
}
func Fail(code responseState.State) ServerResponse {
return ServerResponse{Code: code, Msg: responseState.StateMapping[code]}
}
func FailAndMsg(msg string) ServerResponse {
return ServerResponse{Code: responseState.StateBusinessError, Msg: msg}
}
......@@ -8,6 +8,7 @@ const (
StateParamsError
StateSignatureMismatch
StateBusinessError
StateAlreadyDel
)
type StateMap map[State]string
......@@ -18,4 +19,5 @@ var StateMapping = StateMap{
StateParamsError: "params error",
StateSignatureMismatch: "Signature mismatch",
StateBusinessError: "business error, please checking",
StateAlreadyDel: "info is deleted",
}
package v1
import (
"dc_golang_server_1/exception/catch"
"fmt"
"github.com/gin-gonic/gin"
"log"
"net/http"
wechatService "dc_golang_server_1/service/wechat"
)
//InitWechatPartnerRouter 在InitRouter中添加InitWechatPayConfigRout(r),进行路由初始化
func InitWechatPartnerRouter(engine *gin.Engine) {
engine.POST("/wechat/partner/pay/notify/:mchID", payNotify)
engine.POST("/wechat/partner/refund/notify/:mchID", refundNotify)
}
// WeLogin 微信登录
// @Tags 家长微信
// @Summary 登录
......@@ -25,3 +35,29 @@ func WeLogin(c *gin.Context) {
"token": "diohafuahfuihaeuifhrgjkngk3134",
})
}
func payNotify(c *gin.Context) {
defer catch.ExceptionCatch(c)
mchID := c.Param("mchID")
log.Println("微信支付回调通知处理开始:" + mchID)
success := wechatService.PayBack(mchID, c.Request)
if success {
c.JSON(http.StatusOK, map[string]string{
"code": "SUCCESS",
"message": "成功",
})
}
}
func refundNotify(c *gin.Context) {
defer catch.ExceptionCatch(c)
mchID := c.Param("mchID")
log.Println("微信退款回调通知处理开始:" + mchID)
success := wechatService.RefundBack(mchID, c.Request)
if success {
c.JSON(http.StatusOK, map[string]string{
"code": "SUCCESS",
"message": "成功",
})
}
}
package cache
import (
"dc_golang_server_1/logger"
"dc_golang_server_1/wechat/model"
"github.com/goinggo/mapstructure"
"sync"
)
var UWeCustomerMap sync.Map //
func FindUWeCustomerByUnionid(unionid string) (customer model.UWeCustomer, err error) {
customerCache, ok := UWeCustomerMap.Load(unionid)
if ok {
logger.Log.Debug("UWeCustomerMap缓存命中:" + unionid)
} else {
logger.Log.Debug("UWeCustomerMap缓存未命中:" + unionid)
}
if err = mapstructure.Decode(customerCache, &customer); err != nil {
logger.Log.Error(err.Error())
}
return customer, err
}
func SetUWeCustomerByUnionid(customer model.UWeCustomer) {
UWeCustomerMap.Store(customer.Unionid, customer)
}
package dao
import (
"dc_golang_server_1/wechat/model"
)
func FindUWeCustomerByUnionid(unionid string) model.UWeCustomer {
var customer model.UWeCustomer
PgDb.First(&customer, "unionid = ?", unionid)
return customer
}
func NewCustomer(customer model.UWeCustomer) model.UWeCustomer {
PgDb.Create(&customer)
return customer
}
package model
import "time"
type UWeCustomer struct {
Id uint32 `json:"id"`
Openid string `json:"openid"` //微信小程序对应的openid
Unionid string `json:"unionid"` //微信小程序对应的unionid
Nickname string `json:"nickname"` //微信昵称
Avatarurl string `json:"avatarurl"` //微信头像
Country string `json:"country"`
Province string `json:"province"`
Language string `json:"language"`
CreateAt time.Time `json:"createAt"`
LoginAt time.Time `json:"loginAt"`
TestRole bool `json:"testRole"` //是否是有测试权限
updaterId uint32
}
package service
import (
"dc_golang_server_1/wechat/cache"
"dc_golang_server_1/wechat/dao"
"dc_golang_server_1/wechat/model"
"dc_golang_server_1/wechat/web/response"
responseState "dc_golang_server_1/wechat/web/response/state"
)
func FindUWeCustomerByUnionid(unionid string) model.UWeCustomer {
customer, _ := cache.FindUWeCustomerByUnionid(unionid)
if customer == (model.UWeCustomer{}) {
customer = dao.FindUWeCustomerByUnionid(unionid)
cache.SetUWeCustomerByUnionid(customer)
}
return customer
}
func initUWeCustomer(unionid string, openid string) {
var customer model.UWeCustomer
customer.Unionid = unionid
customer.Openid = openid
dao.NewCustomer(customer)
}
type LoginFromWechatAppletData struct {
token string
tokenCreatTime uint32
testRole uint8 // 测试用户(可看到测试校区)
nickname string //微信昵称
masterCardUserID []uint32 //自己创建的学生用户
slaveCardUserID []uint32 //被邀请查看的学生用户
}
//微信小程序登陆
func LoginFromWechatApplet(code string) (LoginFromWechatAppletData, response.ServerErrorResponse) {
var loginFromWechatAppletData LoginFromWechatAppletData
wxOAuth2ResponseData, err := JsCode2session(code)
if err != nil {
return loginFromWechatAppletData, response.ServerErrorResponse{Code: responseState.StateBusinessError, Msg: err.Error()}
} else {
var customer model.UWeCustomer
customer = FindUWeCustomerByUnionid(wxOAuth2ResponseData.Unionid)
if customer == (model.UWeCustomer{}) {
initUWeCustomer(wxOAuth2ResponseData.Unionid, wxOAuth2ResponseData.Openid)
}
}
return loginFromWechatAppletData, response.ServerErrorResponse{}
}
package response
import (
responseState "dc_golang_server_1/wechat/web/response/state"
)
type ServerErrorResponse struct {
Code responseState.State `json:"code"` //1000为成功,其他都是失败
Msg string `json:"msg"`
}
type ServerDataResponse struct {
Code responseState.State `json:"code"` //1000为成功,其他都是失败
Data interface{} `json:"data"`
}
package web
import "github.com/gin-gonic/gin"
// InitWechatRout 在InitRouter中添加InitWechatRout(r),进行路由初始化
func InitWechatRout(engine *gin.Engine) {
engine.GET("/v1/test", test)
engine.POST("/v1/login", weLogin)
}
package web
import (
"dc_golang_server_1/wechat/model"
wechatService "dc_golang_server_1/wechat/service"
"dc_golang_server_1/wechat/web/response"
responseState "dc_golang_server_1/wechat/web/response/state"
"github.com/gin-gonic/gin"
"net/http"
)
type TestResponse struct {
Code responseState.State `json:"code"` //1000为成功,其他都是失败
Msg string `json:"msg"` //code非1000时的错误描述
Data model.UWeCustomer `json:"data"`
}
// @Tags 微信
// @Summary 测试
// @Description 测试使用
// @Param unionid query string true "用户微信的unionid"
// @Product json
// @Success 200 {object} TestResponse"
// @Router /v1/test [GET]
func test(c *gin.Context) {
unionid := c.Query("unionid")
var customer model.UWeCustomer
customer = wechatService.FindUWeCustomerByUnionid(unionid)
if customer.Id == 0 {
c.JSON(http.StatusOK, response.ServerErrorResponse{Code: responseState.StateBusinessError, Msg: "用户不存在"})
} else {
c.JSON(http.StatusOK, response.ServerDataResponse{Code: responseState.StateOK, Data: customer})
}
}
// @Tags 微信
// @Summary 登录
// @Description 微信code传至后台,后台返回token,token有效时长:24小时
// @Param code path string true "用户微信的code"
// @Product json
// @Success 200 {object} service.LoginFromWechatAppletResponse"
// @Router /v1/login/{code} [POST]
func weLogin(c *gin.Context) {
code := c.Param("code")
responseData, err := wechatService.LoginFromWechatApplet(code)
if err != (response.ServerErrorResponse{}) {
c.JSON(http.StatusOK, err)
} else {
c.JSON(http.StatusOK, responseData)
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment