diff --git a/RTSPtoWeb.exe b/RTSPtoWeb.exe index 585a794..792e6b7 100644 Binary files a/RTSPtoWeb.exe and b/RTSPtoWeb.exe differ diff --git a/database.go b/database.go index 181d834..c882347 100644 --- a/database.go +++ b/database.go @@ -37,6 +37,7 @@ type Camera struct { NvrProduce sql.NullString `json:"nvr_produce" db:"nvr_produce"` NvrPath sql.NullString `json:"nvr_path" db:"nvr_path"` PlayBack sql.NullString `json:"play_back" db:"play_back"` + Enabled bool `json:"enabled" db:"enabled"` // 兼容字段,如果数据库中没有此列则默认为true DelFlag string `json:"del_flag" db:"del_flag"` CreateBy sql.NullString `json:"create_by" db:"create_by"` CreateTime sql.NullTime `json:"create_time" db:"create_time"` @@ -147,29 +148,53 @@ func (dm *DatabaseManager) initTables() error { return err } -// GetAllCameras 获取所有摄像头 - 适配qsc_camera表 +// GetAllCameras 获取所有摄像头列表 - 适配qsc_camera表 func (dm *DatabaseManager) GetAllCameras() ([]Camera, error) { + // 首先尝试查询包含enabled列的SQL query := `SELECT camera_id, ip, port, username, password, url, camera_produce, - camera_name, device_type, unit_code, nvr_produce, nvr_path, play_back, + camera_name, device_type, unit_code, nvr_produce, nvr_path, play_back, enabled, del_flag, create_by, create_time, update_by, update_time, user_id, dept_id FROM qsc_camera WHERE del_flag != '1'` rows, err := dm.db.Query(query) if err != nil { - return nil, err + // 如果查询失败(可能是因为enabled列不存在),尝试不包含enabled列的查询 + query = `SELECT camera_id, ip, port, username, password, url, camera_produce, + camera_name, device_type, unit_code, nvr_produce, nvr_path, play_back, + del_flag, create_by, create_time, update_by, update_time, user_id, dept_id + FROM qsc_camera WHERE del_flag != '1'` + + rows, err = dm.db.Query(query) + if err != nil { + return nil, err + } } defer rows.Close() var cameras []Camera for rows.Next() { var camera Camera - err := rows.Scan(&camera.CameraID, &camera.IP, &camera.Port, &camera.Username, - &camera.Password, &camera.URL, &camera.CameraProduce, &camera.CameraName, - &camera.DeviceType, &camera.UnitCode, &camera.NvrProduce, &camera.NvrPath, - &camera.PlayBack, &camera.DelFlag, &camera.CreateBy, &camera.CreateTime, - &camera.UpdateBy, &camera.UpdateTime, &camera.UserID, &camera.DeptID) - if err != nil { - return nil, err + // 检查列数来决定如何扫描 + columns, _ := rows.Columns() + if len(columns) == 21 { // 包含enabled列 + err := rows.Scan(&camera.CameraID, &camera.IP, &camera.Port, &camera.Username, + &camera.Password, &camera.URL, &camera.CameraProduce, &camera.CameraName, + &camera.DeviceType, &camera.UnitCode, &camera.NvrProduce, &camera.NvrPath, + &camera.PlayBack, &camera.Enabled, &camera.DelFlag, &camera.CreateBy, &camera.CreateTime, + &camera.UpdateBy, &camera.UpdateTime, &camera.UserID, &camera.DeptID) + if err != nil { + return nil, err + } + } else { // 不包含enabled列 + camera.Enabled = true // 默认启用 + err := rows.Scan(&camera.CameraID, &camera.IP, &camera.Port, &camera.Username, + &camera.Password, &camera.URL, &camera.CameraProduce, &camera.CameraName, + &camera.DeviceType, &camera.UnitCode, &camera.NvrProduce, &camera.NvrPath, + &camera.PlayBack, &camera.DelFlag, &camera.CreateBy, &camera.CreateTime, + &camera.UpdateBy, &camera.UpdateTime, &camera.UserID, &camera.DeptID) + if err != nil { + return nil, err + } } cameras = append(cameras, camera) } @@ -179,27 +204,51 @@ func (dm *DatabaseManager) GetAllCameras() ([]Camera, error) { // GetCamerasByUnitCode 根据unitcode获取摄像头列表 - 适配qsc_camera表 func (dm *DatabaseManager) GetCamerasByUnitCode(unitCode string) ([]Camera, error) { + // 首先尝试查询包含enabled列的SQL query := `SELECT camera_id, ip, port, username, password, url, camera_produce, - camera_name, device_type, unit_code, nvr_produce, nvr_path, play_back, + camera_name, device_type, unit_code, nvr_produce, nvr_path, play_back, enabled, del_flag, create_by, create_time, update_by, update_time, user_id, dept_id FROM qsc_camera WHERE unit_code = ? AND del_flag != '1'` rows, err := dm.db.Query(query, unitCode) if err != nil { - return nil, err + // 如果查询失败(可能是因为enabled列不存在),尝试不包含enabled列的查询 + query = `SELECT camera_id, ip, port, username, password, url, camera_produce, + camera_name, device_type, unit_code, nvr_produce, nvr_path, play_back, + del_flag, create_by, create_time, update_by, update_time, user_id, dept_id + FROM qsc_camera WHERE unit_code = ? AND del_flag != '1'` + + rows, err = dm.db.Query(query, unitCode) + if err != nil { + return nil, err + } } defer rows.Close() var cameras []Camera for rows.Next() { var camera Camera - err := rows.Scan(&camera.CameraID, &camera.IP, &camera.Port, &camera.Username, - &camera.Password, &camera.URL, &camera.CameraProduce, &camera.CameraName, - &camera.DeviceType, &camera.UnitCode, &camera.NvrProduce, &camera.NvrPath, - &camera.PlayBack, &camera.DelFlag, &camera.CreateBy, &camera.CreateTime, - &camera.UpdateBy, &camera.UpdateTime, &camera.UserID, &camera.DeptID) - if err != nil { - return nil, err + // 检查列数来决定如何扫描 + columns, _ := rows.Columns() + if len(columns) == 21 { // 包含enabled列 + err := rows.Scan(&camera.CameraID, &camera.IP, &camera.Port, &camera.Username, + &camera.Password, &camera.URL, &camera.CameraProduce, &camera.CameraName, + &camera.DeviceType, &camera.UnitCode, &camera.NvrProduce, &camera.NvrPath, + &camera.PlayBack, &camera.Enabled, &camera.DelFlag, &camera.CreateBy, &camera.CreateTime, + &camera.UpdateBy, &camera.UpdateTime, &camera.UserID, &camera.DeptID) + if err != nil { + return nil, err + } + } else { // 不包含enabled列 + camera.Enabled = true // 默认启用 + err := rows.Scan(&camera.CameraID, &camera.IP, &camera.Port, &camera.Username, + &camera.Password, &camera.URL, &camera.CameraProduce, &camera.CameraName, + &camera.DeviceType, &camera.UnitCode, &camera.NvrProduce, &camera.NvrPath, + &camera.PlayBack, &camera.DelFlag, &camera.CreateBy, &camera.CreateTime, + &camera.UpdateBy, &camera.UpdateTime, &camera.UserID, &camera.DeptID) + if err != nil { + return nil, err + } } cameras = append(cameras, camera) } diff --git a/java-integration-example/src/main/java/com/example/rtspweb/RTSPWebClient.java b/java-integration-example/src/main/java/com/example/rtspweb/RTSPWebClient.java deleted file mode 100644 index 204c41b..0000000 --- a/java-integration-example/src/main/java/com/example/rtspweb/RTSPWebClient.java +++ /dev/null @@ -1,342 +0,0 @@ -package com.example.rtspweb; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.springframework.http.*; -import org.springframework.web.client.RestTemplate; -import org.springframework.web.client.HttpClientErrorException; - -import java.time.LocalDateTime; -import java.util.List; -import java.util.Map; - -/** - * RTSPtoWeb Java客户端 - * 用于与RTSPtoWeb Go服务进行API交互 - */ -public class RTSPWebClient { - - private final String baseUrl; - private final RestTemplate restTemplate; - private final ObjectMapper objectMapper; - - public RTSPWebClient(String baseUrl) { - this.baseUrl = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl; - this.restTemplate = new RestTemplate(); - this.objectMapper = new ObjectMapper(); - } - - /** - * 获取摄像头列表 - */ - public ApiResponse getCameras(String unitCode, String status, int page, int size) { - try { - String url = String.format("%s/api/java/cameras?page=%d&size=%d", baseUrl, page, size); - if (unitCode != null && !unitCode.isEmpty()) { - url += "&unit_code=" + unitCode; - } - if (status != null && !status.isEmpty()) { - url += "&status=" + status; - } - - ResponseEntity response = restTemplate.getForEntity(url, String.class); - - TypeReference> typeRef = new TypeReference>() {}; - return objectMapper.readValue(response.getBody(), typeRef); - } catch (Exception e) { - return createErrorResponse("获取摄像头列表失败: " + e.getMessage()); - } - } - - /** - * 获取流列表 - */ - public ApiResponse getStreams(int page, int size) { - try { - String url = String.format("%s/api/java/streams?page=%d&size=%d", baseUrl, page, size); - - ResponseEntity response = restTemplate.getForEntity(url, String.class); - - TypeReference> typeRef = new TypeReference>() {}; - return objectMapper.readValue(response.getBody(), typeRef); - } catch (Exception e) { - return createErrorResponse("获取流列表失败: " + e.getMessage()); - } - } - - /** - * 获取摄像头详情 - */ - public ApiResponse getCameraDetail(String cameraId) { - try { - String url = String.format("%s/api/java/camera/%s", baseUrl, cameraId); - - ResponseEntity response = restTemplate.getForEntity(url, String.class); - - TypeReference> typeRef = new TypeReference>() {}; - return objectMapper.readValue(response.getBody(), typeRef); - } catch (HttpClientErrorException e) { - if (e.getStatusCode() == HttpStatus.NOT_FOUND) { - return createErrorResponse("摄像头不存在"); - } - return createErrorResponse("获取摄像头详情失败: " + e.getMessage()); - } catch (Exception e) { - return createErrorResponse("获取摄像头详情失败: " + e.getMessage()); - } - } - - /** - * 获取系统信息 - */ - public ApiResponse getSystemInfo() { - try { - String url = String.format("%s/api/java/system/info", baseUrl); - - ResponseEntity response = restTemplate.getForEntity(url, String.class); - - TypeReference> typeRef = new TypeReference>() {}; - return objectMapper.readValue(response.getBody(), typeRef); - } catch (Exception e) { - return createErrorResponse("获取系统信息失败: " + e.getMessage()); - } - } - - /** - * 添加摄像头 - */ - public ApiResponse addCamera(Camera camera) { - try { - String url = String.format("%s/camera/add", baseUrl); - - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_JSON); - - HttpEntity request = new HttpEntity<>(camera, headers); - - ResponseEntity response = restTemplate.postForEntity(url, request, String.class); - - TypeReference> typeRef = new TypeReference>() {}; - return objectMapper.readValue(response.getBody(), typeRef); - } catch (Exception e) { - return createErrorResponse("添加摄像头失败: " + e.getMessage()); - } - } - - /** - * 刷新摄像头状态 - */ - public ApiResponse refreshCameras() { - try { - String url = String.format("%s/cameras/refresh", baseUrl); - - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_JSON); - - HttpEntity request = new HttpEntity<>("{}", headers); - - ResponseEntity response = restTemplate.postForEntity(url, request, String.class); - - TypeReference> typeRef = new TypeReference>() {}; - return objectMapper.readValue(response.getBody(), typeRef); - } catch (Exception e) { - return createErrorResponse("刷新摄像头状态失败: " + e.getMessage()); - } - } - - private ApiResponse createErrorResponse(String message) { - ApiResponse response = new ApiResponse<>(); - response.setCode(500); - response.setMessage(message); - response.setSuccess(false); - return response; - } - - // 数据模型类 - public static class ApiResponse { - private int code; - private String message; - private T data; - private boolean success; - - // Getters and Setters - public int getCode() { return code; } - public void setCode(int code) { this.code = code; } - - public String getMessage() { return message; } - public void setMessage(String message) { this.message = message; } - - public T getData() { return data; } - public void setData(T data) { this.data = data; } - - public boolean isSuccess() { return success; } - public void setSuccess(boolean success) { this.success = success; } - } - - public static class CameraListResponse { - private int total; - private List cameras; - - public int getTotal() { return total; } - public void setTotal(int total) { this.total = total; } - - public List getCameras() { return cameras; } - public void setCameras(List cameras) { this.cameras = cameras; } - } - - public static class StreamListResponse { - private int total; - private List streams; - - public int getTotal() { return total; } - public void setTotal(int total) { this.total = total; } - - public List getStreams() { return streams; } - public void setStreams(List streams) { this.streams = streams; } - } - - public static class Camera { - private String id; - private String name; - private String ip; - private int port; - private String username; - private String password; - @JsonProperty("rtsp_url") - private String rtspUrl; - private String status; - private boolean enabled; - @JsonProperty("device_type") - private String deviceType; - @JsonProperty("unit_code") - private String unitCode; - @JsonProperty("created_at") - private LocalDateTime createdAt; - @JsonProperty("updated_at") - private LocalDateTime updatedAt; - @JsonProperty("play_urls") - private PlayUrls playUrls; - - // Getters and Setters - public String getId() { return id; } - public void setId(String id) { this.id = id; } - - public String getName() { return name; } - public void setName(String name) { this.name = name; } - - public String getIp() { return ip; } - public void setIp(String ip) { this.ip = ip; } - - public int getPort() { return port; } - public void setPort(int port) { this.port = port; } - - public String getUsername() { return username; } - public void setUsername(String username) { this.username = username; } - - public String getPassword() { return password; } - public void setPassword(String password) { this.password = password; } - - public String getRtspUrl() { return rtspUrl; } - public void setRtspUrl(String rtspUrl) { this.rtspUrl = rtspUrl; } - - public String getStatus() { return status; } - public void setStatus(String status) { this.status = status; } - - public boolean isEnabled() { return enabled; } - public void setEnabled(boolean enabled) { this.enabled = enabled; } - - public String getDeviceType() { return deviceType; } - public void setDeviceType(String deviceType) { this.deviceType = deviceType; } - - public String getUnitCode() { return unitCode; } - public void setUnitCode(String unitCode) { this.unitCode = unitCode; } - - public LocalDateTime getCreatedAt() { return createdAt; } - public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } - - public LocalDateTime getUpdatedAt() { return updatedAt; } - public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; } - - public PlayUrls getPlayUrls() { return playUrls; } - public void setPlayUrls(PlayUrls playUrls) { this.playUrls = playUrls; } - } - - public static class Stream { - private String id; - private String name; - private Map channels; - @JsonProperty("play_urls") - private PlayUrls playUrls; - - // Getters and Setters - public String getId() { return id; } - public void setId(String id) { this.id = id; } - - public String getName() { return name; } - public void setName(String name) { this.name = name; } - - public Map getChannels() { return channels; } - public void setChannels(Map channels) { this.channels = channels; } - - public PlayUrls getPlayUrls() { return playUrls; } - public void setPlayUrls(PlayUrls playUrls) { this.playUrls = playUrls; } - } - - public static class PlayUrls { - private String hls; - private String webrtc; - private String mse; - private String all; - - // Getters and Setters - public String getHls() { return hls; } - public void setHls(String hls) { this.hls = hls; } - - public String getWebrtc() { return webrtc; } - public void setWebrtc(String webrtc) { this.webrtc = webrtc; } - - public String getMse() { return mse; } - public void setMse(String mse) { this.mse = mse; } - - public String getAll() { return all; } - public void setAll(String all) { this.all = all; } - } - - public static class SystemInfo { - private String version; - @JsonProperty("database_enabled") - private boolean databaseEnabled; - @JsonProperty("demo_enabled") - private boolean demoEnabled; - @JsonProperty("total_streams") - private int totalStreams; - @JsonProperty("total_cameras") - private int totalCameras; - @JsonProperty("online_cameras") - private int onlineCameras; - @JsonProperty("server_time") - private String serverTime; - - // Getters and Setters - public String getVersion() { return version; } - public void setVersion(String version) { this.version = version; } - - public boolean isDatabaseEnabled() { return databaseEnabled; } - public void setDatabaseEnabled(boolean databaseEnabled) { this.databaseEnabled = databaseEnabled; } - - public boolean isDemoEnabled() { return demoEnabled; } - public void setDemoEnabled(boolean demoEnabled) { this.demoEnabled = demoEnabled; } - - public int getTotalStreams() { return totalStreams; } - public void setTotalStreams(int totalStreams) { this.totalStreams = totalStreams; } - - public int getTotalCameras() { return totalCameras; } - public void setTotalCameras(int totalCameras) { this.totalCameras = totalCameras; } - - public int getOnlineCameras() { return onlineCameras; } - public void setOnlineCameras(int onlineCameras) { this.onlineCameras = onlineCameras; } - - public String getServerTime() { return serverTime; } - public void setServerTime(String serverTime) { this.serverTime = serverTime; } - } -} \ No newline at end of file diff --git a/java-integration-example/src/main/java/com/example/rtspweb/RTSPWebController.java b/java-integration-example/src/main/java/com/example/rtspweb/RTSPWebController.java deleted file mode 100644 index f21c088..0000000 --- a/java-integration-example/src/main/java/com/example/rtspweb/RTSPWebController.java +++ /dev/null @@ -1,252 +0,0 @@ -package com.example.rtspweb; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Controller; -import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.*; - -import javax.annotation.PostConstruct; -import java.util.HashMap; -import java.util.Map; - -/** - * RTSPtoWeb集成控制器 - * 提供Web界面和REST API来管理摄像头和视频流 - */ -@Controller -@RequestMapping("/rtsp") -public class RTSPWebController { - - @Value("${rtspweb.server.url:http://localhost:8083}") - private String rtspWebServerUrl; - - private RTSPWebClient rtspWebClient; - - @PostConstruct - public void init() { - this.rtspWebClient = new RTSPWebClient(rtspWebServerUrl); - } - - /** - * 摄像头管理页面 - */ - @GetMapping("/cameras") - public String camerasPage(Model model) { - model.addAttribute("serverUrl", rtspWebServerUrl); - return "rtsp/cameras"; - } - - /** - * 视频流展示页面 - */ - @GetMapping("/streams") - public String streamsPage(Model model) { - model.addAttribute("serverUrl", rtspWebServerUrl); - return "rtsp/streams"; - } - - /** - * 视频播放页面 - */ - @GetMapping("/player/{cameraId}") - public String playerPage(@PathVariable String cameraId, Model model) { - RTSPWebClient.ApiResponse response = rtspWebClient.getCameraDetail(cameraId); - if (response.isSuccess()) { - model.addAttribute("camera", response.getData()); - model.addAttribute("serverUrl", rtspWebServerUrl); - return "rtsp/player"; - } else { - model.addAttribute("error", response.getMessage()); - return "rtsp/error"; - } - } - - /** - * 嵌入式播放器页面(iframe使用) - */ - @GetMapping("/embed/player/{cameraId}") - public String embedPlayerPage(@PathVariable String cameraId, - @RequestParam(defaultValue = "hls") String type, - Model model) { - RTSPWebClient.ApiResponse response = rtspWebClient.getCameraDetail(cameraId); - if (response.isSuccess()) { - RTSPWebClient.Camera camera = response.getData(); - String playUrl = getPlayUrl(camera, type); - - model.addAttribute("camera", camera); - model.addAttribute("playUrl", playUrl); - model.addAttribute("playType", type); - model.addAttribute("serverUrl", rtspWebServerUrl); - return "rtsp/embed-player"; - } else { - model.addAttribute("error", response.getMessage()); - return "rtsp/error"; - } - } - - // REST API 接口 - - /** - * 获取摄像头列表API - */ - @GetMapping("/api/cameras") - @ResponseBody - public ResponseEntity> getCameras( - @RequestParam(required = false) String unitCode, - @RequestParam(required = false) String status, - @RequestParam(defaultValue = "1") int page, - @RequestParam(defaultValue = "20") int size) { - - RTSPWebClient.ApiResponse response = - rtspWebClient.getCameras(unitCode, status, page, size); - - return ResponseEntity.ok(response); - } - - /** - * 获取流列表API - */ - @GetMapping("/api/streams") - @ResponseBody - public ResponseEntity> getStreams( - @RequestParam(defaultValue = "1") int page, - @RequestParam(defaultValue = "20") int size) { - - RTSPWebClient.ApiResponse response = - rtspWebClient.getStreams(page, size); - - return ResponseEntity.ok(response); - } - - /** - * 获取摄像头详情API - */ - @GetMapping("/api/camera/{cameraId}") - @ResponseBody - public ResponseEntity> getCameraDetail( - @PathVariable String cameraId) { - - RTSPWebClient.ApiResponse response = - rtspWebClient.getCameraDetail(cameraId); - - return ResponseEntity.ok(response); - } - - /** - * 获取系统信息API - */ - @GetMapping("/api/system/info") - @ResponseBody - public ResponseEntity> getSystemInfo() { - RTSPWebClient.ApiResponse response = - rtspWebClient.getSystemInfo(); - - return ResponseEntity.ok(response); - } - - /** - * 添加摄像头API - */ - @PostMapping("/api/camera") - @ResponseBody - public ResponseEntity> addCamera( - @RequestBody RTSPWebClient.Camera camera) { - - RTSPWebClient.ApiResponse response = - rtspWebClient.addCamera(camera); - - return ResponseEntity.ok(response); - } - - /** - * 刷新摄像头状态API - */ - @PostMapping("/api/cameras/refresh") - @ResponseBody - public ResponseEntity> refreshCameras() { - RTSPWebClient.ApiResponse response = rtspWebClient.refreshCameras(); - return ResponseEntity.ok(response); - } - - /** - * 获取播放地址API - */ - @GetMapping("/api/camera/{cameraId}/play-url") - @ResponseBody - public ResponseEntity> getPlayUrl( - @PathVariable String cameraId, - @RequestParam(defaultValue = "hls") String type) { - - Map result = new HashMap<>(); - - RTSPWebClient.ApiResponse response = - rtspWebClient.getCameraDetail(cameraId); - - if (response.isSuccess()) { - RTSPWebClient.Camera camera = response.getData(); - String playUrl = getPlayUrl(camera, type); - - result.put("success", true); - result.put("playUrl", playUrl); - result.put("type", type); - result.put("camera", camera); - } else { - result.put("success", false); - result.put("message", response.getMessage()); - } - - return ResponseEntity.ok(result); - } - - /** - * 健康检查API - */ - @GetMapping("/api/health") - @ResponseBody - public ResponseEntity> healthCheck() { - Map result = new HashMap<>(); - - try { - RTSPWebClient.ApiResponse response = - rtspWebClient.getSystemInfo(); - - if (response.isSuccess()) { - result.put("status", "UP"); - result.put("rtspWebServer", "CONNECTED"); - result.put("serverUrl", rtspWebServerUrl); - result.put("systemInfo", response.getData()); - } else { - result.put("status", "DOWN"); - result.put("rtspWebServer", "DISCONNECTED"); - result.put("error", response.getMessage()); - } - } catch (Exception e) { - result.put("status", "DOWN"); - result.put("rtspWebServer", "ERROR"); - result.put("error", e.getMessage()); - } - - return ResponseEntity.ok(result); - } - - // 工具方法 - - private String getPlayUrl(RTSPWebClient.Camera camera, String type) { - if (camera.getPlayUrls() == null) { - return ""; - } - - switch (type.toLowerCase()) { - case "hls": - return camera.getPlayUrls().getHls(); - case "webrtc": - return camera.getPlayUrls().getWebrtc(); - case "mse": - return camera.getPlayUrls().getMse(); - case "all": - default: - return camera.getPlayUrls().getAll(); - } - } -} diff --git a/web/templates/cameras.tmpl b/web/templates/cameras.tmpl index 7f3219e..f399283 100644 --- a/web/templates/cameras.tmpl +++ b/web/templates/cameras.tmpl @@ -79,26 +79,91 @@