api.go 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. // Package order 出口自助扫码支付模块。
  2. // 公开接口供出口屏 kiosk 页面轮询(可用 X-Kiosk-Token 鉴权);
  3. // 调试接口仅在 System.DebugRoutes 开启时注册,用于无真实硬件时打通全链路。
  4. package order
  5. import (
  6. "net/http"
  7. "strings"
  8. "github.com/gin-gonic/gin"
  9. "wails-app/internal/global"
  10. "wails-app/internal/model/common/response"
  11. "wails-app/internal/modules/order/service"
  12. paymentChannel "wails-app/internal/modules/payment/channel"
  13. )
  14. var kioskService = service.NewOrderService()
  15. // checkKioskToken 配置了 exit-kiosk.token 时校验 X-Kiosk-Token 请求头。
  16. func checkKioskToken(c *gin.Context) bool {
  17. token := global.GVA_CONFIG.ExitKiosk.Token
  18. if token == "" || c.GetHeader("X-Kiosk-Token") == token {
  19. return true
  20. }
  21. c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"code": 1, "msg": "出口屏令牌无效"})
  22. return false
  23. }
  24. // KioskState 出口屏主轮询:最近出场识别 + 费用 + 订单(含节流查单与放行联动)。
  25. func KioskState(c *gin.Context) {
  26. if !checkKioskToken(c) {
  27. return
  28. }
  29. state, err := kioskService.KioskState(c.Query("device_code"))
  30. if err != nil {
  31. response.FailWithMessage(err.Error(), c)
  32. return
  33. }
  34. response.OkWithData(state, c)
  35. }
  36. // OrderStatus 单订单状态查询(含节流查单与放行联动)。
  37. func OrderStatus(c *gin.Context) {
  38. if !checkKioskToken(c) {
  39. return
  40. }
  41. view, err := kioskService.GetOrderStatus(c.Param("order_no"))
  42. if err != nil {
  43. response.FailWithMessage(err.Error(), c)
  44. return
  45. }
  46. response.OkWithData(view, c)
  47. }
  48. // DebugRecognize 模拟出口 LPR 识别:走真实 HandlePassage 管线
  49. // (临时车有费用会被拒绝放行并记录通道事件,与真实相机行为一致)。
  50. func DebugRecognize(c *gin.Context) {
  51. var req struct {
  52. DeviceCode string `json:"device_code"` // 留空自动取第一个出场设备
  53. PlateNumber string `json:"plate_number" binding:"required"`
  54. Direction string `json:"direction"` // 留空时 inout 通道走方向推断
  55. }
  56. if err := c.ShouldBindJSON(&req); err != nil {
  57. response.FailWithMessage(err.Error(), c)
  58. return
  59. }
  60. deviceCode := strings.TrimSpace(req.DeviceCode)
  61. if deviceCode == "" {
  62. exitDevice, err := service.FirstExitDevice()
  63. if err != nil {
  64. response.FailWithMessage(err.Error(), c)
  65. return
  66. }
  67. deviceCode = exitDevice
  68. }
  69. result, err := service.SimulateRecognition(deviceCode, req.PlateNumber, req.Direction)
  70. if err != nil {
  71. response.FailWithMessage("识别事件已按真实管线处理(预期内拒绝): "+err.Error(), c)
  72. return
  73. }
  74. response.OkWithData(result, c)
  75. }
  76. // DebugEntry 模拟入口 LPR 识别:走真实管线建停车会话与数字票。
  77. // device_code 留空时自动取第一个绑定了进场通道的启用设备。
  78. func DebugEntry(c *gin.Context) {
  79. var req struct {
  80. DeviceCode string `json:"device_code"` // 留空自动取第一个进场设备
  81. PlateNumber string `json:"plate_number" binding:"required"`
  82. Hours int `json:"hours"` // 回拨入场时间小时数(如 26),保证出场时有费用
  83. }
  84. if err := c.ShouldBindJSON(&req); err != nil {
  85. response.FailWithMessage(err.Error(), c)
  86. return
  87. }
  88. deviceCode := strings.TrimSpace(req.DeviceCode)
  89. if deviceCode == "" {
  90. entryDevice, err := service.FirstEntryDevice()
  91. if err != nil {
  92. response.FailWithMessage(err.Error(), c)
  93. return
  94. }
  95. deviceCode = entryDevice
  96. }
  97. result, err := service.SimulateRecognition(deviceCode, req.PlateNumber, "in")
  98. if err != nil {
  99. response.FailWithMessage("模拟入场失败: "+err.Error(), c)
  100. return
  101. }
  102. // 测试支付流程时回拨入场时间,跨过免费窗口
  103. if req.Hours > 0 {
  104. if err := service.BackfillEntryTime(req.PlateNumber, req.Hours); err != nil {
  105. response.FailWithMessage("入场成功但回拨入场时间失败: "+err.Error(), c)
  106. return
  107. }
  108. }
  109. response.OkWithData(result, c)
  110. }
  111. // DebugFullFlow 一键联调:确保在场 → 回拨入场时间 → 出口识别 → mock 支付 → 查单确认开闸。
  112. // hours 为入场时间回拨小时数(默认 26),保证费用必然产生。
  113. func DebugFullFlow(c *gin.Context) {
  114. var req struct {
  115. DeviceCode string `json:"device_code"`
  116. PlateNumber string `json:"plate_number" binding:"required"`
  117. Hours int `json:"hours"`
  118. }
  119. if err := c.ShouldBindJSON(&req); err != nil {
  120. response.FailWithMessage(err.Error(), c)
  121. return
  122. }
  123. state, err := kioskService.DebugFullFlow(req.DeviceCode, req.PlateNumber, req.Hours)
  124. if err != nil {
  125. response.FailWithMessage(err.Error(), c)
  126. return
  127. }
  128. response.OkWithData(state, c)
  129. }
  130. // DebugMockPay mock 渠道模拟"车主已完成支付",随后查单即确认到账。
  131. func DebugMockPay(c *gin.Context) {
  132. var req struct {
  133. OrderNo string `json:"order_no" binding:"required"`
  134. }
  135. if err := c.ShouldBindJSON(&req); err != nil {
  136. response.FailWithMessage(err.Error(), c)
  137. return
  138. }
  139. if err := paymentChannel.MarkMockPaid(req.OrderNo); err != nil {
  140. response.FailWithMessage(err.Error(), c)
  141. return
  142. }
  143. response.OkWithMessage("mock 渠道已标记支付,等待查单确认", c)
  144. }
  145. // SetupExitKioskPublicRouter 出口屏轮询接口(公开组,可选令牌)。
  146. func SetupExitKioskPublicRouter(router *gin.RouterGroup) {
  147. group := router.Group("/exit-kiosk")
  148. group.GET("/state", KioskState)
  149. group.GET("/order/:order_no", OrderStatus)
  150. }
  151. // SetupExitKioskDebugRouter 联调端点,仅 System.DebugRoutes 开启时注册。
  152. func SetupExitKioskDebugRouter(router *gin.RouterGroup, enabled bool) {
  153. if !enabled {
  154. return
  155. }
  156. group := router.Group("/exit-kiosk/debug")
  157. group.POST("/entry", DebugEntry)
  158. group.POST("/recognize", DebugRecognize)
  159. group.POST("/mock-pay", DebugMockPay)
  160. group.POST("/full-flow", DebugFullFlow)
  161. }