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) }