|
|
@@ -0,0 +1,239 @@
|
|
|
+package centralpayment
|
|
|
+
|
|
|
+import (
|
|
|
+ "net/http"
|
|
|
+ "strconv"
|
|
|
+ "strings"
|
|
|
+ "time"
|
|
|
+
|
|
|
+ "github.com/gin-gonic/gin"
|
|
|
+ "go.uber.org/zap"
|
|
|
+ "wails-app/internal/global"
|
|
|
+ "wails-app/internal/modules/central-payment/model/request"
|
|
|
+ responseModel "wails-app/internal/modules/central-payment/model/response"
|
|
|
+ centralService "wails-app/internal/modules/central-payment/service"
|
|
|
+)
|
|
|
+
|
|
|
+var svc = centralService.New()
|
|
|
+
|
|
|
+func requestFields(c *gin.Context, endpoint string, fields ...zap.Field) []zap.Field {
|
|
|
+ base := []zap.Field{
|
|
|
+ zap.String("endpoint", endpoint),
|
|
|
+ zap.String("source_ip", c.ClientIP()),
|
|
|
+ zap.String("content_type", c.ContentType()),
|
|
|
+ zap.Int64("content_length", c.Request.ContentLength),
|
|
|
+ }
|
|
|
+ return append(base, fields...)
|
|
|
+}
|
|
|
+
|
|
|
+func logInfo(c *gin.Context, message, endpoint string, fields ...zap.Field) {
|
|
|
+ if global.GVA_LOG != nil {
|
|
|
+ global.GVA_LOG.Info(message, requestFields(c, endpoint, fields...)...)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func logWarn(c *gin.Context, message, endpoint string, fields ...zap.Field) {
|
|
|
+ if global.GVA_LOG != nil {
|
|
|
+ global.GVA_LOG.Warn(message, requestFields(c, endpoint, fields...)...)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func passwordFormat(password string) string {
|
|
|
+ trimmed := strings.TrimSpace(password)
|
|
|
+ if trimmed == "" {
|
|
|
+ return "empty"
|
|
|
+ }
|
|
|
+ if len(trimmed) == 32 {
|
|
|
+ isHex := true
|
|
|
+ for _, ch := range trimmed {
|
|
|
+ if !((ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F')) {
|
|
|
+ isHex = false
|
|
|
+ break
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if isHex {
|
|
|
+ return "md5-32-hex_unsupported"
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if len(trimmed) == 60 && (strings.HasPrefix(trimmed, "$2a$") || strings.HasPrefix(trimmed, "$2b$") || strings.HasPrefix(trimmed, "$2y$")) {
|
|
|
+ return "bcrypt-hash_unsupported"
|
|
|
+ }
|
|
|
+ return "plain-text_expected"
|
|
|
+}
|
|
|
+
|
|
|
+func fail(c *gin.Context, endpoint, deviceID, remark string, body interface{}) {
|
|
|
+ svc.Log(endpoint, deviceID, c.ClientIP(), body, 0, remark)
|
|
|
+ logWarn(c, "中央缴费机请求处理失败", endpoint, zap.String("device_id", deviceID), zap.String("reason", remark))
|
|
|
+ c.JSON(http.StatusOK, responseModel.Envelope{Result: 0, Data: struct{}{}, ReMark: remark})
|
|
|
+}
|
|
|
+
|
|
|
+func Login(c *gin.Context) {
|
|
|
+ var req request.LoginRequest
|
|
|
+ if err := c.ShouldBindJSON(&req); err != nil {
|
|
|
+ logWarn(c, "中央缴费机收到无法解析的登录请求", "/api/login", zap.Error(err))
|
|
|
+ fail(c, "/api/login", "", "请求参数错误", struct{}{})
|
|
|
+ return
|
|
|
+ }
|
|
|
+ format := passwordFormat(req.Password)
|
|
|
+ logInfo(c, "中央缴费机收到登录请求", "/api/login",
|
|
|
+ zap.String("username", strings.TrimSpace(req.UserName)),
|
|
|
+ zap.String("password_format", format), zap.Int("password_length", len(req.Password)))
|
|
|
+ token, err := svc.Login(req)
|
|
|
+ if err != nil {
|
|
|
+ fail(c, "/api/login", "", err.Error(), map[string]interface{}{
|
|
|
+ "username": strings.TrimSpace(req.UserName), "password_format": format, "password_length": len(req.Password),
|
|
|
+ })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ svc.Log("/api/login", "", c.ClientIP(), map[string]interface{}{
|
|
|
+ "username": strings.TrimSpace(req.UserName), "password_format": format, "password_length": len(req.Password),
|
|
|
+ }, 1, "success")
|
|
|
+ logInfo(c, "中央缴费机登录成功", "/api/login", zap.String("username", strings.TrimSpace(req.UserName)))
|
|
|
+ c.JSON(http.StatusOK, responseModel.LoginResponse{Result: 1, Token: token, ReMark: "success"})
|
|
|
+}
|
|
|
+
|
|
|
+func QueryPrice(c *gin.Context) {
|
|
|
+ var req request.QueryPriceRequest
|
|
|
+ if err := c.ShouldBindJSON(&req); err != nil {
|
|
|
+ logWarn(c, "中央缴费机收到无法解析的费用查询请求", "/api/queryprice", zap.Error(err))
|
|
|
+ fail(c, "/api/queryprice", "", "请求参数错误", struct{}{})
|
|
|
+ return
|
|
|
+ }
|
|
|
+ logInfo(c, "中央缴费机收到费用查询请求", "/api/queryprice", zap.String("car_no", req.Data.CarNo), zap.Bool("token_present", strings.TrimSpace(req.Token) != ""))
|
|
|
+ device, err := svc.Authenticate(req.Token)
|
|
|
+ if err != nil {
|
|
|
+ fail(c, "/api/queryprice", "", err.Error(), req.Data)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ price, err := svc.QueryPrice(device, req.Data.CarNo)
|
|
|
+ if err != nil {
|
|
|
+ fail(c, "/api/queryprice", device.DeviceID, err.Error(), req.Data)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ data := responseModel.QueryPriceData{
|
|
|
+ OrderID: price.OrderID, CarNo: price.CarNo,
|
|
|
+ InTime: price.InTime.Format("2006-01-02 15:04:05"),
|
|
|
+ OutTime: price.OutTime.Format("2006-01-02 15:04:05"),
|
|
|
+ StopTime: price.StopTime, Price: price.Price,
|
|
|
+ CardNo: "", Remark: "系统实时计费", InPic: "",
|
|
|
+ }
|
|
|
+ svc.Log("/api/queryprice", device.DeviceID, c.ClientIP(), req.Data, 1, "success")
|
|
|
+ logInfo(c, "中央缴费机费用查询成功", "/api/queryprice", zap.String("device_id", device.DeviceID), zap.String("car_no", req.Data.CarNo), zap.String("order_id", price.OrderID))
|
|
|
+ c.JSON(http.StatusOK, responseModel.Envelope{Result: 1, Data: data, ReMark: "success"})
|
|
|
+}
|
|
|
+
|
|
|
+func PaySuccess(c *gin.Context) {
|
|
|
+ var req request.PaySuccessRequest
|
|
|
+ if err := c.ShouldBindJSON(&req); err != nil {
|
|
|
+ logWarn(c, "中央缴费机收到无法解析的支付通知", "/api/paysuccess", zap.Error(err))
|
|
|
+ fail(c, "/api/paysuccess", "", "请求参数错误", struct{}{})
|
|
|
+ return
|
|
|
+ }
|
|
|
+ logInfo(c, "中央缴费机收到支付通知", "/api/paysuccess",
|
|
|
+ zap.String("order_id", strings.TrimSpace(req.Data.OrderID)),
|
|
|
+ zap.String("car_no", strings.TrimSpace(req.Data.CarNo)),
|
|
|
+ zap.String("device_id", strings.TrimSpace(req.Data.DevID)),
|
|
|
+ zap.Int("pay_type", req.Data.PayType),
|
|
|
+ zap.Int("pay_dev", req.Data.PayDev),
|
|
|
+ zap.String("money_raw", strings.TrimSpace(string(req.Data.Money))),
|
|
|
+ zap.String("pay_time", req.Data.PayTime),
|
|
|
+ zap.Bool("token_present", strings.TrimSpace(req.Token) != ""))
|
|
|
+ device, err := svc.Authenticate(req.Token)
|
|
|
+ if err != nil {
|
|
|
+ fail(c, "/api/paysuccess", "", err.Error(), req.Data)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ callbackDeviceID := strings.TrimSpace(req.Data.DevID)
|
|
|
+ logInfo(c, "中央缴费机支付身份校验", "/api/paysuccess",
|
|
|
+ zap.String("token_device_id", device.DeviceID),
|
|
|
+ zap.String("callback_device_id", callbackDeviceID),
|
|
|
+ zap.Bool("device_id_match", callbackDeviceID == device.DeviceID))
|
|
|
+ if err := svc.PaySuccess(device, req); err != nil {
|
|
|
+ fail(c, "/api/paysuccess", device.DeviceID, err.Error(), req.Data)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ svc.Log("/api/paysuccess", device.DeviceID, c.ClientIP(), req.Data, 1, "success")
|
|
|
+ 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))))
|
|
|
+ c.JSON(http.StatusOK, responseModel.Envelope{Result: 1, Data: struct{}{}, ReMark: "success"})
|
|
|
+}
|
|
|
+
|
|
|
+func PayStatus(c *gin.Context) {
|
|
|
+ var req request.PayStatusRequest
|
|
|
+ if err := c.ShouldBindJSON(&req); err != nil {
|
|
|
+ logWarn(c, "中央缴费机收到无法解析的支付状态查询", "/api/paystatus", zap.Error(err))
|
|
|
+ fail(c, "/api/paystatus", "", "请求参数错误", struct{}{})
|
|
|
+ return
|
|
|
+ }
|
|
|
+ logInfo(c, "中央缴费机收到支付状态查询", "/api/paystatus",
|
|
|
+ zap.String("order_id", strings.TrimSpace(req.Data.OrderID)),
|
|
|
+ zap.String("car_no", strings.TrimSpace(req.Data.CarNo)),
|
|
|
+ zap.String("device_id", strings.TrimSpace(req.Data.DevID)),
|
|
|
+ zap.Bool("token_present", strings.TrimSpace(req.Token) != ""))
|
|
|
+ device, err := svc.Authenticate(req.Token)
|
|
|
+ if err != nil {
|
|
|
+ fail(c, "/api/paystatus", "", err.Error(), req.Data)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ callbackDeviceID := strings.TrimSpace(req.Data.DevID)
|
|
|
+ logInfo(c, "中央缴费机支付状态身份校验", "/api/paystatus",
|
|
|
+ zap.String("token_device_id", device.DeviceID),
|
|
|
+ zap.String("callback_device_id", callbackDeviceID),
|
|
|
+ zap.Bool("device_id_match", callbackDeviceID == "" || callbackDeviceID == device.DeviceID))
|
|
|
+ ticket, record, err := svc.PayStatus(device, req)
|
|
|
+ if err != nil {
|
|
|
+ fail(c, "/api/paystatus", device.DeviceID, err.Error(), req.Data)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ payStatus := "unpaid"
|
|
|
+ if ticket.State == "paid" || ticket.State == "exited" || record.PaymentStatus == "paid" {
|
|
|
+ payStatus = "paid"
|
|
|
+ }
|
|
|
+ payTime := ""
|
|
|
+ if ticket.PaidAt != nil {
|
|
|
+ payTime = ticket.PaidAt.Format("2006-01-02 15:04:05")
|
|
|
+ }
|
|
|
+ data := responseModel.PayStatusData{
|
|
|
+ OrderID: ticket.TicketNo, CarNo: ticket.PlateNumber, State: ticket.State,
|
|
|
+ PayStatus: payStatus, PaymentStatus: record.PaymentStatus,
|
|
|
+ Fee: strconv.FormatFloat(ticket.Fee, 'f', 2, 64), Money: strconv.FormatFloat(ticket.PaidAmount, 'f', 2, 64),
|
|
|
+ PaidAmount: strconv.FormatFloat(ticket.PaidAmount, 'f', 2, 64), PayTime: payTime,
|
|
|
+ }
|
|
|
+ svc.Log("/api/paystatus", device.DeviceID, c.ClientIP(), req.Data, 1, "success")
|
|
|
+ logInfo(c, "中央缴费机支付状态查询成功", "/api/paystatus",
|
|
|
+ zap.String("device_id", device.DeviceID), zap.String("order_id", ticket.TicketNo),
|
|
|
+ zap.String("state", ticket.State), zap.String("record_payment_status", record.PaymentStatus),
|
|
|
+ zap.String("pay_status", payStatus), zap.Float64("fee", ticket.Fee),
|
|
|
+ zap.Float64("paid_amount", ticket.PaidAmount), zap.String("payment_entry", ticket.PaymentEntry),
|
|
|
+ zap.String("payment_method", ticket.PaymentMethod), zap.String("pay_time", payTime))
|
|
|
+ c.JSON(http.StatusOK, responseModel.Envelope{Result: 1, Data: data, ReMark: "success"})
|
|
|
+}
|
|
|
+
|
|
|
+func Heartbeat(c *gin.Context) {
|
|
|
+ var req request.HeartbeatRequest
|
|
|
+ if err := c.ShouldBindJSON(&req); err != nil {
|
|
|
+ logWarn(c, "中央缴费机收到无法解析的心跳请求", "/api/heartbeat", zap.Error(err))
|
|
|
+ fail(c, "/api/heartbeat", "", "请求参数错误", struct{}{})
|
|
|
+ return
|
|
|
+ }
|
|
|
+ logInfo(c, "中央缴费机收到心跳请求", "/api/heartbeat", zap.Bool("token_present", strings.TrimSpace(req.Token) != ""))
|
|
|
+ device, err := svc.Authenticate(req.Token)
|
|
|
+ if err != nil {
|
|
|
+ fail(c, "/api/heartbeat", "", err.Error(), struct{}{})
|
|
|
+ return
|
|
|
+ }
|
|
|
+ data := map[string]string{"SystemTime": time.Now().Format("2006-01-02 15:04:05")}
|
|
|
+ svc.Log("/api/heartbeat", device.DeviceID, c.ClientIP(), struct{}{}, 1, "success")
|
|
|
+ logInfo(c, "中央缴费机心跳处理成功", "/api/heartbeat", zap.String("device_id", device.DeviceID))
|
|
|
+ c.JSON(http.StatusOK, responseModel.Envelope{Result: 1, Data: data, ReMark: "success"})
|
|
|
+}
|
|
|
+
|
|
|
+// SetupPublicRouter is intentionally unauthenticated at the Gin level. Each
|
|
|
+// POF endpoint applies its own isolated device-token authentication.
|
|
|
+func SetupPublicRouter(router *gin.RouterGroup) {
|
|
|
+ group := router.Group("/api")
|
|
|
+ group.POST("/login", Login)
|
|
|
+ group.POST("/queryprice", QueryPrice)
|
|
|
+ group.POST("/paysuccess", PaySuccess)
|
|
|
+ group.POST("/paystatus", PayStatus)
|
|
|
+ group.POST("/heartbeat", Heartbeat)
|
|
|
+}
|