| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686 |
- package service
- import (
- "encoding/json"
- "errors"
- "fmt"
- "time"
- "wails-app/internal/dao"
- "wails-app/internal/global"
- "wails-app/internal/modules/incident/repository"
- "go.uber.org/zap"
- )
- // ===================== 常量 =====================
- // 异常状态
- const (
- StatusPending = "pending"
- StatusProcessing = "processing"
- StatusResolved = "resolved"
- StatusClosed = "closed"
- )
- // 异常来源
- const (
- SourcePassage = "passage"
- SourceManual = "manual"
- SourceDevice = "device"
- SourcePayment = "payment"
- SourcePrinter = "printer"
- SourceSystem = "system"
- )
- // 异常分类
- const (
- CategoryGateFailed = "gate_failed"
- CategoryDuplicateEntry = "duplicate_entry"
- CategoryBlacklist = "blacklist"
- CategoryLotFull = "lot_full"
- CategoryNoEntryExit = "no_entry_exit"
- CategoryManualRaise = "manual_raise"
- CategoryDeviceOffline = "device_offline"
- CategoryPrintFailed = "print_failed"
- CategoryPaymentFailed = "payment_failed"
- CategoryPaymentUncertain = "payment_uncertain"
- )
- // 异常等级
- const (
- LevelInfo = "info"
- LevelWarning = "warning"
- LevelCritical = "critical"
- )
- // 处置方式
- const (
- HandleManualGate = "manual_gate"
- HandleForceFree = "force_free"
- HandleReset = "reset"
- HandleDeviceRepaired = "device_repaired"
- HandleReprint = "reprint"
- HandleIgnore = "ignore"
- HandleOther = "other"
- )
- // 指令流水结果
- const (
- CommandSuccess = "success"
- CommandFailed = "failed"
- )
- // 资金相关分类(统计与权限关注)
- var fundRelatedCategories = map[string]bool{
- CategoryPaymentFailed: true,
- CategoryPaymentUncertain: true,
- CategoryGateFailed: true,
- CategoryNoEntryExit: true,
- }
- // 分类默认等级
- var categoryDefaultLevel = map[string]string{
- CategoryGateFailed: LevelCritical,
- CategoryDuplicateEntry: LevelWarning,
- CategoryBlacklist: LevelWarning,
- CategoryLotFull: LevelInfo,
- CategoryNoEntryExit: LevelCritical,
- CategoryManualRaise: LevelWarning,
- CategoryDeviceOffline: LevelWarning,
- CategoryPrintFailed: LevelWarning,
- CategoryPaymentFailed: LevelWarning,
- CategoryPaymentUncertain: LevelCritical,
- }
- // 状态机:仅允许以下流转
- var allowedTransitions = map[string]map[string]bool{
- StatusPending: {StatusProcessing: true, StatusResolved: true},
- StatusProcessing: {StatusResolved: true},
- StatusResolved: {StatusClosed: true, StatusPending: true},
- StatusClosed: {StatusPending: true},
- }
- var (
- ErrIncidentNotFound = errors.New("异常记录不存在")
- ErrIncidentChanged = errors.New("异常状态已被他人变更,请刷新后重试")
- )
- // ===================== 服务 =====================
- // logError/logWarn/logInfo 带 nil 防护的日志(测试环境与单测中 GVA_LOG 可为空)。
- func logError(msg string, fields ...zap.Field) {
- if global.GVA_LOG != nil {
- global.GVA_LOG.Error(msg, fields...)
- }
- }
- func logWarn(msg string, fields ...zap.Field) {
- if global.GVA_LOG != nil {
- global.GVA_LOG.Warn(msg, fields...)
- }
- }
- func logInfo(msg string, fields ...zap.Field) {
- if global.GVA_LOG != nil {
- global.GVA_LOG.Info(msg, fields...)
- }
- }
- type IncidentService struct {
- repo *repository.IncidentRepository
- }
- func NewIncidentService() *IncidentService {
- return &IncidentService{repo: &repository.IncidentRepository{}}
- }
- // RecordIncidentRequest 自动埋点入参。HandleType/HandleRemark 非空时创建即置为已解决
- // (用于人工抬杆等"动作已完成、仅需留痕"的场景)。
- type RecordIncidentRequest struct {
- Category string
- Source string
- Level string
- VehicleRecordID uint
- DigitalTicketID uint
- PaymentRecordID uint
- GateCommandID uint
- TicketNo string
- PlateNumber string
- RFIDTag string
- ParkingLotID uint
- ParkingLotName string
- ChannelID uint
- ChannelCode string
- ChannelName string
- DeviceCode string
- DeviceName string
- OperatorID uint
- Description string
- Detail interface{}
- HandleType string
- HandleRemark string
- }
- func enrichSessionAssociation(req *RecordIncidentRequest) {
- var record dao.VehicleRecord
- if req.VehicleRecordID != 0 && global.GVA_DB.First(&record, req.VehicleRecordID).Error == nil {
- if req.PlateNumber == "" {
- req.PlateNumber = record.PlateNumber
- }
- if req.RFIDTag == "" {
- req.RFIDTag = record.RFIDTag
- }
- if req.ParkingLotID == 0 {
- req.ParkingLotID = record.ParkingLotID
- }
- if req.ChannelID == 0 {
- if record.ExitTime != nil && record.ExitChannelID != 0 {
- req.ChannelID = record.ExitChannelID
- } else {
- req.ChannelID = record.EntryChannelID
- }
- }
- if req.ChannelCode == "" {
- if record.ExitTime != nil && record.ExitChannelCode != "" {
- req.ChannelCode = record.ExitChannelCode
- } else {
- req.ChannelCode = record.EntryChannelCode
- }
- }
- if req.ChannelName == "" {
- if record.ExitTime != nil && record.ExitChannelName != "" {
- req.ChannelName = record.ExitChannelName
- } else {
- req.ChannelName = record.EntryChannelName
- }
- }
- if req.DeviceCode == "" {
- if record.ExitTime != nil && record.ExitDeviceCode != "" {
- req.DeviceCode = record.ExitDeviceCode
- } else {
- req.DeviceCode = record.EntryDeviceCode
- }
- }
- if req.DeviceName == "" {
- if record.ExitTime != nil && record.ExitDeviceName != "" {
- req.DeviceName = record.ExitDeviceName
- } else {
- req.DeviceName = record.EntryDeviceName
- }
- }
- }
- }
- // enrichAssociations 根据稳定 ID、票号和设备编码补齐异常上下文快照。
- // 业务埋点通常只知道会话或设备,统一在异常服务中解析,避免各调用方重复查询。
- func enrichAssociations(req *RecordIncidentRequest) {
- if req == nil || global.GVA_DB == nil {
- return
- }
- enrichSessionAssociation(req)
- var ticket dao.DigitalTicket
- if req.DigitalTicketID != 0 {
- _ = global.GVA_DB.First(&ticket, req.DigitalTicketID).Error
- } else if req.TicketNo != "" {
- _ = global.GVA_DB.Where("ticket_no = ?", req.TicketNo).First(&ticket).Error
- } else if req.VehicleRecordID != 0 {
- _ = global.GVA_DB.Where("vehicle_record_id = ?", req.VehicleRecordID).First(&ticket).Error
- }
- if ticket.ID != 0 {
- req.DigitalTicketID = ticket.ID
- if req.VehicleRecordID == 0 {
- req.VehicleRecordID = ticket.VehicleRecordID
- }
- if req.TicketNo == "" {
- req.TicketNo = ticket.TicketNo
- }
- if req.PlateNumber == "" {
- req.PlateNumber = ticket.PlateNumber
- }
- }
- // 仅传票号时会在上一步得到停车会话 ID,此处继续补齐车辆与进出场位置快照。
- enrichSessionAssociation(req)
- if req.PaymentRecordID == 0 && req.VehicleRecordID != 0 {
- var payment dao.PaymentRecord
- if global.GVA_DB.Where("record_id = ?", req.VehicleRecordID).Order("id DESC").First(&payment).Error == nil {
- req.PaymentRecordID = payment.ID
- }
- }
- if req.GateCommandID != 0 {
- var command dao.DeviceCommandLog
- if global.GVA_DB.First(&command, req.GateCommandID).Error == nil {
- if req.DeviceCode == "" {
- req.DeviceCode = command.DeviceCode
- }
- if req.DeviceName == "" {
- req.DeviceName = command.DeviceName
- }
- }
- }
- if req.ChannelID != 0 {
- var channel dao.Channel
- if global.GVA_DB.First(&channel, req.ChannelID).Error == nil {
- if req.ChannelCode == "" {
- req.ChannelCode = channel.ChannelCode
- }
- if req.ChannelName == "" {
- req.ChannelName = channel.ChannelName
- }
- if req.ParkingLotID == 0 {
- req.ParkingLotID = channel.ParkingLotID
- }
- }
- }
- if req.DeviceCode != "" {
- var device dao.UHFReader
- if global.GVA_DB.Where("device_code = ?", req.DeviceCode).First(&device).Error == nil {
- if req.DeviceName == "" {
- req.DeviceName = device.DeviceName
- }
- if req.ChannelID == 0 {
- req.ChannelID = device.ChannelID
- }
- if req.ParkingLotID == 0 {
- req.ParkingLotID = device.ParkingLotID
- }
- }
- }
- // 设备可能刚刚补齐通道 ID,再查询一次通道名称和所属停车场。
- if req.ChannelID != 0 && (req.ChannelCode == "" || req.ChannelName == "" || req.ParkingLotID == 0) {
- var channel dao.Channel
- if global.GVA_DB.First(&channel, req.ChannelID).Error == nil {
- if req.ChannelCode == "" {
- req.ChannelCode = channel.ChannelCode
- }
- if req.ChannelName == "" {
- req.ChannelName = channel.ChannelName
- }
- if req.ParkingLotID == 0 {
- req.ParkingLotID = channel.ParkingLotID
- }
- }
- }
- if req.ParkingLotID != 0 && req.ParkingLotName == "" {
- var lot dao.ParkingLot
- if global.GVA_DB.First(&lot, req.ParkingLotID).Error == nil {
- req.ParkingLotName = lot.LotName
- }
- }
- }
- func validateAssociationIDs(req CreateIncidentRequest) error {
- checks := []struct {
- id uint
- model interface{}
- name string
- }{
- {req.VehicleRecordID, &dao.VehicleRecord{}, "停车会话"},
- {req.DigitalTicketID, &dao.DigitalTicket{}, "数字票"},
- {req.PaymentRecordID, &dao.PaymentRecord{}, "支付流水"},
- {req.GateCommandID, &dao.DeviceCommandLog{}, "闸机指令"},
- {req.ParkingLotID, &dao.ParkingLot{}, "停车场"},
- {req.ChannelID, &dao.Channel{}, "通道"},
- }
- for _, check := range checks {
- if check.id == 0 {
- continue
- }
- var count int64
- if err := global.GVA_DB.Model(check.model).Where("id = ?", check.id).Count(&count).Error; err != nil {
- return err
- }
- if count == 0 {
- return fmt.Errorf("关联的%s不存在: %d", check.name, check.id)
- }
- }
- return nil
- }
- // RecordIncident 自动埋点入口(best-effort):
- // 内部任何错误只记日志并返回 0,绝不影响业务主流程;成功返回异常记录 ID(供指令流水关联)。
- func (s *IncidentService) RecordIncident(req RecordIncidentRequest) uint {
- if req.Category == "" {
- return 0
- }
- if global.GVA_DB == nil {
- logWarn("异常埋点跳过:数据库未初始化", zap.String("category", req.Category))
- return 0
- }
- enrichAssociations(&req)
- if req.Source == "" {
- req.Source = SourceSystem
- }
- if req.Level == "" {
- req.Level = categoryDefaultLevel[req.Category]
- if req.Level == "" {
- req.Level = LevelWarning
- }
- }
- // 去重:离线异常同设备未关闭不重复;通行类同标识时间窗内不重复
- var exists bool
- var err error
- if req.Category == CategoryDeviceOffline {
- exists, err = s.repo.HasOpenDeviceOffline(req.DeviceCode)
- } else {
- exists, err = s.repo.ExistsRecent(req.DeviceCode, req.Category, req.PlateNumber, req.RFIDTag, 5)
- }
- if err != nil {
- logWarn("异常去重查询失败,继续记录", zap.Error(err))
- } else if exists {
- return 0
- }
- status := StatusPending
- if req.HandleType != "" {
- status = StatusResolved
- }
- detail := ""
- if req.Detail != nil {
- if data, marshalErr := json.Marshal(req.Detail); marshalErr == nil {
- detail = string(data)
- if len(detail) > 1000 {
- detail = detail[:1000]
- }
- }
- }
- no, err := s.generateIncidentNo()
- if err != nil {
- logError("生成异常编号失败", zap.Error(err))
- return 0
- }
- now := time.Now()
- event := map[string]interface{}{"event": "created", "source": req.Source}
- record := &dao.IncidentRecord{
- IncidentNo: no,
- Category: req.Category,
- Source: req.Source,
- Level: req.Level,
- Status: status,
- VehicleRecordID: req.VehicleRecordID,
- DigitalTicketID: req.DigitalTicketID,
- PaymentRecordID: req.PaymentRecordID,
- GateCommandID: req.GateCommandID,
- TicketNo: req.TicketNo,
- PlateNumber: req.PlateNumber,
- RFIDTag: req.RFIDTag,
- ParkingLotID: req.ParkingLotID,
- ParkingLotName: req.ParkingLotName,
- ChannelID: req.ChannelID,
- ChannelCode: req.ChannelCode,
- ChannelName: req.ChannelName,
- DeviceCode: req.DeviceCode,
- DeviceName: req.DeviceName,
- OperatorID: req.OperatorID,
- Description: req.Description,
- Detail: detail,
- HandlerID: req.OperatorID,
- HandleType: req.HandleType,
- HandleRemark: req.HandleRemark,
- EventLog: appendEvent("[]", event),
- }
- if status == StatusResolved {
- record.HandledAt = &now
- }
- if err := s.repo.Create(record); err != nil {
- logError("记录异常事件失败",
- zap.Error(err), zap.String("category", req.Category), zap.String("device_code", req.DeviceCode))
- return 0
- }
- logInfo("异常事件已记录",
- zap.String("incident_no", no), zap.String("category", req.Category), zap.String("status", status))
- return record.ID
- }
- // CreateIncidentRequest 人工上报入参。
- type CreateIncidentRequest struct {
- Category string `json:"category" binding:"required"`
- Level string `json:"level"`
- VehicleRecordID uint `json:"vehicle_record_id"`
- DigitalTicketID uint `json:"digital_ticket_id"`
- PaymentRecordID uint `json:"payment_record_id"`
- GateCommandID uint `json:"gate_command_id"`
- TicketNo string `json:"ticket_no"`
- PlateNumber string `json:"plate_number"`
- RFIDTag string `json:"rfid_tag"`
- ParkingLotID uint `json:"parking_lot_id"`
- ParkingLotName string `json:"parking_lot_name"`
- ChannelID uint `json:"channel_id"`
- ChannelCode string `json:"channel_code"`
- ChannelName string `json:"channel_name"`
- DeviceCode string `json:"device_code"`
- DeviceName string `json:"device_name"`
- Description string `json:"description" binding:"required"`
- Detail string `json:"detail"`
- }
- // CreateIncident 人工上报异常(来源 manual,待处理状态)。
- func (s *IncidentService) CreateIncident(req CreateIncidentRequest, operatorID uint) (*dao.IncidentRecord, error) {
- if _, known := categoryDefaultLevel[req.Category]; !known {
- return nil, errors.New("未知异常分类: " + req.Category)
- }
- if err := validateAssociationIDs(req); err != nil {
- return nil, err
- }
- level := req.Level
- if level == "" {
- level = categoryDefaultLevel[req.Category]
- if level == "" {
- level = LevelWarning
- }
- }
- autoReq := RecordIncidentRequest{
- VehicleRecordID: req.VehicleRecordID, DigitalTicketID: req.DigitalTicketID, PaymentRecordID: req.PaymentRecordID, GateCommandID: req.GateCommandID,
- TicketNo: req.TicketNo, PlateNumber: req.PlateNumber, RFIDTag: req.RFIDTag, ParkingLotID: req.ParkingLotID, ParkingLotName: req.ParkingLotName,
- ChannelID: req.ChannelID, ChannelCode: req.ChannelCode, ChannelName: req.ChannelName, DeviceCode: req.DeviceCode, DeviceName: req.DeviceName,
- }
- enrichAssociations(&autoReq)
- no, err := s.generateIncidentNo()
- if err != nil {
- return nil, err
- }
- record := &dao.IncidentRecord{
- IncidentNo: no,
- Category: req.Category,
- Source: SourceManual,
- Level: level,
- Status: StatusPending,
- VehicleRecordID: autoReq.VehicleRecordID,
- DigitalTicketID: autoReq.DigitalTicketID,
- PaymentRecordID: autoReq.PaymentRecordID,
- GateCommandID: autoReq.GateCommandID,
- TicketNo: autoReq.TicketNo,
- PlateNumber: autoReq.PlateNumber,
- RFIDTag: autoReq.RFIDTag,
- ParkingLotID: autoReq.ParkingLotID,
- ParkingLotName: autoReq.ParkingLotName,
- ChannelID: autoReq.ChannelID,
- ChannelCode: autoReq.ChannelCode,
- ChannelName: autoReq.ChannelName,
- DeviceCode: autoReq.DeviceCode,
- DeviceName: autoReq.DeviceName,
- OperatorID: operatorID,
- Description: req.Description,
- Detail: req.Detail,
- EventLog: appendEvent("[]", map[string]interface{}{"event": "created", "source": SourceManual}),
- }
- if err := s.repo.Create(record); err != nil {
- return nil, err
- }
- return record, nil
- }
- // TransitionRequest 状态流转+处置入参。
- type TransitionRequest struct {
- ToStatus string `json:"to_status" binding:"required"`
- HandleType string `json:"handle_type"`
- HandleRemark string `json:"handle_remark"`
- ForceFreeAmount float64 `json:"force_free_amount"`
- }
- // TransitionIncident 状态流转(CAS 条件更新)。
- // isAdmin 表示是否允许资金类处置(force_free)。
- func (s *IncidentService) TransitionIncident(id uint, req TransitionRequest, operatorID uint, isAdmin bool) (*dao.IncidentRecord, error) {
- incident, err := s.repo.GetByID(id)
- if err != nil {
- return nil, ErrIncidentNotFound
- }
- toStatus := req.ToStatus
- if toStatus == incident.Status {
- return incident, nil
- }
- if !allowedTransitions[incident.Status][toStatus] {
- return nil, fmt.Errorf("状态转换不合法: %s → %s", incident.Status, toStatus)
- }
- // 处置校验
- if toStatus == StatusResolved {
- if req.HandleType == "" {
- return nil, errors.New("解决异常必须填写处置方式")
- }
- if req.HandleRemark == "" {
- return nil, errors.New("解决异常必须填写处置备注")
- }
- }
- if req.HandleType == HandleForceFree {
- if !isAdmin {
- return nil, errors.New("强制免费处置需要管理员权限")
- }
- if req.ForceFreeAmount < 0 {
- return nil, errors.New("强制免费金额不能为负数")
- }
- }
- updates := map[string]interface{}{
- "handler_id": operatorID,
- "handle_type": req.HandleType,
- "handle_remark": req.HandleRemark,
- }
- event := map[string]interface{}{"event": toStatus, "handler_id": operatorID}
- if toStatus == StatusResolved {
- now := time.Now()
- updates["handled_at"] = now
- updates["force_free_amount"] = req.ForceFreeAmount
- event["handle_type"] = req.HandleType
- if req.HandleType == HandleForceFree {
- event["force_free_amount"] = req.ForceFreeAmount
- }
- }
- updates["event_log"] = appendEvent(incident.EventLog, event)
- updated, err := s.repo.UpdateStateIfCurrent(id, incident.Status, toStatus, updates)
- if err != nil {
- return nil, err
- }
- if !updated {
- return nil, ErrIncidentChanged
- }
- return s.repo.GetByID(id)
- }
- // GetIncident 查询详情。
- func (s *IncidentService) GetIncident(id uint) (*dao.IncidentRecord, error) {
- return s.repo.GetByID(id)
- }
- // ListIncidents 分页筛选。
- func (s *IncidentService) ListIncidents(q repository.IncidentListQuery) ([]repository.IncidentListItem, int64, error) {
- return s.repo.List(q)
- }
- // GetIncidentStats 异常统计。
- func (s *IncidentService) GetIncidentStats() (repository.IncidentStats, error) {
- return s.repo.Stats()
- }
- // ResolveDeviceOffline 设备恢复在线:关闭该设备未处理的离线异常(best-effort)。
- func (s *IncidentService) ResolveDeviceOffline(deviceCode string) {
- if err := s.repo.ResolveDeviceOfflineByDevice(deviceCode, "设备恢复在线"); err != nil {
- logWarn("关闭设备离线异常失败",
- zap.Error(err), zap.String("device_code", deviceCode))
- }
- }
- // GateCommandRecord 道闸指令流水入参。
- type GateCommandRecord struct {
- DeviceCode string
- DeviceName string
- Action string // open / close
- Source string // passage / manual / test
- OperatorID uint
- SessionID uint
- Result string // success / failed
- ErrorMessage string
- DurationMs int64
- }
- // RecordGateCommand 记录道闸指令流水(best-effort),返回流水 ID 供异常关联。
- func (s *IncidentService) RecordGateCommand(rec GateCommandRecord) uint {
- if rec.DeviceCode == "" {
- return 0
- }
- if rec.Source == "" {
- rec.Source = SourcePassage
- }
- log := &dao.DeviceCommandLog{
- DeviceCode: rec.DeviceCode,
- DeviceName: rec.DeviceName,
- Action: rec.Action,
- Source: rec.Source,
- OperatorID: rec.OperatorID,
- SessionID: rec.SessionID,
- Result: rec.Result,
- ErrorMessage: rec.ErrorMessage,
- DurationMs: rec.DurationMs,
- }
- if err := s.repo.CreateCommandLog(log); err != nil {
- logWarn("记录道闸指令流水失败", zap.Error(err))
- return 0
- }
- return log.ID
- }
- // LinkIncidentToCommand 回填指令流水关联的异常事件 ID(best-effort)。
- func (s *IncidentService) LinkIncidentToCommand(logID, incidentID uint) {
- if err := s.repo.UpdateIncidentID(logID, incidentID); err != nil {
- logWarn("回填指令流水异常关联失败", zap.Error(err))
- }
- }
- // generateIncidentNo 生成唯一编号:INC + yyyyMMdd + 当日序号(冲突时重试)。
- func (s *IncidentService) generateIncidentNo() (string, error) {
- prefix := "INC" + time.Now().Format("20060102")
- for attempt := 0; attempt < 5; attempt++ {
- var count int64
- if err := global.GVA_DB.Model(&dao.IncidentRecord{}).
- Where("incident_no LIKE ?", prefix+"%").Count(&count).Error; err != nil {
- return "", err
- }
- no := fmt.Sprintf("%s-%04d", prefix, count+1)
- var dup int64
- if err := global.GVA_DB.Model(&dao.IncidentRecord{}).
- Where("incident_no = ?", no).Count(&dup).Error; err != nil {
- return "", err
- }
- if dup == 0 {
- return no, nil
- }
- }
- return "", errors.New("生成异常编号失败,请重试")
- }
- // appendEvent 向事件日志 JSON 数组追加事件(沿用数字票 event_log 约定)。
- func appendEvent(log string, event map[string]interface{}) string {
- var events []map[string]interface{}
- json.Unmarshal([]byte(log), &events)
- event["at"] = time.Now().Format(time.RFC3339)
- events = append(events, event)
- data, _ := json.Marshal(events)
- return string(data)
- }
|