Tinger 1 yıl önce
ebeveyn
işleme
618e355fc3
53 değiştirilmiş dosya ile 1694 ekleme ve 623 silme
  1. 7 0
      .idea/dataSources.xml
  2. 6 0
      .idea/statistic.xml
  3. 6 0
      .idea/vcs.xml
  4. 5 0
      Dockerfile
  5. 8 1
      config.json
  6. 4 4
      docker-compose.yml
  7. 1 0
      go.mod
  8. 2 0
      go.sum
  9. 22 0
      handlers/debugger/params.go
  10. 8 0
      handlers/debugger/router.go
  11. 68 0
      handlers/debugger/spliter.go
  12. 113 0
      handlers/debugger/worker.go
  13. 33 0
      handlers/manager/params.go
  14. 1 2
      handlers/manager/router.go
  15. 101 16
      handlers/manager/spliter.go
  16. 165 42
      handlers/manager/worker.go
  17. 42 0
      handlers/seller/params.go
  18. 1 0
      handlers/seller/router.go
  19. 36 10
      handlers/seller/spliter.go
  20. 184 73
      handlers/seller/worker.go
  21. 101 39
      handlers/setup.go
  22. 0 9
      handlers/test/router.go
  23. 0 194
      handlers/test/worker.go
  24. 5 6
      handlers/worker/spliter.go
  25. 10 6
      handlers/worker/worker.go
  26. BIN
      main
  27. 2 3
      main.go
  28. BIN
      static/files/23112701.apk
  29. BIN
      static/files/23112702.apk
  30. BIN
      static/files/RE.apk
  31. BIN
      static/files/wine-client-231026.apk
  32. BIN
      static/files/wine-client-231101.apk
  33. BIN
      static/files/wine-client-23112703.apk
  34. BIN
      static/images/wine/10101.png
  35. BIN
      static/images/wine/10102.png
  36. BIN
      static/images/wine/10103.png
  37. BIN
      static/images/wine/10104.png
  38. BIN
      static/images/wine/10105.png
  39. 27 0
      utils/cert/client-private.pem
  40. 9 0
      utils/cert/client-public.pem
  41. 27 0
      utils/cert/server-private.pem
  42. 9 0
      utils/cert/server-public.pem
  43. 41 13
      utils/com.go
  44. 31 2
      utils/errors_here.go
  45. 165 40
      utils/lib.go
  46. 18 6
      utils/tables/advertise.go
  47. 0 75
      utils/tables/create-tables.txt
  48. 140 41
      utils/tables/device.go
  49. 65 9
      utils/tables/manager.go
  50. 17 5
      utils/tables/params.go
  51. 105 0
      utils/tables/trade.go
  52. 17 5
      utils/tables/version.go
  53. 92 22
      utils/tables/wine.go

+ 7 - 0
.idea/dataSources.xml

@@ -22,5 +22,12 @@
       <jdbc-url>jdbc:mysql://122.112.224.199:3306/wine</jdbc-url>
       <working-dir>$ProjectFileDir$</working-dir>
     </data-source>
+    <data-source source="LOCAL" name="solo" uuid="99065d68-1e56-436c-98fc-90dd82e0a000">
+      <driver-ref>mysql.8</driver-ref>
+      <synchronize>true</synchronize>
+      <jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
+      <jdbc-url>jdbc:mysql://tinger.host:3306/solo</jdbc-url>
+      <working-dir>$ProjectFileDir$</working-dir>
+    </data-source>
   </component>
 </project>

+ 6 - 0
.idea/statistic.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="Statistic">
+    <option name="fileTypes" value="class;svn-base;svn-work;Extra;gif;png;jpg;jpeg;bmp;tga;tiff;ear;war;zip;jar;iml;iws;ipr;bz2;gz;pyc;apk;mp4" />
+  </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>

+ 5 - 0
Dockerfile

@@ -0,0 +1,5 @@
+FROM ubuntu:latest
+WORKDIR /srv
+
+RUN apt-get -qq update && apt-get -qq install -y --no-install-recommends ca-certificates curl && \
+  ln -snf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo Asia/Shanghai > /etc/timezone

+ 8 - 1
config.json

@@ -14,8 +14,15 @@
   "redis_pass": "hm123456",
   "redis_database": 1,
 
+  "wx_pay_title": "贵州醴泉古酿酒业",
   "wx_merchant_acc": "1658095373",
+  "wx_app_id": "wx7363e2fe8926d76d",
   "wx_api_cert_seq": "34931049643AF4C82415D882E6FF2875E2F5F1B7",
   "wx_api_cert_path": "./utils/cert/apiclient_key.pem",
-  "wx_api_v3_key": "0HangZhou1GuiZhou2LiQuanGuNiang3"
+  "wx_api_v3_key": "3HangZhou2GuiZhou1LiQuanGuNiang0",
+
+  "server_private": "./utils/cert/server-private.pem",
+  "server_public": "./utils/cert/server-public.pem",
+  "client_private": "./utils/cert/client-private.pem",
+  "client_public": "./utils/cert/client-public.pem"
 }

+ 4 - 4
docker-compose.yml

@@ -2,14 +2,14 @@ version: "3"
 
 services:
   WineServer:
-    image: ubuntu:latest
+    image: ubuntu:certified
     container_name: WineServer
     restart: unless-stopped
-    working_dir: /srv
+    environment:
+
     ports:
       - 4090:4090
-    environment:
-      - TZ=Asia/Shanghai
     volumes:
       - ./:/srv
+      - /etc/localtime:/etc/localtime:ro
     command: ["./main", "config.json"]

+ 1 - 0
go.mod

