dxc il y a 2 ans
commit
b7913bc10f

+ 2 - 0
.gitignore

@@ -0,0 +1,2 @@
+/callback
+/logs

+ 8 - 0
.idea/.gitignore

@@ -0,0 +1,8 @@
+# 默认忽略的文件
+/shelf/
+/workspace.xml
+# 基于编辑器的 HTTP 客户端请求
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml

+ 9 - 0
.idea/callback.iml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="WEB_MODULE" version="4">
+  <component name="Go" enabled="true" />
+  <component name="NewModuleRootManager">
+    <content url="file://$MODULE_DIR$" />
+    <orderEntry type="inheritedJdk" />
+    <orderEntry type="sourceFolder" forTests="false" />
+  </component>
+</module>

+ 8 - 0
.idea/modules.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ProjectModuleManager">
+    <modules>
+      <module fileurl="file://$PROJECT_DIR$/.idea/callback.iml" filepath="$PROJECT_DIR$/.idea/callback.iml" />
+    </modules>
+  </component>
+</project>

+ 6 - 0
.idea/vcs.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="VcsDirectoryMappings">
+    <mapping directory="$PROJECT_DIR$" vcs="Git" />
+  </component>
+</project>

+ 40 - 0
api/callback.api

@@ -0,0 +1,40 @@
+syntax = "v1"
+
+info(
+	title: "type title here"
+	desc: "type desc here"
+	author: "type author here"
+	email: "type email here"
+	version: "type version here"
+)
+
+type CallbackRequest {
+	MsgSignature string `form:"msg_signature"`
+	Timestamp    string `form:"timestamp"`
+	Nonce        string `form:"nonce"`
+	Echostr      string `form:"echostr"`
+}
+
+type CallbackMsgRequest {
+	MsgSignature string `form:"msg_signature"`
+	Timestamp    string `form:"timestamp"`
+	Nonce        string `form:"nonce"`
+}
+
+type CallbackResponse {
+	Code int    `json:"code"`
+	Msg  string `json:"msg"`
+	Data string `json:"data"`
+}
+
+@server(
+	//	jwt: Auth
+	group: callback
+	timeout: 3s
+)
+service callback {
+	@handler callbackHandler
+	get /callback (CallbackRequest)
+	@handler callbackMsgHandler
+	post /callback (CallbackMsgRequest)
+}

+ 35 - 0
callback.go

@@ -0,0 +1,35 @@
+package main
+
+import (
+	"callback/internal/middleware"
+	"flag"
+	"fmt"
+	"github.com/zeromicro/go-zero/core/logc"
+
+	"callback/internal/config"
+	"callback/internal/handler"
+	"callback/internal/svc"
+
+	"github.com/zeromicro/go-zero/core/conf"
+	"github.com/zeromicro/go-zero/rest"
+)
+
+var configFile = flag.String("f", "etc/callback.yaml", "the config file")
+
+func main() {
+	flag.Parse()
+
+	var c config.Config
+	conf.MustLoad(*configFile, &c)
+	logc.MustSetup(c.Log)
+
+	server := rest.MustNewServer(c.RestConf)
+	defer server.Stop()
+
+	server.Use(middleware.NewCorsMiddleware().Handle)
+	ctx := svc.NewServiceContext(c)
+	handler.RegisterHandlers(server, ctx)
+
+	fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port)
+	server.Start()
+}

+ 11 - 0
doc/cb_customer.sql

