Commit fa5f2cdb by wang

供应商合并数据相关脚本

parent 01371268
......@@ -28,3 +28,6 @@ cmd.exe~
/regconfig
/cmd/http/regTest
/cmd/http/redisTest
/cmd/http
/resources
/cmd/test
package boot
import (
"golang_open_platform/pkg/config"
"golang_open_platform/pkg/gredis"
"golang_open_platform/pkg/logger"
"golang_open_platform/pkg/mongo"
"golang_open_platform/pkg/mysql"
"go_supplier_task/pkg/config"
"go_supplier_task/pkg/gredis"
"go_supplier_task/pkg/mongo"
"go_supplier_task/pkg/mysql"
)
func Boot(configPath string) (err error) {
......@@ -24,6 +23,6 @@ func Boot(configPath string) (err error) {
panic(err)
return
}
logger.Loginit()
//logger.Loginit()
return
}
package main
import (
"flag"
"fmt"
logger "github.com/ichunt2019/log"
"go_supplier_task/boot"
syncLog "go_supplier_task/pkg/logger/sync"
"go_supplier_task/service"
)
func main() {
defer func() {
if err:=recover(); err!=nil{
fmt.Println(err)
}
}()
var path string
var taskName string
flag.StringVar(&path, "config", "conf", "配置文件")
flag.StringVar(&taskName, "taskName", "supplierMerge", "任务名称")
flag.Parse()
if err := boot.Boot(path); err != nil {
panic(err)
}
//flag.StringVar(&taskName, "taskName", "supplierMerge", "任务名称")
//flag.Parse()
service.NewErpService().SyncErp()
//service.NewServiceLy().Merger()//联营sku整合
return
switch taskName {
case "supplierMerge":
fmt.Println("供应商合并")
return
service.NewServiceSupplier().MergerCriteria()
break
case "erpMapping":
fmt.Println("erp映射")
return
service.NewServiceSupplier().MappingHandle(2)//映射处理(1,联营 2,erp 3,自营)
break
case "zyMapping":
fmt.Println("自营映射")
return
service.NewServiceSupplier().MappingHandle(3)//映射处理(1,联营 2,erp 3,自营)
break
case "lyMapping":
fmt.Println("联营映射")
return
service.NewServiceSupplier().MappingHandle(1)//映射处理(1,联营 2,erp 3,自营)
break
case "lySkuMerge":
fmt.Println("联营sku整合")
return
service.NewServiceLy().Merger()//联营sku整合
break
}
//fmt.Println("啥也没执行")
//service.NewServiceSupplier().MergerCriteria()//标准供应商处理
//service.NewServiceSupplier().MappingHandle(2)//映射处理(1,联营 2,erp 3,自营)
}
func logTest() {
//logger.SyncInsert("abc_d","djflsdfj",logger.LogLevelFatal)
//logger.SyncInsert("abc_d","abc")
//logger.SyncInsert("abc_d","abc123123")
//time.Sleep(time.Second*2)
syncLog.SyncInsertLog("sdfsdfsdf","def",syncLog.LogLevelFatal)
//log.SyncInsert("dfsdf","def")
//log.SyncInsert("的火箭分离事件的发生地发","def")
//test2()
//test2()
//logTest2()
//logTest2()
}
func logTest2() {
syncLog.SyncInsertLog("a_b","def",syncLog.LogLevelFatal)
/*log.SyncInsert("ws_a","豆腐干大范甘迪")
log.SyncInsert("ws_a","手动阀手动阀")
log.SyncInsert("ws_a","手动阀手动阀打发士大夫")*/
//log.SyncInsert("ws_a","dfsdf")
//log.SyncInsert("ws_a","dfsdf")
//log.SyncInsert("ws_a","dfsdf")
}
func test2() {
logConfig := make(map[string]string)
logConfig["log_path"] = "logs/test1"
logConfig["log_chan_size"] = "1"
logConfig["log_name"] = "xxxxx"
logConfig["open_sync"] = "1"
log ,_:=logger.InitLogger("file",logConfig)
log.Init()
/*for i := 0; i < 500; i++ {
d:=fmt.Sprintf("%d",i)
log.Info(d)
}*/
log.Info("dfsdf")
log.SyncWait()
}
\ No newline at end of file
......@@ -5,9 +5,9 @@ import (
"github.com/gin-gonic/gin"
_ "github.com/ichunt2019/ichunt-micro-registry/registry/etcd"
"github.com/micro/go-micro/v2/web"
"golang_open_platform/boot"
"golang_open_platform/pkg/config"
"golang_open_platform/routes"
"go_supplier_task/boot"
"go_supplier_task/pkg/config"
"go_supplier_task/routes"
)
func main() {
......
[xorm]
ShowSQL = true
[pool_class]
user_name = root
password = root
host = 192.168.2.250
database = liexin_pool_class
table_prefix =lie_
type = mysql
[supp]
user_name = liexin_ass
password = `liexin_ass#zsyM`
host = 192.168.2.232
database = liexin_ass
table_prefix =lie_
type = mysql
[cms]
user_name = ichuntcms
password = `ichuntcms#zsyM`
host = 192.168.2.232
database = ichuntcms
table_prefix =
type = mysql
[sku0]
user_name = spu
password = spu
host = 192.168.1.235
database = liexin_sku_0
table_prefix =lie_
type = mysql
[sku_0]
user_name = spu
password = spu
host = 192.168.1.235
database = liexin_sku_0
table_prefix =lie_
type = mysql
[sku_1]
user_name = spu
password = spu
host = 192.168.1.235
database = liexin_sku_1
table_prefix =lie_
type = mysql
[sku_2]
user_name = spu
password = spu
host = 192.168.1.235
database = liexin_sku_2
table_prefix =lie_
type = mysql
[sku_3]
user_name = spu
password = spu
host = 192.168.1.235
database = liexin_sku_3
table_prefix =lie_
type = mysql
[sku_4]
user_name = spu
password = spu
host = 192.168.1.235
database = liexin_sku_4
table_prefix =lie_
type = mysql
[sku_5]
user_name = spu
password = spu
host = 192.168.1.235
database = liexin_sku_5
table_prefix =lie_
type = mysql
[sku_6]
user_name = spu
password = spu
host = 192.168.1.235
database = liexin_sku_6
table_prefix =lie_
type = mysql
[sku_7]
user_name = spu
password = spu
host = 192.168.1.235
database = liexin_sku_7
table_prefix =lie_
type = mysql
[sku_8]
user_name = spu
password = spu
host = 192.168.1.235
database = liexin_sku_8
table_prefix =lie_
type = mysql
[sku_9]
user_name = spu
password = spu
host = 192.168.1.235
database = liexin_sku_9
table_prefix =lie_
type = mysql
\ No newline at end of file
......@@ -4,4 +4,5 @@ host = 192.168.1.237:27017
username = "ichunt"
password = "huntmon6699"
database = ichunt
maxPoolSize=8000
\ No newline at end of file
maxPoolSize=8000
;存放rabmq连接信息
[rabmq]
url = amqp://guest:guest@192.168.2.232:5672/
;存放本系统所有的队列名称
[rabmq_all]
; bom任务id
MQ_BOM_ITEMS_LIST=bom_items_list
wms_name = wms_service_test
package controller
import (
"fmt"
"github.com/gin-gonic/gin"
"golang_open_platform/framework/gin_"
"golang_open_platform/open"
"golang_open_platform/pkg/common"
"golang_open_platform/pkg/config"
"golang_open_platform/pkg/e"
"golang_open_platform/pkg/logger"
"net/http"
)
//放 通用中间件
//检查服务是否可用
func Check_Middleware() gin_.Middleware {
return func(next gin_.Endpoint) gin_.Endpoint {
return func(context *gin.Context, request interface{}) (response interface{}, err error) {
return next(context, request)
}
}
}
/**
@param tnterfaceName 接口别名
*/
func Open_Middleware(tnterfaceName string) gin.HandlerFunc {
return func(ctx *gin.Context) {
tokenStr:= ctx.Request.FormValue("token")
openObj:=open.NewOpen(tokenStr)
e.CheckError(openObj.Validate.Check(ctx,tnterfaceName))
ctx.Next()
}
}
func Cors_Middleware() gin_.Middleware {
return func(next gin_.Endpoint) gin_.Endpoint {
return func(c *gin.Context, request interface{}) (response interface{}, err error) {
method := c.Request.Method
corsDomains := config.Get("web.cors.domain").Strings(",")
for _, domain := range corsDomains {
c.Header("Access-Control-Allow-Origin", domain)
}
c.Header("Access-Control-Allow-Origin", "https://bom.ichunt.com")
c.Header("Access-Control-Allow-Headers", "Content-Type,AccessToken,X-CSRF-Token, Authorization, Token")
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type")
c.Header("Access-Control-Allow-Credentials", "true")
//放行所有OPTIONS方法
if method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
}
// 处理请求
return next(c, request)
}
}
}
/**
@author wangsong
捕获 系统内部 panic 异常,并加入日志
*/
func Error_Middleware(logPaths ...string) gin.HandlerFunc {
logPath:="zysku_save"
if(len(logPaths)>0){
logPath=logPaths[0]
}
return func(ctx *gin.Context) {
defer func() {
if err:=recover(); err!=nil{
if apiError,ok:=err.(*e.ApiError);ok{
fmt.Println(apiError.ErrMsg)
common.NResponse(apiError.ErrMsg,apiError.Code).SetLogHandel(errlogHandle(logPath)).OpenParamLog().OutPut(ctx)
}else{
errMsg:=fmt.Sprintf("%s",err)
fmt.Println(errMsg)
common.NResponse("service error",500).SetLogHandel(errlogHandle(logPath)).OpenParamLog().OutPut(ctx)
}
ctx.Abort()
}
}()
ctx.Next()
}
}
/**
错误日志处理,是给 NResponse的SetLogHandel 方法使用(其他同事用不到NResponse的SetLogHandel可以不用管它)
@param logPath 对应 log.ini的配置文件 比如 lysku_save
@return func
*/
func errlogHandle(logPath string) func(msg string){
return func(msg string) {
logger.Select(logPath).Error(msg)
}
}
\ No newline at end of file
package controller
import (
"github.com/gin-gonic/gin"
"github.com/gogf/gf/util/gconv"
"golang_open_platform/model"
"golang_open_platform/open"
"golang_open_platform/pkg/common"
"golang_open_platform/pkg/e"
"golang_open_platform/service"
)
/*
健康监测
*/
func Hbsdata(ctx *gin.Context) {
ctx.String(200, "ok")
}
/**
用classId获取sku列表
*/
func GetSkuListByClass(ctx *gin.Context) {
req:=&model.QuerySkuCreq{}
e.CheckError(model.ValidateBind(ctx,req))
rsp,err:=service.NewSkuService().GetSkuListByClass(req)
e.CheckError(err)
common.NResponse("",0,rsp).OutPut(ctx)
}
/**
用goods_id 获取sku列表完整字段
*/
func GetSkuListFull(ctx *gin.Context) {
req:=&model.QuerySkuReq{}
e.CheckError(model.ValidateBind(ctx,req))
rsp,err:=service.NewSkuService().GetSkuListFull(req)
e.CheckError(err)
common.NResponse("",0,rsp).OutPut(ctx)
}
/**
用goods_id 获取sku列表价格库存字段
*/
func GetSkuListPrice(ctx *gin.Context) {
req:=&model.QuerySkuReq{}
e.CheckError(model.ValidateBind(ctx,req))
rsp,err:=service.NewSkuService().GetSkuListPrice(req)
e.CheckError(err)
common.NResponse("",0,rsp).OutPut(ctx)
}
/**
用goods_id 获取sku列表价格库存字段
*/
func GetClassList(ctx *gin.Context) {
parent_id:= ctx.Request.FormValue("parent_id")
if(parent_id==""){
common.NResponse("parent_id 必填",open.PARAM1,nil).OutPut(ctx)
return
}
rsp,err:=service.NewClassService().GetClassList(gconv.Int(parent_id))
e.CheckError(err)
common.NResponse("",0,rsp).OutPut(ctx)
}
......@@ -2,8 +2,8 @@ package dao
import (
"fmt"
"golang_open_platform/model"
"golang_open_platform/pkg/mysql"
"go_supplier_task/model"
"go_supplier_task/pkg/mysql"
)
/**
......
package dao
import (
"golang_open_platform/model"
"golang_open_platform/pkg/common"
"golang_open_platform/pkg/mongo"
"go_supplier_task/model"
"go_supplier_task/pkg/common"
"go_supplier_task/pkg/mongo"
"gopkg.in/mgo.v2"
"gopkg.in/mgo.v2/bson"
)
......
......@@ -120,7 +120,7 @@ func newWhiteBlack() {
jdWhiteList:=jdToken+"_"+openWhiteList
jdBlackList:=jdToken+"_"+openBlackList
config:=map[string]string{
jdWhiteList:"192.168.2.72 222.92.21.99 192.168.2.129 ::1",
jdWhiteList:"192.168.2.72 222.92.21.99 192.168.2.129 ::1 119.123.79.225 180.163.220.66",
jdBlackList:"::1 ",
}
......@@ -176,8 +176,8 @@ func redisHashSet(key string,values map[string]interface{}) {
b,_=json.Marshal(string(b))
hsetStr:="Hset "+key+" "+string(hashk)+" "+string(b)
fmt.Println(hsetStr)
//fmt.Println("对应计时的min key\nflowUse_"+hashk+"_min")
//fmt.Println("对应计时的day key\nflowUse_"+hashk+"_day")
fmt.Println("对应计时的min key\nflowUse_"+hashk+"_min")
fmt.Println("对应计时的day key\nflowUse_"+hashk+"_day")
}
}
......
module golang_open_platform
module go_supplier_task
go 1.14
require (
github.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/gin-gonic/gin v1.6.3
......
package model
//根据class获取sku列表 返回参数
type QueryClassRsp struct {
ClassId int `json:"class_id"`
ClassName string `json:"class_name"`
ParentId int `json:"parent_id"`
}
\ No newline at end of file
package model
import (
"github.com/gin-gonic/gin"
"golang_open_platform/pkg/common"
"golang_open_platform/pkg/e"
validator "golang_open_platform/pkg/validate"
)
//过滤
func SkuFilter(remoteData RemoteSkuData,filedStr []string) (ArraySkuData) {
arraySku:=filterData(remoteData,filedStr)
if(len(arraySku)==0){
return nil
}
return arraySku
}
/**
过滤
@param remoteData 原始商详数据
@param filterField 需要过滤的字段
@return data 返回新的数据
*/
func filterData(remoteData RemoteSkuData,filterField []string)(ArraySkuData){
//arraySku:=make([]map[string]interface{},0)
arraySkud:=ArraySkuData{}
for skuId,skuInfo:=range remoteData{
newItem:=make(map[string]interface{})
/*if _,ok:=skuInfo.(bool);ok{
delete(remoteData,skuId)
continue
}*/
if info,ok:=skuInfo.(map[string]interface{});ok{
for _,field:=range filterField{
if _,ok:=info[field];ok{
newItem[field]=info[field]
if(field=="ladder_price"){
newItem[field]=filterLadder(info[field])
}
}else{
newItem[field]=""
//common.PrintStdout().Printf("skuId:%s,字段:%s 不存在",string(skuId),field)
}
}
arraySkud=append(arraySkud,newItem)
//remoteData[skuId]=newItem
}else{//有问题的格式
common.PrintStdout().Printf("skuId:%s,是个有问题的格式",string(skuId))
//delete(remoteData,skuId)
continue
}
}
return arraySkud
}
func filterLadder(ladder interface{}) interface{}{
var newLadderS = []map[string]interface{}{}
fileds:=[]string{"purchases"/*,"price_us"*/,"price_cn"}
if ladderSlice,ok:=ladder.([]interface{});ok{
if(len(ladderSlice)<=0){
return ladderSlice
}
for _,ladderOne:=range ladderSlice{
newLadderOne:=make(map[string]interface{})
if ladderMap,ok:=ladderOne.(map[string]interface{});ok{
for _,filed:=range fileds {
if _,ok:=ladderMap[filed];ok{
newLadderOne[filed]=ladderMap[filed]
}
}
newLadderS=append(newLadderS,newLadderOne)
}else{
common.PrintStdout().Printf("阶梯价数组其中的一个 不能转换成map")
}
}
}
return newLadderS
}
//end
//验证并绑定
func ValidateBind(ctx *gin.Context,req interface{}) (err error){
err1:=validator.NewValidator().GinShouldBind(ctx,req)
if(err1!=nil){
return e.NewApiError(err1.ErrMsg,err1.Code)
}
return
}
\ No newline at end of file
package model
import (
"go_supplier_task/pkg/common"
)
/**
@author wangsong
联营sku实体-新增联营sku用到
为什么不用ly_sku 那个实体,因为ly_sku 包含了spu的信息,LySkuEntity只是针对sku表的实体
*/
type LySkuEntity struct {
GoodsId string `json:"goods_id"`
Encoded string `json:"encoded"`//内部编码
Canal string `json:"canal" form:"canal" `//供应商编码-渠道标签
UpdateTime int `json:"update_time" xorm:"created"`
}
//将 LySkuEntity 的值赋值到 SkuRedisInfo(两个结构字段和类型不一样不会赋值)
func (LS *LySkuEntity) ToRedisSku() LySkuRedisInfo{
skuRedisInfo:=LySkuRedisInfo{}
common.CopyStruct(LS,&skuRedisInfo)
return skuRedisInfo
}
//将 LySkuEntity 的值赋值到 model.LySkuMongo(两个结构字段和类型不一样不会赋值)
/*func (LS *LySkuEntity) ToMongoSku() LySkuMongo{
LySkuMongo:=LySkuMongo{}
common.CopyStruct(LS,&LySkuMongo)
LySkuMongo.SpuId=gconv.Int64(LS.SpuId)//spuID在mangoDB是int64位
LySkuMongo.GoodsId=gconv.Int64(LS.GoodsId)//spuID在mangoDB是int64位
return LySkuMongo
}*/
package model
type SkuMergeLog struct {
GoodsId string `json:"goods_id"`
Encoded string `json:"encoded"`//内部编码
OldEncoded string `json:"old_encoded"`//内部编码
OldCanal string `json:"canal" form:"old_canal" `//供应商编码-渠道标签
Canal string `json:"canal" form:"canal" `//供应商编码-渠道标签
UpdateTime int `json:"update_time" xorm:"updated"`
}
\ No newline at end of file
package model
//根据class获取sku列表 请求参数
type QuerySkuCreq struct {
Page int `json:"page" form:"page" binding:"required" requiredMsg:"page为必填" requiredCode:"80001"`
ClassId int `json:"class_id" form:"class_id" binding:"required" requiredMsg:"class_id为必填" requiredCode:"80001"`
}
//=======================================end================================================
//根据class获取sku列表 返回参数
type QuerySkuCrsp struct {
Count int `json:"count"`
SkuData ArraySkuData `json:"sku_data"`
PageSize int `json:"page_size"`
Page int `json:"page"`
}
func (this *QuerySkuCrsp)GetClassFilter() []string{
field:=[]string{"spu_id","attrs","supplier_name","goods_images","hk_delivery_time"}
return field
}
//============end=============
//根据goodsId获取sku列表 请求参数
type QuerySkuReq struct {
GoodsIds string `json:"goods_id" form:"goods_id" binding:"required" requiredMsg:"goods_id必填" requiredCode:"80001"`
}
//=======================================end================================================
//根据goodsId获取sku列表 返回参数
type QuerySkuRsp struct {
SkuData ArraySkuData `json:"sku_data"`
}
//============end=============
//商详接口返回的数据 data字段
type RemoteSkuData map[string]interface{}
//
type ArraySkuData []map[string]interface{}
package model
//对应mongo SKU
type LySkuMongo struct {
SpuId int64 `bson:"spu_id"`
GoodsId int64 `json:"goods_id" bson:"goods_id"`
SupplierId int `json:"supplier_id" bson:"supplier_id"`
Moq int `json:"moq" bson:"moq"`
Encoded int `json:"encoded" bson:"encoded"`
Canal string `json:"canal" bson:"canal"`
OldGoodsId int `json:"old_goods_id" bson:"old_goods_id"`//创建sku接口 不包含OldGoodsId
}
\ No newline at end of file
package model
type MongoSkuOpen struct {
ClassId int `bson:"class_id2"`
Page int `bson:"page"`
Count int `bson:"count"`
SkuIds string `bson:"sku_ids"`
}
package model
/**
@author wangsong
联营 redis hash sku 结构,主要是用作新增插入用
poolSkuSave 插入redis就是这些字段
*/
type LySkuRedisInfo struct {
SpuId string `json:"spu_id"`
Encoded int `json:"encoded"`//供应商编码
Moq int `json:"moq" form:"moq" binding:"required"`//起订量
Mpq int `json:"mpq" form:"mpq" binding:"required"`//标准包装量
OldGoodsId int64 `json:"old_goods_id"`//老商品ID
GoodsType int `json:"goods_type"`//'0:自营 1:联营 2:专卖',
GoodsStatus int64 `json:"goods_status"`//sku状态 '商品状态 0:待审核 1:审核通过(上架)2:审核不通过 3:下架 4:删除'
BatchSn string `json:"batch_sn" form:"batch_sn" `//批次
Stock int `json:"stock" form:"stock" `//库存
HkDeliveryTime string `json:"hk_delivery_time" form:"hk_delivery_time" `//香港货期
CnDeliveryTime string `json:"cn_delivery_time" form:"cn_delivery_time" `//大陆货期
LadderPrice interface{} `json:"ladder_price" form:"ladder_price" `//阶梯价钱
UpdateTime int `json:"update_time" xorm:"created"`
GoodsImages string `json:"goods_images" form:"goods_images" `//商品图片 所属spu
Canal string `json:"canal" form:"canal" `//渠道开发员ID
SupplierId int `json:"supplier_id" form:"supplier_id" binding:"required"`
CpTime int `json:"cp_time" form:"cp_time" `//茂则成本添加时间
}
\ No newline at end of file
package open
const (
//======================开放平台错误码,81开头========================
//商家配置 811
BUSINESSERR1 = 81105 //读取商家列表 redis错误
BUSINESSERR2 = 81104 //商家不存在 token不存在
BUSINESSERR3 = 81106 //平台没有配置商家列表
//接口配置812
BUSINESSCONFIG1 = 81205 //读取商家与接口关系 redis错误(openBusinessInterface)
BUSINESSCONFIG2 = 81206 //平台没有配置 商家与接口关系 (redis hash openBusinessInterface 空)
BUSINESSCONFIG3 = 812409 //商家没有此接口的权限
//白名单配置813
WHILTREDISEERR1 = 813505//白名单读取redis错误
WHILTREDISEERR2 = 813404//ip不在白名单里面
WHILTREDISEERR5 = 813408//黑名单限制
WHILTREDISEERR3 = 813406//没有设置白名单
WHILTREDISEERR4 = 813407//没有设置黑名单
//锁814
FLOWERR1 = 814505//lua脚本执行出错
FLOWERR5 = 814408//脚本执行超时
FLOWERR3 = 814406//触发分钟级流量限制
FLOWERR4 = 814407//触发天级流量限制
//lock===========815=========
//sku接口===================================== 80开头 获取sku错误码
PARAM1 = 80001 //参数问题(缺失or数据格式不对)
OTHERERROR = 80500 //其他错误,比如读取redis出错
REMOTESKUINFO = 80501 //调用远程商详接口报错
//class
CLASSDBERR = 80504 //获取class mysql报错
)
package open
import (
"github.com/tidwall/gjson"
"golang_open_platform/pkg/common"
"golang_open_platform/pkg/e"
"strconv"
)
//商家 比如京东
type business struct {
dao *Dao
token string
interfaceConfig *interfaceConfigOne
}
func NewBusiness(token string) *business {
return &business{token:token,dao:&Dao{}}
}
type interfaceConfigOne struct {
dayMaxNum int64 `json:"dayMaxNum"`
minMaxNum int64 `json:"minMaxNum"`
totalMaxNum int64 `json:"totalMaxNum"`
}
//获取商家与接口配置
func (this *business) GetInterfaceConfig(interfaceName string) (configOne *interfaceConfigOne,err error ){
if(this.interfaceConfig!=nil){
return this.interfaceConfig,nil
} else{
str,err:=this.dao.GetbusinessInterface(this.token,interfaceName)
if(err!=nil){
return configOne,err
}
if(str==""){
common.PrintStdout().Printf(strconv.Itoa(BUSINESSCONFIG2)+":平台没有设置此商家的接口配置")
return nil,e.NewApiError("business err",BUSINESSCONFIG2)
}
//config:=&interfaceConfigOne{}
//json.Unmarshal([]byte(str),config)//防止有人插入redis的时候数据格式不对,就不用这个方法了
this.interfaceConfig=&interfaceConfigOne{}
this.interfaceConfig.dayMaxNum=gjson.Parse(str).Get("dayMaxNum").Int()
this.interfaceConfig.minMaxNum=gjson.Parse(str).Get("minMaxNum").Int()
this.interfaceConfig.totalMaxNum=gjson.Parse(str).Get("totalMaxNum").Int()
return this.interfaceConfig,nil
}
return
}
package open
import (
"github.com/gomodule/redigo/redis"
"golang_open_platform/pkg/common"
"golang_open_platform/pkg/e"
"golang_open_platform/pkg/gredis"
"strconv"
)
//此struct ,包含简单读 数据的一些操作
type Dao struct {
}
//获取白名单列表 openWhiteList
/*func (this *Dao) getOpenWhiteList()(whitestr string,err error){
redisReadConn := gredis.Conn("search_r")
defer redisReadConn.Close()
whitestr,err=redis.String(redisReadConn.Do("Get","openWhiteList"))
if(err!=nil && err!=redis.ErrNil){
common.PrintStdout().Printf(strconv.Itoa(WHILTREDISEERR1)+":读取redis "+err.Error())
return "",e.NewApiError("white err",WHILTREDISEERR1)
}
return whitestr,nil
}
//获取商家列表 openTokenBusinessList
func (this *Dao) getBusinessList()(listStr string,err error){
redisReadConn := gredis.Conn("search_r")
defer redisReadConn.Close()
listStr,err=redis.String(redisReadConn.Do("Get","openTokenBusinessList"))
if(err!=nil && err!=redis.ErrNil){
common.PrintStdout().Printf(strconv.Itoa(BUSINESSERR1)+":读取redis token列表错误"+err.Error())
return "",e.NewApiError("business",BUSINESSERR1)
}
return listStr,nil
}*/
//获取商家接口配置 openBusinessInterface
func (this *Dao) GetbusinessInterface(token string,interfaceName string) (Str string,err error) {
redisReadConn := gredis.Conn("search_r")
defer redisReadConn.Close()
key:=token+"_"+interfaceName
Str,err=redis.String(redisReadConn.Do("Hget","openBusinessInterface",key))
if(err!=nil && err!=redis.ErrNil){
common.PrintStdout().Printf(strconv.Itoa(BUSINESSCONFIG1)+":读取redis 商家接口配置错误"+err.Error())
return "",e.NewApiError("business err",BUSINESSCONFIG1)
}
return Str,nil
}
//获取计数 redis set
func (this *Dao) getRedisFlowNum(key string) (num int,err error){
redisReadConn := gredis.Conn("search_r")
defer redisReadConn.Close()
num,err= redis.Int(redisReadConn.Do("Get", key))
if(err!=nil && err!=redis.ErrNil){
common.PrintStdout().Printf("读取redis "+key+"错误"+err.Error())
return 0,e.NewApiError("service error",FLOWERR1)
}
return num,nil
}
func (this *Dao) setRedisFlowEx(key string,expire interface{}) error{
redisWriteConn := gredis.Conn("search_w")
defer redisWriteConn.Close()
_, err:= redisWriteConn.Do("SET", key, "0", "EX", expire)
if(err!=nil){
common.PrintStdout().Printf("设置redis "+key+"错误")
return e.NewApiError("service error",FLOWERR1)
}
return nil
}
func (this *Dao)incrRedisFlowNum(key string) (num int,err error){
redisWriteConn := gredis.Conn("search_w")
defer redisWriteConn.Close()
num,err= redis.Int(redisWriteConn.Do("INCR", key))
if(err!=nil){
common.PrintStdout().Printf("INCR redis "+key+"错误")
return 0,e.NewApiError("service error",FLOWERR1)
}
return num,nil
}
package open
import (
"context"
"github.com/gogf/gf/util/gconv"
"github.com/gomodule/redigo/redis"
"golang_open_platform/pkg/common"
"golang_open_platform/pkg/e"
"golang_open_platform/pkg/gredis"
"sync"
"time"
)
const (
INCRTYPEMIN = iota //分
INCRTYPEDAY //天
)
var lock sync.Mutex
type flowmeter struct {
dao *Dao
flowLimitType int //INCRTYPEMIN 分限制 INCRTYPEDAY 天限制
token string //token
interfaceName string //接口名称
}
const incrLua=`
local dayNum = tonumber(redis.call('get', KEYS[1]) or 0)
local minNum = tonumber(redis.call('get', KEYS[2]) or 0)
local dayMax = tonumber(ARGV[1])
local minMax = tonumber(ARGV[2])
print("Hello World!")
if (dayNum + 1 > dayMax) then
return 1
else
if (dayNum == 0) then
redis.call('SETEX', KEYS[1], 86400,0)
end
end
if (minNum + 1 > minMax) then
return 2
else
if (minNum == 0) then
redis.call('SETEX', KEYS[2], 60,0)
end
end
redis.call('incrby', KEYS[2], 1)
redis.call('incrby', KEYS[1], 1)
return 0
`
func NewFlow(token string,interfaceName string) *flowmeter{
return &flowmeter{dao:&Dao{},token:token,interfaceName:interfaceName}
}
//获取 对应的计数redis key 和过期时间
func (this *flowmeter) getFlowKeyAndEx(flowLimitType int) (key string,expireTime int64){
key="flowUse_"+this.token+"_"+this.interfaceName
switch flowLimitType {
case INCRTYPEMIN:
key+="_min"
expireTime=int64(time.Minute.Seconds())
break
case INCRTYPEDAY:
key+="_day"
expireTime=int64(time.Hour.Seconds()*24)
break
default:
key+="_min"
expireTime=int64(time.Minute.Seconds())
break
}
return
}
/**
验证
*/
func (this *flowmeter) checkout(business *business) error{
redisWriteConn := gredis.Conn("search_w")
defer redisWriteConn.Close()
interfaceConfig,err:=business.GetInterfaceConfig(this.interfaceName)//商家接口配置,包含一小时最大流量,一天最大流量
if(err!=nil){
return err
}
dayMax:=interfaceConfig.dayMaxNum
minMax:=interfaceConfig.minMaxNum
dayKey,_:=this.getFlowKeyAndEx(INCRTYPEDAY)
minKey,_:=this.getFlowKeyAndEx(INCRTYPEMIN)
/*fmt.Println(dayKey)
fmt.Println(minKey)*/
script:=redis.NewScript(2,incrLua)
//脚本执行5秒算它卡死,用script kill杀死此次脚本
context,cancel:=context.WithTimeout(context.Background(),time.Second*1)
defer cancel()
errRes:=make(chan error)
intRes:=make(chan int)
go func() {
s,err:=script.Do(redisWriteConn,dayKey,minKey,dayMax,minMax)
if(err!=nil){
errRes<-err
return
}
intRes<-gconv.Int(s)
}()
select {
case <-context.Done():
common.PrintStdout().Printf("脚本执行失败")
Kill()
return e.NewApiError("service error",FLOWERR5)
case err:=<-errRes:
common.PrintStdout().Printf("脚本出错:"+err.Error())
return e.NewApiError("service error",FLOWERR1)
case num:=<-intRes:
if(num==1){
common.PrintStdout().Printf("触发天限制")
return e.NewApiError("触发天限制",FLOWERR4)
}
if(num==2){
common.PrintStdout().Printf("触发分钟级限制")
return e.NewApiError("触发分钟级限制",FLOWERR3)
}
break
}
return nil
}
func Kill() {
redisWriteConn := gredis.Conn("search_w")
defer redisWriteConn.Close()
_,err:=redisWriteConn.Do("script","kill")
if(err!=nil){
common.PrintStdout().Printf("kill失败"+err.Error())
return
}
common.PrintStdout().Printf("kill成功")
}
\ No newline at end of file
package open
import (
logger "github.com/ichunt2019/log"
"golang_open_platform/pkg/common"
"golang_open_platform/pkg/e"
"golang_open_platform/pkg/gredis"
"sync"
"time"
)
const (
INCRTYPEMIN = iota //
INCRTYPEDAY //
)
var lock sync.Mutex
type flowmeter struct {
dao *Dao
flowLimitType int //INCRTYPEMIN 分限制 INCRTYPEDAY 天限制
token string //token
interfaceName string //接口名称
}
func NewFlow(token string,interfaceName string) *flowmeter{
return &flowmeter{dao:&Dao{},token:token,interfaceName:interfaceName}
}
//获取 对应的计数redis key 和过期时间
func (this *flowmeter) getFlowKeyAndEx() (key string,expireTime int64){
key="flowUse_"+this.token+"_"+this.interfaceName
switch this.flowLimitType {
case INCRTYPEMIN:
key+="_min"
expireTime=int64(time.Minute.Seconds())
break
case INCRTYPEDAY:
key+="_day"
expireTime=int64(time.Hour.Seconds()*24)
break
default:
key+="_min"
expireTime=int64(time.Minute.Seconds())
break
}
return
}
//设置流量限制类型 INCRTYPEMIN 分限制 INCRTYPEDAY 天限制
func (this *flowmeter) setFlowKey(flowLimitType int) {
this.flowLimitType=flowLimitType
}
func (this *flowmeter) getNum()(int,error){
key,expireTime:=this.getFlowKeyAndEx()
//获取计数 ( redis key)
num,err:=this.dao.getRedisFlowNum(key)
//common.PrintStdout().Printf("读取 计数key:%s,num 为: %d",key,num)
if(err!=nil){
return 0,err
}
if(num==0){//计数不存在(redis没有此key),就新建一个带有超时的key
err:=this.dao.setRedisFlowEx(key,expireTime)
if(err!=nil){
return 0,err
}
}
return num,nil
}
/**
比较
*/
func (this *flowmeter) compare(maxNum int64) (error,bool){
num,err:=this.getNum()
if(err!=nil){
return err,false
}
if(int64(num)>=maxNum){
common.PrintStdout().Printf("最大流量为: %d < %d",int(maxNum),num)
return nil,false
}
return nil,true
}
/**
验证
*/
func (this *flowmeter) checkout(business *business) error{
return nil
interfaceConfig,err:=business.GetInterfaceConfig(this.interfaceName)//商家接口配置,包含一小时最大流量,一天最大流量
key,_:=this.getFlowKeyAndEx()
defer this.delOnlyLock(key)
err=this.addOnlyLock(key)//加锁
if(err!=nil){
common.PrintStdout().Printf("加锁失败")
this.dao.incrRedisFlowNum("err_num")
return err
}
//lock.Lock()
//defer lock.Unlock()
//验证天流量限制
this.setFlowKey(INCRTYPEDAY)
err,isPass:=this.compare(interfaceConfig.dayMaxNum)
if(err!=nil){
return err
}
if(err!=nil){
logger.Error("sku_query","比较失败")
return err
}
if(isPass==false){
return e.NewApiError("触发天级流控",FLOWERR4)
}
//分钟流量验证
this.setFlowKey(INCRTYPEMIN)
err,isPass=this.compare(interfaceConfig.minMaxNum)
if(err!=nil){
return err
}
if(isPass==false){
return e.NewApiError("触发分钟级流控",FLOWERR4)
}
//增加分钟计数
err=this.incr()
if(err!=nil){
common.PrintStdout().Printf("增加fen分计数报错")
return nil
}
//增加天计数
this.setFlowKey(INCRTYPEDAY)
err=this.incr()
if(err!=nil){
common.PrintStdout().Printf("增加天计数报错")
return err
}
return nil
}
//自增
func (this *flowmeter)incr() (err error) {
key,_:=this.getFlowKeyAndEx()
_,err=this.dao.incrRedisFlowNum(key)
if(err!=nil){
return err
}
//common.PrintStdout().Printf("加1后的值是 %d",num)
return nil
}
//如果锁不存在并设置锁
func (this *flowmeter)addOnlyLock(key string)error{
redisWriteConn := gredis.Conn("search_w")
defer redisWriteConn.Close()
name:="flow_lock_"+key
//common.PrintStdout().Printf("加锁 key:%s",name)
s, err:= redisWriteConn.Do("SET", name, "1", "EX", "100","NX")//锁两秒没主动删就自动关闭(防止某个流程卡死,没执行到删除锁)
if(err!=nil){
common.PrintStdout().Printf("读取redis 锁 key:%d 报错",key)
}
if(s!=nil){//读到了
//common.PrintStdout().Printf("加锁完成 key :%s",name)
return nil
}
common.PrintStdout().Printf("没读到")
return e.NewApiError("请重试",LOCKERR1)
}
//删除锁
func (this *flowmeter) delOnlyLock(key string){
redisWriteConn := gredis.Conn("search_w")
defer redisWriteConn.Close()
name:="flow_lock_"+key
_, err:= redisWriteConn.Do("DEL", name)
if(err!=nil){
logger.Error("sku_query","删除锁失败")
println(err.Error())
}
//common.PrintStdout().Printf("解锁完成")
}
\ No newline at end of file
package open
type OpenIchunt struct {
Validate *openValidate
Business *business //openObj.Business 现在用不到,后续应该能用到
}
func NewOpen(token string) *OpenIchunt {
openObj:=&OpenIchunt{}
openObj.Business=NewBusiness(token)
openObj.Validate=NewopenValidate(openObj.Business)
return openObj
}
package open
import (
"github.com/tidwall/gjson"
)
func InArrayGjson(needle string, haystack []gjson.Result) bool {
for _,ip:=range haystack{
if(ip.String()==needle){
return true
}
}
return false
}
package open
import (
"github.com/gin-gonic/gin"
"github.com/gomodule/redigo/redis"
"golang_open_platform/pkg/common"
"golang_open_platform/pkg/e"
"golang_open_platform/pkg/gredis"
"strconv"
)
type openValidate struct {
business *business
dao *Dao
}
func NewopenValidate(business *business) *openValidate{
return &openValidate{business:business,dao:&Dao{}}
}
//ip限制验证(白名单黑名单)
func (this *openValidate) ipValidate(ctx *gin.Context,token string) error {
redisCon := gredis.Conn("search_w")
defer redisCon.Close()
ip:=ctx.ClientIP()
openWhiteList:=token+"_openWhiteList"//白名单
openBlackList:=token+"_openBlackList"//黑名单
boolInt,err:=redis.Int(redisCon.Do("SISMEMBER",openBlackList,ip))
if(err!=nil && err!=redis.ErrNil){
common.PrintStdout().Printf("读取黑名单redis 出错")
return e.NewApiError("service err",WHILTREDISEERR5)
}
if(boolInt==1){
common.PrintStdout().Printf("黑名单限制:"+ctx.ClientIP())
return e.NewApiError("ip黑名单限制",WHILTREDISEERR5)
}
boolInt,err=redis.Int(redisCon.Do("SISMEMBER",openWhiteList,ip))
if(err!=nil && err!=redis.ErrNil){
common.PrintStdout().Printf("读取白名单redis 出错")
return e.NewApiError("service err",WHILTREDISEERR1)
}
if(boolInt!=1){
common.PrintStdout().Printf("ip不在白名单:"+ctx.ClientIP())
return e.NewApiError("ip白名单限制",WHILTREDISEERR2)
}
return nil
}
//白名单验证
/*func (this *openValidate) whiteValidate(ctx *gin.Context)error {
whiteStr,err:=this.dao.getOpenWhiteList()
if(err!=nil){
return err
}
white:=gjson.Parse(whiteStr)
if (whiteStr == "" || white.IsArray()!=true || len(white.Array())<=0){
common.PrintStdout().Printf(strconv.Itoa(WHILTREDISEERR3)+":redis 白名单没有设置")
return e.NewApiError("white err",WHILTREDISEERR3)
}
if(InArrayGjson(ctx.ClientIP(),white.Array())){
return nil
}
common.PrintStdout().Printf(strconv.Itoa(WHILTREDISEERR2)+":Not on the white list ip:"+ctx.ClientIP())
return e.NewApiError("Not on the white list",WHILTREDISEERR2)
}*/
/**
验证token是否存在
根据token到 商家表查询,如果存在,token就存在
*/
func (this *openValidate) exitValidate(token string) error{
redisCon := gredis.Conn("search_w")
defer redisCon.Close()
key:="openTokenBusinessList"
boolInt,err:=redis.Int(redisCon.Do("SISMEMBER",key,token))
if(err!=nil && err!=redis.ErrNil){
common.PrintStdout().Printf("读取商redis列表出错 :"+err.Error())
return e.NewApiError("service err",BUSINESSERR1)
}
if(boolInt!=1){
common.PrintStdout().Printf(strconv.Itoa(BUSINESSERR2)+":token 无效:"+token)
return e.NewApiError("token invalid",BUSINESSERR2)
}
return nil
}
/**
根据token 和 接口名称,到商家接口配置表查询,如果存在代表是有权限
*/
func (this *openValidate) permissionValidate(interfaceName string) error{
_,err:=this.business.GetInterfaceConfig(interfaceName)
if(err!=nil){
return err
}
return nil
}
//check验证
func (this *openValidate) Check(ctx *gin.Context,interfaceName string) error {
err:=this.ipValidate(ctx,this.business.token)
if(err!=nil){
return err
}
tokenStr:=this.business.token
//验证token的合法性
err=this.exitValidate(tokenStr)
if(err!=nil){
return err
}
//接口权限验证
err=this.permissionValidate(interfaceName)
if(err!=nil){
return err
}
//流量控制验证
err=NewFlow(tokenStr,interfaceName).checkout(this.business)
if(err!=nil){
return err
}
return nil
}
package common
import "math"
/**
简单分页结构
*/
type Paging struct {
Total int //总条数
//Page int64 //当前页
Pagesize int //每页条数
PageCount int //总页数
}
/**
@param page 当前页
return limit start length
*/
func (p *Paging) Limit(page int)(int,int) {
start:=(page-1)*p.Pagesize
length:=p.Pagesize
if((start+length)>p.Total){//如果是最后一页
length=p.Total-start
}
return start,length
}
/**
实例Paging
参照 https://studygolang.com/articles/12857
*/
func CreatePaging( pagesize, total int) *Paging {
if pagesize < 1 {
pagesize = 10
}
page_count := math.Ceil(float64(total) / float64(pagesize))
paging := new(Paging)
paging.Pagesize = pagesize
paging.Total = total
paging.PageCount = int(page_count)
return paging
}
......@@ -14,8 +14,8 @@ import (
"github.com/gogf/gf/util/gconv"
"github.com/syyongx/php2go"
"github.com/tidwall/gjson"
"golang_open_platform/pkg/config"
"golang_open_platform/pkg/vars"
"go_supplier_task/pkg/config"
"go_supplier_task/pkg/vars"
"log"
"math"
"math/big"
......
......@@ -4,8 +4,8 @@ import (
"github.com/gomodule/redigo/redis"
"github.com/syyongx/php2go"
"github.com/tidwall/gjson"
"golang_open_platform/pkg/config"
"golang_open_platform/pkg/gredis"
"go_supplier_task/pkg/config"
"go_supplier_task/pkg/gredis"
"strings"
)
......
......@@ -6,9 +6,9 @@ import (
"github.com/gomodule/redigo/redis"
"github.com/ichunt2019/logger"
log "github.com/sirupsen/logrus"
"golang_open_platform/pkg/config"
"golang_open_platform/pkg/gredis"
"golang_open_platform/pkg/message"
"go_supplier_task/pkg/config"
"go_supplier_task/pkg/gredis"
"go_supplier_task/pkg/message"
"os"
"strconv"
"strings"
......
......@@ -21,7 +21,90 @@ func BuildDatabaseList() (DatabaseList map[string]BaseDatabase) {
Database: Get("pool_class.database").String(),
Prefix: Get("pool_class.table_prefix").String(),
},
"supp": {
UserName: Get("supp.user_name").String(),
Password: Get("supp.password").String(),
Host: Get("supp.host").String(),
Database: Get("supp.database").String(),
Prefix: Get("supp.table_prefix").String(),
},
"cms": {
UserName: Get("cms.user_name").String(),
Password: Get("cms.password").String(),
Host: Get("cms.host").String(),
Database: Get("cms.database").String(),
Prefix: Get("cms.table_prefix").String(),
},
"sku_0": {
UserName: Get("sku_0.user_name").String(),
Password: Get("sku_0.password").String(),
Host: Get("sku_0.host").String(),
Database: Get("sku_0.database").String(),
Prefix: Get("sku_0.table_prefix").String(),
},
"sku_1": {
UserName: Get("sku_1.user_name").String(),
Password: Get("sku_1.password").String(),
Host: Get("sku_1.host").String(),
Database: Get("sku_1.database").String(),
Prefix: Get("sku_1.table_prefix").String(),
},
"sku_2": {
UserName: Get("sku_2.user_name").String(),
Password: Get("sku_2.password").String(),
Host: Get("sku_2.host").String(),
Database: Get("sku_2.database").String(),
Prefix: Get("sku_2.table_prefix").String(),
},
"sku_3": {
UserName: Get("sku_3.user_name").String(),
Password: Get("sku_3.password").String(),
Host: Get("sku_3.host").String(),
Database: Get("sku_3.database").String(),
Prefix: Get("sku_3.table_prefix").String(),
},
"sku_4": {
UserName: Get("sku_4.user_name").String(),
Password: Get("sku_4.password").String(),
Host: Get("sku_4.host").String(),
Database: Get("sku_4.database").String(),
Prefix: Get("sku_4.table_prefix").String(),
},
"sku_5": {
UserName: Get("sku_5.user_name").String(),
Password: Get("sku_5.password").String(),
Host: Get("sku_5.host").String(),
Database: Get("sku_5.database").String(),
Prefix: Get("sku_5.table_prefix").String(),
},
"sku_6": {
UserName: Get("sku_6.user_name").String(),
Password: Get("sku_6.password").String(),
Host: Get("sku_6.host").String(),
Database: Get("sku_6.database").String(),
Prefix: Get("sku_6.table_prefix").String(),
},
"sku_7": {
UserName: Get("sku_7.user_name").String(),
Password: Get("sku_7.password").String(),
Host: Get("sku_7.host").String(),
Database: Get("sku_7.database").String(),
Prefix: Get("sku_7.table_prefix").String(),
},
"sku_8": {
UserName: Get("sku_8.user_name").String(),
Password: Get("sku_8.password").String(),
Host: Get("sku_8.host").String(),
Database: Get("sku_8.database").String(),
Prefix: Get("sku_8.table_prefix").String(),
},
"sku_9": {
UserName: Get("sku_9.user_name").String(),
Password: Get("sku_9.password").String(),
Host: Get("sku_9.host").String(),
Database: Get("sku_9.database").String(),
Prefix: Get("sku_9.table_prefix").String(),
},
}
}
......@@ -4,7 +4,7 @@ import (
"github.com/imroc/req"
"math/rand"
"net/http"
"golang_open_platform/pkg/config"
"go_supplier_task/pkg/config"
)
type Response struct {
......
......@@ -3,7 +3,7 @@ package gredis
import (
"fmt"
"github.com/gomodule/redigo/redis"
"golang_open_platform/pkg/config"
"go_supplier_task/pkg/config"
"time"
)
......
......@@ -3,19 +3,12 @@ package logger
import (
"fmt"
"github.com/ichunt2019/log"
"golang_open_platform/pkg/config"
"go_supplier_task/pkg/config"
syncLog "go_supplier_task/pkg/logger/sync"
"strings"
"sync"
)
const (
LogLevelDebug = iota
LogLevelTrace
LogLevelInfo
LogLevelWarn
LogLevelError
LogLevelFatal
)
var (
logD map[string]logger.LogInterface
......@@ -29,7 +22,7 @@ func Loginit() {
for _,configOne:=range configs{
sliceConfig:=strings.Split(configOne, ",")
log:=getLog(sliceConfig[0],sliceConfig[1],"0")
log.Init()
logD[sliceConfig[0]]=log
}
})
......@@ -37,15 +30,12 @@ func Loginit() {
/**
@param path 路径 比如sku_save 即文件夹是 sku 文件名类似 是save_2020-12-10.log
@param msg 错误文本
@Level 默认是 logger.LogLevelInfo
示例
*/
func SyncInsert(path string,msg string,Levels ...int) {
log:=getLog(path,"10","1")
/*func SyncInsert(path string,msg string,Levels ...int) {
fmt.Print(msg)
log:=getLog(path,"1","1")
log.Init()
level:=LogLevelInfo
if(len(Levels)>0){
level=Levels[0]
......@@ -67,6 +57,15 @@ func SyncInsert(path string,msg string,Levels ...int) {
log.Info(msg)
}
log.SyncWait()
}*/
/**
@param path 路径 比如sku_save 即文件夹是 sku 文件名类似 是save_2020-12-10.log
@param msg 错误文本
@Level 默认是 logger.LogLevelInfo
*/
func SyncInsert(path string,msg string,Levels ...int) {
syncLog.SyncInsertLog(path,msg,Levels...)
}
//入口
......@@ -84,7 +83,7 @@ func Select(logFiled string) logger.LogInterface {
//内部方法,获取log
func getLog(path string,chan_size string,openSync string) logger.LogInterface {
logConfig := make(map[string]string)
logConfig["log_chan_size"]="10"
logConfig["log_chan_size"]="1000"
slicePath:=strings.Split(path, "_")
if(slicePath[0]=="" || slicePath[1]==""){
panic("配置文件出错:文件/路径配置出错;提示:第一个参数格式应为A_B")
......@@ -101,7 +100,7 @@ func getLog(path string,chan_size string,openSync string) logger.LogInterface {
if err != nil {
panic("初始化log包出错:")
}
log.Init()
return log
}
......
package logger
const (
LogLevelDebug = iota
LogLevelTrace
LogLevelInfo
LogLevelWarn
LogLevelError
LogLevelFatal
)
const (
LogSplitTypeHour = iota
LogSplitTypeSize
)
func getLevelText(level int) string {
switch level {
case LogLevelDebug:
return "DEBUG"
case LogLevelTrace:
return "TRACE"
case LogLevelInfo:
return "INFO"
case LogLevelWarn:
return "WARN"
case LogLevelError:
return "ERROR"
case LogLevelFatal:
return "FATAL"
}
return "UNKNOWN"
}
func getLogLevel(level string) int {
switch level {
case "debug":
return LogLevelDebug
case "trace":
return LogLevelTrace
case "info":
return LogLevelInfo
case "warn":
return LogLevelWarn
case "error":
return LogLevelError
case "fatal":
return LogLevelFatal
default:
return LogLevelInfo
}
return LogLevelDebug
}
package logger
import (
"encoding/json"
"fmt"
"os"
"strings"
"time"
)
/**
@param path 路径 比如sku_save 即文件夹是 sku 文件名类似 是save_2020-12-10.log
@param msg 错误文本
@Level 默认是 logger.LogLevelInfo
*/
func SyncInsertLog(path string,msg string,Levels ...int) {
logConfig := make(map[string]string)
logConfig["log_chan_size"]="1000"
slicePath:=strings.Split(path, "_")
if(slicePath[0]=="" || slicePath[1]==""){
panic("配置文件出错:文件/路径配置出错;提示:第一个参数格式应为A_B")
}
logConfig["log_path"] = "logs/"+slicePath[0]
logConfig["log_name"] = slicePath[1]
logPath:="logs/"+slicePath[0]
logName:=slicePath[1]
if logName=="" {
logName = time.Now().Format("2006-01-02")
}else{
logName = logName+"_"+time.Now().Format("2006-01-02")
}
filename := fmt.Sprintf("%s/%s.log", logPath, logName)
createFile(logPath)
file, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0755)
defer file.Close()
if err != nil {
panic(fmt.Sprintf("open faile %s failed, err:%v", filename, err))
}
level:=LogLevelInfo
if(len(Levels)>0){
level=Levels[0]
}
//"fileName":"service_mapping.go","method":"service.test","lineNo":85
logData:=writeLog(level,msg)
fmt.Printf("%s;fileinfo:%s\n",msg,logData.Filename)
str,err :=json.Marshal(logData)
if err == nil{
fmt.Fprintf(file, string(str)+"\n")
}
}
//调用os.MkdirAll递归创建文件夹
func createFile(filePath string) error {
if !isExist(filePath) {
err := os.MkdirAll(filePath,os.ModePerm)
return err
}
return nil
}
// 判断所给路径文件/文件夹是否存在(返回true是存在)
func isExist(path string) bool {
_, err := os.Stat(path) //os.Stat获取文件信息
if err != nil {
if os.IsExist(err) {
return true
}
return false
}
return true
}
package logger
import (
"fmt"
"path"
"runtime"
"time"
)
type LogData struct {
Message string `json:"msg"`
TimeStr string `json:"dateStr"`
LevelStr string `json:"levelStr"`
Filename string `json:"fileName"`
FuncName string `json:"method"`
LineNo int `json:"lineNo"`
WarnAndFatal bool `json:"warnAndFatal"`
Req string `json:"request"`
}
//util.go 10
func GetLineInfo() (fileName string, funcName string, lineNo int) {
pc, file, line, ok := runtime.Caller(4)
if ok {
fileName = file
funcName = runtime.FuncForPC(pc).Name()
lineNo = line
}
return
}
/*
1. 当业务调用打日志的方法时,我们把日志相关的数据写入到chan(队列)
2. 然后我们有一个后台的线程不断的从chan里面获取这些日志,最终写入到文件。
*/
func writeLog(level int, format string, args ...interface{}) *LogData {
now := time.Now()
nowStr := now.Format("2006-01-02 15:04:05.999")
levelStr := getLevelText(level)
fileName, funcName, lineNo := GetLineInfo()
fileName = path.Base(fileName)
funcName = path.Base(funcName)
msg := fmt.Sprintf(format, args...)
logData := &LogData{
Message: msg,
TimeStr: nowStr,
LevelStr: levelStr,
Filename: fileName,
FuncName: funcName,
LineNo: lineNo,
WarnAndFatal: false,
}
if level == LogLevelError || level == LogLevelWarn || level == LogLevelFatal {
logData.WarnAndFatal = true
}
return logData
//fmt.Fprintf(file, "%s %s (%s:%s:%d) %s\n", nowStr, levelStr, fileName, funcName, lineNo, msg)
}
......@@ -4,7 +4,7 @@ import (
"encoding/json"
"github.com/imroc/req"
"github.com/tidwall/gjson"
"golang_open_platform/pkg/config"
"go_supplier_task/pkg/config"
)
//发送钉钉消息的包
......
......@@ -6,7 +6,7 @@ import (
"github.com/syyongx/php2go"
"net/http"
"net/url"
"golang_open_platform/pkg/config"
"go_supplier_task/pkg/config"
"strconv"
"time"
)
......
package mongo
import (
"golang_open_platform/pkg/config"
"go_supplier_task/pkg/config"
"gopkg.in/mgo.v2"
"strconv"
)
......
......@@ -2,7 +2,7 @@ package mq
import (
"github.com/ichunt2019/go-rabbitmq/utils/rabbitmq"
"golang_open_platform/pkg/config"
"go_supplier_task/pkg/config"
)
func PushMsg(listName string, data string) {
......
......@@ -4,8 +4,8 @@ import (
"fmt"
_ "github.com/go-sql-driver/mysql"
"github.com/go-xorm/xorm"
"golang_open_platform/pkg/config"
"golang_open_platform/pkg/e"
"go_supplier_task/pkg/config"
"go_supplier_task/pkg/e"
)
var DatabaseConMap map[string]*xorm.Engine
......@@ -24,6 +24,7 @@ func Setup() error {
dataSourceName := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8", userName, password, host, database)
DatabaseConMap[conName], err = xorm.NewEngine("mysql", dataSourceName)
//xorm.NewEngineGroup()
if err != nil {
return e.NewFatalError(err.Error()) //这里返回致命异常
}
......@@ -43,5 +44,11 @@ func Setup() error {
}
func Conn(conName string) *xorm.Engine {
return DatabaseConMap[conName]
if value,ok:=DatabaseConMap[conName];ok{
if(value!=nil){
return value
}
return value
}
panic("连接:"+conName+"不存在")
}
......@@ -2,7 +2,7 @@ package routes
import (
"github.com/gin-gonic/gin"
"golang_open_platform/controller"
"go_supplier_task/controller"
)
//初始化路由
......
#!/bin/bash
cd /data/gocode/golang_open_platform
cd /data/gocode/go_supplier_task
git pull origin dev
go build -o ./cmd/http/http_server ./cmd/http/http_server.go
go build -o ./cmd/http/cron ./cmd/http/cron_server.go
supervisorctl restart golang_open_platform_http
\ No newline at end of file
supervisorctl restart go_supplier_task_http
\ No newline at end of file
package service
import (
"golang_open_platform/dao"
"golang_open_platform/model"
"golang_open_platform/open"
"golang_open_platform/pkg/common"
"golang_open_platform/pkg/e"
)
type ClassService struct {
}
//获取sku列表完整字段
func (this *ClassService) GetClassList(parentId int)(rsp []model.QueryClassRsp,err error) {
//skuIds:=strings.Replace(req.GoodsIds," ",",",-1)
//skuIds
rsp,err=dao.GetClassList(parentId)
if(err!=nil){
common.PrintStdout().Printf(err.Error())
return rsp,e.NewApiError("service error",open.CLASSDBERR)
}
return
}
func NewClassService()*ClassService{
return &ClassService{}
}
\ No newline at end of file
package service
import (
"encoding/json"
"fmt"
"github.com/gogf/gf/util/gconv"
syncLog "go_supplier_task/pkg/logger/sync"
"go_supplier_task/pkg/mysql"
"strings"
)
var ErpTaskName ="[同步到erp]"
type ErpService struct {
ServiceCommonSupplier
}
var CurrencyMapping =map[int]interface{}{
1: "CNY",
2 :"USD",
3 : "HKD",
4 : "EUR",
5 : "GBP",
6 : "CHF",
}
var StatusMapping=map[int]interface{}{
2:"1",
-2:"2",
}
func NewErpService() *ErpService{
return &ErpService{}
}
func (this *ErpService) SyncErp() {
dbSpu:= mysql.Conn("supp") //spu实例化链接
poolSupplierEntity:=&PoolSupplierEntity{}
supplierId:=int64(0)
for{
count:=0
rows,err:= dbSpu.Table("lie_supplier_channel").Where("is_type=? and status in (?,?) and supplier_id>? ",0,2,-2,supplierId).Limit(10).Rows(poolSupplierEntity)
if err != nil {
syncLog.SyncInsertLog("erp_merger",ErpTaskName+"查询供应商列表出错,err:"+err.Error())
panic(nil)
}
for rows.Next() {
err = rows.Scan(poolSupplierEntity)
if err != nil {
syncLog.SyncInsertLog("erp_merger",ErpTaskName+"scan出错,err:"+err.Error())
panic(nil)
}
count++
supplierId=gconv.Int64(poolSupplierEntity.SupplierId)
this.pushErp(poolSupplierEntity)
}
if(count==0){
rows.Close()
break
}
}
}
func (this *ErpService) pushErp(poolSupplierEntity *PoolSupplierEntity) {
/**
{"name":"深圳市朋友有限責任公司",
"shortName":"朋友",
"currencyNumber":"CNY",
"groupNumber":"C",
"taxRegisterNo":"66666",
"personName":["张右辉","杨婷婷"],
"PTID":"12324",
"STATUS":"1",
"address":"深圳市龙岗区坂田街道",
"contactPerson":"张莹",
"phone":"15637258888"
}
*/
pushErpData:=make(map[string]interface{})
pushErpData["name"]=poolSupplierEntity.SupplierName
pushErpData["shortName"]=""
pushErpData["currencyNumber"]=CurrencyMapping[poolSupplierEntity.Currency]
pushErpData["taxRegisterNo"]=poolSupplierEntity.TaxNumber
pushErpData["PTID"]=poolSupplierEntity.SupplierCode
pushErpData["STATUS"]=StatusMapping[poolSupplierEntity.Status]
personName:=this.GetPersonNames(poolSupplierEntity)
pushErpData["personName"]=personName
pushErpData["address"]=poolSupplierEntity.SupplierAddress
bytes,_:=json.Marshal(pushErpData)
fmt.Println(string(bytes))
//fmt.Println(poolSupplierEntity)
}
/*
待续
*/
func (this *ErpService) GetContact(poolSupplierEntity *PoolSupplierEntity) {
dbSpu := mysql.Conn("supp") //spu实例化链接
sql := " SELECT t.supplier_consignee,t.supplier_telephone,t.supplier_mobile from lie_supplier_channel c INNER JOIN lie_supplier_contact t on c.supplier_id=t.supplier_id where c.supplier_id = ? order by t.contact_id limit 1"
res, err := dbSpu.QueryString(sql, poolSupplierEntity.SupplierId)
if err != nil {
syncLog.SyncInsertLog("erp_merger", ErpTaskName+"查询lie_supplier_contact出错,err:"+err.Error())
}
fmt.Println(res)
}
func (this *ErpService)GetPersonNames(poolSupplierEntity *PoolSupplierEntity)[]string{
poolSupplierEntity.ChannelUid="1366,1363"
adminIds:=strings.Split(poolSupplierEntity.ChannelUid, ",")
codeIds:=make([]interface{},0)
for _,id:=range adminIds {
codeIds=append(codeIds,id)
}
//组装采购员
cmsEntitys:=[]CmsEntity{}//采购员 cmsDB数据
dbCms:= mysql.Conn("cms") //spu实例化链接
err:=dbCms.Table("user_info").
Join("INNER","lie_intracode","user_info.userId=lie_intracode.admin_id").
In("lie_intracode.admin_id",codeIds...).Find(&cmsEntitys)
if(err!=nil){
errmsg:=fmt.Sprintf(ErpTaskName+":供应商采购员编码::%s;%s;id:%d",poolSupplierEntity.ChannelUid,err.Error(),poolSupplierEntity.SupplierId)
syncLog.SyncInsertLog("mapping_insert",errmsg,syncLog.LogLevelFatal)
panic(nil)
}
channelNameS:=make([]string,0)
for _,cmsOne:=range cmsEntitys{
channelNameS=append(channelNameS,cmsOne.Name)
}
return channelNameS
}
package service
import (
"encoding/json"
"fmt"
dmysql "github.com/go-sql-driver/mysql"
"github.com/go-xorm/xorm"
"github.com/gogf/gf/util/gconv"
"go_supplier_task/model"
"go_supplier_task/pkg/common"
syncLog "go_supplier_task/pkg/logger/sync"
"go_supplier_task/pkg/mysql"
"gopkg.in/mgo.v2/bson"
"strings"
"time"
)
var LyTaskName ="[联营sku整合]"
var skuCache SkuCacheService
type ServiceLy struct {
}
type ZySkuEntity struct {
GoodsId int `json:"goods_id" form:"goods_id" xorm:"autoincr pk"`
Encoded string `json:"encoded" `//内部编码,供应商渠道开发编码
Canal string `json:"canal" bson:"canal"`//供应商编码
UpdateTime int `json:"update_time" xorm:"updated"`
}
func NewServiceLy() (*ServiceLy) {
return new(ServiceLy)
}
func (this *ServiceLy) Merger() {
maxNumTable:=10
maxNumDataBase:=10
for dataBaseNum:=0;dataBaseNum<maxNumDataBase;dataBaseNum++ {
common.PrintStdout().Printf("%s,第%d个数据库开始",LyTaskName,dataBaseNum)
for tableNum:=0;tableNum<maxNumTable;tableNum++ {
common.PrintStdout().Printf("%s,第%d个表开始",LyTaskName,tableNum)
dataBaseConn:=fmt.Sprintf("sku_%d",dataBaseNum)
tableName:=fmt.Sprintf("lie_sku_%d",tableNum)
skuDb:= mysql.Conn(dataBaseConn) //连接
goodsId:=int64(0)
for {
count:=0
lYSkuEntity:= &model.LySkuEntity{}
rows,err:= skuDb.Table(tableName).Where("goods_id>? and supplier_id=? and canal!=''",goodsId,17).OrderBy("goods_id",).Limit(10).Rows(lYSkuEntity)
if err != nil {
syncLog.SyncInsertLog("ly_merger",LyTaskName+"查询sku出错,err:"+err.Error())
panic(nil)
}
for rows.Next() {
err = rows.Scan(lYSkuEntity)
if err != nil {
syncLog.SyncInsertLog("ly_merger",LyTaskName+"scan出错,err:"+err.Error())
panic(nil)
}
count++
goodsId=gconv.Int64(lYSkuEntity.GoodsId)
this.updateDBRedisMongo(lYSkuEntity,skuDb,tableName)//修改redis、mongoDb、mysqldb、日志
}
if(count==0){
rows.Close()
break
}
}
}
}
}
//操作redis mongo
func (this *ServiceLy) updateDBRedisMongo(lySkuEntity *model.LySkuEntity,dbconn *xorm.Engine,tableName string){
common.PrintStdout().Printf("goodsId:%s,开始操作",lySkuEntity.GoodsId)
isExist,err:=skuCache.RedisSisMember("lysku_merge_ids",lySkuEntity.GoodsId)
if(err!=nil){
msg:=fmt.Sprintf("%sSISMEMBER lysku_merge_ids 出错,goodsId:%s,err:%s",LyTaskName,lySkuEntity.GoodsId,err.Error())
syncLog.SyncInsertLog("ly_merger",msg)
panic(nil)
}
if(isExist){//如果存在代表已经合并过了,跳过
common.PrintStdout().Printf("goodsId:%s,已经合并成功,跳过操作",lySkuEntity.GoodsId)
return
}
var canal string //新供应商编码
if(lySkuEntity.Canal=="" || lySkuEntity.Canal=="0"){
common.PrintStdout().Printf("goodsid:%s对应的cannal:%s,为空",lySkuEntity.GoodsId,lySkuEntity.Canal)
return
}
supp:= mysql.Conn("supp") //spu实例化链接
//获取映射
sql:="SELECT supplier_code from lie_supplier_merger_mapping WHERE old_supplier_code=?"
res,err:=supp.QueryString(sql,lySkuEntity.Canal)
if(err!=nil){
msg:=fmt.Sprintf("%s获取映射数据出错,goodsId:%s,err:%s",LyTaskName,lySkuEntity.GoodsId,err.Error())
syncLog.SyncInsertLog("ly_merger",msg)
return //应该跳过
//panic(nil)
}
if(len(res)>0){//有对应的映射数据
if(res[0]["supplier_code"]!="" && res[0]["supplier_code"]!="0"){
canal=res[0]["supplier_code"]//供应商编码
}
//===================修改redis action========================
oldSkuRedisInfoMap:=this.getOldRedisSkuInfo(lySkuEntity.GoodsId)
newSkuRedisInfoMap:=oldSkuRedisInfoMap
newSkuRedisInfoMap["canal"]=canal
newSkuRedisInfoMap["update_time"]=int(time.Now().Unix())
this.insertRedisSkuInfo(lySkuEntity.GoodsId,newSkuRedisInfoMap)
//===================修改redis end========================
//========================修改mongo=======================
update:=bson.M{"canal": canal}
err=skuCache.MongoUpdateSku(lySkuEntity.GoodsId,update)
if(err!=nil){
msg:=fmt.Sprintf("%s 修改mongosku数据出错,goodsId:%s,err:%s,updateData:%s",LyTaskName,lySkuEntity.GoodsId,err.Error(),update)
syncLog.SyncInsertLog("ly_merger",msg)
//回滚redis
common.PrintStdout().Printf("因mongodb错误,正在回滚redis\n")
oldSkuRedisInfoMap["canal"]=lySkuEntity.Canal
this.insertRedisSkuInfo(lySkuEntity.GoodsId,newSkuRedisInfoMap)
common.PrintStdout().Printf("回滚redis成功, 程序结束\n")
panic(nil)
}
//=====================修改mongo end======================
//===================修改DB 和增加日志====================
oldCanal:=lySkuEntity.Canal //老的canal
lySkuEntity.Canal=canal
_,err=dbconn.Table(tableName).Where("goods_id=?",lySkuEntity.GoodsId).Update(lySkuEntity)
if(err!=nil){
msg:=fmt.Sprintf("%s mysql修改 sku失败 ,goodsId:%s,err:%s",LyTaskName,lySkuEntity.GoodsId,err.Error())
syncLog.SyncInsertLog("ly_merger",msg)
//回滚mongoDB
common.PrintStdout().Printf("因mysql错误正在回滚mongo\n")
update:=bson.M{"canal": oldCanal}
err=skuCache.MongoUpdateSku(lySkuEntity.GoodsId,update)
if(err!=nil){///如果这个时候mongo又报错,这套娃了(无解没办法再回滚了)
msg:=fmt.Sprintf("%s 因msyql错误 回滚mongosku数据出错,goodsId:%s,err:%s,updateData:%s",LyTaskName,lySkuEntity.GoodsId,err.Error(),update)
syncLog.SyncInsertLog("ly_merger",msg)
}
common.PrintStdout().Printf("回滚mongodb成功\n")
//回滚redis
common.PrintStdout().Printf("因mysql错误正在回滚redis\n")
oldSkuRedisInfoMap["canal"]=lySkuEntity.Canal
this.insertRedisSkuInfo(lySkuEntity.GoodsId,newSkuRedisInfoMap)
common.PrintStdout().Printf("回滚redis成功, 程序结束\n")
panic(nil)
}
//增加日志
this.recordLog(lySkuEntity.GoodsId,canal,oldCanal)
//将合并成功的skuid存到redis set
err=skuCache.RedisSet("lysku_merge_ids",lySkuEntity.GoodsId)
if(err!=nil){
msg:=fmt.Sprintf("%s 增加合 并成功goodsId到 redis,goodsId:%s,err:%s\n",LyTaskName,lySkuEntity.GoodsId,err.Error())
syncLog.SyncInsertLog("ly_merger",msg)
panic(nil)
}
common.PrintStdout().Printf("goodsId:%s,操作成功",lySkuEntity.GoodsId)
panic(nil)
//===================修改DB 和增加日志end====================
}else{//没有映射数据
msg:=fmt.Sprintf("goodsid:%s对应的cannal:%s,没有找到映射数据",lySkuEntity.GoodsId,lySkuEntity.Canal)
syncLog.SyncInsertLog("ly_merger",msg)
return
//common.PrintStdout().Printf("goodsid:%s对应的cannal:%s,没有找到映射数据",lySkuEntity.GoodsId,lySkuEntity.Canal)
}
}
func (this *ServiceLy) recordLog(goodsId string,canal string,oldCanal string) {
skuMergeLog:=model.SkuMergeLog{
GoodsId:goodsId,
Canal:canal,
OldCanal:oldCanal,
}
supp:= mysql.Conn("supp") //spu实例化链接
_,err:=supp.Table("liexin_lysku_merger_log").Insert(skuMergeLog)
if(err!=nil){
if mysqlErr,ok:=err.(*dmysql.MySQLError);ok{
if(mysqlErr.Number==1062 && strings.Contains(mysqlErr.Message,"PRIMARY")){//
common.PrintStdout().Printf("%s插入 日志触发 PRIMARY,原因可能是重复插入,跳过;err:%s\n",LyTaskName,err.Error())
return
}
}
msg:=fmt.Sprintf("%s 增加mysql日志数据出错,goodsId:%s,data:%s,err:%s",LyTaskName,goodsId,skuMergeLog,err.Error())
syncLog.SyncInsertLog("ly_merger",msg)
panic(nil)
}
}
/**
获取redis sku信息
return map[string]interface{}
*/
func (this *ServiceLy) getOldRedisSkuInfo(goodsId string) map[string]interface{} {
oldSkuInfoStr,err:=skuCache.RedisGetSkuInfo(goodsId)//获取老的sku数据
if(err!=nil){
msg:=fmt.Sprintf("%s获取redis sku数据出错,goodsId:%s,err:%s",LyTaskName,goodsId,err.Error())
syncLog.SyncInsertLog("ly_merger",msg)
panic(err)
}
var oldSkuInfoMap map[string]interface{}
err=json.Unmarshal([]byte(oldSkuInfoStr),&oldSkuInfoMap)
if(err!=nil){
syncLog.SyncInsertLog("ly_merger","json Unmarshal redis 出错"+err.Error())
panic(err)
}
return oldSkuInfoMap
}
//插入skuInfo到redis
func (this *ServiceLy) insertRedisSkuInfo(goodsId string, redisSkuInfoMap map[string]interface{}) {
newMapbytes,err:=json.Marshal(redisSkuInfoMap)
if(err!=nil){
syncLog.SyncInsertLog("ly_merger","json Marshal redis 出错"+err.Error())
panic(err)
}
//插入redisSku
err=skuCache.RedisInsertSkuInfo(goodsId,string(newMapbytes))
if(err!=nil){
msg:=fmt.Sprintf("%s 插入 redis sku数据出错,goodsId:%s,err:%s",LyTaskName,goodsId,err.Error())
syncLog.SyncInsertLog("ly_merger",msg)
panic(err)
}
}
//记录日志
func recordLog() {
}
package service
import (
"github.com/gogf/gf/util/gconv"
"github.com/gomodule/redigo/redis"
"go_supplier_task/model"
"go_supplier_task/pkg/gredis"
"go_supplier_task/pkg/mongo"
"gopkg.in/mgo.v2"
"gopkg.in/mgo.v2/bson"
)
type SkuCacheService struct {
}
/**
mongoDB 根据 spuId supplierId moq encoded canal 获取sku信息
*/
func (spuCacheService *SkuCacheService) MongoGetOnlySkuInfo(spuId string,supplierId int,moq int,encoded int,canal string)(error, model.LySkuMongo){
var skuMongo model.LySkuMongo
mongodb := mongo.Conn("default")
defer func() {
mongodb.Close()
}()
query:=bson.M{
"spu_id":gconv.Int64(spuId),
"supplier_id":supplierId,
"moq":moq,
"encoded":encoded,
"canal":canal,
}
err := mongodb.DB("ichunt").C("sku").Find(query).One(&skuMongo)
if err != nil {
if(err == mgo.ErrNotFound){
return nil,skuMongo
}
return err,skuMongo
}
return err,skuMongo
}
/**
mongo 插入sku
*/
func (spuCacheService *SkuCacheService) MongoInsertOnlySkuInfo(onlySkuInfo model.LySkuMongo)(error){
mongoConn := mongo.Conn("default");
defer mongoConn.Close()
Mongo:=mongoConn.DB("ichunt").C("sku")
err:=Mongo.Insert(onlySkuInfo)
return err
}
/**
将单条sku数据压入redis
*/
func (spuCacheService *SkuCacheService)RedisInsertSkuInfo(skuId string,skuInfo string) error {
redisWriteConn := gredis.Conn("search_w")
defer redisWriteConn.Close()
_,err:=redisWriteConn.Do("HSet","sku",skuId,skuInfo)
return err;
}
/**
SADD key value
暂时只处理新增一个value
*/
func (spuCacheService *SkuCacheService) RedisSet(key string,value string) error{
redisWriteConn := gredis.Conn("search_w")
defer redisWriteConn.Close()
_,err:=redisWriteConn.Do("SADD",key,value)
return err;
}
func (spuCacheService *SkuCacheService)RedisSisMember(key string,value string) (bool,error) {
redisWriteConn := gredis.Conn("search_w")
defer redisWriteConn.Close()
boolInt,err:=redis.Int(redisWriteConn.Do("SISMEMBER",key,value))
if(err!=nil && err!=redis.ErrNil){
return false,err
}
if(boolInt==1){
return true,nil
}
return false,nil
}
/**
将skuID压入redis队列(list)(消费后将压入ES)
*/
func (spuCacheService *SkuCacheService)RedisPushEsList(skuId string) error {
redisWriteConn := gredis.Conn("search_w")
defer redisWriteConn.Close()
push_name:="update_list_sku";
_,err:=redisWriteConn.Do("Rpush",push_name,skuId)
return err
}
/**
获取sku redis单条信息
*/
func (spuCacheService *SkuCacheService)RedisGetSkuInfo(skuId string) (string,error) {
redisReadConn := gredis.Conn("search_r")
defer redisReadConn.Close()
skuInfoString,err:=redis.String(redisReadConn.Do("Hget","sku",skuId))
return skuInfoString,err
}
/*
修改
*/
func (spuCacheService *SkuCacheService) MongoUpdateSku(goodId string,update interface{}) error {
mongodb := mongo.Conn("default")
defer func() {
mongodb.Close()
}()
query:=bson.M{
"goods_id":gconv.Int(goodId),
}
c:=mongodb.DB("ichunt").C("sku")
err := c.Update(query,bson.M{"$set": update})
if(err!=nil){
return err
}
return nil
}
\ No newline at end of file
package service
import (
"fmt"
dmysql "github.com/go-sql-driver/mysql"
"github.com/syyongx/php2go"
"go_supplier_task/pkg/common"
syncLog "go_supplier_task/pkg/logger/sync"
"go_supplier_task/pkg/mysql"
"strings"
)
var TaskName string
/**
映射处理
*/
func (this *ServiceSupplier)MappingHandle(MappingType int) {
var csvPath string
switch MappingType {
case 1://联营
TaskName="[整合联营映射]"
common.PrintStdout().Printf("[整合联营数据]开始\n")
csvPath=this.getResourcesPath()+"/lySupplier.csv"
break
case 2://金蝶erp
TaskName="[整合ERP映射]"
common.PrintStdout().Printf("[整合金蝶erp]开始\n")
csvPath=this.getResourcesPath()+"/erpSupplier.csv"
break
case 3://自营
TaskName="[整合自营映射]"
common.PrintStdout().Printf("[整合自营]开始\n")
csvPath=this.getResourcesPath()+"/zySupplier.csv"
break
default:
panic(fmt.Sprintf("来源类型出错%d",MappingType))
}
res,err:=this.getCsvRes(csvPath)
common.PrintStdout().Printf("按标准名称分组:%d条\n",len(res))
if(err!=nil){
panic(err)//获取csv报错,终止程序
}
for groupName,data:=range res{
//组装 purchaseNames(里面包含供应商采购商中文 名称)
purchaseNames:=make([]interface{},0)
for _,OldSupplier:=range data{
if(OldSupplier["purchaseName"]!=""){
purchaseNames=append(purchaseNames,OldSupplier["purchaseName"])
}
}
dbSuplierInfo:=this.GetStatus2SupplierInfo(groupName,purchaseNames)
//mappingSupplierS:=make([]SupplierMappingEntity,0)
for _,OldSupplier:=range data{
NewMapping:=SupplierMappingEntity{
OldSupplierCode:OldSupplier["canal"],
SupplierCode:dbSuplierInfo.SupplierCode,
OldSupplierName:OldSupplier["oldName"],
SupplierName:dbSuplierInfo.SupplierName,
SupplierId:int(dbSuplierInfo.SupplierId),
Source:MappingType,
}
//mappingSupplierS=append(mappingSupplierS,NewMapping)
this.InsertMapping(NewMapping)
}
}
}
/**
获取指定 supplierName 的供应商详情(状态是2的,is_type=0的),没有就新增
@purchaseNames 采购员s(excel 一个标准供应商有多个供应商,可能每个供应商都有采购员),用作新增供应商修改采购商
*/
func (this *ServiceSupplier) GetStatus2SupplierInfo(supplierName string,purchaseNames []interface{}) PoolSupplierEntity {
//todo 如果没有就新增,然后返回
dbSpu:= mysql.Conn("supp") //spu实例化链接
poolSupplierEntity:=PoolSupplierEntity{}
has,err:= dbSpu.Table("lie_supplier_channel").Where("is_type=? and status=? and supplier_name=?",0,2,supplierName).Get(&poolSupplierEntity)
if(err!=nil){
panic(TaskName+"[插入映射-查询标准]查询标供应商出错:"+"供应商标准名称:"+supplierName+err.Error())
}
if(!has){//新增
poolSupplierEntity.Status=2
poolSupplierEntity.SupplierName=supplierName
_,err:= dbSpu.Table("lie_supplier_channel").Insert(&poolSupplierEntity)
if(err!=nil){
syncLog.SyncInsertLog(TaskName+"mapping_insert","[插入映射-新增标准]出错:"+"供应商标准名称:"+supplierName+err.Error(),syncLog.LogLevelFatal)
panic(nil)
}
//组装供应商编码供应商编码(内部和外部)
oldStr:=fmt.Sprintf("%d",int64(10000000)+poolSupplierEntity.SupplierId)
poolSupplierEntity.SupplierCode=strings.Replace(oldStr,"1","L",1)
poolSupplierEntity.SupplierSn=strings.Replace(oldStr,"1","O",1)
//end
//组装采购员
cmsEntitys:=[]CmsEntity{}//采购员 cmsDB数据
dbCms:= mysql.Conn("cms") //spu实例化链接
err=dbCms.Table("user_info").
Join("INNER","lie_intracode","user_info.userId=lie_intracode.admin_id").
In("user_info.name",purchaseNames...).Find(&cmsEntitys)
if(err!=nil){
errmsg:=fmt.Sprintf(TaskName+"[获取采购商编码]出错:供应商标准名称:%s;%s;id:%d",poolSupplierEntity.SupplierName,err.Error(),poolSupplierEntity.SupplierId)
syncLog.SyncInsertLog("mapping_insert",errmsg,syncLog.LogLevelFatal)
panic(nil)
}
channelUidString:=""
channelUidS:=make([]string,0)
for _,cmsOne:=range cmsEntitys{
channelUidS=append(channelUidS,cmsOne.CodeId)
}
channelUidString=php2go.Implode(",",channelUidS)
poolSupplierEntity.ChannelUid=channelUidString
//end
//修改
_,err= dbSpu.Table("lie_supplier_channel").Where("supplier_id=?",poolSupplierEntity.SupplierId).Update(&poolSupplierEntity)
if(err!=nil){
errmsg:=fmt.Sprintf(TaskName+"[修改编码]出错:供应商标准名称:%s;%s;id:%d",supplierName,err.Error(),poolSupplierEntity.SupplierId)
syncLog.SyncInsertLog("mapping_insert",errmsg,syncLog.LogLevelFatal)
panic(nil)
//panic("[插入映射-修改编码]出错:"+"供应商标准名称:"+supplierName+err.Error())
}
return poolSupplierEntity
//common.PrintStdout().Printf("[整合标准数据]查询标供应商:"+groupName+"内 供应商编码:"+oneSuplier["canal"]+"没有数据")
}else{
return poolSupplierEntity
}
}
//插入映射
func (this *ServiceSupplier) InsertMapping(SupplierInfoOne SupplierMappingEntity) {
dbSpu:= mysql.Conn("supp") //spu实例化链接
_,err:= dbSpu.Table("lie_supplier_merger_mapping").Insert(SupplierInfoOne)
if(err!=nil){
if mysqlErr,ok:=err.(*dmysql.MySQLError);ok{
/**
触发唯一索引
UNIQUE KEY `source_name_unique` (`old_supplier_name`,`source`)
*/
if(mysqlErr.Number==1062 && strings.Contains(mysqlErr.Message,"source_name_unique")){//
errMsg:=fmt.Sprintf(TaskName+"[插入映射]出错:excel有重复的映射关系 type:%d,标准名称:%s,标准ID:%d,errmsg:%s",SupplierInfoOne.Source,SupplierInfoOne.SupplierName,SupplierInfoOne.SupplierId,err.Error())
syncLog.SyncInsertLog("mapping_insert",errMsg,syncLog.LogLevelFatal)
return
//不退出进程
//panic(nil)
}
}
errMsg:=fmt.Sprintf(TaskName+"[插入映射]出错 type:%d,标准名称:%s,errmsg:%s",SupplierInfoOne.Source,SupplierInfoOne.SupplierName,err.Error())
syncLog.SyncInsertLog("mapping_insert",errMsg,syncLog.LogLevelFatal)
panic(nil)
return
}
common.PrintStdout().Printf("%s[插入映射]成功,data:%s\n",TaskName,fmt.Sprint(SupplierInfoOne))
}
package service
import (
"encoding/json"
"fmt"
"github.com/guonaihong/gout"
"github.com/tidwall/gjson"
"golang_open_platform/dao"
"golang_open_platform/model"
"golang_open_platform/open"
"golang_open_platform/pkg/common"
"golang_open_platform/pkg/config"
"golang_open_platform/pkg/e"
"strings"
)
type SkuService struct {
}
func NewSkuService() *SkuService{
return &SkuService{}
}
/*
用classId获取sku列表
*/
func (this *SkuService)GetSkuListByClass(req *model.QuerySkuCreq) (rsp *model.QuerySkuCrsp,err error ){
pageData,err:=dao.GetMongoOpenSku(req.ClassId,0)
if(err!=nil){
return nil,e.NewApiError("service error",open.OTHERERROR)
}
rsp=&model.QuerySkuCrsp{Count:pageData.Count,PageSize:10,Page:req.Page}
if(pageData.Count<=0){//没数据
return
}
if(req.Page>0){
//验证下page是否有效
/*if(math.Ceil(float64(pageData.Count/rsp.PageSize))<float64(req.Page)){
return nil,e.NewApiError("page invalid",open.PARAM1)
}*/
mongoOpenSku,err:=dao.GetMongoOpenSku(req.ClassId,req.Page)
if(err!=nil){
return nil,e.NewApiError("service error",open.OTHERERROR)
}
if(mongoOpenSku.SkuIds==""){
if(rsp.Count>0){
common.PrintStdout().Printf("classID:%d count 大于0 ids 却为空",req.ClassId)
}
return rsp,nil
}
skuIds:=strings.Replace(mongoOpenSku.SkuIds," ",",",-1)
//skuIds
remoteData,err:=this.getRemoteSku(skuIds)
if(err!=nil){
common.PrintStdout().Printf(err.Error())
return rsp,e.NewApiError("service error",open.REMOTESKUINFO)
}
field:=[]string{"goods_id","spu_id","brand_id","brand_name","goods_name", "stock","moq","mpq",
"class_id1","class_id2","class_id1_name","class_id2_name","attrs","ladder_price","supplier_name",}
//field:=[]string{"spu_id","attrs","supplier_name","goods_images","hk_delivery_time"}
rsp.SkuData=model.SkuFilter(*remoteData,field)
}
return rsp,nil
}
//获取sku列表完整字段
func (this *SkuService) GetSkuListFull(req * model.QuerySkuReq)(rsp *model.QuerySkuRsp,err error) {
//skuIds:=strings.Replace(req.GoodsIds," ",",",-1)
//skuIds
rsp=&model.QuerySkuRsp{}
remoteData,err:=this.getRemoteSku(req.GoodsIds)
if(err!=nil){
common.PrintStdout().Printf(err.Error())
return rsp,e.NewApiError("service error",open.REMOTESKUINFO)
}
field:=[]string{"goods_id","spu_id","brand_id","brand_name","goods_name", "stock","moq","mpq",
"class_id1","class_id2","class_id1_name","class_id2_name","attrs","ladder_price","supplier_name"}
rsp.SkuData=model.SkuFilter(*remoteData,field)
return
}
//获取sku列表 价格库存相关字段
func (this *SkuService) GetSkuListPrice(req * model.QuerySkuReq)(rsp model.QuerySkuRsp,err error) {
remoteData,err:=this.getRemoteSku(req.GoodsIds)
if(err!=nil){
common.PrintStdout().Printf(err.Error())
return rsp,e.NewApiError("service error",open.REMOTESKUINFO)
}
field:=[]string{"goods_id","stock","ladder_price"}
rsp.SkuData=model.SkuFilter(*remoteData,field)
return
}
//获取远端商详接口
func (this *SkuService) getRemoteSku(ids string) (remoteData *model.RemoteSkuData,err error){
resStr:=""
err=gout.POST(config.Get("sku_server.api_domain").String()+"/synchronization")/*.Debug(true)*/.SetForm(gout.H{"goods_id": ids}).BindBody(&resStr).Do()
if(err!=nil){
return remoteData,fmt.Errorf("调用远端商详接口报错:"+err.Error())
}
gjsonRes:=gjson.Parse(resStr)
if(gjsonRes.Exists()==false){
return remoteData,fmt.Errorf("返回的参数不能被json解析")
}
if(gjsonRes.Get("errcode").Int()!=0){
msg:=gjsonRes.Get("errmsg").String()
return remoteData,fmt.Errorf(msg)
}
dataGjsonObj:=gjsonRes.Get("data")
if(dataGjsonObj.IsObject()==false){
return remoteData,fmt.Errorf("商详返回的 data数据格式不对")
}
remoteData=&model.RemoteSkuData{}
err=json.Unmarshal([]byte(dataGjsonObj.String()),remoteData)
if(err!=nil){
return remoteData,fmt.Errorf("data 不能被json.Unmarshal解析")
}
return remoteData,nil
}
package service
import (
"fmt"
"go_supplier_task/pkg/common"
"go_supplier_task/pkg/gredis"
"go_supplier_task/pkg/mysql"
"sort"
)
var SupplierEerrKey="mergerSupplierErr"
type ServiceSupplier struct {
ServiceCommonSupplier
}
func NewServiceSupplier() (*ServiceSupplier) {
return new(ServiceSupplier)
}
//合并标准供应商数据
func (this *ServiceSupplier)MergerCriteria() {
//res
common.PrintStdout().Printf("[整合标准数据]开始\n")
//获取csv数据
res,err:=this.getCsvRes(this.getResourcesPath()+"/lySupplier.csv")
if(err!=nil){
panic(err)//终止程序
}
common.PrintStdout().Printf("[整合标准数据]以标准供应商名称分组:%d条\n",len(res))
dbSpu:= mysql.Conn("supp") //spu实例化链接
//数据整理 groupName 标准名称;supplierDataList 供应商列表
for groupName,supplierDataList:=range res{//groupName 标准名称
PoolSupplierEntitys := make([]PoolSupplierEntity, 0)
for _,oneSuplier:=range supplierDataList{
poolSupplierEntity:=PoolSupplierEntity{}
has,err:= dbSpu.Table("lie_supplier_channel").Where("is_type=? and supplier_code=?",0,oneSuplier["canal"]).Get(&poolSupplierEntity)
if(err!=nil){
panic("[整合标准数据]查询标供应商出错:"+"供应商编码"+oneSuplier["canal"]+err.Error())
}
if(!has){
common.PrintStdout().Printf("[整合标准数据]查询标供应商:"+groupName+"内 供应商编码:"+oneSuplier["canal"]+"没有数据")
continue
}else{
PoolSupplierEntitys=append(PoolSupplierEntitys,poolSupplierEntity)
}
}
sort.Sort(SupplierEntitySorter(PoolSupplierEntitys))
/**
启用 禁用规则:
以原始编码去标准供应商表查询状态,如果有两个以上是启用状态,随机选择一个启用,其他皆为禁用。
如果只有一个供应商启用状态,不用改变
如果都不是启用,选择最新的启用,其他都是禁用
*/
if(len(PoolSupplierEntitys)>0){
isUpdate:=false//组里有状态为2(启用)的,并且对其更新了
for _,SupplierOne:=range PoolSupplierEntitys{
SupplierOne.SupplierName=groupName
if(SupplierOne.Status==2){
if(isUpdate){
//todo 修改状态为禁用,更改成标准供应商名
SupplierOne.Status=-2
}else{
//todo 修改状态为启用(其实本来就是启用),更改成标准供应商名
isUpdate=true
SupplierOne.Status=2
}
}else{
//todo 修状态为禁用,更改成标准供应商名
SupplierOne.Status=-2
}
this.updateSupplier(SupplierOne)
}
//遍历到最后了还没有找到启用的供应商,就把第一个当成启用的供应商
if(!isUpdate){
//todo 修改状态为启用(其实本来就是启用),更改成标准供应商名
PoolSupplierEntitys[0].SupplierName=groupName
PoolSupplierEntitys[0].Status=2
this.updateSupplier(PoolSupplierEntitys[0])
}
}else{
common.PrintStdout().Printf("[整合标准数据]标准供应商:"+groupName+" 组内没有对应联营供应商数据")
}
}
}
/**
更新供应商数据状态(单条)
@param status
@param supplierName
@param SupplierCode
*/
func (this *ServiceSupplier) updateSupplier(supplierEntity PoolSupplierEntity) {
dbSpu:= mysql.Conn("supp") //spu实例化链接
_,err:= dbSpu.Table("lie_supplier_channel").Where("supplier_id=?",supplierEntity.SupplierId).Update(supplierEntity)
if(err!=nil){
errMsg:=fmt.Sprintf("修改失败:错误:%s;数据:%s",err.Error(),fmt.Sprintln(supplierEntity))
common.PrintStdout().Printf(errMsg)
this.RecordRedisErr(SupplierEerrKey,supplierEntity.SupplierId,errMsg)
}
common.PrintStdout().Printf("修改成功:%s",fmt.Sprintln(supplierEntity))
}
func (this *ServiceSupplier)RecordRedisErr(hashkey string,key interface{},value string) {
redisWriteConn := gredis.Conn("search_w")
defer redisWriteConn.Close()
_,err:=redisWriteConn.Do("HSET",hashkey,key,value)
if(err!=nil){
common.PrintStdout().Printf("HSET 失败"+err.Error())
}
}
package service
import (
"encoding/csv"
"fmt"
"github.com/axgle/mahonia"
"go_supplier_task/pkg/common"
"os"
)
type ServiceCommonSupplier struct {
}
//联营供应商表DB实体(暂时只放用到的字段)
type PoolSupplierEntity struct {
SupplierId int64 `json:"supplier_id" xorm:"autoincr pk"`//供应商编码
SupplierCode string `json:"supplier_code"`//供应商编码
SupplierName string `json:"supplier_name" `//供应商名称
Status int `json:"status" `//状态
SupplierSn string `json:"supplier_sn" `//状态
PurchaseUid string `json:"purchase_uid" `//采购员(20210311改成渠道开发员或者其他,基本不用它了)
ChannelUid string `json:"channel_uid" `//渠道开发员(20210311改成采购员)
Currency int `json:"currency" `//币种
TaxNumber string `json:"tax_number" `//币种
SupplierAddress string `json:"supplier_address" `//币种
UpdateTime int `json:"update_time" xorm:"updated"`
}
//映射表
type SupplierMappingEntity struct {
OldSupplierCode string `json:"old_supplier_code"`//老编码
SupplierCode string `json:"supplier_code"`//新编码
SupplierId int `json:"supplier_id"`//供应商ID
//OldSupplierId int `json:"old_supplier_id"`//老供应商ID
OldSupplierName string `json:"old_supplier_name" `//老供应商名称
SupplierName string `json:"supplier_name" `//供应商名称
Source int `json:"source" `//供应商名称
UpdateTime int `json:"update_time" xorm:"updated"`//更新时间
CreateTime int `json:"create_time" xorm:"created"`//创建时间
}
//映射表
type CmsEntity struct {
CodeId string `json:"code_id"`//
Name string `json:"name"`//
}
//获取Resources 后续要分离出来
func (this *ServiceCommonSupplier) getResourcesPath() (path string){
path,err:=os.Getwd()
if(err!=nil){
panic("Getwd出错"+err.Error())
}
return path+"/resources"
}
/**
newData 示例如下
{
"D0000018": [
{
"canal": "D0000018",
"newName": "Future Electronics(Hong Kong)Ltd",
"oldName": "Future Electronics (HongKong)Ltd",
"purchaseuid": "朱小丽",
"sn": "1187",
"time": "2021-02-25"
},
{
"canal": "D0000019",
"newName": "Future Electronics(Hong Kong)Ltd",
"oldName": "Future Electronics (HongKong)Ltd",
"purchaseuid": "朱小丽",
"sn": "1187",
"time": "2021-02-25"
}
],
....
}
*/
func (this *ServiceCommonSupplier) getCsvRes(csvPath string) (newData map[string][]map[string]string,err error){
newData=make(map[string][]map[string]string)//组装成以canal 为key的数组
res,err:=this.getCsvData(csvPath);
if(err!=nil){
return
}
if(len(res)<=0){
err=fmt.Errorf("csv没有数据,path:"+csvPath)
return
}
common.PrintStdout().Printf("excel 总条数:%d\n",len(res))
keys:=map[int]string{
0:"sn",
1:"oldName",
2:"newName",
3:"time",
4:"canal",
5:"purchaseName",
}
for _, OneData := range res {
mapData:= make(map[string]string)//map
for i:=0;i<len(OneData);i++{
mapData[keys[i]]=OneData[i]
}
newData[OneData[2]]=append(newData[OneData[2]],mapData)
}
return
}
//获取csv文件里的数据
func (this *ServiceCommonSupplier) getCsvData(csvPath string) (res [][]string,err error) {
_,err = os.Lstat(csvPath)
if(os.IsNotExist(err)){
return res,fmt.Errorf("文件不存在:"+csvPath)
}
file,err:=os.OpenFile(csvPath,os.O_RDONLY,0)
if(err!=nil){
return res,fmt.Errorf("打开文件:"+csvPath+"出错;"+err.Error())
}
defer file.Close()
decoder := mahonia.NewDecoder("gbk")
r:=csv.NewReader(decoder.NewReader(file))//以jbk读取文件
res,err=r.ReadAll()
if(err!=nil){
return res,fmt.Errorf("csv ReadAll出错:path:"+csvPath+err.Error())
}
//res=append(res[0:],res[1:]...)
//res=append(res[:1],res[2])
return
}
//阶梯价格排序算法
type SupplierEntitySorter []PoolSupplierEntity
func (a SupplierEntitySorter) Len() int {
return len(a)
}
func (a SupplierEntitySorter) Swap(i, j int) {
a[i], a[j] = a[j], a[i]
}
func (a SupplierEntitySorter) Less(i, j int) bool {
return a[j].UpdateTime > a[i].UpdateTime
}
......@@ -12,6 +12,6 @@ go env -w GOPROXY=https://goproxy.cn,direct
go build -o ${Cur_Dir}"/cmd/http/http" ${Cur_Dir}"/cmd/http/http_server.go"
chmod +x ${Cur_Dir}"/cmd/http/http"
chmod +x ${Cur_Dir}"/update.sh"
supervisorctl restart golang_open_platform_60006:*
supervisorctl restart go_supplier_task_60006:*
echo "更新执行成功"
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 sign in to comment