@@ -29,6 +29,7 @@ require (
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/reflect2 v1.0.2 // indirect
 	github.com/pelletier/go-toml/v2 v2.1.0 // indirect
+	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
 	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
 	github.com/ugorji/go/codec v1.2.11 // indirect
 	github.com/wechatpay-apiv3/wechatpay-go v0.2.18 // indirect

+ 2 - 0
go.sum

@@ -86,6 +86,8 @@ github.com/redis/go-redis/v9 v9.1.0/go.mod h1:urWj3He21Dj5k4TK1y59xH8Uj6ATueP8AH
 github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
 github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
 github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
+github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
+github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
 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/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=

+ 22 - 0
handlers/debugger/params.go

@@ -0,0 +1,22 @@
+package debugger
+
+type debugParam struct {
+	Seq   string `json:"seq"`
+	Debug bool   `json:"debug"`
+}
+
+type openParam struct {
+	Seq  string `json:"seq"`
+	Kind string `json:"kind"`
+}
+
+type locationParam struct {
+	Seq string `json:"seq"`
+	Loc string `json:"loc"`
+}
+
+type ppvParam struct {
+	Seq   string `json:"seq"`
+	Index uint8  `json:"index"`
+	Value uint8  `json:"value"`
+}

+ 8 - 0
handlers/debugger/router.go

@@ -0,0 +1,8 @@
+package debugger
+
+import "github.com/gin-gonic/gin"
+
+func Router(route *gin.Engine) {
+	debugger := route.Group("debugger")
+	debugger.GET("/socket/:id", SocketHandler)
+}

+ 68 - 0
handlers/debugger/spliter.go

@@ -0,0 +1,68 @@
+package debugger
+
+import (
+	"Wine-Server/utils"
+	"github.com/gin-gonic/gin"
+	"github.com/gorilla/websocket"
+)
+
+func debuggerConnect(did string, conn *websocket.Conn) {
+	utils.DebugLock.Lock()
+	utils.DebugWss[did] = conn
+	utils.DebugLock.Unlock()
+	utils.Logger.Printf("debugger[%s] connected\n", did)
+}
+
+func debuggerDisconnect(did string) {
+	utils.DebugLock.Lock()
+	delete(utils.DebugWss, did)
+	utils.DebugLock.Unlock()
+	utils.Logger.Printf("debugger[%s] disconnected\n", did)
+}
+
+func SocketHandler(ctx *gin.Context) {
+	conn, err := utils.UpgradeHttp2Ws.Upgrade(ctx.Writer, ctx.Request, nil)
+	if err != nil {
+		utils.Logger.Println("can't establish debugger socket connect")
+		ctx.JSON(utils.HttpError, utils.Fail("can't establish socket connect"))
+		return
+	}
+	who := ctx.Param("id")
+	if who == "" {
+		_ = conn.WriteJSON(utils.WsError("debugger id is required"))
+		return
+	}
+
+	debuggerConnect(who, conn)
+	for {
+		var msg utils.WsMsg
+		err = conn.ReadJSON(&msg)
+		if err != nil {
+			debuggerDisconnect(who)
+			return
+		}
+		switch msg.Event {
+		case "pin":
+			keepAlive(conn)
+			break
+		case "listDevices":
+			listDevices(conn)
+			break
+		case "setDebug":
+			setDebug(conn, msg.Data)
+			break
+		case "openGate":
+			openGate(conn, who, msg.Data)
+			break
+		case "setLocation":
+			setLocation(conn, msg.Data)
+			break
+		case "setPpv":
+			setPpv(conn, msg.Data)
+			break
+		default:
+			_ = conn.WriteJSON(utils.WsError("unrecognized event"))
+			break
+		}
+	}
+}

+ 113 - 0
handlers/debugger/worker.go

@@ -0,0 +1,113 @@
+package debugger
+
+import (
+	"Wine-Server/utils"
+	"Wine-Server/utils/tables"
+	"github.com/gorilla/websocket"
+)
+
+func keepAlive(conn *websocket.Conn) {
+	_ = conn.WriteJSON(utils.WsEvent("pon", nil))
+}
+
+func listDevices(conn *websocket.Conn) {
+	result := utils.JsonType{}
+	for seq, device := range utils.SellerDevices {
+		table := tables.DeviceTable{Id: seq}
+		_ = table.Get()
+		result[seq] = utils.JsonType{
+			"seq": seq, "online": device.Online, "location": table.Addr,
+			"ppv1": table.Wines[0].Ppv, "ppv2": table.Wines[1].Ppv,
+			"ppv3": table.Wines[2].Ppv, "ppv4": table.Wines[3].Ppv,
+		}
+	}
+	_ = conn.WriteJSON(utils.WsEvent("devices", result))
+}
+
+func setDebug(conn *websocket.Conn, data any) {
+	var param debugParam
+	err := utils.AnyTrans(data, &param)
+	if err != nil {
+		_ = conn.WriteJSON(utils.WsError("params error"))
+		return
+	}
+	if device, exist := utils.SellerDevices[param.Seq]; exist {
+		if device.Online {
+			_ = device.Conn.WriteJSON(utils.WsEvent("setDebug", param.Debug))
+			return
+		}
+		_ = conn.WriteJSON(utils.WsError("device offline"))
+		return
+	}
+	_ = conn.WriteJSON(utils.WsError("no such device"))
+}
+
+func openGate(conn *websocket.Conn, who string, data any) {
+	var param openParam
+	err := utils.AnyTrans(data, &param)
+	if err != nil {
+		_ = conn.WriteJSON(utils.WsError("params error"))
+		return
+	}
+	if device, exist := utils.SellerDevices[param.Seq]; exist {
+		if device.Online {
+			_ = device.Conn.WriteJSON(utils.WsEvent("openGate", utils.JsonType{
+				"kind": param.Kind,
+				"who":  who,
+			}))
+			return
+		}
+		_ = conn.WriteJSON(utils.WsError("device offline"))
+		return
+	}
+	_ = conn.WriteJSON(utils.WsError("no such device"))
+}
+
+func setLocation(conn *websocket.Conn, data any) {
+	var param locationParam
+	err := utils.AnyTrans(data, &param)
+	if err != nil {
+		_ = conn.WriteJSON(utils.WsError("params error"))
+		return
+	}
+	table := tables.DeviceTable{Id: param.Seq}
+	_ = table.Get()
+	err = table.Update(utils.JsonType{"addr": param.Loc})
+	if err != nil {
+		utils.Logger.Printf("update location failed, device[%s], addr[%s], error: %s\n", param.Seq, param.Loc, err)
+		_ = conn.WriteJSON(utils.WsError("update location failed"))
+	}
+	_ = conn.WriteJSON(utils.WsEvent("locationUpdated", nil))
+}
+
+func setPpv(conn *websocket.Conn, data any) {
+	var param ppvParam
+	err := utils.AnyTrans(data, &param)
+	if err != nil {
+		_ = conn.WriteJSON(utils.WsError("params error"))
+		return
+	}
+	table := tables.DeviceTable{Id: param.Seq}
+	_ = table.Get()
+	err = table.Update(utils.JsonType{utils.Format("ppv%d", param.Index+1): param.Value})
+	if err != nil {
+		utils.Logger.Printf(
+			"update location failed, device[%s], ppv%d[%s], error: %s\n",
+			param.Seq, param.Index, param.Value, err,
+		)
+		_ = conn.WriteJSON(utils.WsError("update ppv failed"))
+	}
+
+	if device, exist := utils.SellerDevices[param.Seq]; exist {
+		if device.Online {
+			_ = device.Conn.WriteJSON(utils.WsEvent("ppvUpdate", utils.JsonType{
+				"index": param.Index,
+				"value": param.Value,
+			}))
+			return
+		}
+		_ = conn.WriteJSON(utils.WsError("device offline"))
+		return
+	}
+	_ = conn.WriteJSON(utils.WsError("no such device"))
+}

+ 33 - 0
handlers/manager/params.go

@@ -0,0 +1,33 @@
+package manager
+
+type loginParam struct {
+	Account  string `json:"account"`
+	Password string `json:"password"`
+}
+
+type superQueryDeviceParam struct {
+	Cond    string `json:"cond"`
+	Manager string `json:"manager"`
+	Page    int    `json:"page"`
+	Limit   int    `json:"limit"`
+}
+
+type superUpdateDeviceParam struct {
+	Did  string `json:"Did"`
+	Mid  string `json:"mid"`
+	Addr string `json:"addr"`
+	Mark string `json:"mark"`
+}
+
+type superBatchAssignParam struct {
+	Devices []string `json:"devices"`
+	Manager string   `json:"manager"`
+}
+
+type superQueryWineParam struct {
+	Cond  string `json:"cond"`
+	Page  int    `json:"page"`
+	Limit int    `json:"limit"`
+}
+
+type superDeleteWineParam []uint16

+ 1 - 2
handlers/manager/router.go

@@ -6,6 +6,5 @@ import (
 
 func Router(route *gin.Engine) {
 	manager := route.Group("manager")
-	manager.POST("/key", PublicKeyHandler)
-	manager.POST("/test", EncryptTestHandler)
+	manager.GET("/socket/:token", SocketHandler)
 }

+ 101 - 16
handlers/manager/spliter.go

@@ -2,55 +2,140 @@ package manager
 
 import (
 	"Wine-Server/utils"
-	"fmt"
+	"Wine-Server/utils/tables"
 	"github.com/gin-gonic/gin"
 	"github.com/gorilla/websocket"
 )
 
-func managerConnected(mid string, conn *websocket.Conn) {
+func managerConnected(mid string, conn *websocket.Conn) *tables.ManagerTable {
+	table := &tables.ManagerTable{Id: mid}
+	err := table.Get()
+	if err != nil {
+		utils.Logger.Println("manager query failed:", err)
+		_ = conn.WriteJSON(utils.WsError("manager query failed"))
+		return nil
+	}
+	table.Last = utils.TimeNow()
+	err = table.Update(utils.JsonType{"last": table.Last})
+	if err != nil {
+		utils.Logger.Println("update last active time failed:", err)
+		_ = conn.WriteJSON(utils.WsError("Update last active time failed"))
+		return nil
+	}
+	if _, exist := utils.ManagerWss[mid]; exist {
+		_ = conn.WriteJSON(utils.WsEvent("onePageOnly", nil))
+		return nil
+	}
 	utils.ManagerLock.Lock()
 	utils.ManagerWss[mid] = conn
 	utils.ManagerLock.Unlock()
-	fmt.Printf("manager[%s] connected\n", mid)
+	utils.Logger.Printf("manager[%s] connected\n", mid)
+	return table
 }
 
 func managerDisconnect(mid string) {
 	utils.ManagerLock.Lock()
 	delete(utils.ManagerWss, mid)
 	utils.ManagerLock.Unlock()
-	fmt.Printf("manager[%s] disconnected\n", mid)
+	utils.Logger.Printf("manager[%s] disconnected\n", mid)
+}
+
+func loginSocket(conn *websocket.Conn) (string, error) {
+	var mid string
+	for {
+		var msg utils.WsMsg
+		err := conn.ReadJSON(&msg)
+		if err != nil {
+			_ = conn.Close()
+			return "", err
+		}
+		switch msg.Event {
+		case "pin":
+			keepAlive(conn)
+			break
+		case "getUserInfo": // in case: redis token expired
+			break
+		case "login":
+			mid, err = userLogin(conn, msg.Data)
+			if err == nil {
+				return mid, nil
+			}
+			break
+		default:
+			utils.Logger.Println(msg.Event)
+			_ = conn.WriteJSON(utils.WsError("loginSocket event only"))
+			break
+		}
+	}
 }
 
 func SocketHandler(ctx *gin.Context) {
 	conn, err := utils.UpgradeHttp2Ws.Upgrade(ctx.Writer, ctx.Request, nil)
 	if err != nil {
+		utils.Logger.Println("can't establish manager socket connect")
 		ctx.JSON(utils.HttpError, utils.Fail("can't establish socket connect"))
 		return
 	}
-	var ok bool
+	getSecrets(conn)
 	var mid string
-	if mid, ok = ctx.GetQuery("token"); ok {
-		if _, err = utils.Redis.Get(ctx, mid).Bool(); err != nil {  // better to get pub,pri secret here
-			// token is invalid
-		} else {
-			// token is valid
-		}
+	token := ctx.Param("token")
+	if token == "undefined" { // no token -> login
+		mid, err = loginSocket(conn)
 	} else {
-		// no token
+		mid, err = utils.Redis.Get(utils.WxPayCli, token).Result()
+		if err != nil { // expired -> login
+			_ = conn.WriteJSON(utils.WsEvent("tokenExpired", nil))
+			mid, err = loginSocket(conn)
+		} else { // update expire time
+			err = utils.Redis.Expire(utils.WxPayCli, token, utils.Duration(7*24*60*60)).Err()
+			if err != nil {
+				utils.Logger.Println(err)
+			}
+		}
 	}
 
-	managerConnected(mid, conn)
+	if err != nil {
+		return
+	}
+	manager := managerConnected(mid, conn)
+	if manager == nil {
+		return
+	}
+	defer managerDisconnect(mid)
+
 	for {
 		var msg utils.WsMsg
 		err = conn.ReadJSON(&msg)
 		if err != nil {
-			managerDisconnect(mid)
 			return
 		}
 		switch msg.Event {
-		case "openGate":
+		case "pin":
+			keepAlive(conn)
+			break
+		case "getUserInfo":
+			getUserInfo(conn, manager)
+			break
+		case "logout":
+			userLogout(conn, msg.Data)
+			return
+		case "superQueryManager":
+			superQueryManager(conn, manager, msg.Data)
+			break
+		case "superQueryDevice":
+			superQueryDevice(conn, manager, msg.Data)
+			break
+		case "superUpdateDevice":
+			superUpdateDevice(conn, manager, msg.Data)
+			break
+		case "superBatchAssign":
+			superBatchAssign(conn, manager, msg.Data)
+			break
+		case "superQueryWine":
+			superQueryWine(conn, manager, msg.Data)
 			break
-		case "orderFinished":
+		case "superDeleteWine":
+			superDeleteWine(conn, manager, msg.Data)
 			break
 		default:
 			_ = conn.WriteJSON(utils.WsError("unrecognized event"))

+ 165 - 42
handlers/manager/worker.go

@@ -2,63 +2,186 @@ package manager
 
 import (
 	"Wine-Server/utils"
-	"fmt"
-	"github.com/gin-gonic/gin"
-	"github.com/redis/go-redis/v9"
+	"Wine-Server/utils/tables"
+	"errors"
+	"github.com/gorilla/websocket"
 )
 
-func PublicKeyHandler(ctx *gin.Context) {
-	ctx.JSON(utils.HttpOk, utils.Success(utils.JsonType{"public": utils.ClientPub, "private": utils.ClientPri}))
+func keepAlive(conn *websocket.Conn) {
+	_ = conn.WriteJSON(utils.WsEvent("pon", nil))
 }
 
-func EncryptTestHandler(ctx *gin.Context) {
-	type Param struct {
-		User string `json:"user"`
-		Pass string `json:"pass"`
+func getSecrets(conn *websocket.Conn) {
+	_ = conn.WriteJSON(utils.WsEvent("secrets", utils.JsonType{
+		"public": utils.ClientPub, "private": utils.ClientPri,
+	}))
+}
+
+func userLogin(conn *websocket.Conn, data any) (string, error) {
+	var param loginParam
+	err := utils.AnyTrans(data, &param)
+	if err != nil {
+		_ = conn.WriteJSON(utils.WsError("param error"))
+		return "", err
 	}
-	var param Param
-	err := ctx.ShouldBind(&param)
+	password, err := utils.Decrypt(param.Password)
+	manager := tables.ManagerTable{Account: param.Account}
+	err = manager.GetByAcc()
 	if err != nil {
-		fmt.Println(err.Error())
-		ctx.JSON(utils.HttpOk, utils.Fail("param error 1"))
+		_ = conn.WriteJSON(utils.WsEvent("loginResult", utils.JsonType{"status": false, "msg": "用户不存在"}))
+		utils.Logger.Println(err)
+		return "", err
+	}
+	if utils.IsPasswordMatch(manager.Password, password) {
+		var token string
+		utils.ManagerLock.Lock()
+		if _, exist := utils.ManagerWss[manager.Id]; exist {
+			utils.ManagerLock.Unlock()
+			_ = conn.WriteJSON(utils.WsEvent("loginResult", utils.JsonType{"status": false, "msg": "账户已在别处登录"}))
+			return "", errors.New("login other where")
+		}
+		for {
+			token = utils.Format("ManagerToken_%s", utils.RandomString(19, utils.AlphaNumPatten))
+			exist, _ := utils.Redis.Exists(utils.WxPayCli, token).Result()
+			if exist != 1 {
+				_ = utils.Redis.Set(utils.WxPayCli, token, manager.Id, utils.Duration(7*24*60*60)).Err()
+				break
+			}
+		}
+		utils.ManagerLock.Unlock()
+		_ = conn.WriteJSON(utils.WsEvent("loginResult", utils.Success(utils.JsonType{"token": token, "super": manager.Super})))
+		return manager.Id, nil
+	} else {
+		utils.Logger.Println(utils.HashPassword(password))
+		_ = conn.WriteJSON(utils.WsEvent("loginResult", utils.Fail("密码错误")))
+		return "", errors.New("password error")
+	}
+}
+
+func getUserInfo(conn *websocket.Conn, manager *tables.ManagerTable) {
+	_ = conn.WriteJSON(utils.WsEvent("userInfo", utils.JsonType{
+		"id": manager.Id, "super": manager.Super, "name": manager.Name,
+		"order": manager.Order, "income": manager.Income,
+	}))
+}
+
+func userLogout(conn *websocket.Conn, data any) {
+	utils.Redis.Del(utils.WxPayCli, data.(string))
+	_ = conn.WriteJSON(utils.WsEvent("logoutResult", nil))
+}
+
+func superQueryManager(conn *websocket.Conn, manager *tables.ManagerTable, data any) {
+	if !manager.Super {
+		_ = conn.WriteJSON(utils.WsError("auth failed."))
 		return
 	}
+	name, _ := data.(string)
+	res, err := tables.QueryManager(name)
+	if err != nil {
+		utils.Logger.Println(err)
+		_ = conn.WriteJSON(utils.WsError("query manager failed"))
+		return
+	}
+	_ = conn.WriteJSON(utils.WsEvent("superQueryManagerRes", res))
+}
+
+func superQueryDevice(conn *websocket.Conn, manager *tables.ManagerTable, data any) {
+	if !manager.Super {
+		_ = conn.WriteJSON(utils.WsError("auth failed."))
+		return
+	}
+	var param superQueryDeviceParam
+	err := utils.AnyTrans(data, &param)
+	if err != nil {
+		_ = conn.WriteJSON(utils.WsError("param error"))
+		return
+	}
+	total, devices, err := tables.QueryDevices(param.Manager, param.Cond, param.Limit, param.Page)
+	if err != nil {
+		utils.Logger.Println(err)
+		_ = conn.WriteJSON(utils.WsError("query devices failed"))
+		return
+	}
+	_ = conn.WriteJSON(utils.WsEvent("superQueryDeviceRes", utils.JsonType{"total": total, "devices": devices}))
+}
 
-	param.Pass, err = utils.Decrypt(param.Pass)
+func superUpdateDevice(conn *websocket.Conn, manager *tables.ManagerTable, data any) {
+	if !manager.Super {
+		_ = conn.WriteJSON(utils.WsError("auth failed."))
+		return
+	}
+	var param superUpdateDeviceParam
+	err := utils.AnyTrans(data, &param)
 	if err != nil {
-		fmt.Println(err.Error())
-		ctx.JSON(utils.HttpOk, utils.Fail("param error 3"))
+		_ = conn.WriteJSON(utils.WsError("param error"))
 		return
 	}
+	device := tables.DeviceTable{Id: param.Did}
+	err = device.Update(utils.JsonType{"manager": param.Mid, "addr": param.Addr, "mark": param.Mark})
+	if err != nil {
+		utils.Logger.Println(err)
+		_ = conn.WriteJSON(utils.WsEvent("superUpdateDeviceRes", utils.Fail("更新失败")))
+		return
+	}
+	_ = conn.WriteJSON(utils.WsEvent("superUpdateDeviceRes", utils.Success(nil)))
+}
 
-	fmt.Printf("user: %s, password: %s\n", param.User, param.Pass)
-	ctx.JSON(utils.HttpOk, utils.Success(param.Pass))
+func superBatchAssign(conn *websocket.Conn, manager *tables.ManagerTable, data any) {
+	if !manager.Super {
+		_ = conn.WriteJSON(utils.WsError("auth failed."))
+		return
+	}
+	var param superBatchAssignParam
+	err := utils.AnyTrans(data, &param)
+	if err != nil {
+		_ = conn.WriteJSON(utils.WsError("param error"))
+		return
+	}
+	err = tables.BatchAssignDeviceManager(param.Devices, param.Manager)
+	if err != nil {
+		utils.Logger.Println(err)
+		_ = conn.WriteJSON(utils.WsEvent("superBatchAssignRes", utils.Fail("分配失败")))
+		return
+	}
+	_ = conn.WriteJSON(utils.WsEvent("superBatchAssignRes", utils.Success(nil)))
 }
 
-func RegisterHandler(ctx *gin.Context) {
-	_, ok := ctx.GetPostForm("device")
-	if !ok {
-		ctx.JSON(utils.HttpOk, utils.Fail("param lost."))
-	}
-	token := utils.RandomString(16, utils.AlphaNumPatten)
-	key := fmt.Sprintf("Seller-%s", token)
-Diff:
-	for {
-		res, err := utils.Redis.Get(ctx, key).Result()
-		switch {
-		case err == redis.Nil:
-			break Diff
-		case err != nil:
-			ctx.JSON(utils.HttpOk, utils.Fail("获取Token失败"))
-			return
-		case res == "":
-			break Diff
-		}
-		token = utils.RandomString(16, utils.AlphaNumPatten)
-		key = fmt.Sprintf("Seller-%s", token)
+func superQueryWine(conn *websocket.Conn, manager *tables.ManagerTable, data any) {
+	if !manager.Super {
+		_ = conn.WriteJSON(utils.WsError("auth failed."))
+		return
 	}
-	utils.Redis.Set(ctx, key, 1, 0)
+	var param superQueryWineParam
+	err := utils.AnyTrans(data, &param)
+	if err != nil {
+		_ = conn.WriteJSON(utils.WsError("param error"))
+		return
+	}
+	total, wines, err := tables.QueryWines(param.Cond, param.Limit, param.Page)
+	if err != nil {
+		utils.Logger.Println(err)
+		_ = conn.WriteJSON(utils.WsError("query wines failed"))
+		return
+	}
+	_ = conn.WriteJSON(utils.WsEvent("superQueryWineRes", utils.JsonType{"total": total, "wines": wines}))
+}
 
-	ctx.Header("Token", token)
-	ctx.JSON(utils.HttpOk, utils.Success(token))
+func superDeleteWine(conn *websocket.Conn, manager *tables.ManagerTable, data any) {
+	if !manager.Super {
+		_ = conn.WriteJSON(utils.WsError("auth failed."))
+		return
+	}
+	var ids []uint16
+	err := utils.AnyTrans(data, &ids)
+	if err != nil {
+		_ = conn.WriteJSON(utils.WsError("param error"))
+		return
+	}
+	err = tables.BatchDeleteWine(ids)
+	if err != nil {
+		utils.Logger.Println(err)
+		_ = conn.WriteJSON(utils.WsEvent("superDeleteWineRes", utils.Fail("删除失败")))
+		return
+	}
+	_ = conn.WriteJSON(utils.WsEvent("superDeleteWineRes", utils.Success(nil)))
 }

+ 42 - 0
handlers/seller/params.go

@@ -0,0 +1,42 @@
+package seller
+
+type notifyParam struct {
+	TradeNo       string `json:"out_trade_no"`
+	TransactionId string `json:"transaction_id"`
+	TradeState    string `json:"trade_state"`
+	Payer         struct {
+		Openid string `json:"openid"`
+	} `json:"payer"`
+}
+
+type tradeRedis struct {
+	Device string `json:"device"`
+	Cell   uint8  `json:"cell"`
+	Wine   uint16 `json:"wine"`
+	Weight uint16 `json:"weight"`
+	Cash   int    `json:"cash"`
+}
+
+type qrcodeParam struct {
+	Id     uint16 `json:"id"`
+	Cell   uint8  `json:"cell"`
+	Weight uint16 `json:"weight"`
+	Cash   int    `json:"cash"`
+}
+
+type authParam struct {
+	Type string `json:"type"`
+	Who  string `json:"who"`
+	Code string `json:"code"`
+}
+
+type resultParam struct {
+	Type   string `json:"type"`
+	Who    string `json:"who"`
+	Result bool   `json:"result"`
+}
+
+type finishParam struct {
+	Type string `json:"type"`
+	Who  string `json:"who"`
+}

+ 1 - 0
handlers/seller/router.go

@@ -5,5 +5,6 @@ import "github.com/gin-gonic/gin"
 func Router(route *gin.Engine) {
 	seller := route.Group("seller")
 	seller.GET("/version", VersionHandler)
+	seller.POST("/wxpay", WxPayHandler)
 	seller.GET("/socket/:seq", SocketHandler)
 }

+ 36 - 10
handlers/seller/spliter.go

@@ -4,7 +4,6 @@ import (
 	"Wine-Server/utils"
 	"Wine-Server/utils/tables"
 	"database/sql"
-	"fmt"
 	"github.com/gin-gonic/gin"
 	"github.com/gorilla/websocket"
 )
@@ -15,6 +14,7 @@ func sellerConnected(seq string, conn *websocket.Conn) *tables.DeviceTable {
 	if err == sql.ErrNoRows { // insert new device
 		err = device.Insert()
 		if err != nil {
+			utils.Logger.Println("new device join failed:", err)
 			_ = conn.WriteJSON(utils.WsError("New device join failed"))
 			return nil
 		}
@@ -23,32 +23,57 @@ func sellerConnected(seq string, conn *websocket.Conn) *tables.DeviceTable {
 		device.Last = utils.TimeNow()
 		err = device.Update(utils.JsonType{"last": device.Last})
 		if err != nil {
+			utils.Logger.Println("update last active time failed:", err)
 			_ = conn.WriteJSON(utils.WsError("Update last active time failed"))
 			return nil
 		}
 	}
 
+	exist := false
 	utils.SellerLock.Lock()
-	utils.SellerWss[seq] = conn
+	if pointer, sin := utils.SellerDevices[seq]; sin {
+		pointer.Online = true
+		exist = true
+		pointer.Conn = conn
+	} else {
+		utils.SellerDevices[seq] = &utils.SellerDevice{Online: true, Conn: conn}
+	}
 	utils.SellerLock.Unlock()
-	fmt.Printf("device connected: %s\n", seq)
+	if exist {
+		for _, debugger := range utils.DebugWss {
+			_ = debugger.WriteJSON(utils.WsEvent("deviceStatusChange", utils.JsonType{"seq": seq, "online": true}))
+		}
+	} else {
+		for _, debugger := range utils.DebugWss {
+			_ = debugger.WriteJSON(utils.WsEvent("newDevice", utils.JsonType{
+				"seq": seq, "online": true, "location": device.Addr,
+				"ppv1": device.Wines[0].Ppv, "ppv2": device.Wines[1].Ppv,
+				"ppv3": device.Wines[2].Ppv, "ppv4": device.Wines[3].Ppv,
+			}))
+		}
+	}
+	utils.Logger.Printf("device[%s] connected\n", seq)
 	return &device
 }
 
 func sellerDisconnect(seq string) {
 	utils.SellerLock.Lock()
-	delete(utils.SellerWss, seq)
+	utils.SellerDevices[seq].Online = false
 	utils.SellerLock.Unlock()
-	fmt.Printf("device[%s] disconnected\n", seq)
+	for _, debugger := range utils.DebugWss {
+		_ = debugger.WriteJSON(utils.WsEvent("deviceStatusChange", utils.JsonType{"seq": seq, "online": false}))
+	}
+	utils.Logger.Printf("device[%s] disconnected\n", seq)
 }
 
 func SocketHandler(ctx *gin.Context) {
 	conn, err := utils.UpgradeHttp2Ws.Upgrade(ctx.Writer, ctx.Request, nil)
 	if err != nil {
+		utils.Logger.Println("can't establish seller socket connect")
 		ctx.JSON(utils.HttpError, utils.Fail("can't establish socket connect"))
 		return
 	}
-	seq := ctx.Param("seq")  //ctx.DefaultQuery("seq", "")
+	seq := ctx.Param("seq")
 	if seq == "" {
 		_ = conn.WriteJSON(utils.WsError("device seq is required"))
 		return
@@ -56,6 +81,7 @@ func SocketHandler(ctx *gin.Context) {
 
 	device := sellerConnected(seq, conn)
 	if device == nil {
+		sellerDisconnect(seq)
 		return
 	}
 	infoOfDevice(device, conn)
@@ -67,14 +93,14 @@ func SocketHandler(ctx *gin.Context) {
 			return
 		}
 		switch msg.Event {
-		case "setLocation":
-			updateLocation(device, conn, msg.Data)
+		case "pin":
+			keepAlive(conn, msg.Data)
 			break
 		case "getQrcode":
-			getQrcode(conn, msg.Data)
+			getQrcode(device, conn, msg.Data)
 			break
 		case "checkAuthCode":
-			checkAuthCode(conn, device.Id, msg.Data)
+			checkAuthCode(device.Id, conn, msg.Data)
 			break
 		case "openResult":
 			openResult(conn, msg.Data)

+ 184 - 73
handlers/seller/worker.go

@@ -3,9 +3,13 @@ package seller
 import (
 	"Wine-Server/utils"
 	"Wine-Server/utils/tables"
-	"fmt"
+	"encoding/json"
+	"errors"
 	"github.com/gin-gonic/gin"
 	"github.com/gorilla/websocket"
+	"github.com/wechatpay-apiv3/wechatpay-go/core/auth/verifiers"
+	"github.com/wechatpay-apiv3/wechatpay-go/core/downloader"
+	"github.com/wechatpay-apiv3/wechatpay-go/core/notify"
 )
 
 func VersionHandler(ctx *gin.Context) {
@@ -18,15 +22,119 @@ func VersionHandler(ctx *gin.Context) {
 	ctx.JSON(utils.HttpOk, utils.Success(version))
 }
 
-func infoOfDevice(device *tables.DeviceTable, conn *websocket.Conn) {
-	_ = conn.WriteJSON(utils.WsEvent("locationResult", utils.JsonType{
-		"val": device.Addr, "close": false,
-	}))
+func updateWineInfo(trade *tradeRedis) (uint16, error) {
+	wine := tables.WineTable{Id: trade.Wine}
+	err := wine.Get()
+	if err != nil {
+		utils.Logger.Println("wine get error:", err)
+		return 0, err
+	}
+	wine.Income += uint64(trade.Cash)
+	wine.Order++
+	err = wine.UpdateSelf()
+	if err != nil {
+		utils.Logger.Println("wine update error:", err)
+		return 0, err
+	}
+	return uint16(50*float64(trade.Weight)/float64(wine.Density)/1000 + 0.5), nil
+}
+
+func updateDeviceInfo(trade *tradeRedis, volume uint16) (string, error) {
+	table := tables.DeviceTable{Id: trade.Device}
+	err := table.Get()
+	if err != nil {
+		utils.Logger.Println("device get error:", err)
+		return "", err
+	}
+	table.Order++
+	table.Income += uint64(trade.Cash)
+	table.Wines[trade.Cell].Remain -= volume
+	err = table.UpdateSelf()
+	if err != nil {
+		utils.Logger.Println("device update error:", err)
+		return "", err
+	}
+
+	return table.Manager, nil
+}
+
+func updateManagerInfo(mid string, trade *tradeRedis) error {
+	if mid == "" {
+		utils.Logger.Println("blank manager id")
+		return errors.New("blank manager id")
+	}
+	table := tables.ManagerTable{Id: mid}
+	err := table.Get()
+	if err != nil {
+		utils.Logger.Println("manager get error:", err)
+		return err
+	}
+	table.Order++
+	table.Income += uint64(trade.Cash)
+	err = table.UpdateSelf()
+	if err != nil {
+		utils.Logger.Println("manager update error:", err)
+		return err
+	}
+	return nil
+}
 
+func WxPayHandler(ctx *gin.Context) {
+	err := downloader.MgrInstance().RegisterDownloaderWithPrivateKey(
+		utils.WxPayCli, utils.WxPrivateKey, utils.WxCertSeq, utils.WxMchId, utils.WxV3Key,
+	)
+	visitor := downloader.MgrInstance().GetCertificateVisitor(utils.WxMchId)
+	handler, _ := notify.NewRSANotifyHandler(utils.WxV3Key, verifiers.NewSHA256WithRSAVerifier(visitor))
+	var wxNotify notifyParam
+	_, err = handler.ParseNotifyRequest(utils.WxPayCli, ctx.Request, &wxNotify)
+	if wxNotify.TradeState != "SUCCESS" {
+		utils.Logger.Printf("trade[%s] status[%s]\n", wxNotify.TradeNo, wxNotify.TradeState)
+		ctx.JSON(utils.HttpOk, utils.Success(nil))
+		return
+	}
+	dbKey := utils.Format("WxPay_%s", wxNotify.TradeNo)
+	exist, err := utils.Redis.Exists(utils.WxPayCli, dbKey).Result()
+	if exist != 1 {
+		utils.Logger.Println("re-notified or expire of:", wxNotify.TradeNo)
+		ctx.JSON(utils.HttpOk, utils.Success(nil))
+		return
+	}
+	res, err := utils.Redis.Get(utils.WxPayCli, dbKey).Result()
+	var dbTrade tradeRedis
+	err = json.Unmarshal([]byte(res), &dbTrade)
+	utils.Redis.Del(utils.WxPayCli, dbKey)
+
+	volume, err := updateWineInfo(&dbTrade)
+	manager, err := updateDeviceInfo(&dbTrade, volume)
+	err = updateManagerInfo(manager, &dbTrade)
+
+	table := tables.TradeTable{
+		Id: wxNotify.TransactionId, Trade: wxNotify.TradeNo, Device: dbTrade.Device, Payer: wxNotify.Payer.Openid,
+		Wine: dbTrade.Wine, Weight: dbTrade.Weight, Cash: dbTrade.Cash, Manager: manager,
+	}
+	err = table.Insert()
+	if err != nil {
+		ctx.JSON(utils.HttpError, utils.Fail("server error"))
+		return
+	}
+	if device, din := utils.SellerDevices[dbTrade.Device]; din {
+		if device.Online {
+			_ = device.Conn.WriteJSON(utils.WsEvent("qrcodeScanned", nil))
+			utils.Sleep(1)
+			_ = device.Conn.WriteJSON(utils.WsEvent("orderPayed", volume))
+
+			ctx.JSON(utils.HttpOk, utils.Success(nil))
+		}
+	}
+	ctx.JSON(utils.HttpError, utils.Fail("server error"))
+}
+
+func infoOfDevice(device *tables.DeviceTable, conn *websocket.Conn) {
 	var err error
 	for i := 0; i < 4; i++ {
 		err = device.Wines[i].Get()
 		if err != nil {
+			utils.Logger.Println("Query wine info failed:", err)
 			_ = conn.WriteJSON(utils.WsError("Query wine info failed"))
 			return
 		}
@@ -36,6 +144,7 @@ func infoOfDevice(device *tables.DeviceTable, conn *websocket.Conn) {
 	var advList []tables.AdvertiseTable
 	advList, err = tables.AdvListAll()
 	if err != nil {
+		utils.Logger.Println("Query advertise failed:", err)
 		_ = conn.WriteJSON(utils.WsError("Query advertise failed"))
 		return
 	}
@@ -44,6 +153,7 @@ func infoOfDevice(device *tables.DeviceTable, conn *websocket.Conn) {
 	var runParams []tables.ParamsTable
 	runParams, err = tables.ParamsListAll()
 	if err != nil {
+		utils.Logger.Println("Query running params failed:", err)
 		_ = conn.WriteJSON(utils.WsError("Query running params failed"))
 		return
 	}
@@ -51,95 +161,99 @@ func infoOfDevice(device *tables.DeviceTable, conn *websocket.Conn) {
 	_ = conn.WriteJSON(utils.WsEvent("initFinish", nil))
 }
 
-func updateLocation(device *tables.DeviceTable, conn *websocket.Conn, data any) {
-	if data == "" {
-		_ = conn.WriteJSON(utils.WsError("Blank address is not allowed"))
+func keepAlive(conn *websocket.Conn, data any) {
+	_ = conn.WriteJSON(utils.WsEvent("pon", data))
+}
+
+func getQrcode(device *tables.DeviceTable, conn *websocket.Conn, data any) {
+	var param qrcodeParam
+	err := utils.AnyTrans(data, &param)
+	if err != nil {
+		_ = conn.WriteJSON(utils.WsError("param error"))
 		return
 	}
-	if device.Addr != data {
-		err := device.Update(utils.JsonType{"addr": data})
-		if err != nil {
-			_ = conn.WriteJSON(utils.WsError("Update location failed"))
-			return
-		}
-		_ = conn.WriteJSON(utils.WsEvent("locationResult", utils.JsonType{
-			"val": data, "close": true,
-		}))
+	wine := tables.WineTable{Id: param.Id}
+	err = wine.Get()
+	if err != nil {
+		utils.Logger.Printf("Query wine[%d] failed: %s\n", param.Id, err)
+		_ = conn.WriteJSON(utils.WsError(utils.Format("no such wine: %d", param.Id)))
 		return
 	}
-	_ = conn.WriteJSON(utils.WsError("same location"))
-}
-
-type wineParam struct {
-	Id     uint16 `json:"id"`
-	Weight int    `json:"weight"`
-}
-
-func getQrcode(conn *websocket.Conn, data any) {
-	var list []wineParam
-	err := utils.AnyTrans(data, &list)
+	cash := int(param.Weight) * int(wine.Price)
+	if cash != param.Cash {
+		utils.Logger.Printf("got a wrong cash[user: %d, need: %d]\n", param.Cash, cash)
+		_ = conn.WriteJSON(utils.WsError(utils.Format("got a wrong cash[you: %d, real: %d]", param.Cash, cash)))
+		return
+	}
+	_ = device.Get()
+	trade := device.Id + utils.TimeString()
+	store := tradeRedis{Device: device.Id, Cell: param.Cell, Wine: wine.Id, Weight: param.Weight, Cash: cash}
+	marshal, err := json.Marshal(store)
 	if err != nil {
-		_ = conn.WriteJSON(utils.WsError("param error"))
+		utils.Logger.Println("marshal trade failed:", err)
+		_ = conn.WriteJSON(utils.WsError("marshal trade failed"))
 		return
 	}
-	sum := 0
-	for _, item := range list {
-		wine := tables.WineTable{Id: item.Id}
-		err = wine.Get()
-		if err != nil {
-			_ = conn.WriteJSON(utils.WsError(fmt.Sprintf("no such wine: %d", item.Id)))
-			return
-		}
-		sum += item.Weight * int(wine.Price)
+	err = utils.Redis.Set(utils.WxPayCli, utils.Format("WxPay_%s", trade), marshal, utils.Duration(1800)).Err()
+	if err != nil {
+		utils.Logger.Println("store trade failed:", err)
+		_ = conn.WriteJSON(utils.WsError("store trade failed"))
+		return
 	}
-	fmt.Println(sum)
-	_ = conn.WriteJSON(utils.WsEvent("qrcodeOkayed", "/seller/icon/qrcode.svg"))
-}
 
-type authParam struct {
-	Type string `json:"type"`
-	Wid  string `json:"wid"`
-	Code string `json:"code"`
+	var img []byte
+	img, err = utils.TryWxPay(trade, wine.Name, cash)
+	if err != nil {
+		utils.Logger.Println("access to wxpay failed:", err)
+		_ = conn.WriteJSON(utils.WsError("access to Wechat Pay failed"))
+		return
+	}
+	_ = conn.WriteJSON(utils.WsEvent("qrcodeOkayed", img))
 }
 
-func checkAuthCode(conn *websocket.Conn, did string, data any) {
+func checkAuthCode(did string, conn *websocket.Conn, data any) {
 	var param authParam
 	err := utils.AnyTrans(data, &param)
 	if err != nil {
 		_ = conn.WriteJSON(utils.WsError("params error"))
 		return
 	}
-	fmt.Printf("checkAuthCode: device[%s], code[%s]\n", did, data)
+	if param.Code != "284655" {
+		_ = conn.WriteJSON(utils.WsEvent("authCodeResult", utils.JsonType{
+			"type": param.Type, "ok": false,
+		}))
+		return
+	}
 	switch param.Type {
 	case "Changing":
-		// TODO: check redis `Change_${did}_${param.wid}` === param.code, query real work
+		// TODO: check redis `${did}_${param.who}_Change` === param.code, query real work
 		_ = conn.WriteJSON(utils.WsEvent("authCodeResult", utils.JsonType{
 			"type": param.Type, "ok": true, "work": []utils.JsonType{
 				{
 					"cell": 1,
-					"old": utils.JsonType{"id": 13026, "name": "某一款酒名-1", "remain": 300},
-					"new": utils.JsonType{"id": 13026, "name": "某一款酒名-1", "remain": 15000},
+					"old":  utils.JsonType{"id": 13026, "name": "某一款酒名-1", "remain": 300},
+					"new":  utils.JsonType{"id": 13026, "name": "某一款酒名-1", "remain": 15000},
 				},
 				{
 					"cell": 2,
-					"old": utils.JsonType{"id": 13027, "name": "某一款酒名-2", "remain": 286},
-					"new": utils.JsonType{"id": 13029, "name": "某一款酒名-3", "remain": 15000},
+					"old":  utils.JsonType{"id": 13027, "name": "某一款酒名-2", "remain": 286},
+					"new":  utils.JsonType{"id": 13029, "name": "某一款酒名-3", "remain": 15000},
 				},
 				{
 					"cell": 3,
-					"old": utils.JsonType{"id": 13027, "name": "某一款酒名-2", "remain": 286},
-					"new": utils.JsonType{"id": 13029, "name": "某一款酒名-3", "remain": 15000},
+					"old":  utils.JsonType{"id": 13027, "name": "某一款酒名-2", "remain": 286},
+					"new":  utils.JsonType{"id": 13029, "name": "某一款酒名-3", "remain": 15000},
 				},
 				{
 					"cell": 4,
-					"old": utils.JsonType{"id": 13027, "name": "某一款酒名-2", "remain": 286},
-					"new": utils.JsonType{"id": 13029, "name": "某一款酒名-3", "remain": 15000},
+					"old":  utils.JsonType{"id": 13027, "name": "某一款酒名-2", "remain": 286},
+					"new":  utils.JsonType{"id": 13029, "name": "某一款酒名-3", "remain": 15000},
 				},
 			},
 		}))
 		break
 	case "Fixing":
-		// TODO: check redis `Fix_${did}_${param.wid}` === param.code
+		// TODO: check redis `${did}_${param.who}_Fix` === param.code
 		_ = conn.WriteJSON(utils.WsEvent("authCodeResult", utils.JsonType{
 			"type": param.Type, "ok": true,
 		}))
@@ -149,12 +263,6 @@ func checkAuthCode(conn *websocket.Conn, did string, data any) {
 	}
 }
 
-type resultParam struct {
-	Type   string `json:"type"`
-	Wid    string `json:"wid"`
-	Result bool   `json:"result"`
-}
-
 func openResult(conn *websocket.Conn, data any) {
 	var param resultParam
 	err := utils.AnyTrans(data, &param)
@@ -162,18 +270,18 @@ func openResult(conn *websocket.Conn, data any) {
 		_ = conn.WriteJSON(utils.WsError("params error"))
 		return
 	}
-	if worker, win := utils.WorkerWss[param.Wid]; win {
+	if worker, win := utils.WorkerWss[param.Who]; win {
 		_ = worker.WriteJSON(utils.WsEvent("openResult", utils.JsonType{
 			"type": param.Type, "result": param.Result,
 		}))
 		return
 	}
-	_ = conn.WriteJSON(utils.WsError("no such worker"))
-}
-
-type finishParam struct {
-	Type string `json:"type"`
-	Wid  string `json:"wid"`
+	if debugger, din := utils.DebugWss[param.Who]; din {
+		_ = debugger.WriteJSON(utils.WsEvent("openResult", utils.JsonType{
+			"type": param.Type, "result": param.Result,
+		}))
+		return
+	}
 }
 
 func workFinished(conn *websocket.Conn, data any) {
@@ -183,9 +291,12 @@ func workFinished(conn *websocket.Conn, data any) {
 		_ = conn.WriteJSON(utils.WsError("params error"))
 		return
 	}
-	if worker, win := utils.WorkerWss[param.Wid]; win {
+	if worker, win := utils.WorkerWss[param.Who]; win {
 		_ = worker.WriteJSON(utils.WsEvent("workFinished", param.Type))
-	} else {
-		_ = conn.WriteJSON(utils.WsError("no such worker"))
+		return
+	}
+	if debugger, din := utils.DebugWss[param.Who]; din {
+		_ = debugger.WriteJSON(utils.WsEvent("workFinished", param.Type))
+		return
 	}
 }

+ 101 - 39
handlers/setup.go

@@ -3,9 +3,19 @@ package handlers
 import (
 	"Wine-Server/utils"
 	"Wine-Server/utils/tables"
-	"fmt"
+	"context"
+	"database/sql"
 	"github.com/gin-contrib/cors"
 	"github.com/gin-gonic/gin"
+	"github.com/redis/go-redis/v9"
+	"github.com/wechatpay-apiv3/wechatpay-go/core"
+	"github.com/wechatpay-apiv3/wechatpay-go/core/auth/verifiers"
+	"github.com/wechatpay-apiv3/wechatpay-go/core/downloader"
+	"github.com/wechatpay-apiv3/wechatpay-go/core/notify"
+	"github.com/wechatpay-apiv3/wechatpay-go/core/option"
+	"github.com/wechatpay-apiv3/wechatpay-go/services/payments/native"
+	wx "github.com/wechatpay-apiv3/wechatpay-go/utils"
+	"log"
 	"os"
 )
 
@@ -13,8 +23,8 @@ type routeFn func(*gin.Engine)
 type createTableFn func() error
 
 type App struct {
-	router *gin.Engine
-	config *utils.Config
+	router  *gin.Engine
+	address string
 }
 
 func lostHandler(ctx *gin.Context) {
@@ -23,7 +33,7 @@ func lostHandler(ctx *gin.Context) {
 
 func loggerFormat(timeFmt string) gin.HandlerFunc {
 	return gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
-		return fmt.Sprintf("[wine] %s - %s [%s %s %s] %s, %s %d %s: %s\n",
+		return utils.Format("[wine] %s - %s [%s %s %s] %s, %s %d %s: %s\n",
 			param.ClientIP,
 			param.TimeStamp.Format(timeFmt),
 			param.MethodColor(), param.Method, param.ResetColor(),
@@ -35,62 +45,114 @@ func loggerFormat(timeFmt string) gin.HandlerFunc {
 }
 
 func CreateApp(config *utils.Config) *App {
-	if config.WxMerchantAcc == "" {
-		fmt.Println("wx-pay-api config is null")
-		os.Exit(-1)
-	}
-	utils.WxMerchantAcc, utils.WxApiV3Key = config.WxMerchantAcc, config.WxApiV3Key
-	utils.WxApiCertSeq, utils.WxApiCertPath = config.WxApiCertSeq, config.WxApiCertPath
-
 	if config.Release {
 		gin.SetMode(gin.ReleaseMode)
 	} else {
 		gin.SetMode(gin.DebugMode)
 	}
 
-	router := gin.New()
-	err := router.SetTrustedProxies([]string{"127.0.0.1"})
+	utils.Logger = log.New(os.Stderr, "[wine] ", log.Ldate|log.Ltime|log.Lshortfile|log.Lmsgprefix)
+
+	initWxPay(config)
+	initDatabase(config)
+	router := createRouter(config)
+
+	return &App{router: router, address: config.ServerAddr}
+}
+
+func (app *App) RouteRegister(routeFns ...routeFn) {
+	for _, fn := range routeFns {
+		fn(app.router)
+	}
+}
+
+func (app *App) Start() {
+	err := app.router.Run(app.address)
 	if err != nil {
-		fmt.Printf("can't trust '127.0.0.1', for: %s", err)
-		os.Exit(-1)
+		utils.Logger.Printf("server can't run on address[%s] for: %s", app.address, err)
 	}
-	if err = utils.GenRsaKey(); err != nil {
-		fmt.Println("can't generate private and public key.")
-		os.Exit(-1)
+}
+
+func initWxPay(config *utils.Config) {
+	utils.WxTitle, utils.WxV3Key = config.WxPayTitle, config.WxApiV3Key
+	utils.WxAppId, utils.WxMchId = config.WxAppId, config.WxMerchantAcc
+	utils.WxCertSeq, utils.WxCertPath = config.WxApiCertSeq, config.WxApiCertPath
+	var err error
+	utils.WxPrivateKey, err = wx.LoadPrivateKeyWithPath(config.WxApiCertPath)
+	if err != nil {
+		utils.Logger.Fatalf("load merchant private key error: %s\n", err)
+	}
+	opts := []core.ClientOption{
+		option.WithWechatPayAutoAuthCipher(config.WxMerchantAcc, config.WxApiCertSeq, utils.WxPrivateKey, config.WxApiV3Key),
+	}
+	utils.WxPayCli = context.Background()
+	client, err := core.NewClient(utils.WxPayCli, opts...)
+	if err != nil {
+		utils.Logger.Fatalf("new wechat pay client error: %s\n", err)
 	}
+	utils.WxPaySrv = native.NativeApiService{Client: client}
 
-	conf := cors.DefaultConfig()
-	allow := append(conf.AllowHeaders, "Token", "Device")
-	conf.AllowAllOrigins, conf.AllowHeaders = true, allow
-	router.Use(loggerFormat(config.TimeFormat), gin.Recovery(), utils.ErrorHandler, cors.New(conf))
-	router.NoRoute(lostHandler)
-	router.Static("/static", "./static")
+	err = downloader.MgrInstance().RegisterDownloaderWithPrivateKey(
+		utils.WxPayCli, utils.WxPrivateKey, config.WxApiCertSeq, config.WxMerchantAcc, config.WxApiV3Key,
+	)
+	visitor := downloader.MgrInstance().GetCertificateVisitor(config.WxMerchantAcc)
+	utils.WxCrtHdr, err = notify.NewRSANotifyHandler(config.WxApiV3Key, verifiers.NewSHA256WithRSAVerifier(visitor))
+	if err != nil {
+		utils.Logger.Fatalf("wxpay notice handler init error: %s\n", err)
+	}
+}
+
+func initDatabase(config *utils.Config) {
+	// init
+	utils.Redis = redis.NewClient(&redis.Options{
+		Addr:     utils.Format("%s:%d", config.RedisHost, config.RedisPort),
+		Password: config.RedisPass, DB: config.RedisDatabase,
+	})
+	db, err := sql.Open(
+		"mysql",
+		utils.Format(
+			"%s:%s@tcp(%s:%d)/%s?loc=Local&charset=utf8&parseTime=true",
+			config.MysqlUser, config.MysqlPass, config.MysqlHost, config.MysqlPort, config.MysqlDatabase,
+		),
+	)
+	if err != nil {
+		utils.Logger.Fatalf("mysql init failed: %s\n", err)
+	}
+	err = db.Ping()
+	if err != nil {
+		utils.Logger.Fatalf("mysql connection failed: %s\n", err)
+	}
+	utils.Mysql = db
 
-	utils.SqlInit(config)
+	// create
 	toCreate := []createTableFn{
 		tables.CreateAdvertiseTable, tables.CreateDeviceTable, tables.CreateManagerTable,
-		tables.CreateParamsTable, tables.CreateVersionTable, tables.CreateWineTable,
+		tables.CreateParamsTable, tables.CreateVersionTable, tables.CreateWineTable, tables.CreateTradeTable,
 	}
 	for _, Fn := range toCreate {
 		err = Fn()
 		if err != nil {
-			fmt.Println("create mysql tables failed.")
-			os.Exit(-1)
+			utils.Logger.Fatalf("create mysql tables failed: %s\n", err)
 		}
 	}
-
-	return &App{router: router, config: config}
 }
 
-func (app *App) RouteRegister(routeFns ...routeFn) {
-	for _, fn := range routeFns {
-		fn(app.router)
-	}
-}
-
-func (app *App) Start() {
-	err := app.router.Run(app.config.ServerAddr)
+func createRouter(config *utils.Config) *gin.Engine {
+	router := gin.New()
+	err := router.SetTrustedProxies([]string{"127.0.0.1"})
 	if err != nil {
-		fmt.Printf("server can't run on address[%s] for: %s", app.config.ServerAddr, err)
+		utils.Logger.Fatalf("can't trust '127.0.0.1', for: %s", err)
 	}
+	if err = utils.LoadRsaKeyPairs(config); err != nil {
+		utils.Logger.Fatalf("can't generate private and public key.")
+	}
+
+	conf := cors.DefaultConfig()
+	allow := append(conf.AllowHeaders, "Token", "Device")
+	conf.AllowAllOrigins, conf.AllowHeaders = true, allow
+	router.Use(loggerFormat(config.TimeFormat), gin.Recovery(), utils.ErrorHandler, cors.New(conf))
+	router.NoRoute(lostHandler)
+	router.Static("/static", "./static")
+
+	return router
 }

+ 0 - 9
handlers/test/router.go

@@ -1,9 +0,0 @@
-package test
-
-import "github.com/gin-gonic/gin"
-
-func Router(route *gin.Engine) {
-	test := route.Group("test")
-	test.GET("/ws", WsHandler)
-	test.GET("/send", SendHandler)
-}

+ 0 - 194
handlers/test/worker.go

@@ -1,194 +0,0 @@
-package test
-
-import (
-	"Wine-Server/utils"
-	"Wine-Server/utils/tables"
-	"database/sql"
-	"fmt"
-	"github.com/gin-gonic/gin"
-	"github.com/gorilla/websocket"
-	"sync"
-)
-
-var upgrader = websocket.Upgrader{
-	ReadBufferSize:  1024,
-	WriteBufferSize: 1024,
-	CheckOrigin:     utils.CheckOrigin,
-}
-
-var (
-	devices    = make(map[string]*websocket.Conn)
-	deviceLock sync.Mutex
-	workers    = make(map[string]*websocket.Conn)
-	workerLock sync.Mutex
-)
-
-func deviceAdd(seq string, conn *websocket.Conn) *tables.DeviceTable {
-	device := tables.DeviceTable{Id: seq}
-	err := device.Get()
-	if err == sql.ErrNoRows { // insert new device
-		err = device.Insert()
-		if err != nil {
-			_ = conn.WriteJSON(utils.WsError("New device join failed"))
-			return nil
-		}
-		_ = device.Get()
-	} else { // update last time
-		device.Last = utils.TimeNow()
-		err = device.Update(utils.JsonType{"last": device.Last})
-		if err != nil {
-			_ = conn.WriteJSON(utils.WsError("Update last active time failed"))
-			return nil
-		}
-	}
-
-	deviceLock.Lock()
-	devices[seq] = conn
-	deviceLock.Unlock()
-	fmt.Printf("device connected: %s\n", seq)
-	return &device
-}
-
-func deviceDel(seq string) {
-	deviceLock.Lock()
-	delete(devices, seq)
-	deviceLock.Unlock()
-	fmt.Printf("device[%s] disconnected\n", seq)
-}
-
-type Message struct {
-	Event string `json:"event"`
-	Data  any    `json:"data"`
-}
-
-func WsHandler(c *gin.Context) {
-	conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
-	if err != nil {
-		c.JSON(utils.HttpError, utils.Fail("can't establish socket connect"))
-		return
-	}
-	seq := c.DefaultQuery("seq", "")
-	if seq == "" {
-		_ = conn.WriteJSON(utils.WsError("device seq is required"))
-		return
-	}
-
-	device := deviceAdd(seq, conn)
-	if device == nil {
-		return
-	}
-	for {
-		var msg Message
-		err = conn.ReadJSON(&msg)
-		if err != nil {
-			deviceDel(seq)
-			return
-		}
-		switch msg.Event {
-		case "getInfo":
-			infoOfDevice(device, conn, msg.Data)
-			break
-		case "setLocation":
-			updateLocation(device, conn, msg.Data)
-			break
-		case "getQrcode":
-			break
-		case "openResult":
-			break
-		default:
-			_ = conn.WriteJSON(utils.WsError("unrecognized event"))
-			break
-		}
-	}
-}
-
-func infoOfDevice(device *tables.DeviceTable, conn *websocket.Conn, data any) {
-	var err error
-	for i := 0; i < 4; i++ {
-		err = device.Wines[i].Get()
-		if err != nil {
-			_ = conn.WriteJSON(utils.WsError("Query wine info failed"))
-			return
-		}
-	}
-	var advList []tables.AdvertiseTable
-	advList, err = tables.AdvListAll()
-	if err != nil {
-		_ = conn.WriteJSON(utils.WsError("Query advertise failed"))
-		return
-	}
-
-	var runParams []tables.ParamsTable
-	runParams, err = tables.ParamsListAll()
-	if err != nil {
-		_ = conn.WriteJSON(utils.WsError("Query running params failed"))
-		return
-	}
-
-	_ = conn.WriteJSON(utils.WsEvent("infoResult", utils.JsonType{
-		"wines":     device.Wines,
-		"position":  device.Addr,
-		"params":    runParams,
-		"advertise": advList,
-	}))
-}
-
-func updateLocation(device *tables.DeviceTable, conn *websocket.Conn, data any) {
-	if data == "" {
-		_ = conn.WriteJSON(utils.WsError("Blank address is not allowed"))
-		return
-	}
-	if device.Addr != data {
-		err := device.Update(utils.JsonType{"addr": data})
-		if err != nil {
-			_ = conn.WriteJSON(utils.WsError("Update location failed"))
-			return
-		}
-		_ = conn.WriteJSON(utils.WsEvent("locationResult", data))
-		return
-	}
-	_ = conn.WriteJSON(utils.WsError("same location"))
-}
-
-type openParam struct {
-	Seq  string `json:"seq"`
-	Uid  string `json:"uid"`
-	Type string `json:"type"`
-}
-
-func SendHandler(c *gin.Context) {
-	conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
-	if err != nil {
-		c.JSON(utils.HttpError, utils.Fail("can't establish socket connect"))
-		return
-	}
-	var param openParam
-	err = c.ShouldBind(&param)
-	if err != nil {
-		_ = conn.WriteJSON(utils.WsError("param error"))
-		return
-	}
-	if param.Seq == "" || param.Uid == "" || param.Type == "" {
-		_ = conn.WriteJSON(utils.WsError("seq, uid, type is required"))
-		return
-	}
-	if device, ok := devices[param.Seq]; ok {
-		_ = device.WriteJSON(utils.WsEvent("openGate", param.Type))
-	} else {
-		_ = conn.WriteJSON(utils.WsError("no such device or device offline"))
-	}
-
-	workerLock.Lock()
-	workers[param.Uid] = conn
-	workerLock.Unlock()
-	for {
-		var msg Message
-		err = conn.ReadJSON(&msg)
-		if err != nil {
-			workerLock.Lock()
-			delete(workers, param.Uid)
-			workerLock.Unlock()
-			return
-		}
-	}
-}

+ 5 - 6
handlers/worker/spliter.go

@@ -2,23 +2,22 @@ package worker
 
 import (
 	"Wine-Server/utils"
-	"fmt"
 	"github.com/gin-gonic/gin"
 	"github.com/gorilla/websocket"
 )
 
-func workerConnected(wid string, conn *websocket.Conn)  {
+func workerConnected(wid string, conn *websocket.Conn) {
 	utils.WorkerLock.Lock()
 	utils.WorkerWss[wid] = conn
 	utils.WorkerLock.Unlock()
-	fmt.Printf("worker[%s] connected\n", wid)
+	utils.Logger.Printf("worker[%s] connected\n", wid)
 }
 
-func workerDisconnect(wid string)  {
+func workerDisconnect(wid string) {
 	utils.WorkerLock.Lock()
 	delete(utils.WorkerWss, wid)
 	utils.WorkerLock.Unlock()
-	fmt.Printf("worker[%s] disconnected\n", wid)
+	utils.Logger.Printf("worker[%s] disconnected\n", wid)
 }
 
 func SocketHandler(ctx *gin.Context) {
@@ -29,7 +28,7 @@ func SocketHandler(ctx *gin.Context) {
 	}
 	wid := ctx.Param("wid")
 	if wid == "" {
-		_ = conn.WriteJSON(utils.WsError("device seq is required"))
+		_ = conn.WriteJSON(utils.WsError("worker id is required"))
 		return
 	}
 

+ 10 - 6
handlers/worker/worker.go

@@ -17,14 +17,18 @@ func openGate(conn *websocket.Conn, wid string, data any) {
 		_ = conn.WriteJSON(utils.WsError("params error"))
 		return
 	}
-	if device, exist := utils.SellerWss[param.Seq]; exist {
-		_ = device.WriteJSON(utils.WsEvent("openGate", utils.JsonType{
-			"kind":   param.Kind,
-			"worker": wid,
-		}))
+	if device, exist := utils.SellerDevices[param.Seq]; exist {
+		if device.Online {
+			_ = device.Conn.WriteJSON(utils.WsEvent("openGate", utils.JsonType{
+				"kind":   param.Kind,
+				"worker": wid,
+			}))
+			return
+		}
+		_ = conn.WriteJSON(utils.WsError("device offline"))
 		return
 	}
-	_ = conn.WriteJSON(utils.WsError("no such device or device offline"))
+	_ = conn.WriteJSON(utils.WsError("no such device"))
 }
 
 func orderFinished(conn *websocket.Conn, obj any) {


+ 2 - 3
main.go

@@ -2,6 +2,7 @@ package main
 
 import (
 	"Wine-Server/handlers"
+	"Wine-Server/handlers/debugger"
 	"Wine-Server/handlers/manager"
 	"Wine-Server/handlers/seller"
 	"Wine-Server/handlers/worker"
@@ -10,9 +11,6 @@ import (
 )
 
 func main() {
-	/*
-	TODO: 微信支付、管理端、(员工端、用户端)小程序
-	*/
 	args, filepath := os.Args, "config.json"
 	if len(args) == 2 {
 		filepath = args[1]
@@ -23,6 +21,7 @@ func main() {
 		seller.Router,
 		manager.Router,
 		worker.Router,
+		debugger.Router,
 	)
 	app.Start()
 }

BIN
static/files/23112701.apk


BIN
static/files/23112702.apk


BIN
static/files/RE.apk


BIN
static/files/wine-client-231026.apk


BIN
static/files/wine-client-231101.apk


BIN
static/files/wine-client-23112703.apk


BIN
static/images/wine/10101.png


BIN
static/images/wine/10102.png


BIN
static/images/wine/10103.png


BIN
static/images/wine/10104.png


BIN
static/images/wine/10105.png


+ 27 - 0
utils/cert/client-private.pem

@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpAIBAAKCAQEAqJl/Zna5pSI5uM+tOwbG3sfnawXIak5jVYNI8i42fGspdGXk
+ZImFpOSkHXNswmq6bbsSpoZDGbrWrFLMG6ZMqGrRGIBlqCb4PvmYI50pL7jkgDJe
+9mxMUEarBiLXk7PTpsY8v3/EkmWSxxrkMCtzvkGRu82BeauFmxoyOB/ZANjFvHKR
+vAfsI9v+3WD/1ux/2rM34p0p5fX2r9qR9moiWzkZfsUv64rZr8UnTntKMumRYUxe
+aeU/Hxyf7fNX9G/QxOYUH1o8fYDdg4+7fhL7I+cy/mkC6KM+s61d5ypU2Jwe9Z7k
+NTG51SEvVcY3NYRJEUef/xIs7QJIhZGixUAKPwIDAQABAoIBAQCPs71d5SXYdTKD
+ric1n1IUAojhtF0dFtEdyrd7DB6Or3v25P3lHz4UzHiBTadOkk9yRbfgzaEVpiVh
+XXTwwC6ogdrUqmN7DjQa4Pxmpoa7UTkZd1VvbeEhrwoIQdxC3Uwx4tdWVZ2DNiYD
+pI0H7ZFKgXKogsGvpZ1MFOjm42Y/jCFuyNLylMwmUeU7tg9WLveQmBCNs9IR4/cc
+tR7RkZyOR9X+NLvLLsdn7dDUzbdOxnS4QFvURErHUl7PBoLwC76ywrLU+eRHr3L0
+9J5qruy2xZ4c41sgtOCb1a43xsBUTL2b2Tnq8Q/iX+welZH40+qNbbfcBB4nCfWu
+0fZRJkzpAoGBAMWx39DTYylDuTWti2o5IV+zfJKjCPIl7Sp89gBvKVuOq2IYkUt1
+183MlqrfxcT6s/fpxOnyNSDtCpoZAm4v2oNnPF1n+A/Tj/uVaxAbdZxhDMxab6II
+BsSAI8vsw3IgpAs+sinjYeGK1KEBSkjgPctfDdpZK6zl4Katkt4LKfmtAoGBANpS
+6N5l9009R6hszgW289sHwMoKnv5N0BoL7WlDiHsph/H4KGFIJ+QeJk1Ouy82nZbe
+pUHzgWT2hMVBh0fOzywqHv5maIZ5PlrLhVdEnh2IfM2zMlS1lhiugZmnCt0dCXOS
+47yyhr9R+fWfinFUsZ7nHdCMLSHNI1MYeX7A7ikbAoGAI0KjgdKCKDT9DrbiW9LO
+wgv4VwsfKFldYKujENbC0KK0rK1nFMdy8zDfWqDI3GY+vSQDzBo4IaRFtNBrH44H
+clJycNJ/aws37g5rZxcOthKVPsmOiZ7W3itgtxmGjo+F0r+e9Vup9JlXbVQWlMLy
+IpjUlrI62P6zP+dLf7EMDo0CgYEApgdtHkwu0GdsyjHMT149fnEb9Mo09THZsyUk
+6+s2rGr9/k547to/s+QVvq7D/PisyWISDOCQo2YxvAzEQyCMPordvRvGG5WyjLu/
+cIk3MUBtoMTI3tClD6oN1vrIcTS2zlSDqimd/Xrq+xbT4rHEzhH2g3VGkTD+zx+K
+rZEszMcCgYAA7svEh1i5XMFCj1U6C/6/VsXGu0X4WsD+v8/6sXNayjEe8HJRLkLZ
+I2AkGodcYKsgZzGRAsSXRENmTCyTlZ3s1hqF4Ay29I9krScNyqKrvOc0K9ZZlnML
+98zlYhfPsUWRTT1WBWXcgINbXeP80PRThx7JWLXrOvxx6Tf7qvxBPw==
+-----END RSA PRIVATE KEY-----

+ 9 - 0
utils/cert/client-public.pem

@@ -0,0 +1,9 @@
+-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqJl/Zna5pSI5uM+tOwbG
+3sfnawXIak5jVYNI8i42fGspdGXkZImFpOSkHXNswmq6bbsSpoZDGbrWrFLMG6ZM
+qGrRGIBlqCb4PvmYI50pL7jkgDJe9mxMUEarBiLXk7PTpsY8v3/EkmWSxxrkMCtz
+vkGRu82BeauFmxoyOB/ZANjFvHKRvAfsI9v+3WD/1ux/2rM34p0p5fX2r9qR9moi
+WzkZfsUv64rZr8UnTntKMumRYUxeaeU/Hxyf7fNX9G/QxOYUH1o8fYDdg4+7fhL7
+I+cy/mkC6KM+s61d5ypU2Jwe9Z7kNTG51SEvVcY3NYRJEUef/xIs7QJIhZGixUAK
+PwIDAQAB
+-----END PUBLIC KEY-----

+ 27 - 0
utils/cert/server-private.pem

@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpAIBAAKCAQEA3cD06TNsxnbW8Vm8Ga045ZUkRWlkriFNDMG15ezHCcvPNUNh
+lFU6SaoycXz29sTmt2dlc3kNqySX1KxIT4I1UP58lQZKi6MrH7morgA/mdBorIyj
+jKLdmsigizrLNCHtYa7dKZ0ByzxkYS2zENQe55OkBKIv+6k/ssfr4nTubM646PMy
+bpN0QkTwmpH43xppahrTSeOlOjSy2gkru1Orxe7ssqn89hzyyT6OCRjSjOFDAVYB
+eyCiEg/MX8r/NsQca1uPXoDLXUV8DVsp8NqTq0oDjZA1qs4tmTbnxQ130v95eDZz
+fiXWXcKUF8hziOFPzXhnxdZVwjn7F48thNdvbwIDAQABAoIBAQDGnO4+eCqyzvnR
+DTgmHznYlu33pV/hfj4OcOlEWAAlS//4irM5MeSU2fV4JB7mApFKLzd0ZzXQ1WOP
+vga9/7dVJbnbl9jonGohbh4gSmSslTdLZGOPsMeevJbeXPhnZwO/++hmPxO+PgcB
+6vZ42iQb8eZzp/gmBP7iV6F0IJrzo7/p9C1+DAcdA6URCkR8KseS4Fulo8zrpxz0
+OecHrwXTF8nYInIIMcG0iBcCYhe7W4OxFkMM9Phsd3XbLdlE1RWpA2WriL4kCTIv
+bOQ+RP+3KGEsyZXzGdM9J6kNcZmYldaDzRUHawMQYeqmvTtDNQCnzsEJDvewLxdy
+1991oVHxAoGBAOARC0gaXWXoe4seubQvNJgE7l1AdQhW+yTpRKwRFycVRMuV309t
+CcMBXLA/lkKZsL0IorqhULIGlsyD9CqZ4MTCTKBqoAqzvduZZEiu289uLXETnLj+
+2iv3nQIsVAPEfKk5HBZTQ6E9P/ii45d3qMLM7WXIyW+xXwaOmOPpmvlbAoGBAP1b
+h56/G9/aynXReAilv3CINnR1Q3EyytgqAqaLIZ6zTyg4w4z/T5dX11lQkytu1fya
+6jqHeH8FBvMqIDzzJPpaqeEChXsGtSho34xhtsLt/QViDD9OG4btDD/adsOxftk9
+t8TDibSuL/S6zmYeQFc3BurkTeezXyOp4B0ChGp9AoGAMr5paaje4gdmgzkwIUhX
+ht4HBYNlfcAFHHTfooA0WBuO+vQ0II63GTyjux6LbwXTatwbzlxeJBMt758qmsNC
+jgzawbGkEQhnxOXWOkevbCitjeA9LAA7dJ2dJzLWzAuhl8lhPQUaWde/NxXcqR7m
+T0eNs/Fm/S6UMK32nhb3i+UCgYEAwNhWloKo2O9Ug/F5CDwBR8qmiGZpe1RKCdeQ
+ROxKHt4lNlxenIgJMQ1voVAvdTPcNXZFK1/KXASbkaNS/pGkZ9tyyw58Q3SLl8Kb
+xeLqld1IZjyHTfhKA52TYVcMZ1BHWVwc8OxVeO3NGt7GE/yaxMfhwBEfK2ng1OiM
+6gfsepECgYBuYg8MJBoXJ3d8CssckK9ckDpWOilqvbSTaqIWOlTZbgQFFDLGZBoy
+3SaaNEc7AtQ4VeF15Ug2a3PB2Q6D+lGw63DhPdLm7I3pUAd352hLacCvyQHei1o5
+N7foQtnDbXJ/Ap1Oaprf82ZHV3558sEKl2uRvyvp6EgqOa7NbqNodw==
+-----END RSA PRIVATE KEY-----

+ 9 - 0
utils/cert/server-public.pem

@@ -0,0 +1,9 @@
+-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3cD06TNsxnbW8Vm8Ga04
+5ZUkRWlkriFNDMG15ezHCcvPNUNhlFU6SaoycXz29sTmt2dlc3kNqySX1KxIT4I1
+UP58lQZKi6MrH7morgA/mdBorIyjjKLdmsigizrLNCHtYa7dKZ0ByzxkYS2zENQe
+55OkBKIv+6k/ssfr4nTubM646PMybpN0QkTwmpH43xppahrTSeOlOjSy2gkru1Or
+xe7ssqn89hzyyT6OCRjSjOFDAVYBeyCiEg/MX8r/NsQca1uPXoDLXUV8DVsp8NqT
+q0oDjZA1qs4tmTbnxQ130v95eDZzfiXWXcKUF8hziOFPzXhnxdZVwjn7F48thNdv
+bwIDAQAB
+-----END PUBLIC KEY-----

+ 41 - 13
utils/com.go

@@ -1,11 +1,15 @@
 package utils
 
 import (
+	"context"
 	"crypto/rsa"
 	"database/sql"
 	_ "github.com/go-sql-driver/mysql"
 	"github.com/gorilla/websocket"
 	"github.com/redis/go-redis/v9"
+	"github.com/wechatpay-apiv3/wechatpay-go/core/notify"
+	"github.com/wechatpay-apiv3/wechatpay-go/services/payments/native"
+	"log"
 	"sync"
 )
 
@@ -54,19 +58,28 @@ type Config struct {
 	RedisPass     string `json:"redis_pass"`
 	RedisDatabase int    `json:"redis_database"`
 
+	WxPayTitle    string `json:"wx_pay_title"`
 	WxMerchantAcc string `json:"wx_merchant_acc"`
+	WxAppId       string `json:"wx_app_id"`
 	WxApiCertSeq  string `json:"wx_api_cert_seq"`
 	WxApiCertPath string `json:"wx_api_cert_path"`
 	WxApiV3Key    string `json:"wx_api_v3_key"`
+
+	ServerPrivate string `json:"server_private"`
+	ServerPublic  string `json:"server_public"`
+	ClientPrivate string `json:"client_private"`
+	ClientPublic  string `json:"client_public"`
 }
 
+var Logger *log.Logger
+
 // database
 var (
-	Redis        *redis.Client
-	Mysql        *sql.DB
+	Redis *redis.Client
+	Mysql *sql.DB
 )
 
-// de-encrypt
+// de-encrypt, SerPri->CliPub, CliPri->SerPub
 var (
 	ClientPri string
 	ClientPub string
@@ -76,12 +89,25 @@ var (
 
 // wechat pay
 var (
-	WxMerchantAcc string
-	WxApiCertSeq  string
-	WxApiCertPath string
-	WxApiV3Key    string
+	WxAppId string
+	WxMchId string
+	WxV3Key string
+	WxTitle string
+
+	WxCertSeq    string
+	WxCertPath   string
+	WxPrivateKey *rsa.PrivateKey
+
+	WxPaySrv native.NativeApiService
+	WxPayCli context.Context
+	WxCrtHdr *notify.Handler
 )
 
+type SellerDevice struct {
+	Online bool
+	Conn   *websocket.Conn
+}
+
 // websocket
 var (
 	UpgradeHttp2Ws = websocket.Upgrader{
@@ -89,10 +115,12 @@ var (
 		WriteBufferSize: 1024,
 		CheckOrigin:     CheckOrigin,
 	}
-	SellerWss   = make(map[string]*websocket.Conn)
-	SellerLock  sync.Mutex
-	ManagerWss  = make(map[string]*websocket.Conn)
-	ManagerLock sync.Mutex
-	WorkerWss   = make(map[string]*websocket.Conn)
-	WorkerLock  sync.Mutex
+	SellerDevices = make(map[string]*SellerDevice)
+	SellerLock    sync.Mutex
+	ManagerWss    = make(map[string]*websocket.Conn)
+	ManagerLock   sync.Mutex
+	WorkerWss     = make(map[string]*websocket.Conn)
+	WorkerLock    sync.Mutex
+	DebugWss      = make(map[string]*websocket.Conn)
+	DebugLock     sync.Mutex
 )

+ 31 - 2
utils/errors_here.go

@@ -3,6 +3,7 @@ package utils
 import (
 	"crypto/rsa"
 	"crypto/x509"
+	"fmt"
 	"github.com/gin-gonic/gin"
 	"math/rand"
 	"net/http"
@@ -15,18 +16,46 @@ func TimeNow() TimeType {
 	return time.Now()
 }
 
-func Sleep(sec int)  {
+func TimeString() string {
+	return time.Now().Format("20060102150405")
+}
+
+func Sleep(sec int) {
 	time.Sleep(time.Duration(sec) * time.Second)
 }
 
+func Duration(sec int) time.Duration {
+	return time.Duration(sec) * time.Second
+}
+
 func RandInt(n int) int {
 	return rand.Intn(n)
 }
 
+func MarshalPrivateKey(key *rsa.PrivateKey) []byte {
+	return x509.MarshalPKCS1PrivateKey(key)
+}
+
+func ParsePrivateKey(key []byte) (*rsa.PrivateKey, error) {
+	return x509.ParsePKCS1PrivateKey(key)
+}
+
 func MarshalPublicKey(key *rsa.PublicKey) ([]byte, error) {
 	return x509.MarshalPKIXPublicKey(key)
 }
 
+func ParsePublicKey(key []byte) (*rsa.PublicKey, error) {
+	pubAny, err := x509.ParsePKIXPublicKey(key)
+	if err != nil {
+		return nil, err
+	}
+	public, ok := pubAny.(*rsa.PublicKey)
+	if !ok {
+		return nil, fmt.Errorf("invalid public key type")
+	}
+	return public, nil
+}
+
 func ErrorHandler(ctx *gin.Context) {
 	defer func() {
 		if recover() != nil {
@@ -38,4 +67,4 @@ func ErrorHandler(ctx *gin.Context) {
 
 func CheckOrigin(r *http.Request) bool {
 	return true
-}
+}

+ 165 - 40
utils/lib.go

@@ -3,13 +3,16 @@ package utils
 import (
 	"crypto/rand"
 	"crypto/rsa"
-	"database/sql"
+	"crypto/sha512"
 	"encoding/base64"
+	"encoding/hex"
 	"encoding/json"
 	"encoding/pem"
 	"fmt"
 	_ "github.com/go-sql-driver/mysql"
-	"github.com/redis/go-redis/v9"
+	"github.com/skip2/go-qrcode"
+	"github.com/wechatpay-apiv3/wechatpay-go/core"
+	"github.com/wechatpay-apiv3/wechatpay-go/services/payments/native"
 	"os"
 )
 
@@ -26,7 +29,7 @@ func WsEvent(event string, data any) WsMsg {
 }
 
 func WsError(msg string) WsMsg {
-	return WsMsg{Event: "_ERROR_", Data: msg}
+	return WsMsg{Event: "__Error_Event__", Data: msg}
 }
 
 func Success(data any) Response {
@@ -41,7 +44,9 @@ func ReadConfig(filepath string) Config {
 	config := Config{
 		Release: false, ServerAddr: "127.0.0.1:3080", TimeFormat: "15:04:05",
 		MysqlHost: "127.0.0.1", MysqlPort: 3306, MysqlUser: "wine", MysqlPass: "Wine-Mysql.1000", MysqlDatabase: "wine",
-		RedisHost: "127.0.0.1", RedisPort: 6379, RedisPass: "Wine-Redis.1000", RedisDatabase: 0,
+		RedisHost: "127.0.0.1", RedisPort: 6379, RedisPass: "Wine-Redis.1000", RedisDatabase: 0, WxPayTitle: "贵州醴泉古酿酒业",
+		ServerPrivate: "./server-private.pem", ServerPublic: "./server-public.pem",
+		ClientPrivate: "./client-private.pem", ClientPublic: "./client-public.pem",
 	}
 
 	file, err := os.Open(filepath)
@@ -64,67 +69,98 @@ func ReadConfig(filepath string) Config {
 	return conf
 }
 
-func SqlInit(config *Config) {
-	Redis = redis.NewClient(&redis.Options{
-		Addr:     fmt.Sprintf("%s:%d", config.RedisHost, config.RedisPort),
-		Password: config.RedisPass, DB: config.RedisDatabase,
-	})
-
-	db, err := sql.Open(
-		"mysql",
-		fmt.Sprintf(
-			"%s:%s@tcp(%s:%d)/%s?loc=Local&charset=utf8&parseTime=true",
-			config.MysqlUser, config.MysqlPass, config.MysqlHost, config.MysqlPort, config.MysqlDatabase,
-		),
-	)
+func loadPrivate(filename string) (*rsa.PrivateKey, error) {
+	data, err := os.ReadFile(filename)
 	if err != nil {
-		fmt.Println("mysql init failed.")
-		os.Exit(-1)
+		return nil, err
+	}
+	block, _ := pem.Decode(data)
+	if block == nil {
+		return nil, fmt.Errorf("failed to decode[%s] PEM block", filename)
 	}
-	err = db.Ping()
+	return ParsePrivateKey(block.Bytes)
+}
+
+func loadPublic(filename string) (*rsa.PublicKey, error) {
+	keyData, err := os.ReadFile(filename)
 	if err != nil {
-		fmt.Println("mysql connection failed.")
-		os.Exit(-1)
+		return nil, err
 	}
+	block, _ := pem.Decode(keyData)
+	if block == nil {
+		return nil, fmt.Errorf("failed to decode[%s] PEM block", filename)
+	}
+	return ParsePublicKey(block.Bytes)
+}
 
-	Mysql = db
+func privateKeyToPEM(pri *rsa.PrivateKey) string {
+	private := MarshalPrivateKey(pri)
+	block := &pem.Block{
+		Type:  "RSA PRIVATE KEY",
+		Bytes: private,
+	}
+	return string(pem.EncodeToMemory(block))
 }
 
-func GenRsaKey() error {
-	var err error
-	var bytes []byte
+func publicKeyToPEM(pub *rsa.PublicKey) (string, error) {
+	public, err := MarshalPublicKey(pub)
+	if err != nil {
+		return "", err
+	}
+	block := &pem.Block{
+		Type:  "PUBLIC KEY",
+		Bytes: public,
+	}
+	return string(pem.EncodeToMemory(block)), nil
+}
 
-	ServerPri, err = rsa.GenerateKey(rand.Reader, 2048)
+func LoadRsaKeyPairs(conf *Config) error {
+	pri1, err := loadPrivate(conf.ServerPrivate)
 	if err != nil {
 		return err
 	}
-	bytes, err = MarshalPublicKey(&ServerPri.PublicKey)
+	pub1, err := loadPublic(conf.ServerPublic)
 	if err != nil {
 		return err
 	}
-	publicBlock := &pem.Block{
-		Type:  "PUBLIC KEY",
-		Bytes: bytes,
+	ClientPub, err = publicKeyToPEM(pub1)
+	if err != nil {
+		return err
 	}
-	ClientPub = string(pem.EncodeToMemory(publicBlock))
+	ServerPri = pri1
 
+	pri2, err := loadPrivate(conf.ServerPrivate)
+	if err != nil {
+		return err
+	}
+	pub2, err := loadPublic(conf.ServerPublic)
+	if err != nil {
+		return err
+	}
+	ClientPri = privateKeyToPEM(pri2)
+	ServerPub = pub2
 	return nil
 }
 
-func Encrypt(data string) (string, error) {
-	return "", nil
+func Encrypt(text string) (string, error) {
+	plain := []byte(text)
+	ciphertext, err := rsa.EncryptPKCS1v15(rand.Reader, ServerPub, plain)
+	if err != nil {
+		return "", err
+	}
+	return base64.StdEncoding.EncodeToString(ciphertext), nil
 }
 
-func Decrypt(data string) (string, error) {
-	bytes, err := base64.StdEncoding.DecodeString(data)
+func Decrypt(cipher string) (string, error) {
+	ciphertext, err := base64.StdEncoding.DecodeString(cipher)
 	if err != nil {
 		return "", err
 	}
-	bytes, err = rsa.DecryptPKCS1v15(rand.Reader, ServerPri, bytes)
+	plain, err := rsa.DecryptPKCS1v15(rand.Reader, ServerPri, ciphertext)
 	if err != nil {
 		return "", err
 	}
-	return string(bytes), nil
+	return string(plain), nil
 }
 
 func UnZip(list JsonType) ([]string, []any) {
@@ -140,11 +176,35 @@ func UnZip(list JsonType) ([]string, []any) {
 func SqlFields(names []string) string {
 	var res string
 	for _, name := range names {
-		res += fmt.Sprintf("`%s`=?, ", name)
+		res += fmt.Sprintf("`%s`=?,", name)
+	}
+	ll := len(res)
+	if ll > 0 {
+		return res[:ll-1]
+	}
+	return res
+}
+
+func SqlStringListJoin(values []string) string {
+	var res string
+	for _, item := range values {
+		res += fmt.Sprintf("'%s',", item)
+	}
+	ll := len(res)
+	if ll > 0 {
+		return res[:ll-1]
+	}
+	return res
+}
+
+func SqlUint16ListJoin(values []uint16) string {
+	var res string
+	for _, item := range values {
+		res += fmt.Sprintf("%d,", item)
 	}
 	ll := len(res)
 	if ll > 0 {
-		return res[:ll-2]
+		return res[:ll-1]
 	}
 	return res
 }
@@ -160,3 +220,68 @@ func AnyTrans(data any, aim any) error {
 	}
 	return nil
 }
+
+func TryWxPay(tradeNo string, name string, cash int) ([]byte, error) {
+	resp, _, err := WxPaySrv.Prepay(WxPayCli,
+		native.PrepayRequest{
+			Appid:       core.String(WxAppId),
+			Mchid:       core.String(WxMchId),
+			Description: core.String(WxTitle + "-" + name),
+			OutTradeNo:  core.String(tradeNo),
+			NotifyUrl:   core.String("https://wine.ifarmcloud.com/api/seller/wxpay"),
+			Amount:      &native.Amount{Total: core.Int64(int64(1))}, // cash
+		},
+	)
+	if err != nil {
+		fmt.Printf("wxpay error: %s\n", err)
+		return nil, err
+	}
+	return qrcode.Encode(*resp.CodeUrl, qrcode.Medium, 512)
+}
+
+func Format(str string, v ...any) string {
+	return fmt.Sprintf(str, v...)
+}
+
+func HashPassword(password string) string {
+	hash := sha512.New()
+	hash.Write([]byte(password))
+	hashedPassword := hex.EncodeToString(hash.Sum(nil))
+	return hashedPassword
+}
+
+func IsPasswordMatch(pwdInDb, pwdFromUser string) bool {
+	return pwdInDb == HashPassword(pwdFromUser)
+}
+
+func Query(SQL string) ([]JsonType, error) {
+	rows, err := Mysql.Query(SQL)
+	if err != nil {
+		return nil, err
+	}
+	columns, _ := rows.Columns()
+	count := len(columns)
+	var values = make([]interface{}, count)
+	for i := range values {
+		var ii interface{}
+		values[i] = &ii
+	}
+	ret := make([]JsonType, 0)
+	for rows.Next() {
+		err = rows.Scan(values...)
+		m := make(JsonType)
+		if err != nil {
+			return nil, err
+		}
+		for i, colName := range columns {
+			m[colName] = *(values[i].(*interface{}))
+		}
+		ret = append(ret, m)
+	}
+
+	defer func() {
+		_ = rows.Close()
+	}()
+
+	return ret, nil
+}

+ 18 - 6
utils/tables/advertise.go

@@ -2,7 +2,6 @@ package tables
 
 import (
 	"Wine-Server/utils"
-	"fmt"
 )
 
 type AdvertiseTable struct {
@@ -30,12 +29,12 @@ func CreateAdvertiseTable() error {
 }
 
 func (row *AdvertiseTable) Insert() error {
-	sql := "INSERT INTO `advertise` (`id`, `order`, `type`, `src`, `duration`) VALUES (?, ?, ?, ?, ?);"
+	sql := "INSERT INTO `advertise`(`order`,`type`,`src`,`duration`) VALUES(?,?,?,?);"
 	pre, err := utils.Mysql.Prepare(sql)
 	if err != nil {
 		return err
 	}
-	_, err = pre.Exec(row.Id, row.Order, row.Type, row.Src, row.Duration)
+	_, err = pre.Exec(row.Order, row.Type, row.Src, row.Duration)
 	if err != nil {
 		return err
 	}
@@ -56,7 +55,7 @@ func (row *AdvertiseTable) Delete() error {
 
 func (row *AdvertiseTable) Update(args utils.JsonType) error {
 	keys, values := utils.UnZip(args)
-	sql := fmt.Sprintf("UPDATE `advertise` SET %s WHERE `id`=%d;", utils.SqlFields(keys), row.Id)
+	sql := utils.Format("UPDATE `advertise` SET %s WHERE `id`=%d;", utils.SqlFields(keys), row.Id)
 	pre, err := utils.Mysql.Prepare(sql)
 	if err != nil {
 		return err
@@ -68,8 +67,21 @@ func (row *AdvertiseTable) Update(args utils.JsonType) error {
 	return nil
 }
 
+func (row *AdvertiseTable) UpdateSelf() error {
+	sql := "UPDATE `advertise` SET `order`=?,`src`=?,`type`=?,`duration`=? WHERE `id`=?;"
+	pre, err := utils.Mysql.Prepare(sql)
+	if err != nil {
+		return err
+	}
+	_, err = pre.Exec(row.Order, row.Src, row.Type, row.Duration, row.Id)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
 func (row *AdvertiseTable) Get() error {
-	sql := "SELECT `order`, `time`, `src`, `type`, `duration` FROM `advertise` WHERE `id`=?;"
+	sql := "SELECT `order`,`time`,`src`,`type`,`duration` FROM `advertise` WHERE `id`=?;"
 	pre, err := utils.Mysql.Prepare(sql)
 	if err != nil {
 		return err
@@ -83,7 +95,7 @@ func (row *AdvertiseTable) Get() error {
 
 func AdvListAll() ([]AdvertiseTable, error) {
 	var res []AdvertiseTable
-	query, err := utils.Mysql.Query("SELECT `id`, `order`, `time`, `src`, `type`, `duration` FROM `advertise` ORDER BY `order`;")
+	query, err := utils.Mysql.Query("SELECT `id`,`order`,`time`,`src`,`type`,`duration` FROM `advertise` ORDER BY `order`;")
 	if err != nil {
 		return nil, err
 	}

+ 0 - 75
utils/tables/create-tables.txt

@@ -1,75 +0,0 @@
-# test
-CREATE TABLE IF NOT EXISTS `test` (
-    `id`   INT AUTO_INCREMENT PRIMARY KEY,
-    `name` VARCHAR(12) NOT NULL,
-    `time` DATETIME DEFAULT CURRENT_TIMESTAMP
-);
-
-# device
-CREATE TABLE IF NOT EXISTS `device` (
-    `id`           VARCHAR(32) PRIMARY KEY,     -- 板卡SN号为device-id,最大长度不确定
-    `addr`         VARCHAR(128) NOT NULL,
-    `first`        DATETIME          DEFAULT CURRENT_TIMESTAMP,
-    `last`         DATETIME          DEFAULT CURRENT_TIMESTAMP,
-    `wine1`        SMALLINT UNSIGNED,
-    `remain1`      SMALLINT UNSIGNED DEFAULT 0, -- 单位:ml, max=65535ml=65.535L
-    `wine2`        SMALLINT UNSIGNED,
-    `remain2`      SMALLINT UNSIGNED DEFAULT 0,
-    `wine3`        SMALLINT UNSIGNED,
-    `remain3`      SMALLINT UNSIGNED DEFAULT 0,
-    `wine4`        SMALLINT UNSIGNED,
-    `remain4`      SMALLINT UNSIGNED DEFAULT 0,
-    `manager`      VARCHAR(16),
-    `mark`         VARCHAR(256),
-    `order`        INT UNSIGNED      DEFAULT 0, -- max=42,9496,7295
-    `income`       DOUBLE            DEFAULT 0
-);
-
-# wine
-CREATE TABLE IF NOT EXISTS `wine` (
-    `id`       SMALLINT UNSIGNED PRIMARY KEY,
-    `name`     VARCHAR(16)  NOT NULL,
-    `price`    SMALLINT UNSIGNED DEFAULT 9999, -- 单位为:分
-    `density`  DOUBLE            DEFAULT 1,    -- 单位:g/ml
-    `picture`  VARCHAR(128) NOT NULL,
-    `time`     DATETIME          DEFAULT CURRENT_TIMESTAMP,
-    `describe` VARCHAR(256),
-    `order`    INT UNSIGNED      DEFAULT 0,
-    `income`   DOUBLE            DEFAULT 0
-);
-# data
-10100,开发用酒-1,2888,http://localhost:3080/static/images/wine/10100.png,2023-09-22 11:42:53,kai-fa-yong-jiu-1,0,0
-10101,开发用酒-2,2689,http://localhost:3080/static/images/wine/10101.png,2023-09-22 11:42:53,kai-fa-yong-jiu-2,0,0
-10103,开发用酒-3,2566,http://localhost:3080/static/images/wine/10103.png,2023-09-22 11:42:53,kai-fa-yong-jiu-3,0,0
-10105,开发用酒-4,2199,http://localhost:3080/static/images/wine/10105.png,2023-09-22 11:42:53,kai-fa-yong-jiu-4,0,0
-
-
-# manager
-CREATE TABLE IF NOT EXISTS `manager` (
-    `id`           VARCHAR(16) PRIMARY KEY,
-    `super`        BOOL         DEFAULT FALSE,
-    `first`        DATETIME     DEFAULT CURRENT_TIMESTAMP,
-    `last`         DATETIME     DEFAULT CURRENT_TIMESTAMP,
-    `name`         VARCHAR(16)  NOT NULL,
-    `account`      VARCHAR(32)  NOT NULL,
-    `password`     VARCHAR(512) NOT NULL,
-    `order`        INT UNSIGNED DEFAULT 0,
-    `income`       DOUBLE       DEFAULT 0
-);
-
-# advertise
-CREATE TABLE IF NOT EXISTS `advertise` (
-    `id`       INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
-    `order`    TINYINT UNSIGNED NOT NULL,
-    `time`     DATETIME          DEFAULT CURRENT_TIMESTAMP,
-    `type`     BOOL              DEFAULT TRUE,
-    `src`      VARCHAR(128)     NOT NULL,
-    `duration` SMALLINT UNSIGNED DEFAULT 3000
-);
-
-# params
-CREATE TABLE IF NOT EXISTS `params` (
-    `id`  INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
-    `key` VARCHAR(32) NOT NULL,
-    `value`  INT UNSIGNED DEFAULT 0
-);

+ 140 - 41
utils/tables/device.go

@@ -2,33 +2,36 @@ package tables
 
 import (
 	"Wine-Server/utils"
-	"fmt"
+	"database/sql"
 )
 
 type deviceWine struct {
-	Id       uint16  `json:"id"`
-	Name     string  `json:"name"`
-	Price    uint16  `json:"price"`
-	Density  float64 `json:"density"`
-	Picture  string  `json:"picture"`
-	Describe string  `json:"describe"`
-	Remain   uint16  `json:"remain"`
+	Id       uint16 `json:"id"`
+	Name     string `json:"name"`
+	Price    uint16 `json:"price"`
+	Degree   uint16 `json:"degree"`
+	Density  uint16 `json:"density"`
+	Picture  string `json:"picture"`
+	Describe string `json:"describe"`
+	Remain   uint16 `json:"remain"`
+	Ppv      uint16 `json:"ppv"`
 }
 
-func (dv *deviceWine) Get() error {
-	if dv.Id == 0 {
+func (dw *deviceWine) Get() error {
+	if dw.Id == 0 {
 		return nil
 	}
-	src := &WineTable{Id: dv.Id}
+	src := &WineTable{Id: dw.Id}
 	err := src.Get()
 	if err != nil {
 		return err
 	}
-	dv.Name = src.Name
-	dv.Price = src.Price
-	dv.Density = src.Density
-	dv.Picture = src.Picture
-	dv.Describe = src.Describe
+	dw.Name = src.Name
+	dw.Price = src.Price
+	dw.Degree = src.Degree
+	dw.Density = src.Density
+	dw.Picture = src.Picture
+	dw.Describe = src.Describe
 	return nil
 }
 
@@ -37,27 +40,27 @@ type DeviceTable struct {
 	Addr  string
 	First utils.TimeType
 	Last  utils.TimeType
-	Wines [4]deviceWine // db col-name: wine1, remain1, ...
+	Wines [4]deviceWine
 
-	Manager     string
-	Mark        string
-	OrderCount  uint32
-	TotalIncome float64
+	Manager string
+	Mark    string
+	Order   uint32
+	Income  uint64
 }
 
 func CreateDeviceTable() error {
-	sql := "CREATE TABLE IF NOT EXISTS `device`(" +
+	SQL := "CREATE TABLE IF NOT EXISTS `device`(" +
 		"`id` VARCHAR(32) PRIMARY KEY," +
-		"`addr` VARCHAR(128) NOT NULL," +
+		"`addr` VARCHAR(128) DEFAULT ''," +
 		"`first` DATETIME DEFAULT CURRENT_TIMESTAMP," +
 		"`last` DATETIME DEFAULT CURRENT_TIMESTAMP," +
-		"`wine1` SMALLINT UNSIGNED,`remain1` SMALLINT UNSIGNED DEFAULT 0," +
-		"`wine2` SMALLINT UNSIGNED,`remain2` SMALLINT UNSIGNED DEFAULT 0," +
-		"`wine3` SMALLINT UNSIGNED,`remain3` SMALLINT UNSIGNED DEFAULT 0," +
-		"`wine4` SMALLINT UNSIGNED,`remain4` SMALLINT UNSIGNED DEFAULT 0," +
-		"`manager` VARCHAR(16),`mark` VARCHAR(256)," +
-		"`order` INT UNSIGNED DEFAULT 0,`income` DOUBLE DEFAULT 0);"
-	_, err := utils.Mysql.Exec(sql)
+		"`wine1` SMALLINT UNSIGNED DEFAULT 10100,`remain1` SMALLINT UNSIGNED DEFAULT 0,`ppv1` SMALLINT UNSIGNED DEFAULT 2," +
+		"`wine2` SMALLINT UNSIGNED DEFAULT 10100,`remain2` SMALLINT UNSIGNED DEFAULT 0,`ppv2` SMALLINT UNSIGNED DEFAULT 2," +
+		"`wine3` SMALLINT UNSIGNED DEFAULT 10100,`remain3` SMALLINT UNSIGNED DEFAULT 0,`ppv3` SMALLINT UNSIGNED DEFAULT 2," +
+		"`wine4` SMALLINT UNSIGNED DEFAULT 10100,`remain4` SMALLINT UNSIGNED DEFAULT 0,`ppv4` SMALLINT UNSIGNED DEFAULT 2," +
+		"`manager` VARCHAR(16) DEFAULT '',`mark` VARCHAR(256) DEFAULT ''," +
+		"`order` INT UNSIGNED DEFAULT 0,`income` BIGINT UNSIGNED DEFAULT 0);"
+	_, err := utils.Mysql.Exec(SQL)
 	if err != nil {
 		return err
 	}
@@ -65,11 +68,11 @@ func CreateDeviceTable() error {
 }
 
 func (row *DeviceTable) Insert() error {
-	pre, err := utils.Mysql.Prepare("INSERT INTO `device` (`id`, `addr`) VALUES (?, ?);")
+	pre, err := utils.Mysql.Prepare("INSERT INTO `device`(`id`) VALUES(?);")
 	if err != nil {
 		return err
 	}
-	_, err = pre.Exec(row.Id, row.Addr)
+	_, err = pre.Exec(row.Id)
 	if err != nil {
 		return err
 	}
@@ -90,8 +93,8 @@ func (row *DeviceTable) Delete() error {
 
 func (row *DeviceTable) Update(args utils.JsonType) error {
 	keys, values := utils.UnZip(args)
-	sql := fmt.Sprintf("UPDATE `device` SET %s WHERE `id`='%s';", utils.SqlFields(keys), row.Id)
-	pre, err := utils.Mysql.Prepare(sql)
+	SQL := utils.Format("UPDATE `device` SET %s WHERE `id`='%s';", utils.SqlFields(keys), row.Id)
+	pre, err := utils.Mysql.Prepare(SQL)
 	if err != nil {
 		return err
 	}
@@ -102,22 +105,118 @@ func (row *DeviceTable) Update(args utils.JsonType) error {
 	return nil
 }
 
+func (row *DeviceTable) UpdateSelf() error {
+	SQL := "UPDATE `device` SET `addr`=?,`last`=?,`wine1`=?,`remain1`=?,`ppv1`=?,`wine2`=?,`remain2`=?,`ppv2`=?," +
+		"`wine3`=?,`remain3`=?,`ppv3`=?,`wine4`=?,`remain4`=?,`ppv4`=?,`manager`=?,`mark`=?,`order`=?," +
+		"`income`=? WHERE `id`=?;"
+	pre, err := utils.Mysql.Prepare(SQL)
+	if err != nil {
+		return err
+	}
+	row.Last = utils.TimeNow()
+	_, err = pre.Exec(
+		row.Addr, row.Last,
+		row.Wines[0].Id, row.Wines[0].Remain, row.Wines[0].Ppv,
+		row.Wines[1].Id, row.Wines[1].Remain, row.Wines[1].Ppv,
+		row.Wines[2].Id, row.Wines[2].Remain, row.Wines[2].Ppv,
+		row.Wines[3].Id, row.Wines[3].Remain, row.Wines[3].Ppv,
+		row.Manager, row.Mark, row.Order, row.Income, row.Id,
+	)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
 func (row *DeviceTable) Get() error {
-	sql := "SELECT `addr`, `first`, `last`, IFNULL(`wine1`, 10100), `remain1`, IFNULL(`wine2`, 10100), `remain2`, " +
-		"IFNULL(`wine3`, 10100), `remain3`, IFNULL(`wine4`, 10100), `remain4`, IFNULL(`manager`, '-'), " +
-		"IFNULL(`mark`, '-'), `order`, `income` " +
+	SQL := "SELECT `addr`,`first`,`last`,`wine1`,`remain1`,`ppv1`,`wine2`,`remain2`,`ppv2`," +
+		"`wine3`,`remain3`,`ppv3`,`wine4`,`remain4`,`ppv4`,`manager`,`mark`,`order`,`income` " +
 		"FROM `device` WHERE `id`=?;"
-	pre, err := utils.Mysql.Prepare(sql)
+	pre, err := utils.Mysql.Prepare(SQL)
 	if err != nil {
 		return err
 	}
 	err = pre.QueryRow(row.Id).Scan(
-		&row.Addr, &row.First, &row.Last, &row.Wines[0].Id, &row.Wines[0].Remain,
-		&row.Wines[1].Id, &row.Wines[1].Remain, &row.Wines[2].Id, &row.Wines[2].Remain,
-		&row.Wines[3].Id, &row.Wines[3].Remain, &row.Manager, &row.Mark, &row.OrderCount, &row.TotalIncome,
+		&row.Addr, &row.First, &row.Last,
+		&row.Wines[0].Id, &row.Wines[0].Remain, &row.Wines[0].Ppv,
+		&row.Wines[1].Id, &row.Wines[1].Remain, &row.Wines[1].Ppv,
+		&row.Wines[2].Id, &row.Wines[2].Remain, &row.Wines[2].Ppv,
+		&row.Wines[3].Id, &row.Wines[3].Remain, &row.Wines[3].Ppv,
+		&row.Manager, &row.Mark, &row.Order, &row.Income,
 	)
 	if err != nil {
 		return err
 	}
 	return nil
 }
+
+func QueryDevices(manager, cond string, limit, page int) (int, []utils.JsonType, error) {
+	SQL, like := "none", utils.Format("%%%s%%", cond)
+	if manager == "" { // not assigned
+		SQL = "SELECT COUNT(`id`) AS `total` FROM `device` WHERE `manager`='' AND (`id` LIKE ? OR `addr` LIKE ? OR `mark` LIKE ?);"
+	} else if manager == "*" { // all
+		SQL = "SELECT COUNT(`id`) AS `total` FROM `device` WHERE `id` LIKE ? OR `addr` LIKE ? OR `mark` LIKE ?;"
+	} else { // specific one
+		SQL = "SELECT COUNT(`id`) AS `total` FROM `device` WHERE `manager`=? AND (`id` LIKE ? OR `addr` LIKE ? OR `mark` LIKE ?);"
+	}
+	pre, err := utils.Mysql.Prepare(SQL)
+	if err != nil {
+		return 0, nil, err
+	}
+	total := 0
+	if manager == "" || manager == "*" {
+		err = pre.QueryRow(like, like, like).Scan(&total)
+	} else {
+		err = pre.QueryRow(manager, like, like, like).Scan(&total)
+	}
+	if err != nil {
+		return 0, nil, err
+	}
+	if total == 0 {
+		return 0, []utils.JsonType{}, err
+	}
+	if manager == "" { // not assigned
+		SQL = "SELECT d.`id`,d.`addr`,d.`first`,d.`last`,d.`mark`,m.`id`,m.`name` FROM `device` AS d LEFT JOIN `manager` AS m " +
+			"ON d.`manager`=m.`id` WHERE d.`manager`='' AND (d.`id` LIKE ? OR d.`addr` LIKE ? OR d.`mark` LIKE ?) LIMIT ? OFFSET ?;"
+	} else if manager == "*" { // all
+		SQL = "SELECT d.`id`,d.`addr`,d.`first`,d.`last`,d.`mark`,m.`id`,m.`name` FROM `device` AS d LEFT JOIN `manager` AS m " +
+			"ON d.`manager`=m.`id` WHERE d.`id` LIKE ? OR d.`addr` LIKE ? OR d.`mark` LIKE ? LIMIT ? OFFSET ?;"
+	} else { // specific one
+		SQL = "SELECT d.`id`,d.`addr`,d.`first`,d.`last`,d.`mark`,m.`id`,m.`name` FROM `device` AS d LEFT JOIN `manager` AS m " +
+			"ON d.`manager`=m.`id` WHERE d.`manager`=? AND (d.`id` LIKE ? OR d.`addr` LIKE ? OR d.`mark` LIKE ?) LIMIT ? OFFSET ?;"
+	}
+	pre, err = utils.Mysql.Prepare(SQL)
+	if err != nil {
+		return 0, nil, err
+	}
+	var rows *sql.Rows
+	if manager == "" || manager == "*" {
+		rows, err = pre.Query(like, like, like, limit, (page-1)*limit)
+	} else {
+		rows, err = pre.Query(manager, like, like, like, limit, (page-1)*limit)
+	}
+	if err != nil {
+		return 0, nil, err
+	}
+	res := make([]utils.JsonType, 0)
+	for rows.Next() {
+		tId, tAddr, tMark, tMid, tManager := "", "", "", "", ""
+		tFirst, tLast := utils.TimeNow(), utils.TimeNow()
+		_ = rows.Scan(&tId, &tAddr, &tFirst, &tLast, &tMark, &tMid, &tManager)
+		res = append(res, utils.JsonType{
+			"id": tId, "addr": tAddr, "first": tFirst, "last": tLast, "mark": tMark,
+			"manager": utils.JsonType{"id": tMid, "name": tManager},
+		})
+	}
+	err = rows.Close()
+	if err != nil {
+		return 0, nil, err
+	}
+	return total, res, nil
+}
+
+func BatchAssignDeviceManager(ids []string, manager string) error {
+	SQL := utils.Format("UPDATE `device` SET `manager`='%s' WHERE `id` IN (%s);", manager, utils.SqlStringListJoin(ids))
+	_, err := utils.Mysql.Exec(SQL)
+	return err
+}

+ 65 - 9
utils/tables/manager.go

@@ -2,7 +2,6 @@ package tables
 
 import (
 	"Wine-Server/utils"
-	"fmt"
 )
 
 type ManagerTable struct {
@@ -15,8 +14,8 @@ type ManagerTable struct {
 	Account  string
 	Password string
 
-	OrderCount  uint32
-	TotalIncome float64
+	Order  uint32
+	Income uint64
 }
 
 func CreateManagerTable() error {
@@ -27,9 +26,9 @@ func CreateManagerTable() error {
 		"`last` DATETIME DEFAULT CURRENT_TIMESTAMP," +
 		"`name` VARCHAR(16) NOT NULL," +
 		"`account` VARCHAR(32) NOT NULL," +
-		"`password` VARCHAR(512) NOT NULL," +
+		"`password` CHAR(128) NOT NULL," +
 		"`order` INT UNSIGNED DEFAULT 0," +
-		"`income` DOUBLE DEFAULT 0);"
+		"`income` BIGINT UNSIGNED DEFAULT 0);"
 	_, err := utils.Mysql.Exec(sql)
 	if err != nil {
 		return err
@@ -38,7 +37,7 @@ func CreateManagerTable() error {
 }
 
 func (row *ManagerTable) Insert() error {
-	sql := "INSERT INTO `manager` (`id`, `name`, `account`, `password`) VALUES (?, ?, ?, ?);"
+	sql := "INSERT INTO `manager`(`id`,`name`,`account`,`password`) VALUES(?,?,?,?);"
 	pre, err := utils.Mysql.Prepare(sql)
 	if err != nil {
 		return err
@@ -64,7 +63,7 @@ func (row *ManagerTable) Delete() error {
 
 func (row *ManagerTable) Update(args utils.JsonType) error {
 	keys, values := utils.UnZip(args)
-	sql := fmt.Sprintf("UPDATE `manager` SET %s WHERE `id`='%s';", utils.SqlFields(keys), row.Id)
+	sql := utils.Format("UPDATE `manager` SET %s WHERE `id`='%s';", utils.SqlFields(keys), row.Id)
 	pre, err := utils.Mysql.Prepare(sql)
 	if err != nil {
 		return err
@@ -76,15 +75,72 @@ func (row *ManagerTable) Update(args utils.JsonType) error {
 	return nil
 }
 
+func (row *ManagerTable) UpdateSelf() error {
+	sql := "UPDATE `manager` SET `super`=?,`last`=?,`name`=?,`account`=?,`password`=?,`order`=?,`income`=? WHERE `id`=?;"
+	pre, err := utils.Mysql.Prepare(sql)
+	if err != nil {
+		return err
+	}
+	row.Last = utils.TimeNow()
+	_, err = pre.Exec(row.Super, row.Last, row.Name, row.Account, row.Password, row.Order, row.Income, row.Id)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
 func (row *ManagerTable) Get() error {
-	sql := "SELECT `super`, `first`, `last`, `name`, `account`, `password`, `count`, `income` FROM `manager` WHERE `id`=?;"
+	sql := "SELECT `super`,`first`,`last`,`name`,`account`,`password`,`order`,`income` FROM `manager` WHERE `id`=?;"
+	pre, err := utils.Mysql.Prepare(sql)
+	if err != nil {
+		return err
+	}
+	err = pre.QueryRow(row.Id).Scan(
+		&row.Super, &row.First, &row.Last, &row.Name,
+		&row.Account, &row.Password, &row.Order, &row.Income,
+	)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func (row *ManagerTable) GetByAcc() error {
+	sql := "SELECT `id`,`super`,`first`,`last`,`name`,`password`,`order`,`income` FROM `manager` WHERE `account`=? LIMIT 1;"
 	pre, err := utils.Mysql.Prepare(sql)
 	if err != nil {
 		return err
 	}
-	err = pre.QueryRow(row.Id).Scan(&row.Super, &row.First, &row.Last, &row.Name, &row.Account, &row.Password, &row.OrderCount, &row.TotalIncome)
+	err = pre.QueryRow(row.Account).Scan(
+		&row.Id, &row.Super, &row.First, &row.Last,
+		&row.Name, &row.Password, &row.Order, &row.Income,
+	)
 	if err != nil {
 		return err
 	}
 	return nil
 }
+
+func QueryManager(name string) ([]utils.JsonType, error) {
+	like := utils.Format("%%%s%%", name)
+	sql := "SELECT `id`,`name` FROM `manager` WHERE `super`=0 AND `name` LIKE ? LIMIT 10;"
+	pre, err := utils.Mysql.Prepare(sql)
+	if err != nil {
+		return nil, err
+	}
+	rows, err := pre.Query(like)
+	if err != nil {
+		return nil, err
+	}
+	res := make([]utils.JsonType, 0)
+	for rows.Next() {
+		tid, tna := "", ""
+		_ = rows.Scan(&tid, &tna)
+		res = append(res, utils.JsonType{"id": tid, "name": tna})
+	}
+	err = rows.Close()
+	if err != nil {
+		return nil, err
+	}
+	return res, nil
+}

+ 17 - 5
utils/tables/params.go

@@ -2,7 +2,6 @@ package tables
 
 import (
 	"Wine-Server/utils"
-	"fmt"
 )
 
 type ParamsTable struct {
@@ -24,7 +23,7 @@ func CreateParamsTable() error {
 }
 
 func (row *ParamsTable) Insert() error {
-	sql := "INSERT INTO `params` (`key`, `value`) VALUES (?, ?);"
+	sql := "INSERT INTO `params`(`key`,`value`) VALUES(?,?);"
 	pre, err := utils.Mysql.Prepare(sql)
 	if err != nil {
 		return err
@@ -50,7 +49,7 @@ func (row *ParamsTable) Delete() error {
 
 func (row *ParamsTable) Update(args utils.JsonType) error {
 	keys, values := utils.UnZip(args)
-	sql := fmt.Sprintf("UPDATE `params` SET %s WHERE `id`=%d;", utils.SqlFields(keys), row.Id)
+	sql := utils.Format("UPDATE `params` SET %s WHERE `id`=%d;", utils.SqlFields(keys), row.Id)
 	pre, err := utils.Mysql.Prepare(sql)
 	if err != nil {
 		return err
@@ -62,8 +61,21 @@ func (row *ParamsTable) Update(args utils.JsonType) error {
 	return nil
 }
 
+func (row *ParamsTable) UpdateSelf() error {
+	sql := "UPDATE `params` SET `key`=?,`value`=? WHERE `id`=?;"
+	pre, err := utils.Mysql.Prepare(sql)
+	if err != nil {
+		return err
+	}
+	_, err = pre.Exec(row.Key, row.Value, row.Id)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
 func (row *ParamsTable) Get() error {
-	sql := "SELECT `key`, `value` FROM `params` WHERE `id`=?;"
+	sql := "SELECT `key`,`value` FROM `params` WHERE `id`=?;"
 	pre, err := utils.Mysql.Prepare(sql)
 	if err != nil {
 		return err
@@ -77,7 +89,7 @@ func (row *ParamsTable) Get() error {
 
 func ParamsListAll() ([]ParamsTable, error) {
 	var res []ParamsTable
-	query, err := utils.Mysql.Query("SELECT `id`, `key`, `value` FROM `params`;")
+	query, err := utils.Mysql.Query("SELECT `id`,`key`,`value` FROM `params`;")
 	if err != nil {
 		return nil, err
 	}

+ 105 - 0
utils/tables/trade.go

@@ -0,0 +1,105 @@
+package tables
+
+import (
+	"Wine-Server/utils"
+)
+
+type TradeTable struct {
+	Id      string         `json:"id"`
+	Trade   string         `json:"trade"`
+	Device  string         `json:"device"`
+	Time    utils.TimeType `json:"time"`
+	Payer   string         `json:"payer"`
+	Wine    uint16         `json:"wine"`
+	Weight  uint16         `json:"weight"`
+	Cash    int            `json:"cash"`
+	Manager string         `json:"manager"`
+}
+
+func CreateTradeTable() error {
+	sql := "CREATE TABLE IF NOT EXISTS `trade`(" +
+		"`id` VARCHAR(32) PRIMARY KEY NOT NULL," +
+		"`trade` VARCHAR(64) NOT NULL," +
+		"`device` VARCHAR(32) NOT NULL," +
+		"`time` DATETIME DEFAULT CURRENT_TIMESTAMP," +
+		"`payer` VARCHAR(64) NOT NULL," +
+		"`wine` SMALLINT UNSIGNED NOT NULL," +
+		"`weight` SMALLINT UNSIGNED NOT NULL," +
+		"`cash` INT UNSIGNED NOT NULL," +
+		"`manager` VARCHAR(16));"
+	_, err := utils.Mysql.Exec(sql)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func (row *TradeTable) Insert() error {
+	sql := "INSERT INTO `trade`(`id`,`trade`,`device`,`payer`,`wine`,`weight`,`cash`,`manager`) VALUES(?,?,?,?,?,?,?,?);"
+	pre, err := utils.Mysql.Prepare(sql)
+	if err != nil {
+		return err
+	}
+	_, err = pre.Exec(row.Id, row.Trade, row.Device, row.Payer, row.Wine, row.Weight, row.Cash, row.Manager)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func (row *TradeTable) Delete() error {
+	pre, err := utils.Mysql.Prepare("DELETE FROM `trade` WHERE `id`=?;")
+	if err != nil {
+		return err
+	}
+	_, err = pre.Exec(row.Id)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func (row *TradeTable) Update(args utils.JsonType) error {
+	keys, values := utils.UnZip(args)
+	sql := utils.Format("UPDATE `trade` SET %s WHERE `id`=%s;", utils.SqlFields(keys), row.Id)
+	pre, err := utils.Mysql.Prepare(sql)
+	if err != nil {
+		return err
+	}
+	_, err = pre.Exec(values...)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func (row *TradeTable) UpdateSelf() error {
+	sql := "UPDATE `trade` SET `trade`=?,`device`=?,`time`=?,`payer`=?,`wine`=?,`weight`=?,`cash`=?,`manager`=? WHERE `id`=?;"
+	pre, err := utils.Mysql.Prepare(sql)
+	if err != nil {
+		return err
+	}
+	_, err = pre.Exec(row.Trade, row.Device, row.Time, row.Payer, row.Wine, row.Weight, row.Cash, row.Manager, row.Id)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func (row *TradeTable) Get() error {
+	sql := "SELECT `trade`,`device`,`time`,`payer`,`wine`,`weight`,`cash`,`manager` FROM `trade` WHERE `id`=?;"
+	pre, err := utils.Mysql.Prepare(sql)
+	if err != nil {
+		return err
+	}
+	err = pre.QueryRow(row.Id).Scan(
+		&row.Trade, &row.Device, &row.Time, &row.Payer,
+		&row.Wine, &row.Weight, &row.Cash, &row.Manager,
+	)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+// TODO: list by: <device>, <manager>, <payer> with time-duration

+ 17 - 5
utils/tables/version.go

@@ -2,7 +2,6 @@ package tables
 
 import (
 	"Wine-Server/utils"
-	"fmt"
 )
 
 type VersionTable struct {
@@ -24,7 +23,7 @@ func CreateVersionTable() error {
 }
 
 func (row *VersionTable) Insert() error {
-	sql := "INSERT INTO `version` (`ver`, `name`, `url`) VALUES (?, ?, ?);"
+	sql := "INSERT INTO `version` (`ver`,`name`,`url`) VALUES(?,?,?);"
 	pre, err := utils.Mysql.Prepare(sql)
 	if err != nil {
 		return err
@@ -50,7 +49,7 @@ func (row *VersionTable) Delete() error {
 
 func (row *VersionTable) Update(args utils.JsonType) error {
 	keys, values := utils.UnZip(args)
-	sql := fmt.Sprintf("UPDATE `version` SET %s WHERE `ver`=%d;", utils.SqlFields(keys), row.Ver)
+	sql := utils.Format("UPDATE `version` SET %s WHERE `ver`=%d;", utils.SqlFields(keys), row.Ver)
 	pre, err := utils.Mysql.Prepare(sql)
 	if err != nil {
 		return err
@@ -62,8 +61,21 @@ func (row *VersionTable) Update(args utils.JsonType) error {
 	return nil
 }
 
+func (row *VersionTable) UpdateSelf() error {
+	sql := "UPDATE `version` SET `name`=?,`url`=? WHERE `ver`=?;"
+	pre, err := utils.Mysql.Prepare(sql)
+	if err != nil {
+		return err
+	}
+	_, err = pre.Exec(row.Name, row.Url, row.Ver)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
 func (row *VersionTable) GetLatest() error {
-	sql := "SELECT `ver`, `name`, `url` FROM `version` ORDER BY `ver` DESC LIMIT 1;"
+	sql := "SELECT `ver`,`name`,`url` FROM `version` ORDER BY `ver` DESC LIMIT 1;"
 	err := utils.Mysql.QueryRow(sql).Scan(&row.Ver, &row.Name, &row.Url)
 	if err != nil {
 		return err
@@ -73,7 +85,7 @@ func (row *VersionTable) GetLatest() error {
 
 func VersionListAll() ([]VersionTable, error) {
 	var res []VersionTable
-	query, err := utils.Mysql.Query("SELECT `ver`, `name`, `url` FROM `version`;")
+	query, err := utils.Mysql.Query("SELECT `ver`,`name`,`url` FROM `version`;")
 	if err != nil {
 		return nil, err
 	}

+ 92 - 22
utils/tables/wine.go

@@ -2,34 +2,38 @@ package tables
 
 import (
 	"Wine-Server/utils"
-	"fmt"
+	"database/sql"
 )
 
 type WineTable struct {
 	Id       uint16
 	Name     string
 	Price    uint16
-	Density  float64
+	Degree   uint16
+	Density  uint16
 	Picture  string
 	Time     utils.TimeType
 	Describe string
+	Deleted  bool
 
-	OrderCount  uint32
-	TotalIncome float64
+	Order  uint32
+	Income uint64
 }
 
 func CreateWineTable() error {
-	sql := "CREATE TABLE IF NOT EXISTS `wine`(" +
+	SQL := "CREATE TABLE IF NOT EXISTS `wine`(" +
 		"`id` SMALLINT UNSIGNED PRIMARY KEY," +
 		"`name` VARCHAR(16)  NOT NULL," +
 		"`price` SMALLINT UNSIGNED DEFAULT 9999," +
-		"`density` DOUBLE DEFAULT 1," +
+		"`degree` SMALLINT UNSIGNED DEFAULT 0," +
+		"`density` SMALLINT UNSIGNED DEFAULT 1000," +
 		"`picture` VARCHAR(128) NOT NULL," +
 		"`time` DATETIME DEFAULT CURRENT_TIMESTAMP," +
-		"`describe` VARCHAR(256)," +
+		"`describe` VARCHAR(256) DEFAULT ''," +
 		"`order` INT UNSIGNED DEFAULT 0," +
-		"`income` DOUBLE DEFAULT 0);"
-	_, err := utils.Mysql.Exec(sql)
+		"`income` BIGINT UNSIGNED DEFAULT 0," +
+		"`deleted` TINYINT(1) DEFAULT 0);"
+	_, err := utils.Mysql.Exec(SQL)
 	if err != nil {
 		return err
 	}
@@ -37,12 +41,12 @@ func CreateWineTable() error {
 }
 
 func (row *WineTable) Insert() error {
-	sql := "INSERT INTO `wine` (`name`, `price`, `picture`, `describe`) VALUES (?, ?, ?, ?, ?, ?, ?);"
-	pre, err := utils.Mysql.Prepare(sql)
+	SQL := "INSERT INTO `wine` (`name`,`price`,`degree`,`picture`,`describe`) VALUES (?,?,?,?,?);"
+	pre, err := utils.Mysql.Prepare(SQL)
 	if err != nil {
 		return err
 	}
-	_, err = pre.Exec(row.Name, row.Price, row.Picture, row.Describe)
+	_, err = pre.Exec(row.Name, row.Price, row.Degree, row.Picture, row.Describe)
 	if err != nil {
 		return err
 	}
@@ -50,25 +54,31 @@ func (row *WineTable) Insert() error {
 }
 
 func (row *WineTable) Delete() error {
-	pre, err := utils.Mysql.Prepare("DELETE FROM `wine` WHERE `id`=?;")
+	row.Deleted = true
+	return row.UpdateSelf()
+}
+
+func (row *WineTable) Update(args utils.JsonType) error {
+	keys, values := utils.UnZip(args)
+	SQL := utils.Format("UPDATE `wine` SET %s WHERE `id`=%d;", utils.SqlFields(keys), row.Id)
+	pre, err := utils.Mysql.Prepare(SQL)
 	if err != nil {
 		return err
 	}
-	_, err = pre.Exec(row.Id)
+	_, err = pre.Exec(values...)
 	if err != nil {
 		return err
 	}
 	return nil
 }
 
-func (row *WineTable) Update(args utils.JsonType) error {
-	keys, values := utils.UnZip(args)
-	sql := fmt.Sprintf("UPDATE `wine` SET %s WHERE `id`=%d;", utils.SqlFields(keys), row.Id)
-	pre, err := utils.Mysql.Prepare(sql)
+func (row *WineTable) UpdateSelf() error {
+	SQL := "UPDATE `wine` SET `name`=?,`price`=?,`degree`=?,`density`=?,`picture`=?,`describe`=?,`order`=?,`income`=?,`deleted`=? WHERE `id`=?;"
+	pre, err := utils.Mysql.Prepare(SQL)
 	if err != nil {
 		return err
 	}
-	_, err = pre.Exec(values...)
+	_, err = pre.Exec(row.Name, row.Price, row.Degree, row.Density, row.Picture, row.Describe, row.Order, row.Income, row.Deleted, row.Id)
 	if err != nil {
 		return err
 	}
@@ -76,16 +86,76 @@ func (row *WineTable) Update(args utils.JsonType) error {
 }
 
 func (row *WineTable) Get() error {
-	sql := "SELECT `name`, `price`, `density`, `picture`, IFNULL(`describe`, ''), `time`, `order`, `income` FROM `wine` WHERE `id`=?;"
-	pre, err := utils.Mysql.Prepare(sql)
+	SQL := "SELECT `name`,`price`,`degree`,`density`,`picture`,`describe`,`time`,`order`,`income`,`deleted` FROM `wine` WHERE `id`=?;"
+	pre, err := utils.Mysql.Prepare(SQL)
 	if err != nil {
 		return err
 	}
 	err = pre.QueryRow(row.Id).Scan(
-		&row.Name, &row.Price, &row.Density, &row.Picture, &row.Describe, &row.Time, &row.OrderCount, &row.TotalIncome,
+		&row.Name, &row.Price, &row.Degree, &row.Density, &row.Picture,
+		&row.Describe, &row.Time, &row.Order, &row.Income, &row.Deleted,
 	)
 	if err != nil {
 		return err
 	}
 	return nil
 }
+
+func extractJson(wine WineTable) utils.JsonType {
+	return utils.JsonType{
+		"id": wine.Id, "name": wine.Name, "time": wine.Time, "price": wine.Price,
+		"degree": wine.Degree, "density": wine.Density, "picture": wine.Picture, "describe": wine.Describe,
+	}
+}
+
+func QueryWines(cond string, limit, page int) (int, []utils.JsonType, error) {
+	SQL, like := "none", utils.Format("%%%s%%", cond)
+	if page == 1 {
+		SQL = "SELECT COUNT(`id`) AS `total` FROM `wine` " +
+			"WHERE (`id`=10100 OR `deleted`=FALSE) AND (`name` LIKE ? OR `describe` LIKE ?);"
+	} else {
+		SQL = "SELECT COUNT(`id`) AS `total` FROM `wine` WHERE `deleted`=FALSE AND (`name` LIKE ? OR `describe` LIKE ?);"
+	}
+	pre, err := utils.Mysql.Prepare(SQL)
+	if err != nil {
+		return 0, nil, err
+	}
+	total := 0
+	err = pre.QueryRow(like, like).Scan(&total)
+	if err != nil {
+		return 0, nil, err
+	}
+	if page == 1 {
+		SQL = "SELECT `id`,`name`,`time`,`price`,`degree`,`density`,`picture`,`describe` FROM `wine` " +
+			"WHERE (`id`=10100 OR `deleted`=FALSE) AND (`name` LIKE ? OR `describe` LIKE ?) LIMIT ? OFFSET ?;"
+	} else {
+		SQL = "SELECT `id`,`name`,`time`,`price`,`degree`,`density`,`picture`,`describe` FROM `wine` " +
+			"WHERE `deleted`=FALSE AND (`name` LIKE ? OR `describe` LIKE ?) LIMIT ? OFFSET ?;"
+	}
+	pre, err = utils.Mysql.Prepare(SQL)
+	if err != nil {
+		return 0, nil, err
+	}
+	var rows *sql.Rows
+	res := make([]utils.JsonType, 0)
+	rows, err = pre.Query(like, like, limit, (page-1)*limit)
+	if err != nil {
+		return 0, nil, err
+	}
+	for rows.Next() {
+		tmp := WineTable{}
+		_ = rows.Scan(&tmp.Id, &tmp.Name, &tmp.Time, &tmp.Price, &tmp.Degree, &tmp.Density, &tmp.Picture, &tmp.Describe)
+		res = append(res, extractJson(tmp))
+	}
+	err = rows.Close()
+	if err != nil {
+		return 0, nil, err
+	}
+	return total, res, nil
+}
+
+func BatchDeleteWine(ids []uint16) error {
+	SQL := utils.Format("UPDATE `wine` SET `deleted`=TRUE WHERE `id` IN (%s);", utils.SqlUint16ListJoin(ids))
+	_, err := utils.Mysql.Exec(SQL)
+	return err
+}