@@ -0,0 +1,11 @@
+
+CREATE TABLE `cb_customer`  (
+  `id` bigint(20) NOT NULL AUTO_INCREMENT,
+  `external_userid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '微信客户的external_userid',
+  `nickname` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '昵称',
+  `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '图片',
+  `gender` tinyint(4) NOT NULL COMMENT '性别',
+  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '客户信息表' ROW_FORMAT = Compact;

+ 20 - 0
doc/cb_msg.sql

@@ -0,0 +1,20 @@
+
+CREATE TABLE `cb_msg`  (
+  `id` bigint(20) NOT NULL AUTO_INCREMENT,
+  `msgid` varchar(80) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '消息ID',
+  `open_kfid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '客服ID',
+  `external_userid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '客户UserID',
+  `send_time` bigint(20) NOT NULL COMMENT '发送时间',
+  `origin` tinyint(4) NOT NULL COMMENT '消息来源 3-微信客户发送的消息 4-系统推送的事件消息 5-接待人员在企业微信客户端发送的消息',
+  `servicer_userid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '接待人员userid',
+  `msgtype` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '消息类型',
+  `data_info` varchar(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '消息内容(json格式,各个消息类型不一致)',
+  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`) USING BTREE,
+  INDEX `open_kfid`(`open_kfid`) USING BTREE,
+  INDEX `msgid`(`msgid`) USING BTREE,
+  INDEX `external_userid`(`external_userid`) USING BTREE,
+  INDEX `send_time`(`send_time`) USING BTREE,
+  INDEX `servicer_userid`(`servicer_userid`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '客服消息' ROW_FORMAT = Compact;

+ 14 - 0
doc/cb_service.sql

@@ -0,0 +1,14 @@
+
+CREATE TABLE `cb_service`  (
+  `id` bigint(20) NOT NULL AUTO_INCREMENT,
+  `corpid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '企业ID',
+  `open_kfid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '客服ID',
+  `next_cursor` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '获取最后消息的next_cursor',
+  `name` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '客服名称',
+  `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '图片地址',
+  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`) USING BTREE,
+  INDEX `open_kfid`(`open_kfid`) USING BTREE,
+  INDEX `created_at`(`created_at`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '客服账号' ROW_FORMAT = Compact;

+ 16 - 0
doc/cb_servicer.sql

@@ -0,0 +1,16 @@
+
+CREATE TABLE `cb_servicer`  (
+  `id` bigint(20) NOT NULL AUTO_INCREMENT,
+  `corpid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '企业ID',
+  `open_kfid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '客服ID',
+  `userid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '接待人员的userid',
+  `status` tinyint(4) NOT NULL COMMENT '接待人员的接待状态。0:接待中,1:停止接待',
+  `stop_type` tinyint(4) NULL DEFAULT NULL COMMENT '停止接待的子类型。0:停止接待,1:暂时挂起',
+  `department_id` int(11) NOT NULL COMMENT '部门ID',
+  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`) USING BTREE,
+  INDEX `open_kfid`(`open_kfid`) USING BTREE,
+  INDEX `userid`(`userid`) USING BTREE,
+  INDEX `created_at`(`created_at`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '接待人员表' ROW_FORMAT = Compact;

+ 14 - 0
doc/cb_staff.sql

@@ -0,0 +1,14 @@
+
+CREATE TABLE `cb_staff`  (
+  `id` bigint(20) NOT NULL AUTO_INCREMENT,
+  `corpid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '企业ID',
+  `userid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '成员UserID。对应管理端的账号',
+  `name` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '成员名称',
+  `status` tinyint(4) NOT NULL COMMENT '激活状态: 1=已激活,2=已禁用,4=未激活,5=退出企业。',
+  `main_department` int(11) NOT NULL COMMENT '主部门',
+  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`) USING BTREE,
+  INDEX `userid`(`userid`) USING BTREE,
+  INDEX `created_at`(`created_at`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '企业成员表' ROW_FORMAT = Compact;

+ 29 - 0
etc/callback.yaml

@@ -0,0 +1,29 @@
+Name: &Name callback
+Host: 0.0.0.0
+Port: 8888
+
+Log:
+  ServiceName: *Name
+#  Mode: "file"
+  Rotation: "daily"
+  Encoding: "plain"
+
+Auth:
+  AccessSecret: "i9hb5_ep"
+  AccessExpire: 3600
+
+Mysql:
+  Datasource: kftest:123456@tcp(10.8.230.17:3308)/wechat_cs_online?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai
+
+Redis:
+  Host: 10.8.230.17:6379
+  Pass:
+  Type: node
+  Tls: false
+
+Wxwork:
+  Corpid: "wwa0779f3625818a4e"
+  Corpsecret: "bDdsGUmT3TbEAaUtPzEB4Fd2kiCIqX8bFcZ4Oy3F3jM"
+  Token: "8GLSPY3N6mwpWylT7PzHi"
+  ReceiverId: "wwa0779f3625818a4e"
+  EncodingAeskey: "dkobxiOsG6UMg9SXby1LPc2L9sKoDF9mcq2QK78YwWV"

+ 53 - 0
go.mod

@@ -0,0 +1,53 @@
+module callback
+
+go 1.20
+
+require github.com/zeromicro/go-zero v1.6.3
+
+require (
+	github.com/beorn7/perks v1.0.1 // indirect
+	github.com/cenkalti/backoff/v4 v4.2.1 // indirect
+	github.com/cespare/xxhash/v2 v2.2.0 // indirect
+	github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
+	github.com/elliotchance/pie v1.39.0 // indirect
+	github.com/fatih/color v1.16.0 // indirect
+	github.com/go-logr/logr v1.3.0 // indirect
+	github.com/go-logr/stdr v1.2.2 // indirect
+	github.com/go-sql-driver/mysql v1.7.1 // indirect
+	github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
+	github.com/golang/protobuf v1.5.3 // indirect
+	github.com/google/uuid v1.6.0 // indirect
+	github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 // indirect
+	github.com/mattn/go-colorable v0.1.13 // indirect
+	github.com/mattn/go-isatty v0.0.20 // indirect
+	github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
+	github.com/openzipkin/zipkin-go v0.4.2 // indirect
+	github.com/pelletier/go-toml/v2 v2.1.1 // indirect
+	github.com/prometheus/client_golang v1.18.0 // indirect
+	github.com/prometheus/client_model v0.5.0 // indirect
+	github.com/prometheus/common v0.45.0 // indirect
+	github.com/prometheus/procfs v0.12.0 // indirect
+	github.com/redis/go-redis/v9 v9.4.0 // indirect
+	github.com/spaolacci/murmur3 v1.1.0 // indirect
+	github.com/valyala/fastjson v1.6.4 // indirect
+	go.opentelemetry.io/otel v1.19.0 // indirect
+	go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 // indirect
+	go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.19.0 // indirect
+	go.opentelemetry.io/otel/exporters/zipkin v1.19.0 // indirect
+	go.opentelemetry.io/otel/metric v1.19.0 // indirect
+	go.opentelemetry.io/otel/sdk v1.19.0 // indirect
+	go.opentelemetry.io/otel/trace v1.19.0 // indirect
+	go.opentelemetry.io/proto/otlp v1.0.0 // indirect
+	go.uber.org/automaxprocs v1.5.3 // indirect
+	golang.org/x/net v0.21.0 // indirect
+	golang.org/x/sys v0.17.0 // indirect
+	golang.org/x/text v0.14.0 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 // indirect
+	google.golang.org/grpc v1.62.0 // indirect
+	google.golang.org/protobuf v1.32.0 // indirect
+	gopkg.in/yaml.v2 v2.4.0 // indirect
+)

+ 145 - 0
go.sum

@@ -0,0 +1,145 @@
+github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
+github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 h1:uvdUDbHQHO85qeSydJtItA4T55Pw6BtAejd0APRJOCE=
+github.com/alicebob/miniredis/v2 v2.31.1 h1:7XAt0uUg3DtwEKW5ZAGa+K7FZV2DdKQo5K/6TTnfX8Y=
+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
+github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
+github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
+github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
+github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
+github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
+github.com/elliotchance/pie v1.39.0 h1:oudoOFLPYvWwsJ/J2dFv3uHkdHdq7oSv8aNMWqUQVv8=
+github.com/elliotchance/pie v1.39.0/go.mod h1:W/nLuTGZ1dLKzRS0Z2g2N2evWzMenuDnBhk0s6Y9k54=
+github.com/elliotchance/testify-stats v1.0.0/go.mod h1:Mc25k7L4E65uf6CfW+s/pY04XcoiqQBrfIRsWQcgweA=
+github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
+github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
+github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
+github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
+github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
+github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
+github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
+github.com/golang/glog v1.2.0 h1:uCdmnmatrKCgMBlM4rMuJZWOkPDqdbZPnrMXDY4gI68=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 h1:RtRsiaGvWxcwd8y3BiRZxsylPT8hLWZ5SPcfI+3IDNk=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0/go.mod h1:TzP6duP4Py2pHLVPPQp42aoYI92+PCrVotyR5e8Vqlk=
+github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
+github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
+github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/openzipkin/zipkin-go v0.4.2 h1:zjqfqHjUpPmB3c1GlCvvgsM1G4LkvqQbBDueDOCg/jA=
+github.com/openzipkin/zipkin-go v0.4.2/go.mod h1:ZeVkFjuuBiSy13y8vpSDCjMi9GoI3hPpCJSBx/EYFhY=
+github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
+github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
+github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=
+github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=
+github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
+github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
+github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=
+github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
+github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
+github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
+github.com/redis/go-redis/v9 v9.4.0 h1:Yzoz33UZw9I/mFhx4MNrB6Fk+XHO1VukNcCa1+lwyKk=
+github.com/redis/go-redis/v9 v9.4.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
+github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
+github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
+github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
+github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
+github.com/yuin/gopher-lua v1.1.0 h1:BojcDhfyDWgU2f2TOzYK/g5p2gxMrku8oupLDqlnSqE=
+github.com/zeromicro/go-zero v1.6.3 h1:OL0NnHD5LdRNDolfcK9vUkJt7K8TcBE3RkzfM8poOVw=
+github.com/zeromicro/go-zero v1.6.3/go.mod h1:XZL435ZxVi9MSXXtw2MRQhHgx6OoX3++MRMOE9xU70c=
+go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs=
+go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY=
+go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4=
+go.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0 h1:3d+S281UTjM+AbF31XSOYn1qXn3BgIdWl8HNEpx08Jk=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0/go.mod h1:0+KuTDyKL4gjKCF75pHOX4wuzYDUZYfAQdSu43o+Z2I=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
+go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.19.0 h1:Nw7Dv4lwvGrI68+wULbcq7su9K2cebeCUrDjVrUJHxM=
+go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.19.0/go.mod h1:1MsF6Y7gTqosgoZvHlzcaaM8DIMNZgJh87ykokoNH7Y=
+go.opentelemetry.io/otel/exporters/zipkin v1.19.0 h1:EGY0h5mGliP9o/nIkVuLI0vRiQqmsYOcbwCuotksO1o=
+go.opentelemetry.io/otel/exporters/zipkin v1.19.0/go.mod h1:JQgTGJP11yi3o4GHzIWYodhPisxANdqxF1eHwDSnJrI=
+go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPiOKwvpE=
+go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8=
+go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o=
+go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A=
+go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg=
+go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo=
+go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
+go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
+go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8=
+go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0=
+go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
+golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
+golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
+golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 h1:KAeGQVN3M9nD0/bQXnr/ClcEMJ968gUXJQ9pwfSynuQ=
+google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80 h1:Lj5rbfG876hIAYFjqiJnPHfhXbv+nzTWfm04Fg/XSVU=
+google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80/go.mod h1:4jWUdICTdgc3Ibxmr8nAJiiLHwQBY0UI0XZcEMaFKaA=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 h1:AjyfHzEPEFp/NpvfN5g+KDla3EMojjhRVZc1i7cj+oM=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80/go.mod h1:PAREbraiVEVGVdTZsVWjSbbTtSyGbAgIIvni8a8CD5s=
+google.golang.org/grpc v1.62.0 h1:HQKZ/fa1bXkX1oFOvSjmZEUL8wLSaZTjCcLAlmZRtdk=
+google.golang.org/grpc v1.62.0/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
+google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI=

+ 25 - 0
internal/config/config.go

@@ -0,0 +1,25 @@
+package config
+
+import (
+	"github.com/zeromicro/go-zero/core/stores/redis"
+	"github.com/zeromicro/go-zero/rest"
+)
+
+type Config struct {
+	rest.RestConf
+	Redis redis.RedisConf
+	Auth  struct {
+		AccessSecret string
+		AccessExpire int64
+	}
+	Mysql struct {
+		Datasource string
+	}
+	Wxwork struct {
+		Corpid         string
+		Corpsecret     string
+		Token          string
+		ReceiverId     string
+		EncodingAeskey string
+	}
+}

+ 26 - 0
internal/handler/callback/callbackhandler.go

@@ -0,0 +1,26 @@
+package callback
+
+import (
+	"net/http"
+
+	"callback/internal/logic/callback"
+	"callback/internal/svc"
+	"callback/internal/types"
+	"github.com/zeromicro/go-zero/rest/httpx"
+)
+
+func CallbackHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		var req types.CallbackRequest
+		if err := httpx.Parse(r, &req); err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+			return
+		}
+
+		l := callback.NewCallbackLogic(r.Context(), svcCtx)
+		err := l.Callback(&req, w)
+		if err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+		}
+	}
+}

+ 26 - 0
internal/handler/callback/callbackmsghandler.go

@@ -0,0 +1,26 @@
+package callback
+
+import (
+	"net/http"
+
+	"callback/internal/logic/callback"
+	"callback/internal/svc"
+	"callback/internal/types"
+	"github.com/zeromicro/go-zero/rest/httpx"
+)
+
+func CallbackMsgHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		var req types.CallbackMsgRequest
+		if err := httpx.Parse(r, &req); err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+			return
+		}
+
+		l := callback.NewCallbackMsgLogic(r.Context(), svcCtx)
+		err := l.CallbackMsg(&req, w, r)
+		if err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+		}
+	}
+}

+ 30 - 0
internal/handler/routes.go

@@ -0,0 +1,30 @@
+// Code generated by goctl. DO NOT EDIT.
+package handler
+
+import (
+	"net/http"
+	"time"
+
+	callback "callback/internal/handler/callback"
+	"callback/internal/svc"
+
+	"github.com/zeromicro/go-zero/rest"
+)
+
+func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
+	server.AddRoutes(
+		[]rest.Route{
+			{
+				Method:  http.MethodGet,
+				Path:    "/callback",
+				Handler: callback.CallbackHandler(serverCtx),
+			},
+			{
+				Method:  http.MethodPost,
+				Path:    "/callback",
+				Handler: callback.CallbackMsgHandler(serverCtx),
+			},
+		},
+		rest.WithTimeout(3000*time.Millisecond),
+	)
+}

+ 36 - 0
internal/logic/callback/callbacklogic.go

@@ -0,0 +1,36 @@
+package callback
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+
+	"callback/internal/svc"
+	"callback/internal/types"
+
+	"github.com/zeromicro/go-zero/core/logx"
+)
+
+type CallbackLogic struct {
+	logx.Logger
+	ctx    context.Context
+	svcCtx *svc.ServiceContext
+}
+
+func NewCallbackLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CallbackLogic {
+	return &CallbackLogic{
+		Logger: logx.WithContext(ctx),
+		ctx:    ctx,
+		svcCtx: svcCtx,
+	}
+}
+
+func (l *CallbackLogic) Callback(req *types.CallbackRequest, w http.ResponseWriter) (err error) {
+	wxcpt := l.svcCtx.Wxcpt
+	echoStr, cryptErr := wxcpt.VerifyURL(req.MsgSignature, req.Timestamp, req.Nonce, req.Echostr)
+	if nil != cryptErr {
+		fmt.Println("verifyUrl fail", cryptErr)
+	}
+	_, err = w.Write(echoStr)
+	return
+}

+ 242 - 0
internal/logic/callback/callbackmsglogic.go

@@ -0,0 +1,242 @@
+package callback
+
+import (
+	"callback/model"
+	"callback/pkg/lock"
+	"context"
+	"encoding/json"
+	"encoding/xml"
+	"fmt"
+	"github.com/elliotchance/pie/pie"
+	"io/ioutil"
+	"net/http"
+	"strconv"
+	"time"
+
+	"callback/internal/svc"
+	"callback/internal/types"
+
+	"github.com/zeromicro/go-zero/core/logx"
+)
+
+type CallbackMsgLogic struct {
+	logx.Logger
+	ctx    context.Context
+	svcCtx *svc.ServiceContext
+}
+
+func NewCallbackMsgLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CallbackMsgLogic {
+	return &CallbackMsgLogic{
+		Logger: logx.WithContext(ctx),
+		ctx:    ctx,
+		svcCtx: svcCtx,
+	}
+}
+
+func (l *CallbackMsgLogic) CallbackMsg(req *types.CallbackMsgRequest, w http.ResponseWriter, r *http.Request) (err error) {
+	wxcpt := l.svcCtx.Wxcpt
+	defer r.Body.Close()
+	bts, err := ioutil.ReadAll(r.Body)
+	//logx.Info(fmt.Sprintf("--> OriginalParam: %+v ,Error: %v\n", req, err))
+	//logx.Info(fmt.Sprintf("--> OriginalBody: %v ,Error: %v\n", string(bts), err))
+	//xml转json
+	var callbackMsg types.CallbackMsg
+	err = xml.Unmarshal(bts, &callbackMsg)
+	if err != nil {
+		return
+	}
+	bts, err = json.Marshal(callbackMsg)
+	//解密消息
+	msg, err_ := wxcpt.DecryptMsg(req.MsgSignature, req.Timestamp, req.Nonce, bts)
+	if err_ != nil {
+		logx.Error("DecryptMsg Error: ", err_)
+	}
+	//logx.Info(fmt.Sprintf("--> DecryptMsg: %v ,Error: %v\n", string(msg), err_))
+	//xml转结构体
+	var ct types.EventContent
+	err = xml.Unmarshal(msg, &ct)
+	if nil != err {
+		logx.Error("Unmarshal fail ", err)
+	} else {
+		//事件处理
+		if ct.MsgType == types.Event && ct.Event == types.KfMsgOrEvent {
+			//新消息事件
+			go kfMsgOrEventHandle(l.svcCtx, ct)
+		} else if ct.MsgType == types.Event && ct.Event == types.KfAccountAuthChange {
+			//客服账号变动事件
+			go kfAccountAuthChangeHandle(l.svcCtx, ct)
+		}
+	}
+	//加密返回
+	now := time.Now()
+	encryptMsg, cryptErr := wxcpt.EncryptMsg("", strconv.FormatInt(now.Unix(), 10), strconv.FormatInt(now.Unix(), 10))
+	if nil != cryptErr {
+		logx.Error("DecryptMsg fail ", cryptErr)
+	}
+	_, err = w.Write(encryptMsg)
+	return
+}
+
+// 保存新消息或者事件
+func kfMsgOrEventHandle(svcCtx *svc.ServiceContext, ct types.EventContent) {
+	//获取分布式锁
+	rl := lock.NewRedisLock(ct.OpenKfId, svcCtx.Redis)
+	if err := rl.Lock(); err != nil {
+		logx.Error("获取锁失败", err)
+	}
+	defer rl.UnLock()
+	cursor, err := svcCtx.Redis.Get(fmt.Sprintf("cb_cursor:%v", ct.OpenKfId))
+	if err != nil || cursor == "" {
+		var cbService *model.CbService
+		cbService, err = svcCtx.CbServiceModel.GetServiceByOpenKfid(ct.OpenKfId)
+		cursor = cbService.NextCursor
+		if err != nil {
+			go kfAccountAuthChangeHandle(svcCtx, ct)
+		}
+	}
+
+	for true {
+		//查询消息列表
+		err, more, nextCursor, list := svcCtx.WxApi.GetMsgList(cursor, ct.Token, ct.OpenKfId)
+		if err != nil {
+			logx.Error("GetMsgList fail ", err)
+			break
+		}
+		//更新游标
+		cursor = nextCursor
+		err = svcCtx.Redis.Set(fmt.Sprintf("cb_cursor:%v", ct.OpenKfId), cursor)
+		if err != nil {
+			logx.Error(err)
+		}
+		_ = svcCtx.CbServiceModel.UpdateCursorByOpenKfid(ct.OpenKfId, cursor)
+		//保存消息到数据库
+		var userIds []string
+		for _, msg := range list {
+			userIds = append(userIds, msg.ExternalUserid)
+			cbMsg := &model.CbMsg{}
+			cbMsg.Msgid = msg.Msgid
+			cbMsg.OpenKfid = msg.OpenKfid
+			cbMsg.ExternalUserid = msg.ExternalUserid
+			cbMsg.SendTime = int64(msg.SendTime)
+			cbMsg.Origin = int64(msg.Origin)
+			cbMsg.ServicerUserid = msg.ServicerUserid
+			cbMsg.Msgtype = msg.Msgtype
+			cbMsg.DataInfo = msg.DataInfo
+			now := time.Now()
+			cbMsg.CreatedAt = now
+			cbMsg.UpdatedAt = now
+			_, err = svcCtx.CbMsgModel.Insert(nil, cbMsg)
+			if err != nil {
+				logx.Error("Insert cb_msg fail ", err)
+			}
+		}
+		go updateCustomerList(svcCtx, userIds)
+		if more == 0 {
+			break
+		}
+	}
+	return
+}
+
+// 更新客服账号列表、接待人员列表
+func kfAccountAuthChangeHandle(svcCtx *svc.ServiceContext, ct types.EventContent) {
+	err, list := svcCtx.WxApi.GetServiceList()
+	if err != nil {
+		logx.Error("GetServiceList fail ", err)
+		return
+	}
+	for _, s := range list {
+		cbService, err := svcCtx.CbServiceModel.GetServiceByOpenKfid(ct.OpenKfId)
+		//保存客服账号
+		if err != nil {
+			cbService.NextCursor, _ = svcCtx.Redis.Get(fmt.Sprintf("cb_cursor:%v", ct.OpenKfId))
+			cbService.Corpid = svcCtx.Config.Wxwork.Corpid
+			cbService.OpenKfid = s.OpenKfid
+			cbService.Name = s.Name
+			cbService.Avatar = s.Avatar
+			now := time.Now()
+			cbService.CreatedAt = now
+			cbService.UpdatedAt = now
+			_, err := svcCtx.CbServiceModel.Insert(nil, cbService)
+			if err != nil {
+				logx.Error("Insert cb_service fail ", err)
+			}
+		}
+		//保存接待人员列表
+		err, accounts := svcCtx.WxApi.GetServicerList(s.OpenKfid)
+		if err != nil {
+			logx.Error("GetServicerList fail ", err)
+		}
+		for _, a := range accounts {
+			cbServicer, err := svcCtx.CbServicerModel.GetServicer(s.OpenKfid, a.Userid)
+			if err != nil {
+				cbServicer.OpenKfid = s.OpenKfid
+				cbServicer.Corpid = svcCtx.Config.Wxwork.Corpid
+				cbServicer.Userid = a.Userid
+				cbServicer.Status = a.Status
+				cbServicer.DepartmentId = a.DepartmentId
+				now := time.Now()
+				cbServicer.CreatedAt = now
+				cbServicer.UpdatedAt = now
+				_, err := svcCtx.CbServicerModel.Insert(nil, cbServicer)
+				if err != nil {
+					logx.Error("Insert cb_servicer fail ", err)
+				}
+			}
+			//保存成员信息
+			staff, err := svcCtx.CbStaffModel.GetStaff(svcCtx.Config.Wxwork.Corpid, a.Userid)
+			if err != nil {
+				err, d := svcCtx.WxApi.GetStaffList(a.Userid)
+				if err != nil {
+					logx.Error("GetStaffList fail ", err)
+				} else {
+					staff.Corpid = svcCtx.Config.Wxwork.Corpid
+					staff.Userid = d.Userid
+					staff.Name = d.Name
+					staff.Status = d.Status
+					staff.MainDepartment = d.MainDepartment
+					now := time.Now()
+					staff.CreatedAt = now
+					staff.UpdatedAt = now
+					_, err := svcCtx.CbStaffModel.Insert(nil, staff)
+					if err != nil {
+						logx.Error("Insert cb_servicer fail ", err)
+						continue
+					}
+				}
+			}
+		}
+	}
+	return
+}
+
+// 更新客户信息
+func updateCustomerList(svcCtx *svc.ServiceContext, userIds pie.Strings) {
+	for _, uid := range userIds.Unique() {
+		if uid == "" {
+			continue
+		}
+		_, err := svcCtx.CbCustomerModel.GetCustomerByExternalUserid(uid)
+		if err != nil {
+			//此处因为该接口查询多个用户时,如果其中一个超过24小时不回复,会导致所有信息都不会返回,因此这里单独请求
+			err, list := svcCtx.WxApi.GetCustomerList([]string{uid})
+			if err != nil || len(list) == 0 {
+				logx.Error("GetCustomerList fail ", err)
+				continue
+			}
+			c := &model.CbCustomer{}
+			c.ExternalUserid = list[0].ExternalUserid
+			c.Nickname = list[0].Nickname
+			c.Avatar = list[0].Avatar
+			c.Gender = int64(list[0].Gender)
+			now := time.Now()
+			c.CreatedAt = now
+			c.UpdatedAt = now
+			_, err = svcCtx.CbCustomerModel.Insert(nil, c)
+			if err != nil {
+				logx.Error("Insert cb_customer fail ", err)
+			}
+
+		}
+	}
+}

+ 50 - 0
internal/middleware/corsmiddleware.go

@@ -0,0 +1,50 @@
+package middleware
+
+import "net/http"
+
+// CorsMiddleware 跨域请求处理中间件
+type CorsMiddleware struct {
+}
+
+// NewCorsMiddleware 新建跨域请求处理中间件
+func NewCorsMiddleware() *CorsMiddleware {
+	return &CorsMiddleware{}
+}
+
+// Handle 跨域请求处理
+func (m *CorsMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		setHeader(w)
+
+		// 放行所有 OPTIONS 方法
+		if r.Method == "OPTIONS" {
+			w.WriteHeader(http.StatusNoContent)
+			return
+		}
+
+		// 处理请求
+		next(w, r)
+	}
+}
+
+// Handler 跨域请求处理器
+func (m *CorsMiddleware) Handler() http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		setHeader(w)
+
+		if r.Method == "OPTIONS" {
+			w.WriteHeader(http.StatusNoContent)
+		} else {
+			w.WriteHeader(http.StatusNotFound)
+		}
+	})
+}
+
+// setHeader 设置响应头
+func setHeader(w http.ResponseWriter) {
+	w.Header().Set("Access-Control-Allow-Origin", "*")
+	w.Header().Set("Access-Control-Allow-Headers", "*")
+	w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, PATCH")
+	w.Header().Set("Access-Control-Expose-Headers", "Content-Length, Content-Type, Access-Control-Allow-Origin, Access-Control-Allow-Headers")
+	w.Header().Set("Access-Control-Allow-Credentials", "true")
+}

+ 40 - 0
internal/svc/servicecontext.go

@@ -0,0 +1,40 @@
+package svc
+
+import (
+	"callback/internal/config"
+	"callback/model"
+	"callback/pkg/wxwork/wxapi"
+	"callback/pkg/wxwork/wxbizjsonmsgcrypt"
+	"github.com/zeromicro/go-zero/core/stores/redis"
+	"github.com/zeromicro/go-zero/core/stores/sqlx"
+)
+
+type ServiceContext struct {
+	Config config.Config
+	Wxcpt  *wxbizjsonmsgcrypt.WXBizMsgCrypt
+	WxApi  *wxapi.WxApi
+	Redis  *redis.Redis
+
+	CbCustomerModel model.CbCustomerModel
+	CbMsgModel      model.CbMsgModel
+	CbServicerModel model.CbServicerModel
+	CbServiceModel  model.CbServiceModel
+	CbStaffModel    model.CbStaffModel
+}
+
+func NewServiceContext(c config.Config) *ServiceContext {
+	sqlConn := sqlx.NewMysql(c.Mysql.Datasource)
+	newRedis, _ := redis.NewRedis(c.Redis)
+	return &ServiceContext{
+		Config: c,
+		Wxcpt:  wxbizjsonmsgcrypt.NewWXBizMsgCrypt(c.Wxwork.Token, c.Wxwork.EncodingAeskey, c.Wxwork.ReceiverId, wxbizjsonmsgcrypt.JsonType),
+		WxApi:  wxapi.NewWxApi(c.Wxwork.Corpid, c.Wxwork.Corpsecret),
+		Redis:  newRedis,
+
+		CbCustomerModel: model.NewCbCustomerModel(sqlConn),
+		CbMsgModel:      model.NewCbMsgModel(sqlConn),
+		CbServicerModel: model.NewCbServicerModel(sqlConn),
+		CbServiceModel:  model.NewCbServiceModel(sqlConn),
+		CbStaffModel:    model.NewCbStaffModel(sqlConn),
+	}
+}

+ 47 - 0
internal/types/bean.go

@@ -0,0 +1,47 @@
+package types
+
+import "encoding/xml"
+
+type MsgContent struct {
+	ToUsername   string  `json:"ToUserName"`
+	FromUsername string  `json:"FromUserName"`
+	CreateTime   uint32  `json:"CreateTime"`
+	MsgType      string  `json:"MsgType"`      //text,image,voice,video,location
+	MediaId      string  `json:"MediaId"`      //媒体文件id,可以调用获取媒体文件接口拉取数据,仅三天内有效
+	Content      string  `json:"Content"`      //文本消息
+	PicUrl       string  `json:"PicUrl"`       //图片地址
+	Format       string  `json:"Format"`       //语音格式,如amr,speex等
+	ThumbMediaId string  `json:"ThumbMediaId"` //视频消息缩略图的媒体id,可以调用获取媒体文件接口拉取数据,仅三天内有效
+	LocationX    float64 `json:"Location_X"`   //地理位置纬度
+	LocationY    float64 `json:"Location_Y"`   //地理位置经度
+	Scale        float64 `json:"Scale"`        //地图缩放大小
+	Label        float64 `json:"Label"`        //地理位置信息
+	Msgid        uint64  `json:"MsgId"`
+	Agentid      uint32  `json:"AgentId"`
+}
+
+type EventContent struct {
+	XMLName    xml.Name `xml:"xml"`
+	ToUsername string   `xml:"ToUserName" json:"ToUserName"`
+	CreateTime uint32   `xml:"CreateTime" json:"CreateTime"`
+	MsgType    string   `xml:"MsgType" json:"MsgType"` //event
+	Token      string   `xml:"Token" json:"Token"`
+	Event      string   `xml:"Event" json:"Event"` //kf_msg_or_event
+	OpenKfId   string   `xml:"OpenKfId" json:"OpenKfId"`
+}
+
+type CallbackMsg struct {
+	XMLName    xml.Name `xml:"xml"`
+	ToUsername string   `xml:"ToUserName" json:"ToUsername"`
+	Encrypt    string   `xml:"Encrypt" json:"Encrypt"`
+	AgentID    string   `xml:"AgentID" json:"AgentID"`
+}
+type SessionStatusChangeEvent struct {
+	EventType         string `json:"event_type"`
+	OpenKfid          string `json:"open_kfid"`
+	ExternalUserid    string `json:"external_userid"`
+	ChangeType        int    `json:"change_type"`
+	OldServicerUserid string `json:"old_servicer_userid"`
+	NewServicerUserid string `json:"new_servicer_userid"`
+	MsgCode           string `json:"msg_code"`
+}

+ 8 - 0
internal/types/const.go

@@ -0,0 +1,8 @@
+package types
+
+const (
+	Event               = "event"
+	KfMsgOrEvent        = "kf_msg_or_event"
+	KfAccountAuthChange = "kf_account_auth_change"
+	SessionStatusChange = "session_status_change"
+)

+ 21 - 0
internal/types/types.go

@@ -0,0 +1,21 @@
+// Code generated by goctl. DO NOT EDIT.
+package types
+
+type CallbackRequest struct {
+	MsgSignature string `form:"msg_signature"`
+	Timestamp    string `form:"timestamp"`
+	Nonce        string `form:"nonce"`
+	Echostr      string `form:"echostr"`
+}
+
+type CallbackMsgRequest struct {
+	MsgSignature string `form:"msg_signature"`
+	Timestamp    string `form:"timestamp"`
+	Nonce        string `form:"nonce"`
+}
+
+type CallbackResponse struct {
+	Code int    `json:"code"`
+	Msg  string `json:"msg"`
+	Data string `json:"data"`
+}

+ 36 - 0
model/cbcustomermodel.go

@@ -0,0 +1,36 @@
+package model
+
+import (
+	"fmt"
+	"github.com/zeromicro/go-zero/core/stores/sqlx"
+)
+
+var _ CbCustomerModel = (*customCbCustomerModel)(nil)
+
+type (
+	// CbCustomerModel is an interface to be customized, add more methods here,
+	// and implement the added methods in customCbCustomerModel.
+	CbCustomerModel interface {
+		cbCustomerModel
+		GetCustomerByExternalUserid(externalUserid string) (d *CbCustomer, err error)
+	}
+
+	customCbCustomerModel struct {
+		*defaultCbCustomerModel
+	}
+)
+
+// NewCbCustomerModel returns a model for the database table.
+func NewCbCustomerModel(conn sqlx.SqlConn) CbCustomerModel {
+	return &customCbCustomerModel{
+		defaultCbCustomerModel: newCbCustomerModel(conn),
+	}
+}
+
+func (m *customCbCustomerModel) GetCustomerByExternalUserid(externalUserid string) (d *CbCustomer, err error) {
+	query := fmt.Sprintf("select * from %s where `external_userid` = ? limit 1", m.table)
+	var resp CbCustomer
+	err = m.conn.QueryRow(&resp, query, externalUserid)
+	d = &resp
+	return
+}

+ 97 - 0
model/cbcustomermodel_gen.go

@@ -0,0 +1,97 @@
+// Code generated by goctl. DO NOT EDIT.
+
+package model
+
+import (
+	"context"
+	"database/sql"
+	"fmt"
+	"strings"
+	"time"
+
+	"github.com/zeromicro/go-zero/core/stores/builder"
+	"github.com/zeromicro/go-zero/core/stores/sqlc"
+	"github.com/zeromicro/go-zero/core/stores/sqlx"
+	"github.com/zeromicro/go-zero/core/stringx"
+)
+
+var (
+	cbCustomerFieldNames          = builder.RawFieldNames(&CbCustomer{})
+	cbCustomerRows                = strings.Join(cbCustomerFieldNames, ",")
+	cbCustomerRowsExpectAutoSet   = strings.Join(stringx.Remove(cbCustomerFieldNames, "`id`", "`create_at`", "`create_time`", "`created_at`", "`update_at`", "`update_time`", "`updated_at`"), ",")
+	cbCustomerRowsWithPlaceHolder = strings.Join(stringx.Remove(cbCustomerFieldNames, "`id`", "`create_at`", "`create_time`", "`created_at`", "`update_at`", "`update_time`", "`updated_at`"), "=?,") + "=?"
+)
+
+type (
+	cbCustomerModel interface {
+		Insert(ctx context.Context, data *CbCustomer) (sql.Result, error)
+		FindOne(ctx context.Context, id int64) (*CbCustomer, error)
+		Update(ctx context.Context, data *CbCustomer) error
+		Delete(ctx context.Context, id int64) error
+	}
+
+	defaultCbCustomerModel struct {
+		conn  sqlx.SqlConn
+		table string
+	}
+
+	CbCustomer struct {
+		Id             int64     `db:"id"`
+		ExternalUserid string    `db:"external_userid"` // 微信客户的external_userid
+		Nickname       string    `db:"nickname"`        // 昵称
+		Avatar         string    `db:"avatar"`          // 图片
+		Gender         int64     `db:"gender"`          // 性别
+		CreatedAt      time.Time `db:"created_at"`
+		UpdatedAt      time.Time `db:"updated_at"`
+	}
+)
+
+func newCbCustomerModel(conn sqlx.SqlConn) *defaultCbCustomerModel {
+	return &defaultCbCustomerModel{
+		conn:  conn,
+		table: "`cb_customer`",
+	}
+}
+
+func (m *defaultCbCustomerModel) withSession(session sqlx.Session) *defaultCbCustomerModel {
+	return &defaultCbCustomerModel{
+		conn:  sqlx.NewSqlConnFromSession(session),
+		table: "`cb_customer`",
+	}
+}
+
+func (m *defaultCbCustomerModel) Delete(ctx context.Context, id int64) error {
+	query := fmt.Sprintf("delete from %s where `id` = ?", m.table)
+	_, err := m.conn.ExecCtx(ctx, query, id)
+	return err
+}
+
+func (m *defaultCbCustomerModel) FindOne(ctx context.Context, id int64) (*CbCustomer, error) {
+	query := fmt.Sprintf("select %s from %s where `id` = ? limit 1", cbCustomerRows, m.table)
+	var resp CbCustomer
+	err := m.conn.QueryRowCtx(ctx, &resp, query, id)
+	switch err {
+	case nil:
+		return &resp, nil
+	case sqlc.ErrNotFound:
+		return nil, ErrNotFound
+	default:
+		return nil, err
+	}
+}
+
+func (m *defaultCbCustomerModel) Insert(ctx context.Context, data *CbCustomer) (sql.Result, error) {
+	query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?)", m.table, cbCustomerRowsExpectAutoSet)
+	ret, err := m.conn.ExecCtx(ctx, query, data.ExternalUserid, data.Nickname, data.Avatar, data.Gender)
+	return ret, err
+}
+
+func (m *defaultCbCustomerModel) Update(ctx context.Context, data *CbCustomer) error {
+	query := fmt.Sprintf("update %s set %s where `id` = ?", m.table, cbCustomerRowsWithPlaceHolder)
+	_, err := m.conn.ExecCtx(ctx, query, data.ExternalUserid, data.Nickname, data.Avatar, data.Gender, data.Id)
+	return err
+}
+
+func (m *defaultCbCustomerModel) tableName() string {
+	return m.table
+}

+ 24 - 0
model/cbmsgmodel.go

@@ -0,0 +1,24 @@
+package model
+
+import "github.com/zeromicro/go-zero/core/stores/sqlx"
+
+var _ CbMsgModel = (*customCbMsgModel)(nil)
+
+type (
+	// CbMsgModel is an interface to be customized, add more methods here,
+	// and implement the added methods in customCbMsgModel.
+	CbMsgModel interface {
+		cbMsgModel
+	}
+
+	customCbMsgModel struct {
+		*defaultCbMsgModel
+	}
+)
+
+// NewCbMsgModel returns a model for the database table.
+func NewCbMsgModel(conn sqlx.SqlConn) CbMsgModel {
+	return &customCbMsgModel{
+		defaultCbMsgModel: newCbMsgModel(conn),
+	}
+}

+ 101 - 0
model/cbmsgmodel_gen.go

@@ -0,0 +1,101 @@
+// Code generated by goctl. DO NOT EDIT.
+
+package model
+
+import (
+	"context"
+	"database/sql"
+	"fmt"
+	"strings"
+	"time"
+
+	"github.com/zeromicro/go-zero/core/stores/builder"
+	"github.com/zeromicro/go-zero/core/stores/sqlc"
+	"github.com/zeromicro/go-zero/core/stores/sqlx"
+	"github.com/zeromicro/go-zero/core/stringx"
+)
+
+var (
+	cbMsgFieldNames          = builder.RawFieldNames(&CbMsg{})
+	cbMsgRows                = strings.Join(cbMsgFieldNames, ",")
+	cbMsgRowsExpectAutoSet   = strings.Join(stringx.Remove(cbMsgFieldNames, "`id`", "`create_at`", "`create_time`", "`created_at`", "`update_at`", "`update_time`", "`updated_at`"), ",")
+	cbMsgRowsWithPlaceHolder = strings.Join(stringx.Remove(cbMsgFieldNames, "`id`", "`create_at`", "`create_time`", "`created_at`", "`update_at`", "`update_time`", "`updated_at`"), "=?,") + "=?"
+)
+
+type (
+	cbMsgModel interface {
+		Insert(ctx context.Context, data *CbMsg) (sql.Result, error)
+		FindOne(ctx context.Context, id int64) (*CbMsg, error)
+		Update(ctx context.Context, data *CbMsg) error
+		Delete(ctx context.Context, id int64) error
+	}
+
+	defaultCbMsgModel struct {
+		conn  sqlx.SqlConn
+		table string
+	}
+
+	CbMsg struct {
+		Id             int64     `db:"id"`
+		Msgid          string    `db:"msgid"`           // 消息ID
+		OpenKfid       string    `db:"open_kfid"`       // 客服ID
+		ExternalUserid string    `db:"external_userid"` // 客户UserID
+		SendTime       int64     `db:"send_time"`       // 发送时间
+		Origin         int64     `db:"origin"`          // 消息来源 3-微信客户发送的消息 4-系统推送的事件消息 5-接待人员在企业微信客户端发送的消息
+		ServicerUserid string    `db:"servicer_userid"` // 接待人员userid
+		Msgtype        string    `db:"msgtype"`         // 消息类型
+		DataInfo       string    `db:"data_info"`       // 消息内容(json格式,各个消息类型不一致)
+		CreatedAt      time.Time `db:"created_at"`
+		UpdatedAt      time.Time `db:"updated_at"`
+	}
+)
+
+func newCbMsgModel(conn sqlx.SqlConn) *defaultCbMsgModel {
+	return &defaultCbMsgModel{
+		conn:  conn,
+		table: "`cb_msg`",
+	}
+}
+
+func (m *defaultCbMsgModel) withSession(session sqlx.Session) *defaultCbMsgModel {
+	return &defaultCbMsgModel{
+		conn:  sqlx.NewSqlConnFromSession(session),
+		table: "`cb_msg`",
+	}
+}
+
+func (m *defaultCbMsgModel) Delete(ctx context.Context, id int64) error {
+	query := fmt.Sprintf("delete from %s where `id` = ?", m.table)
+	_, err := m.conn.ExecCtx(ctx, query, id)
+	return err
+}
+
+func (m *defaultCbMsgModel) FindOne(ctx context.Context, id int64) (*CbMsg, error) {
+	query := fmt.Sprintf("select %s from %s where `id` = ? limit 1", cbMsgRows, m.table)
+	var resp CbMsg
+	err := m.conn.QueryRowCtx(ctx, &resp, query, id)
+	switch err {
+	case nil:
+		return &resp, nil
+	case sqlc.ErrNotFound:
+		return nil, ErrNotFound
+	default:
+		return nil, err
+	}
+}
+
+func (m *defaultCbMsgModel) Insert(ctx context.Context, data *CbMsg) (sql.Result, error) {
+	query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?, ?, ?, ?)", m.table, cbMsgRowsExpectAutoSet)
+	ret, err := m.conn.ExecCtx(ctx, query, data.Msgid, data.OpenKfid, data.ExternalUserid, data.SendTime, data.Origin, data.ServicerUserid, data.Msgtype, data.DataInfo)
+	return ret, err
+}
+
+func (m *defaultCbMsgModel) Update(ctx context.Context, data *CbMsg) error {
+	query := fmt.Sprintf("update %s set %s where `id` = ?", m.table, cbMsgRowsWithPlaceHolder)
+	_, err := m.conn.ExecCtx(ctx, query, data.Msgid, data.OpenKfid, data.ExternalUserid, data.SendTime, data.Origin, data.ServicerUserid, data.Msgtype, data.DataInfo, data.Id)
+	return err
+}
+
+func (m *defaultCbMsgModel) tableName() string {
+	return m.table
+}

+ 43 - 0
model/cbservicemodel.go

@@ -0,0 +1,43 @@
+package model
+
+import (
+	"fmt"
+	"github.com/zeromicro/go-zero/core/stores/sqlx"
+)
+
+var _ CbServiceModel = (*customCbServiceModel)(nil)
+
+type (
+	// CbServiceModel is an interface to be customized, add more methods here,
+	// and implement the added methods in customCbServiceModel.
+	CbServiceModel interface {
+		cbServiceModel
+		GetServiceByOpenKfid(openKfid string) (d *CbService, err error)
+		UpdateCursorByOpenKfid(openKfid string, cursor string) (err error)
+	}
+
+	customCbServiceModel struct {
+		*defaultCbServiceModel
+	}
+)
+
+// NewCbServiceModel returns a model for the database table.
+func NewCbServiceModel(conn sqlx.SqlConn) CbServiceModel {
+	return &customCbServiceModel{
+		defaultCbServiceModel: newCbServiceModel(conn),
+	}
+}
+
+func (m *customCbServiceModel) GetServiceByOpenKfid(openKfid string) (d *CbService, err error) {
+	query := fmt.Sprintf("select * from %s where `open_kfid` = ? limit 1", m.table)
+	var resp CbService
+	err = m.conn.QueryRow(&resp, query, openKfid)
+	d = &resp
+	return
+}
+
+func (m *customCbServiceModel) UpdateCursorByOpenKfid(openKfid string, cursor string) (err error) {
+	query := fmt.Sprintf("update %s set `next_cursor` = ? where `open_kfid` = ?", m.table)
+	_, err = m.conn.Exec(query, cursor, openKfid)
+	return err
+}

+ 98 - 0
model/cbservicemodel_gen.go

@@ -0,0 +1,98 @@
+// Code generated by goctl. DO NOT EDIT.
+
+package model
+
+import (
+	"context"
+	"database/sql"
+	"fmt"
+	"strings"
+	"time"
+
+	"github.com/zeromicro/go-zero/core/stores/builder"
+	"github.com/zeromicro/go-zero/core/stores/sqlc"
+	"github.com/zeromicro/go-zero/core/stores/sqlx"
+	"github.com/zeromicro/go-zero/core/stringx"
+)
+
+var (
+	cbServiceFieldNames          = builder.RawFieldNames(&CbService{})
+	cbServiceRows                = strings.Join(cbServiceFieldNames, ",")
+	cbServiceRowsExpectAutoSet   = strings.Join(stringx.Remove(cbServiceFieldNames, "`id`", "`create_at`", "`create_time`", "`created_at`", "`update_at`", "`update_time`", "`updated_at`"), ",")
+	cbServiceRowsWithPlaceHolder = strings.Join(stringx.Remove(cbServiceFieldNames, "`id`", "`create_at`", "`create_time`", "`created_at`", "`update_at`", "`update_time`", "`updated_at`"), "=?,") + "=?"
+)
+
+type (
+	cbServiceModel interface {
+		Insert(ctx context.Context, data *CbService) (sql.Result, error)
+		FindOne(ctx context.Context, id int64) (*CbService, error)
+		Update(ctx context.Context, data *CbService) error
+		Delete(ctx context.Context, id int64) error
+	}
+
+	defaultCbServiceModel struct {
+		conn  sqlx.SqlConn
+		table string
+	}
+
+	CbService struct {
+		Id         int64     `db:"id"`
+		Corpid     string    `db:"corpid"`      // 企业ID
+		OpenKfid   string    `db:"open_kfid"`   // 客服ID
+		NextCursor string    `db:"next_cursor"` // 获取最后消息的next_cursor
+		Name       string    `db:"name"`        // 客服名称
+		Avatar     string    `db:"avatar"`      // 图片地址
+		CreatedAt  time.Time `db:"created_at"`
+		UpdatedAt  time.Time `db:"updated_at"`
+	}
+)
+
+func newCbServiceModel(conn sqlx.SqlConn) *defaultCbServiceModel {
+	return &defaultCbServiceModel{
+		conn:  conn,
+		table: "`cb_service`",
+	}
+}
+
+func (m *defaultCbServiceModel) withSession(session sqlx.Session) *defaultCbServiceModel {
+	return &defaultCbServiceModel{
+		conn:  sqlx.NewSqlConnFromSession(session),
+		table: "`cb_service`",
+	}
+}
+
+func (m *defaultCbServiceModel) Delete(ctx context.Context, id int64) error {
+	query := fmt.Sprintf("delete from %s where `id` = ?", m.table)
+	_, err := m.conn.ExecCtx(ctx, query, id)
+	return err
+}
+
+func (m *defaultCbServiceModel) FindOne(ctx context.Context, id int64) (*CbService, error) {
+	query := fmt.Sprintf("select %s from %s where `id` = ? limit 1", cbServiceRows, m.table)
+	var resp CbService
+	err := m.conn.QueryRowCtx(ctx, &resp, query, id)
+	switch err {
+	case nil:
+		return &resp, nil
+	case sqlc.ErrNotFound:
+		return nil, ErrNotFound
+	default:
+		return nil, err
+	}
+}
+
+func (m *defaultCbServiceModel) Insert(ctx context.Context, data *CbService) (sql.Result, error) {
+	query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?)", m.table, cbServiceRowsExpectAutoSet)
+	ret, err := m.conn.ExecCtx(ctx, query, data.Corpid, data.OpenKfid, data.NextCursor, data.Name, data.Avatar)
+	return ret, err
+}
+
+func (m *defaultCbServiceModel) Update(ctx context.Context, data *CbService) error {
+	query := fmt.Sprintf("update %s set %s where `id` = ?", m.table, cbServiceRowsWithPlaceHolder)
+	_, err := m.conn.ExecCtx(ctx, query, data.Corpid, data.OpenKfid, data.NextCursor, data.Name, data.Avatar, data.Id)
+	return err
+}
+
+func (m *defaultCbServiceModel) tableName() string {
+	return m.table
+}

+ 36 - 0
model/cbservicermodel.go

@@ -0,0 +1,36 @@
+package model
+
+import (
+	"fmt"
+	"github.com/zeromicro/go-zero/core/stores/sqlx"
+)
+
+var _ CbServicerModel = (*customCbServicerModel)(nil)
+
+type (
+	// CbServicerModel is an interface to be customized, add more methods here,
+	// and implement the added methods in customCbServicerModel.
+	CbServicerModel interface {
+		cbServicerModel
+		GetServicer(openKfid, userid string) (d *CbServicer, err error)
+	}
+
+	customCbServicerModel struct {
+		*defaultCbServicerModel
+	}
+)
+
+// NewCbServicerModel returns a model for the database table.
+func NewCbServicerModel(conn sqlx.SqlConn) CbServicerModel {
+	return &customCbServicerModel{
+		defaultCbServicerModel: newCbServicerModel(conn),
+	}
+}
+
+func (m *customCbServicerModel) GetServicer(openKfid, userid string) (d *CbServicer, err error) {
+	query := fmt.Sprintf("select * from %s where `open_kfid` = ? AND `userid` = ? limit 1", m.table)
+	var resp CbServicer
+	err = m.conn.QueryRow(&resp, query, openKfid, userid)
+	d = &resp
+	return
+}

+ 99 - 0
model/cbservicermodel_gen.go

@@ -0,0 +1,99 @@
+// Code generated by goctl. DO NOT EDIT.
+
+package model
+
+import (
+	"context"
+	"database/sql"
+	"fmt"
+	"strings"
+	"time"
+
+	"github.com/zeromicro/go-zero/core/stores/builder"
+	"github.com/zeromicro/go-zero/core/stores/sqlc"
+	"github.com/zeromicro/go-zero/core/stores/sqlx"
+	"github.com/zeromicro/go-zero/core/stringx"
+)
+
+var (
+	cbServicerFieldNames          = builder.RawFieldNames(&CbServicer{})
+	cbServicerRows                = strings.Join(cbServicerFieldNames, ",")
+	cbServicerRowsExpectAutoSet   = strings.Join(stringx.Remove(cbServicerFieldNames, "`id`", "`create_at`", "`create_time`", "`created_at`", "`update_at`", "`update_time`", "`updated_at`"), ",")
+	cbServicerRowsWithPlaceHolder = strings.Join(stringx.Remove(cbServicerFieldNames, "`id`", "`create_at`", "`create_time`", "`created_at`", "`update_at`", "`update_time`", "`updated_at`"), "=?,") + "=?"
+)
+
+type (
+	cbServicerModel interface {
+		Insert(ctx context.Context, data *CbServicer) (sql.Result, error)
+		FindOne(ctx context.Context, id int64) (*CbServicer, error)
+		Update(ctx context.Context, data *CbServicer) error
+		Delete(ctx context.Context, id int64) error
+	}
+
+	defaultCbServicerModel struct {
+		conn  sqlx.SqlConn
+		table string
+	}
+
+	CbServicer struct {
+		Id           int64         `db:"id"`
+		Corpid       string        `db:"corpid"`        // 企业ID
+		OpenKfid     string        `db:"open_kfid"`     // 客服ID
+		Userid       string        `db:"userid"`        // 接待人员的userid
+		Status       int64         `db:"status"`        // 接待人员的接待状态。0:接待中,1:停止接待
+		StopType     sql.NullInt64 `db:"stop_type"`     // 停止接待的子类型。0:停止接待,1:暂时挂起
+		DepartmentId int64         `db:"department_id"` // 部门ID
+		CreatedAt    time.Time     `db:"created_at"`
+		UpdatedAt    time.Time     `db:"updated_at"`
+	}
+)
+
+func newCbServicerModel(conn sqlx.SqlConn) *defaultCbServicerModel {
+	return &defaultCbServicerModel{
+		conn:  conn,
+		table: "`cb_servicer`",
+	}
+}
+
+func (m *defaultCbServicerModel) withSession(session sqlx.Session) *defaultCbServicerModel {
+	return &defaultCbServicerModel{
+		conn:  sqlx.NewSqlConnFromSession(session),
+		table: "`cb_servicer`",
+	}
+}
+
+func (m *defaultCbServicerModel) Delete(ctx context.Context, id int64) error {
+	query := fmt.Sprintf("delete from %s where `id` = ?", m.table)
+	_, err := m.conn.ExecCtx(ctx, query, id)
+	return err
+}
+
+func (m *defaultCbServicerModel) FindOne(ctx context.Context, id int64) (*CbServicer, error) {
+	query := fmt.Sprintf("select %s from %s where `id` = ? limit 1", cbServicerRows, m.table)
+	var resp CbServicer
+	err := m.conn.QueryRowCtx(ctx, &resp, query, id)
+	switch err {
+	case nil:
+		return &resp, nil
+	case sqlc.ErrNotFound:
+		return nil, ErrNotFound
+	default:
+		return nil, err
+	}
+}
+
+func (m *defaultCbServicerModel) Insert(ctx context.Context, data *CbServicer) (sql.Result, error) {
+	query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?, ?)", m.table, cbServicerRowsExpectAutoSet)
+	ret, err := m.conn.ExecCtx(ctx, query, data.Corpid, data.OpenKfid, data.Userid, data.Status, data.StopType, data.DepartmentId)
+	return ret, err
+}
+
+func (m *defaultCbServicerModel) Update(ctx context.Context, data *CbServicer) error {
+	query := fmt.Sprintf("update %s set %s where `id` = ?", m.table, cbServicerRowsWithPlaceHolder)
+	_, err := m.conn.ExecCtx(ctx, query, data.Corpid, data.OpenKfid, data.Userid, data.Status, data.StopType, data.DepartmentId, data.Id)
+	return err
+}
+
+func (m *defaultCbServicerModel) tableName() string {
+	return m.table
+}

+ 36 - 0
model/cbstaffmodel.go

@@ -0,0 +1,36 @@
+package model
+
+import (
+	"fmt"
+	"github.com/zeromicro/go-zero/core/stores/sqlx"
+)
+
+var _ CbStaffModel = (*customCbStaffModel)(nil)
+
+type (
+	// CbStaffModel is an interface to be customized, add more methods here,
+	// and implement the added methods in customCbStaffModel.
+	CbStaffModel interface {
+		cbStaffModel
+		GetStaff(corpid, userid string) (d *CbStaff, err error)
+	}
+
+	customCbStaffModel struct {
+		*defaultCbStaffModel
+	}
+)
+
+// NewCbStaffModel returns a model for the database table.
+func NewCbStaffModel(conn sqlx.SqlConn) CbStaffModel {
+	return &customCbStaffModel{
+		defaultCbStaffModel: newCbStaffModel(conn),
+	}
+}
+
+func (m *customCbStaffModel) GetStaff(corpid, userid string) (d *CbStaff, err error) {
+	query := fmt.Sprintf("select * from %s where `corpid` = ? AND `userid` = ? limit 1", m.table)
+	var resp CbStaff
+	err = m.conn.QueryRow(&resp, query, corpid, userid)
+	d = &resp
+	return
+}

+ 98 - 0
model/cbstaffmodel_gen.go

@@ -0,0 +1,98 @@
+// Code generated by goctl. DO NOT EDIT.
+
+package model
+
+import (
+	"context"
+	"database/sql"
+	"fmt"
+	"strings"
+	"time"
+
+	"github.com/zeromicro/go-zero/core/stores/builder"
+	"github.com/zeromicro/go-zero/core/stores/sqlc"
+	"github.com/zeromicro/go-zero/core/stores/sqlx"
+	"github.com/zeromicro/go-zero/core/stringx"
+)
+
+var (
+	cbStaffFieldNames          = builder.RawFieldNames(&CbStaff{})
+	cbStaffRows                = strings.Join(cbStaffFieldNames, ",")
+	cbStaffRowsExpectAutoSet   = strings.Join(stringx.Remove(cbStaffFieldNames, "`id`", "`create_at`", "`create_time`", "`created_at`", "`update_at`", "`update_time`", "`updated_at`"), ",")
+	cbStaffRowsWithPlaceHolder = strings.Join(stringx.Remove(cbStaffFieldNames, "`id`", "`create_at`", "`create_time`", "`created_at`", "`update_at`", "`update_time`", "`updated_at`"), "=?,") + "=?"
+)
+
+type (
+	cbStaffModel interface {
+		Insert(ctx context.Context, data *CbStaff) (sql.Result, error)
+		FindOne(ctx context.Context, id int64) (*CbStaff, error)
+		Update(ctx context.Context, data *CbStaff) error
+		Delete(ctx context.Context, id int64) error
+	}
+
+	defaultCbStaffModel struct {
+		conn  sqlx.SqlConn
+		table string
+	}
+
+	CbStaff struct {
+		Id             int64     `db:"id"`
+		Corpid         string    `db:"corpid"`          // 企业ID
+		Userid         string    `db:"userid"`          // 成员UserID。对应管理端的账号
+		Name           string    `db:"name"`            // 成员名称
+		Status         int64     `db:"status"`          // 激活状态: 1=已激活,2=已禁用,4=未激活,5=退出企业。
+		MainDepartment int64     `db:"main_department"` // 主部门
+		CreatedAt      time.Time `db:"created_at"`
+		UpdatedAt      time.Time `db:"updated_at"`
+	}
+)
+
+func newCbStaffModel(conn sqlx.SqlConn) *defaultCbStaffModel {
+	return &defaultCbStaffModel{
+		conn:  conn,
+		table: "`cb_staff`",
+	}
+}
+
+func (m *defaultCbStaffModel) withSession(session sqlx.Session) *defaultCbStaffModel {
+	return &defaultCbStaffModel{
+		conn:  sqlx.NewSqlConnFromSession(session),
+		table: "`cb_staff`",
+	}
+}
+
+func (m *defaultCbStaffModel) Delete(ctx context.Context, id int64) error {
+	query := fmt.Sprintf("delete from %s where `id` = ?", m.table)
+	_, err := m.conn.ExecCtx(ctx, query, id)
+	return err
+}
+
+func (m *defaultCbStaffModel) FindOne(ctx context.Context, id int64) (*CbStaff, error) {
+	query := fmt.Sprintf("select %s from %s where `id` = ? limit 1", cbStaffRows, m.table)
+	var resp CbStaff
+	err := m.conn.QueryRowCtx(ctx, &resp, query, id)
+	switch err {
+	case nil:
+		return &resp, nil
+	case sqlc.ErrNotFound:
+		return nil, ErrNotFound
+	default:
+		return nil, err
+	}
+}
+
+func (m *defaultCbStaffModel) Insert(ctx context.Context, data *CbStaff) (sql.Result, error) {
+	query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?)", m.table, cbStaffRowsExpectAutoSet)
+	ret, err := m.conn.ExecCtx(ctx, query, data.Corpid, data.Userid, data.Name, data.Status, data.MainDepartment)
+	return ret, err
+}
+
+func (m *defaultCbStaffModel) Update(ctx context.Context, data *CbStaff) error {
+	query := fmt.Sprintf("update %s set %s where `id` = ?", m.table, cbStaffRowsWithPlaceHolder)
+	_, err := m.conn.ExecCtx(ctx, query, data.Corpid, data.Userid, data.Name, data.Status, data.MainDepartment, data.Id)
+	return err
+}
+
+func (m *defaultCbStaffModel) tableName() string {
+	return m.table
+}

+ 5 - 0
model/vars.go

@@ -0,0 +1,5 @@
+package model
+
+import "github.com/zeromicro/go-zero/core/stores/sqlx"
+
+var ErrNotFound = sqlx.ErrNotFound

+ 41 - 0
pkg/lock/lock.go

@@ -0,0 +1,41 @@
+package lock
+
+import (
+	"errors"
+	"fmt"
+	"github.com/zeromicro/go-zero/core/stores/redis"
+	"time"
+)
+
+func NewRedisLock(key string, r *redis.Redis) *RedisLock {
+	lock := redis.NewRedisLock(r, fmt.Sprintf("LOCK_%v", key))
+	lock.SetExpire(5)
+	return &RedisLock{lock}
+}
+
+type RedisLock struct {
+	*redis.RedisLock
+}
+
+func (rl *RedisLock) Lock() error {
+	for {
+		acquire, err := rl.Acquire()
+		switch {
+		case err != nil:
+			return err
+		case acquire:
+			// 获取到锁
+			return nil
+		case !acquire:
+			time.Sleep(200 * time.Millisecond)
+		}
+	}
+}
+
+func (rl *RedisLock) UnLock() error {
+	release, err := rl.Release()
+	if !release {
+		err = errors.New("解锁失败")
+	}
+	return err
+}

+ 92 - 0
pkg/wxwork/wxapi/bean.go

@@ -0,0 +1,92 @@
+package wxapi
+
+type TokenResp struct {
+	Errcode     int    `json:"errcode"`
+	Errmsg      string `json:"errmsg"`
+	AccessToken string `json:"access_token"`
+	ExpiresIn   int64  `json:"expires_in"`
+}
+
+type ServiceAccount struct {
+	OpenKfid        string `json:"open_kfid"`
+	Name            string `json:"name"`
+	Avatar          string `json:"avatar"`
+	ManagePrivilege bool   `json:"manage_privilege"`
+}
+
+type ServiceListResp struct {
+	Errcode     int              `json:"errcode"`
+	Errmsg      string           `json:"errmsg"`
+	AccountList []ServiceAccount `json:"account_list"`
+}
+
+type ServicerAccount struct {
+	Userid       string `json:"userid"`
+	Status       int64  `json:"status"`
+	DepartmentId int64  `json:"department_id"`
+}
+
+type ServicerListResp struct {
+	Errcode      int               `json:"errcode"`
+	Errmsg       string            `json:"errmsg"`
+	ServicerList []ServicerAccount `json:"servicer_list"`
+}
+
+type Staff struct {
+	Userid         string `json:"userid"`
+	Name           string `json:"name"`
+	Status         int64  `json:"status"`
+	MainDepartment int64  `json:"main_department"`
+}
+
+type StaffResp struct {
+	Errcode int    `json:"errcode"`
+	Errmsg  string `json:"errmsg"`
+	Staff
+}
+
+type Customer struct {
+	ExternalUserid string `json:"external_userid"`
+	Nickname       string `json:"nickname"`
+	Avatar         string `json:"avatar"`
+	Gender         int    `json:"gender"`
+}
+
+type CustomerParam struct {
+	ExternalUseridList      []string `json:"external_userid_list"`
+	NeedEnterSessionContext int      `json:"need_enter_session_context"`
+}
+
+type CustomerListResp struct {
+	Errcode               int        `json:"errcode"`
+	Errmsg                string     `json:"errmsg"`
+	CustomerList          []Customer `json:"customer_list"`
+	InvalidExternalUserid []string   `json:"invalid_external_userid"`
+}
+
+type MsgParam struct {
+	Cursor      string `json:"cursor"`
+	Token       string `json:"token"`
+	Limit       int    `json:"limit"`
+	VoiceFormat int    `json:"voice_format"`
+	OpenKfid    string `json:"open_kfid"`
+}
+
+type Msg struct {
+	Msgid          string  `json:"msgid"`
+	OpenKfid       string  `json:"open_kfid"`
+	ExternalUserid string  `json:"external_userid"`
+	SendTime       float64 `json:"send_time"`
+	Origin         float64 `json:"origin"`
+	ServicerUserid string  `json:"servicer_userid"`
+	Msgtype        string  `json:"msgtype"`
+	DataInfo       string  `json:"data_info"`
+}
+
+type MsgListResp struct {
+	Errcode    int                      `json:"errcode"`
+	Errmsg     string                   `json:"errmsg"`
+	NextCursor string                   `json:"next_cursor"`
+	HasMore    int                      `json:"has_more"`
+	MsgList    []map[string]interface{} `json:"msg_list"`
+}

+ 257 - 0
pkg/wxwork/wxapi/wxapi.go

@@ -0,0 +1,257 @@
+package wxapi
+
+import (
+	"bytes"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"strings"
+	"sync"
+	"time"
+)
+
+const (
+	GetTokenUrl    = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=ID&corpsecret=SECRET"
+	GetServiceUrl  = "https://qyapi.weixin.qq.com/cgi-bin/kf/account/list?access_token=ACCESS_TOKEN"
+	GetServicerUrl = "https://qyapi.weixin.qq.com/cgi-bin/kf/servicer/list?access_token=ACCESS_TOKEN&open_kfid=OPEN_KFID"
+	GetStaffUrl    = "https://qyapi.weixin.qq.com/cgi-bin/user/get?access_token=ACCESS_TOKEN&userid=USERID"
+	GetCustomerUrl = "https://qyapi.weixin.qq.com/cgi-bin/kf/customer/batchget?access_token=ACCESS_TOKEN"
+	GetMsgUrl      = "https://qyapi.weixin.qq.com/cgi-bin/kf/sync_msg?access_token=ACCESS_TOKEN"
+)
+
+// 自定义封装企业微信的API接口
+type WxApi struct {
+	lock             sync.Mutex
+	corpid           string
+	corpsecret       string
+	accessToken      string
+	tokenExpiresTime int64
+	expiresIn        int64
+}
+
+func NewWxApi(corpid, corpsecret string) *WxApi {
+	return &WxApi{corpid: corpid, corpsecret: corpsecret}
+}
+
+// 刷新token
+func (a *WxApi) refreshAccessToken() error {
+	if a.accessToken == "" || (a.tokenExpiresTime-time.Now().Unix()) < (a.expiresIn/10) {
+		a.lock.Lock()
+		defer a.lock.Unlock()
+		if !((a.tokenExpiresTime - time.Now().Unix()) < (a.expiresIn / 10)) {
+			return nil
+		}
+		url := GetTokenUrl
+		url = strings.ReplaceAll(url, "ID", a.corpid)
+		url = strings.ReplaceAll(url, "SECRET", a.corpsecret)
+		resp, err := http.Get(url)
+		if err != nil {
+			return err
+		}
+		defer resp.Body.Close()
+		bts, err := ioutil.ReadAll(resp.Body)
+		if err != nil {
+			return err
+		}
+		var data TokenResp
+		err = json.Unmarshal(bts, &data)
+		if err != nil {
+			return err
+		}
+		if data.Errcode != 0 {
+			return errors.New(fmt.Sprintf("errcode: %v,errmsg: %v", data.Errcode, data.Errmsg))
+		}
+		a.accessToken = data.AccessToken
+		a.expiresIn = data.ExpiresIn
+		a.tokenExpiresTime = time.Now().Add(time.Duration(data.ExpiresIn) * time.Second).Unix()
+	}
+	return nil
+}
+
+// GetServiceList 获取客服账号列表
+func (a *WxApi) GetServiceList() (err error, list []ServiceAccount) {
+	err = a.refreshAccessToken()
+	if err != nil {
+		return
+	}
+	url := GetServiceUrl
+	url = strings.ReplaceAll(url, "ACCESS_TOKEN", a.accessToken)
+	res, err := http.Post(url, "application/json", strings.NewReader(`{"offset":0,"limit":999}`))
+	if err != nil {
+		return
+	}
+	defer res.Body.Close()
+	bodyBts, err := ioutil.ReadAll(res.Body)
+	if err != nil {
+		return
+	}
+	var data ServiceListResp
+	err = json.Unmarshal(bodyBts, &data)
+	if err != nil {
+		return
+	}
+	if data.Errcode != 0 {
+		return errors.New(fmt.Sprintf("errcode: %v,errmsg: %v", data.Errcode, data.Errmsg)), nil
+	}
+	list = data.AccountList
+	return
+}
+
+// GetServicerList 获取接待账号列表
+func (a *WxApi) GetServicerList(openKfid string) (err error, list []ServicerAccount) {
+	err = a.refreshAccessToken()
+	if err != nil {
+		return
+	}
+	url := GetServicerUrl
+	url = strings.ReplaceAll(url, "ACCESS_TOKEN", a.accessToken)
+	url = strings.ReplaceAll(url, "OPEN_KFID", openKfid)
+	res, err := http.Get(url)
+	if err != nil {
+		return
+	}
+	defer res.Body.Close()
+	bodyBts, err := ioutil.ReadAll(res.Body)
+	if err != nil {
+		return
+	}
+	var data ServicerListResp
+	err = json.Unmarshal(bodyBts, &data)
+	if err != nil {
+		return
+	}
+	if data.Errcode != 0 {
+		return errors.New(fmt.Sprintf("errcode: %v,errmsg: %v", data.Errcode, data.Errmsg)), nil
+	}
+	list = data.ServicerList
+	return
+}
+
+// 读取成员
+func (a *WxApi) GetStaffList(userid string) (err error, d Staff) {
+	err = a.refreshAccessToken()
+	if err != nil {
+		return
+	}
+	url := GetStaffUrl
+	url = strings.ReplaceAll(url, "ACCESS_TOKEN", a.accessToken)
+	url = strings.ReplaceAll(url, "USERID", userid)
+	res, err := http.Get(url)
+	if err != nil {
+		return
+	}
+	defer res.Body.Close()
+	bodyBts, err := ioutil.ReadAll(res.Body)
+	if err != nil {
+		return
+	}
+	var data StaffResp
+	err = json.Unmarshal(bodyBts, &data)
+	if err != nil {
+		return
+	}
+	if data.Errcode != 0 {
+		return errors.New(fmt.Sprintf("errcode: %v,errmsg: %v", data.Errcode, data.Errmsg)), Staff{}
+	}
+	d = data.Staff
+	return
+}
+
+// GetCustomerList 获取客户基础信息列表
+func (a *WxApi) GetCustomerList(userIds []string) (err error, list []Customer) {
+	err = a.refreshAccessToken()
+	if err != nil {
+		return
+	}
+	url := GetCustomerUrl
+	url = strings.ReplaceAll(url, "ACCESS_TOKEN", a.accessToken)
+	body := CustomerParam{
+		ExternalUseridList:      userIds,
+		NeedEnterSessionContext: 0,
+	}
+	bts, _ := json.Marshal(body)
+	res, err := http.Post(url, "application/json", bytes.NewReader(bts))
+	if err != nil {
+		return
+	}
+	defer res.Body.Close()
+	bodyBts, err := ioutil.ReadAll(res.Body)
+	if err != nil {
+		return
+	}
+	var data CustomerListResp
+	err = json.Unmarshal(bodyBts, &data)
+	if err != nil {
+		return
+	}
+	if data.Errcode != 0 {
+		return errors.New(fmt.Sprintf("errcode: %v,errmsg: %v", data.Errcode, data.Errmsg)), nil
+	}
+	list = data.CustomerList
+	return
+}
+
+// GetMsgList 读取消息
+func (a *WxApi) GetMsgList(cursor, token, openKfid string) (err error, hasMore int, nextCursor string, list []Msg) {
+	err = a.refreshAccessToken()
+	if err != nil {
+		return
+	}
+	url := GetMsgUrl
+	url = strings.ReplaceAll(url, "ACCESS_TOKEN", a.accessToken)
+	body := MsgParam{
+		Cursor:      cursor,
+		Token:       token,
+		Limit:       1000,
+		VoiceFormat: 0,
+		OpenKfid:    openKfid,
+	}
+	bts, _ := json.Marshal(body)
+	res, err := http.Post(url, "application/json", bytes.NewReader(bts))
+	if err != nil {
+		return
+	}
+	defer res.Body.Close()
+	bodyBts, err := ioutil.ReadAll(res.Body)
+	if err != nil {
+		return
+	}
+	var data MsgListResp
+	err = json.Unmarshal(bodyBts, &data)
+	if err != nil {
+		return
+	}
+	if data.Errcode != 0 {
+		err = errors.New(fmt.Sprintf("errcode: %v,errmsg: %v", data.Errcode, data.Errmsg))
+		return
+	}
+	hasMore = data.HasMore
+	nextCursor = data.NextCursor
+	for _, msg := range data.MsgList {
+		var m Msg
+		m.Msgid, _ = msg["msgid"].(string)
+		m.OpenKfid, _ = msg["open_kfid"].(string)
+		m.ExternalUserid, _ = msg["external_userid"].(string)
+		m.SendTime, _ = msg["send_time"].(float64)
+		m.Origin, _ = msg["origin"].(float64)
+		m.ServicerUserid, _ = msg["servicer_userid"].(string)
+		m.Msgtype, _ = msg["msgtype"].(string)
+		if m.Msgtype != "" {
+			content, _ := msg[m.Msgtype]
+			eventBts, _ := json.Marshal(content)
+			m.DataInfo = string(eventBts)
+			if m.Msgtype == "event" {
+				var event = make(map[string]interface{})
+				_ = json.Unmarshal(eventBts, &event)
+				if _, b := event["event_type"].(string); b {
+					m.OpenKfid, _ = event["open_kfid"].(string)
+					m.ExternalUserid, _ = event["external_userid"].(string)
+				}
+			}
+		}
+		list = append(list, m)
+	}
+	return
+}

+ 47 - 0
pkg/wxwork/wxapi/wxapi_test.go

@@ -0,0 +1,47 @@
+package wxapi
+
+import (
+	"encoding/json"
+	"fmt"
+	"testing"
+)
+
+var wxApi *WxApi
+
+func init() {
+	wxApi = NewWxApi("wwa0779f3625818a4e", "bDdsGUmT3TbEAaUtPzEB4Fd2kiCIqX8bFcZ4Oy3F3jM")
+}
+
+func Test_GetServiceList(t *testing.T) {
+	err, list := wxApi.GetServiceList()
+	marshal, _ := json.Marshal(list)
+	fmt.Println(err, string(marshal))
+}
+
+func Test_GetServicerList(t *testing.T) {
+	err, list := wxApi.GetServicerList("wkcvcABwAA92mwE3_UthAg7KCdGH1Odw")
+	marshal, _ := json.Marshal(list)
+	fmt.Println(err, string(marshal))
+}
+
+func Test_GetStaffList(t *testing.T) {
+	err, list := wxApi.GetStaffList("YiLianKeFu-YueYue")
+	marshal, _ := json.Marshal(list)
+	fmt.Println(err, string(marshal))
+}
+
+func Test_GetCustomerList(t *testing.T) {
+	err, list := wxApi.GetCustomerList([]string{"wmcvcABwAA2yO9tBuwQ0XK6chDfOEMZg"})
+	marshal, _ := json.Marshal(list)
+	fmt.Println(err, string(marshal))
+}
+
+func Test_GetMsgList(t *testing.T) {
+	err, _, _, list := wxApi.GetMsgList("", "ENCG7oWFoYPR5GrJnRnWzUzStr1Bmv8YjsVrXhVzGPMjPbn", "wkcvcABwAAv4c-1Acoxcmy63yONrFSDw")
+	fmt.Println(err)
+	for _, msg := range list {
+		if msg.Msgtype != "event" {
+			fmt.Printf("%+v\n", msg)
+		}
+	}
+}

+ 311 - 0
pkg/wxwork/wxbizjsonmsgcrypt/wxbizjsonmsgcrypt.go

@@ -0,0 +1,311 @@
+package wxbizjsonmsgcrypt
+
+import (
+	"bytes"
+	"crypto/aes"
+	"crypto/cipher"
+	"crypto/sha1"
+	"encoding/base64"
+	"encoding/binary"
+	"encoding/json"
+	"fmt"
+	"math/rand"
+	"sort"
+	"strings"
+)
+
+const letterBytes = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
+
+const (
+	ValidateSignatureError int = -40001
+	ParseJsonError         int = -40002
+	ComputeSignatureError  int = -40003
+	IllegalAesKey          int = -40004
+	ValidateCorpidError    int = -40005
+	EncryptAESError        int = -40006
+	DecryptAESError        int = -40007
+	IllegalBuffer          int = -40008
+	EncodeBase64Error      int = -40009
+	DecodeBase64Error      int = -40010
+	GenJsonError           int = -40011
+	IllegalProtocolType    int = -40012
+)
+
+type ProtocolType int
+
+const (
+	JsonType ProtocolType = 1
+)
+
+type CryptError struct {
+	ErrCode int
+	ErrMsg  string
+}
+
+func NewCryptError(err_code int, err_msg string) *CryptError {
+	return &CryptError{ErrCode: err_code, ErrMsg: err_msg}
+}
+
+type WXBizJsonMsg4Recv struct {
+	Tousername string `json:"tousername"`
+	Encrypt    string `json:"encrypt"`
+	Agentid    string `json:"agentid"`
+}
+
+type WXBizJsonMsg4Send struct {
+	Encrypt   string `json:"encrypt"`
+	Signature string `json:"msgsignature"`
+	Timestamp string `json:"timestamp"`
+	Nonce     string `json:"nonce"`
+}
+
+func NewWXBizJsonMsg4Send(encrypt, signature, timestamp, nonce string) *WXBizJsonMsg4Send {
+	return &WXBizJsonMsg4Send{Encrypt: encrypt, Signature: signature, Timestamp: timestamp, Nonce: nonce}
+}
+
+type ProtocolProcessor interface {
+	parse(src_data []byte) (*WXBizJsonMsg4Recv, *CryptError)
+	serialize(msg_send *WXBizJsonMsg4Send) ([]byte, *CryptError)
+}
+
+type WXBizMsgCrypt struct {
+	token              string
+	encoding_aeskey    string
+	receiver_id        string
+	protocol_processor ProtocolProcessor
+}
+
+type JsonProcessor struct {
+}
+
+func (self *JsonProcessor) parse(src_data []byte) (*WXBizJsonMsg4Recv, *CryptError) {
+	var msg4_recv WXBizJsonMsg4Recv
+	err := json.Unmarshal(src_data, &msg4_recv)
+	if nil != err {
+		fmt.Println("Unmarshal fail", err)
+		return nil, NewCryptError(ParseJsonError, "json to msg fail")
+	}
+	return &msg4_recv, nil
+}
+
+func (self *JsonProcessor) serialize(msg4_send *WXBizJsonMsg4Send) ([]byte, *CryptError) {
+	json_msg, err := json.Marshal(msg4_send)
+	if nil != err {
+		return nil, NewCryptError(GenJsonError, err.Error())
+	}
+
+	return json_msg, nil
+}
+
+func NewWXBizMsgCrypt(token, encoding_aeskey, receiver_id string, protocol_type ProtocolType) *WXBizMsgCrypt {
+	var protocol_processor ProtocolProcessor
+	if protocol_type != JsonType {
+		panic("unsupport protocal")
+	} else {
+		protocol_processor = new(JsonProcessor)
+	}
+
+	return &WXBizMsgCrypt{token: token, encoding_aeskey: (encoding_aeskey + "="), receiver_id: receiver_id, protocol_processor: protocol_processor}
+}
+
+func (self *WXBizMsgCrypt) randString(n int) string {
+	b := make([]byte, n)
+	for i := range b {
+		b[i] = letterBytes[rand.Int63()%int64(len(letterBytes))]
+	}
+	return string(b)
+}
+
+func (self *WXBizMsgCrypt) pKCS7Padding(plaintext string, block_size int) []byte {
+	padding := block_size - (len(plaintext) % block_size)
+	padtext := bytes.Repeat([]byte{byte(padding)}, padding)
+	var buffer bytes.Buffer
+	buffer.WriteString(plaintext)
+	buffer.Write(padtext)
+	return buffer.Bytes()
+}
+
+func (self *WXBizMsgCrypt) pKCS7Unpadding(plaintext []byte, block_size int) ([]byte, *CryptError) {
+	plaintext_len := len(plaintext)
+	if nil == plaintext || plaintext_len == 0 {
+		return nil, NewCryptError(DecryptAESError, "pKCS7Unpadding error nil or zero")
+	}
+	if plaintext_len%block_size != 0 {
+		return nil, NewCryptError(DecryptAESError, "pKCS7Unpadding text not a multiple of the block size")
+	}
+	padding_len := int(plaintext[plaintext_len-1])
+	return plaintext[:plaintext_len-padding_len], nil
+}
+
+func (self *WXBizMsgCrypt) cbcEncrypter(plaintext string) ([]byte, *CryptError) {
+	aeskey, err := base64.StdEncoding.DecodeString(self.encoding_aeskey)
+	if nil != err {
+		return nil, NewCryptError(DecodeBase64Error, err.Error())
+	}
+	const block_size = 32
+	pad_msg := self.pKCS7Padding(plaintext, block_size)
+
+	block, err := aes.NewCipher(aeskey)
+	if err != nil {
+		return nil, NewCryptError(EncryptAESError, err.Error())
+	}
+
+	ciphertext := make([]byte, len(pad_msg))
+	iv := aeskey[:aes.BlockSize]
+
+	mode := cipher.NewCBCEncrypter(block, iv)
+
+	mode.CryptBlocks(ciphertext, pad_msg)
+	base64_msg := make([]byte, base64.StdEncoding.EncodedLen(len(ciphertext)))
+	base64.StdEncoding.Encode(base64_msg, ciphertext)
+
+	return base64_msg, nil
+}
+
+func (self *WXBizMsgCrypt) cbcDecrypter(base64_encrypt_msg string) ([]byte, *CryptError) {
+	aeskey, err := base64.StdEncoding.DecodeString(self.encoding_aeskey)
+	if nil != err {
+		return nil, NewCryptError(DecodeBase64Error, err.Error())
+	}
+
+	encrypt_msg, err := base64.StdEncoding.DecodeString(base64_encrypt_msg)
+	if nil != err {
+		return nil, NewCryptError(DecodeBase64Error, err.Error())
+	}
+
+	block, err := aes.NewCipher(aeskey)
+	if err != nil {
+		return nil, NewCryptError(DecryptAESError, err.Error())
+	}
+
+	if len(encrypt_msg) < aes.BlockSize {
+		return nil, NewCryptError(DecryptAESError, "encrypt_msg size is not valid")
+	}
+
+	iv := aeskey[:aes.BlockSize]
+
+	if len(encrypt_msg)%aes.BlockSize != 0 {
+		return nil, NewCryptError(DecryptAESError, "encrypt_msg not a multiple of the block size")
+	}
+
+	mode := cipher.NewCBCDecrypter(block, iv)
+
+	mode.CryptBlocks(encrypt_msg, encrypt_msg)
+
+	return encrypt_msg, nil
+}
+
+func (self *WXBizMsgCrypt) calSignature(timestamp, nonce, data string) string {
+	sort_arr := []string{self.token, timestamp, nonce, data}
+	sort.Strings(sort_arr);
+	var buffer bytes.Buffer
+	for _, value := range sort_arr {
+		buffer.WriteString(value)
+	}
+
+	sha := sha1.New()
+	sha.Write(buffer.Bytes())
+	signature := fmt.Sprintf("%x", sha.Sum(nil))
+	return string(signature)
+}
+
+func (self *WXBizMsgCrypt) ParsePlainText(plaintext []byte) ([]byte, uint32, []byte, []byte, *CryptError) {
+	const block_size = 32
+	plaintext, err := self.pKCS7Unpadding(plaintext, block_size)
+	if nil != err {
+		return nil, 0, nil, nil, err
+	}
+
+	text_len := uint32(len(plaintext))
+	if text_len < 20 {
+		return nil, 0, nil, nil, NewCryptError(IllegalBuffer, "plain is to small 1")
+	}
+	random := plaintext[:16]
+	msg_len := binary.BigEndian.Uint32(plaintext[16:20])
+	if text_len < (20 + msg_len) {
+		return nil, 0, nil, nil, NewCryptError(IllegalBuffer, "plain is to small 2")
+	}
+
+	msg := plaintext[20 : 20+msg_len]
+	receiver_id := plaintext[20+msg_len:]
+
+	return random, msg_len, msg, receiver_id, nil
+}
+
+func (self *WXBizMsgCrypt) VerifyURL(msg_signature, timestamp, nonce, echostr string) ([]byte, *CryptError) {
+	signature := self.calSignature(timestamp, nonce, echostr)
+
+	if strings.Compare(signature, msg_signature) != 0 {
+		return nil, NewCryptError(ValidateSignatureError, "signature not equal")
+	}
+
+	plaintext, err := self.cbcDecrypter(echostr)
+	if nil != err {
+		return nil, err
+	}
+
+	_, _, msg, receiver_id, err := self.ParsePlainText(plaintext)
+	if nil != err {
+		return nil, err
+	}
+
+	if len(self.receiver_id) > 0 && strings.Compare(string(receiver_id), self.receiver_id) != 0 {
+		fmt.Println(string(receiver_id), self.receiver_id, len(receiver_id), len(self.receiver_id))
+		return nil, NewCryptError(ValidateCorpidError, "receiver_id is not equil")
+	}
+
+	return msg, nil
+}
+
+func (self *WXBizMsgCrypt) EncryptMsg(reply_msg, timestamp, nonce string) ([]byte, *CryptError) {
+	rand_str := self.randString(16)
+	var buffer bytes.Buffer
+	buffer.WriteString(rand_str)
+
+	msg_len_buf := make([]byte, 4)
+	binary.BigEndian.PutUint32(msg_len_buf, uint32(len(reply_msg)))
+	buffer.Write(msg_len_buf)
+	buffer.WriteString(reply_msg);
+	buffer.WriteString(self.receiver_id);
+
+	tmp_ciphertext, err := self.cbcEncrypter(buffer.String());
+	if nil != err {
+		return nil, err
+	}
+	ciphertext := string(tmp_ciphertext)
+
+	signature := self.calSignature(timestamp, nonce, ciphertext)
+
+	msg4_send := NewWXBizJsonMsg4Send(ciphertext, signature, timestamp, nonce)
+	return self.protocol_processor.serialize(msg4_send)
+}
+
+func (self *WXBizMsgCrypt) DecryptMsg(msg_signature, timestamp, nonce string, post_data []byte) ([]byte, *CryptError) {
+	msg4_recv, crypt_err := self.protocol_processor.parse(post_data)
+	if nil != crypt_err {
+		return nil, crypt_err
+	}
+
+	signature := self.calSignature(timestamp, nonce, msg4_recv.Encrypt)
+
+	if strings.Compare(signature, msg_signature) != 0 {
+		return nil, NewCryptError(ValidateSignatureError, "signature not equal")
+	}
+
+	plaintext, crypt_err := self.cbcDecrypter(msg4_recv.Encrypt)
+	if nil != crypt_err {
+		return nil, crypt_err
+	}
+
+	_, _, msg, receiver_id, crypt_err := self.ParsePlainText(plaintext)
+	if nil != crypt_err {
+		return nil, crypt_err
+	}
+
+	if len(self.receiver_id) > 0 && strings.Compare(string(receiver_id), self.receiver_id) != 0 {
+		return nil, NewCryptError(ValidateCorpidError, "receiver_id is not equil")
+	}
+
+	return msg, nil
+}
+

+ 23 - 0
test/redis_test.go

@@ -0,0 +1,23 @@
+package test
+
+import (
+	"fmt"
+	"github.com/zeromicro/go-zero/core/stores/redis"
+	"testing"
+)
+
+func TestName(t *testing.T) {
+	newRedis, err := redis.NewRedis(redis.RedisConf{
+		Host:     "10.8.230.17:6379",
+		Type:     "node",
+		Pass:     "",
+		Tls:      false,
+		NonBlock: false,
+	})
+	if err != nil {
+		fmt.Println(err)
+	} else {
+		fmt.Println(newRedis.Ping())
+	}
+
+}