api.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. package centralpayment
  2. import (
  3. "net/http"
  4. "strconv"
  5. "strings"
  6. "time"
  7. "github.com/gin-gonic/gin"
  8. "go.uber.org/zap"
  9. "wails-app/internal/global"
  10. "wails-app/internal/modules/central-payment/model/request"
  11. responseModel "wails-app/internal/modules/central-payment/model/response"
  12. centralService "wails-app/internal/modules/central-payment/service"
  13. )
  14. var svc = centralService.New()
  15. func requestFields(c *gin.Context, endpoint string, fields ...zap.Field) []zap.Field {
  16. base := []zap.Field{
  17. zap.String("endpoint", endpoint),
  18. zap.String("source_ip", c.ClientIP()),
  19. zap.String("content_type", c.ContentType()),
  20. zap.Int64("content_length", c.Request.ContentLength),
  21. }
  22. return append(base, fields...)
  23. }
  24. func logInfo(c *gin.Context, message, endpoint string, fields ...zap.Field) {
  25. if global.GVA_LOG != nil {
  26. global.GVA_LOG.Info(message, requestFields(c, endpoint, fields...)...)
  27. }
  28. }
  29. func logWarn(c *gin.Context, message, endpoint string, fields ...zap.Field) {
  30. if global.GVA_LOG != nil {
  31. global.GVA_LOG.Warn(message, requestFields(c, endpoint, fields...)...)
  32. }
  33. }
  34. func passwordFormat(password string) string {
  35. trimmed := strings.TrimSpace(password)
  36. if trimmed == "" {
  37. return "empty"
  38. }
  39. if len(trimmed) == 32 {
  40. isHex := true
  41. for _, ch := range trimmed {
  42. if !((ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F')) {
  43. isHex = false
  44. break
  45. }
  46. }
  47. if isHex {
  48. return "md5-32-hex_unsupported"
  49. }
  50. }
  51. if len(trimmed) == 60 && (strings.HasPrefix(trimmed, "$2a$") || strings.HasPrefix(trimmed, "$2b$") || strings.HasPrefix(trimmed, "$2y$")) {
  52. return "bcrypt-hash_unsupported"
  53. }
  54. return "plain-text_expected"
  55. }
  56. func fail(c *gin.Context, endpoint, deviceID, remark string, body interface{}) {
  57. svc.Log(endpoint, deviceID, c.ClientIP(), body, 0, remark)
  58. logWarn(c, "中央缴费机请求处理失败", endpoint, zap.String("device_id", deviceID), zap.String("reason", remark))
  59. c.JSON(http.StatusOK, responseModel.Envelope{Result: 0, Data: struct{}{}, ReMark: remark})
  60. }
  61. func Login(c *gin.Context) {
  62. var req request.LoginRequest
  63. if err := c.ShouldBindJSON(&req); err != nil {
  64. logWarn(c, "中央缴费机收到无法解析的登录请求", "/api/login", zap.Error(err))
  65. fail(c, "/api/login", "", "请求参数错误", struct{}{})
  66. return
  67. }
  68. format := passwordFormat(req.Password)
  69. logInfo(c, "中央缴费机收到登录请求", "/api/login",
  70. zap.String("username", strings.TrimSpace(req.UserName)),
  71. zap.String("password_format", format), zap.Int("password_length", len(req.Password)))
  72. token, err := svc.Login(req)
  73. if err != nil {
  74. fail(c, "/api/login", "", err.Error(), map[string]interface{}{
  75. "username": strings.TrimSpace(req.UserName), "password_format": format, "password_length": len(req.Password),
  76. })
  77. return
  78. }
  79. svc.Log("/api/login", "", c.ClientIP(), map[string]interface{}{
  80. "username": strings.TrimSpace(req.UserName), "password_format": format, "password_length": len(req.Password),
  81. }, 1, "success")
  82. logInfo(c, "中央缴费机登录成功", "/api/login", zap.String("username", strings.TrimSpace(req.UserName)))
  83. c.JSON(http.StatusOK, responseModel.LoginResponse{Result: 1, Token: token, ReMark: "success"})
  84. }
  85. func QueryPrice(c *gin.Context) {
  86. var req request.QueryPriceRequest
  87. if err := c.ShouldBindJSON(&req); err != nil {
  88. logWarn(c, "中央缴费机收到无法解析的费用查询请求", "/api/queryprice", zap.Error(err))
  89. fail(c, "/api/queryprice", "", "请求参数错误", struct{}{})
  90. return
  91. }
  92. logInfo(c, "中央缴费机收到费用查询请求", "/api/queryprice", zap.String("car_no", req.Data.CarNo), zap.Bool("token_present", strings.TrimSpace(req.Token) != ""))
  93. device, err := svc.Authenticate(req.Token)
  94. if err != nil {
  95. fail(c, "/api/queryprice", "", err.Error(), req.Data)
  96. return
  97. }
  98. price, err := svc.QueryPrice(device, req.Data.CarNo)
  99. if err != nil {
  100. fail(c, "/api/queryprice", device.DeviceID, err.Error(), req.Data)
  101. return
  102. }
  103. data := responseModel.QueryPriceData{
  104. OrderID: price.OrderID, CarNo: price.CarNo,
  105. InTime: price.InTime.Format("2006-01-02 15:04:05"),
  106. OutTime: price.OutTime.Format("2006-01-02 15:04:05"),
  107. StopTime: price.StopTime, Price: price.Price,
  108. CardNo: "", Remark: "系统实时计费", InPic: "",
  109. }
  110. svc.Log("/api/queryprice", device.DeviceID, c.ClientIP(), req.Data, 1, "success")
  111. logInfo(c, "中央缴费机费用查询成功", "/api/queryprice", zap.String("device_id", device.DeviceID), zap.String("car_no", req.Data.CarNo), zap.String("order_id", price.OrderID))
  112. c.JSON(http.StatusOK, responseModel.Envelope{Result: 1, Data: data, ReMark: "success"})
  113. }
  114. func PaySuccess(c *gin.Context) {
  115. var req request.PaySuccessRequest
  116. if err := c.ShouldBindJSON(&req); err != nil {
  117. logWarn(c, "中央缴费机收到无法解析的支付通知", "/api/paysuccess", zap.Error(err))
  118. fail(c, "/api/paysuccess", "", "请求参数错误", struct{}{})
  119. return
  120. }
  121. logInfo(c, "中央缴费机收到支付通知", "/api/paysuccess",
  122. zap.String("order_id", strings.TrimSpace(req.Data.OrderID)),
  123. zap.String("car_no", strings.TrimSpace(req.Data.CarNo)),
  124. zap.String("device_id", strings.TrimSpace(req.Data.DevID)),
  125. zap.Int("pay_type", req.Data.PayType),
  126. zap.Int("pay_dev", req.Data.PayDev),
  127. zap.String("money_raw", strings.TrimSpace(string(req.Data.Money))),
  128. zap.String("pay_time", req.Data.PayTime),
  129. zap.Bool("token_present", strings.TrimSpace(req.Token) != ""))
  130. device, err := svc.Authenticate(req.Token)
  131. if err != nil {
  132. fail(c, "/api/paysuccess", "", err.Error(), req.Data)
  133. return
  134. }
  135. callbackDeviceID := strings.TrimSpace(req.Data.DevID)
  136. logInfo(c, "中央缴费机支付身份校验", "/api/paysuccess",
  137. zap.String("token_device_id", device.DeviceID),
  138. zap.String("callback_device_id", callbackDeviceID),
  139. zap.Bool("device_id_match", callbackDeviceID == device.DeviceID))
  140. if err := svc.PaySuccess(device, req); err != nil {
  141. fail(c, "/api/paysuccess", device.DeviceID, err.Error(), req.Data)
  142. return
  143. }
  144. svc.Log("/api/paysuccess", device.DeviceID, c.ClientIP(), req.Data, 1, "success")
  145. logInfo(c, "中央缴费机支付通知处理成功", "/api/paysuccess", zap.String("device_id", device.DeviceID), zap.String("order_id", strings.TrimSpace(req.Data.OrderID)), zap.String("money_raw", strings.TrimSpace(string(req.Data.Money))))
  146. c.JSON(http.StatusOK, responseModel.Envelope{Result: 1, Data: struct{}{}, ReMark: "success"})
  147. }
  148. func PayStatus(c *gin.Context) {
  149. var req request.PayStatusRequest
  150. if err := c.ShouldBindJSON(&req); err != nil {
  151. logWarn(c, "中央缴费机收到无法解析的支付状态查询", "/api/paystatus", zap.Error(err))
  152. fail(c, "/api/paystatus", "", "请求参数错误", struct{}{})
  153. return
  154. }
  155. logInfo(c, "中央缴费机收到支付状态查询", "/api/paystatus",
  156. zap.String("order_id", strings.TrimSpace(req.Data.OrderID)),
  157. zap.String("car_no", strings.TrimSpace(req.Data.CarNo)),
  158. zap.String("device_id", strings.TrimSpace(req.Data.DevID)),
  159. zap.Bool("token_present", strings.TrimSpace(req.Token) != ""))
  160. device, err := svc.Authenticate(req.Token)
  161. if err != nil {
  162. fail(c, "/api/paystatus", "", err.Error(), req.Data)
  163. return
  164. }
  165. callbackDeviceID := strings.TrimSpace(req.Data.DevID)
  166. logInfo(c, "中央缴费机支付状态身份校验", "/api/paystatus",
  167. zap.String("token_device_id", device.DeviceID),
  168. zap.String("callback_device_id", callbackDeviceID),
  169. zap.Bool("device_id_match", callbackDeviceID == "" || callbackDeviceID == device.DeviceID))
  170. ticket, record, err := svc.PayStatus(device, req)
  171. if err != nil {
  172. fail(c, "/api/paystatus", device.DeviceID, err.Error(), req.Data)
  173. return
  174. }
  175. payStatus := "unpaid"
  176. if ticket.State == "paid" || ticket.State == "exited" || record.PaymentStatus == "paid" {
  177. payStatus = "paid"
  178. }
  179. payTime := ""
  180. if ticket.PaidAt != nil {
  181. payTime = ticket.PaidAt.Format("2006-01-02 15:04:05")
  182. }
  183. data := responseModel.PayStatusData{
  184. OrderID: ticket.TicketNo, CarNo: ticket.PlateNumber, State: ticket.State,
  185. PayStatus: payStatus, PaymentStatus: record.PaymentStatus,
  186. Fee: strconv.FormatFloat(ticket.Fee, 'f', 2, 64), Money: strconv.FormatFloat(ticket.PaidAmount, 'f', 2, 64),
  187. PaidAmount: strconv.FormatFloat(ticket.PaidAmount, 'f', 2, 64), PayTime: payTime,
  188. }
  189. svc.Log("/api/paystatus", device.DeviceID, c.ClientIP(), req.Data, 1, "success")
  190. logInfo(c, "中央缴费机支付状态查询成功", "/api/paystatus",
  191. zap.String("device_id", device.DeviceID), zap.String("order_id", ticket.TicketNo),
  192. zap.String("state", ticket.State), zap.String("record_payment_status", record.PaymentStatus),
  193. zap.String("pay_status", payStatus), zap.Float64("fee", ticket.Fee),
  194. zap.Float64("paid_amount", ticket.PaidAmount), zap.String("payment_entry", ticket.PaymentEntry),
  195. zap.String("payment_method", ticket.PaymentMethod), zap.String("pay_time", payTime))
  196. c.JSON(http.StatusOK, responseModel.Envelope{Result: 1, Data: data, ReMark: "success"})
  197. }
  198. func Heartbeat(c *gin.Context) {
  199. var req request.HeartbeatRequest
  200. if err := c.ShouldBindJSON(&req); err != nil {
  201. logWarn(c, "中央缴费机收到无法解析的心跳请求", "/api/heartbeat", zap.Error(err))
  202. fail(c, "/api/heartbeat", "", "请求参数错误", struct{}{})
  203. return
  204. }
  205. logInfo(c, "中央缴费机收到心跳请求", "/api/heartbeat", zap.Bool("token_present", strings.TrimSpace(req.Token) != ""))
  206. device, err := svc.Authenticate(req.Token)
  207. if err != nil {
  208. fail(c, "/api/heartbeat", "", err.Error(), struct{}{})
  209. return
  210. }
  211. data := map[string]string{"SystemTime": time.Now().Format("2006-01-02 15:04:05")}
  212. svc.Log("/api/heartbeat", device.DeviceID, c.ClientIP(), struct{}{}, 1, "success")
  213. logInfo(c, "中央缴费机心跳处理成功", "/api/heartbeat", zap.String("device_id", device.DeviceID))
  214. c.JSON(http.StatusOK, responseModel.Envelope{Result: 1, Data: data, ReMark: "success"})
  215. }
  216. // SetupPublicRouter is intentionally unauthenticated at the Gin level. Each
  217. // POF endpoint applies its own isolated device-token authentication.
  218. func SetupPublicRouter(router *gin.RouterGroup) {
  219. group := router.Group("/api")
  220. group.POST("/login", Login)
  221. group.POST("/queryprice", QueryPrice)
  222. group.POST("/paysuccess", PaySuccess)
  223. group.POST("/paystatus", PayStatus)
  224. group.POST("/heartbeat", Heartbeat)
  225. }