feat(wm-revenue): #6 营收管理平台+报装管理系统增强(审计+应用接入+报装概览/任务/查询/报表)
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
-- =============================================
|
||||
-- 智慧水务管理系统 - 营收管理平台与报装管理系统增强
|
||||
-- 版本: V4 (Issue #6)
|
||||
-- =============================================
|
||||
|
||||
-- 平台运维审计日志
|
||||
CREATE TABLE IF NOT EXISTS rev_audit_log (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id VARCHAR(50) NOT NULL,
|
||||
user_name VARCHAR(100) NOT NULL,
|
||||
action VARCHAR(50) NOT NULL, -- CREATE/UPDATE/DELETE/LOGIN/EXPORT
|
||||
target_type VARCHAR(50), -- customer/meter/bill/app
|
||||
target_id VARCHAR(50),
|
||||
detail TEXT,
|
||||
ip VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
COMMENT ON TABLE rev_audit_log IS '平台运维审计日志表';
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_user ON rev_audit_log(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_action ON rev_audit_log(action);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_created ON rev_audit_log(created_at);
|
||||
|
||||
-- 应用接入注册表
|
||||
CREATE TABLE IF NOT EXISTS rev_app_registry (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
app_id VARCHAR(50) UNIQUE NOT NULL,
|
||||
app_secret VARCHAR(100) NOT NULL,
|
||||
app_name VARCHAR(100) NOT NULL,
|
||||
redirect_uris TEXT, -- JSON array of redirect URIs
|
||||
enabled SMALLINT DEFAULT 1, -- 0:disabled 1:enabled
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
COMMENT ON TABLE rev_app_registry IS '应用接入注册表';
|
||||
CREATE INDEX IF NOT EXISTS idx_app_enabled ON rev_app_registry(enabled);
|
||||
|
||||
-- 报装任务表
|
||||
CREATE TABLE IF NOT EXISTS rev_install_task (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
task_id VARCHAR(50) UNIQUE NOT NULL,
|
||||
apply_no VARCHAR(50) NOT NULL,
|
||||
task_type VARCHAR(50) NOT NULL, -- design/construction/inspection
|
||||
assignee_id BIGINT,
|
||||
assignee_name VARCHAR(100),
|
||||
description TEXT,
|
||||
status VARCHAR(20) DEFAULT 'pending', -- pending/in_progress/completed/cancelled
|
||||
remark TEXT,
|
||||
completed_at TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
COMMENT ON TABLE rev_install_task IS '报装任务表';
|
||||
CREATE INDEX IF NOT EXISTS idx_task_apply ON rev_install_task(apply_no);
|
||||
CREATE INDEX IF NOT EXISTS idx_task_assignee ON rev_install_task(assignee_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_task_status ON rev_install_task(status);
|
||||
|
||||
-- 增强报装表(添加缺失的时间戳字段)
|
||||
ALTER TABLE rev_installation ADD COLUMN IF NOT EXISTS dispatched_at TIMESTAMP;
|
||||
ALTER TABLE rev_installation ADD COLUMN IF NOT EXISTS construction_started_at TIMESTAMP;
|
||||
ALTER TABLE rev_installation ADD COLUMN IF NOT EXISTS customer_type VARCHAR(20);
|
||||
|
||||
COMMENT ON COLUMN rev_installation.dispatched_at IS '派单时间';
|
||||
COMMENT ON COLUMN rev_installation.construction_started_at IS '施工开始时间';
|
||||
COMMENT ON COLUMN rev_installation.customer_type IS '客户类型';
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.water.revenue.controller;
|
||||
|
||||
import com.water.common.core.result.R;
|
||||
import com.water.revenue.service.AppAccessService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Tag(name = "应用接入管理")
|
||||
@RestController
|
||||
@RequestMapping("/revenue/apps")
|
||||
@RequiredArgsConstructor
|
||||
public class AppAccessController {
|
||||
|
||||
private final AppAccessService appAccessService;
|
||||
|
||||
@Operation(summary = "注册应用")
|
||||
@PostMapping
|
||||
public R<Map<String, Object>> registerApp(@RequestBody Map<String, Object> request) {
|
||||
String appName = (String) request.get("appName");
|
||||
String redirectUris = (String) request.get("redirectUris");
|
||||
return R.ok(appAccessService.registerApp(appName, redirectUris));
|
||||
}
|
||||
|
||||
@Operation(summary = "启用/禁用应用")
|
||||
@PutMapping("/{appId}/toggle")
|
||||
public R<Map<String, Object>> toggleApp(
|
||||
@PathVariable String appId,
|
||||
@RequestBody Map<String, Object> request) {
|
||||
Boolean enabled = (Boolean) request.get("enabled");
|
||||
return R.ok(appAccessService.toggleApp(appId, enabled));
|
||||
}
|
||||
|
||||
@Operation(summary = "应用列表")
|
||||
@GetMapping
|
||||
public R<List<Map<String, Object>>> listApps(
|
||||
@RequestParam(required = false) String keyword,
|
||||
@RequestParam(defaultValue = "1") Integer page,
|
||||
@RequestParam(defaultValue = "10") Integer size) {
|
||||
return R.ok(appAccessService.listApps(keyword, page, size));
|
||||
}
|
||||
|
||||
@Operation(summary = "应用详情")
|
||||
@GetMapping("/{appId}")
|
||||
public R<Map<String, Object>> getAppDetail(@PathVariable String appId) {
|
||||
return R.ok(appAccessService.getAppDetail(appId));
|
||||
}
|
||||
|
||||
@Operation(summary = "删除应用")
|
||||
@DeleteMapping("/{appId}")
|
||||
public R<Void> deleteApp(@PathVariable String appId) {
|
||||
appAccessService.deleteApp(appId);
|
||||
return R.ok();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.water.revenue.controller;
|
||||
|
||||
import com.water.common.core.result.R;
|
||||
import com.water.revenue.service.InstallReportService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Tag(name = "报装报表分析")
|
||||
@RestController
|
||||
@RequestMapping("/revenue/install/reports")
|
||||
@RequiredArgsConstructor
|
||||
public class InstallReportController {
|
||||
|
||||
private final InstallReportService installReportService;
|
||||
|
||||
@Operation(summary = "报装周期分析")
|
||||
@GetMapping("/cycle")
|
||||
public R<Map<String, Object>> cycleAnalysis(
|
||||
@RequestParam String startDate,
|
||||
@RequestParam String endDate) {
|
||||
return R.ok(installReportService.cycleAnalysis(startDate, endDate));
|
||||
}
|
||||
|
||||
@Operation(summary = "区域分布统计")
|
||||
@GetMapping("/area")
|
||||
public R<List<Map<String, Object>>> areaDistribution(
|
||||
@RequestParam String startDate,
|
||||
@RequestParam String endDate) {
|
||||
return R.ok(installReportService.areaDistribution(startDate, endDate));
|
||||
}
|
||||
|
||||
@Operation(summary = "月度趋势")
|
||||
@GetMapping("/monthly")
|
||||
public R<List<Map<String, Object>>> monthlyTrend(@RequestParam(defaultValue = "2026") String year) {
|
||||
return R.ok(installReportService.monthlyTrend(year));
|
||||
}
|
||||
|
||||
@Operation(summary = "客户类型分布")
|
||||
@GetMapping("/customer-type")
|
||||
public R<List<Map<String, Object>>> customerTypeDistribution(
|
||||
@RequestParam String startDate,
|
||||
@RequestParam String endDate) {
|
||||
return R.ok(installReportService.customerTypeDistribution(startDate, endDate));
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package com.water.revenue.controller;
|
||||
|
||||
import com.water.common.core.result.R;
|
||||
import com.water.revenue.service.InstallationOverviewService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Tag(name = "报装首页概览")
|
||||
@RestController
|
||||
@RequestMapping("/revenue/install/overview")
|
||||
@RequiredArgsConstructor
|
||||
public class InstallationOverviewController {
|
||||
|
||||
private final InstallationOverviewService installationOverviewService;
|
||||
|
||||
@Operation(summary = "概览数据")
|
||||
@GetMapping
|
||||
public R<Map<String, Object>> getOverview() {
|
||||
return R.ok(installationOverviewService.getOverview());
|
||||
}
|
||||
|
||||
@Operation(summary = "按月统计")
|
||||
@GetMapping("/stats/month")
|
||||
public R<List<Map<String, Object>>> statsByMonth(@RequestParam(defaultValue = "2026") String year) {
|
||||
return R.ok(installationOverviewService.statsByMonth(year));
|
||||
}
|
||||
|
||||
@Operation(summary = "按区域统计")
|
||||
@GetMapping("/stats/area")
|
||||
public R<List<Map<String, Object>>> statsByArea() {
|
||||
return R.ok(installationOverviewService.statsByArea());
|
||||
}
|
||||
|
||||
@Operation(summary = "转化率分析")
|
||||
@GetMapping("/conversion")
|
||||
public R<Map<String, Object>> conversionAnalysis() {
|
||||
return R.ok(installationOverviewService.conversionAnalysis());
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package com.water.revenue.controller;
|
||||
|
||||
import com.water.common.core.result.R;
|
||||
import com.water.revenue.service.InstallationQueryService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Tag(name = "报装综合查询")
|
||||
@RestController
|
||||
@RequestMapping("/revenue/install/query")
|
||||
@RequiredArgsConstructor
|
||||
public class InstallationQueryController {
|
||||
|
||||
private final InstallationQueryService installationQueryService;
|
||||
|
||||
@Operation(summary = "综合查询")
|
||||
@GetMapping
|
||||
public R<Map<String, Object>> query(
|
||||
@RequestParam(required = false) String startDate,
|
||||
@RequestParam(required = false) String endDate,
|
||||
@RequestParam(required = false) String status,
|
||||
@RequestParam(required = false) String area,
|
||||
@RequestParam(required = false) String customerType,
|
||||
@RequestParam(required = false) String keyword,
|
||||
@RequestParam(defaultValue = "1") Integer page,
|
||||
@RequestParam(defaultValue = "10") Integer size) {
|
||||
return R.ok(installationQueryService.query(startDate, endDate, status, area, customerType, keyword, page, size));
|
||||
}
|
||||
|
||||
@Operation(summary = "导出CSV")
|
||||
@GetMapping("/export")
|
||||
public R<List<Map<String, Object>>> exportCsv(
|
||||
@RequestParam(required = false) String startDate,
|
||||
@RequestParam(required = false) String endDate,
|
||||
@RequestParam(required = false) String status,
|
||||
@RequestParam(required = false) String area,
|
||||
@RequestParam(required = false) String customerType) {
|
||||
return R.ok(installationQueryService.exportCsv(startDate, endDate, status, area, customerType));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.water.revenue.controller;
|
||||
|
||||
import com.water.common.core.result.R;
|
||||
import com.water.revenue.service.InstallationTaskService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Tag(name = "报装任务管理")
|
||||
@RestController
|
||||
@RequestMapping("/revenue/install/tasks")
|
||||
@RequiredArgsConstructor
|
||||
public class InstallationTaskController {
|
||||
|
||||
private final InstallationTaskService installationTaskService;
|
||||
|
||||
@Operation(summary = "创建任务")
|
||||
@PostMapping
|
||||
public R<Map<String, Object>> createTask(@RequestBody Map<String, Object> request) {
|
||||
String applyNo = (String) request.get("applyNo");
|
||||
String taskType = (String) request.get("taskType");
|
||||
Long assigneeId = Long.parseLong(String.valueOf(request.get("assigneeId")));
|
||||
String assigneeName = (String) request.get("assigneeName");
|
||||
String description = (String) request.get("description");
|
||||
return R.ok(installationTaskService.createTask(applyNo, taskType, assigneeId, assigneeName, description));
|
||||
}
|
||||
|
||||
@Operation(summary = "更新任务状态")
|
||||
@PutMapping("/{taskId}/status")
|
||||
public R<Map<String, Object>> updateTaskStatus(
|
||||
@PathVariable String taskId,
|
||||
@RequestBody Map<String, Object> request) {
|
||||
String status = (String) request.get("status");
|
||||
String remark = (String) request.get("remark");
|
||||
return R.ok(installationTaskService.updateTaskStatus(taskId, status, remark));
|
||||
}
|
||||
|
||||
@Operation(summary = "任务列表")
|
||||
@GetMapping
|
||||
public R<List<Map<String, Object>>> listTasks(
|
||||
@RequestParam(required = false) String applyNo,
|
||||
@RequestParam(required = false) String assigneeName,
|
||||
@RequestParam(required = false) String status,
|
||||
@RequestParam(defaultValue = "1") Integer page,
|
||||
@RequestParam(defaultValue = "10") Integer size) {
|
||||
return R.ok(installationTaskService.listTasks(applyNo, assigneeName, status, page, size));
|
||||
}
|
||||
|
||||
@Operation(summary = "任务看板")
|
||||
@GetMapping("/kanban")
|
||||
public R<Map<String, Object>> kanban() {
|
||||
return R.ok(installationTaskService.kanban());
|
||||
}
|
||||
|
||||
@Operation(summary = "任务详情")
|
||||
@GetMapping("/{taskId}")
|
||||
public R<Map<String, Object>> getTaskDetail(@PathVariable String taskId) {
|
||||
return R.ok(installationTaskService.getTaskDetail(taskId));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.water.revenue.controller;
|
||||
|
||||
import com.water.common.core.result.R;
|
||||
import com.water.revenue.service.RevAuditService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Tag(name = "平台运维审计")
|
||||
@RestController
|
||||
@RequestMapping("/revenue/audit")
|
||||
@RequiredArgsConstructor
|
||||
public class RevAuditController {
|
||||
|
||||
private final RevAuditService revAuditService;
|
||||
|
||||
@Operation(summary = "查询审计日志")
|
||||
@GetMapping("/logs")
|
||||
public R<Map<String, Object>> queryLogs(
|
||||
@RequestParam(required = false) String userId,
|
||||
@RequestParam(required = false) String action,
|
||||
@RequestParam(required = false) String startDate,
|
||||
@RequestParam(required = false) String endDate,
|
||||
@RequestParam(defaultValue = "1") Integer page,
|
||||
@RequestParam(defaultValue = "10") Integer size) {
|
||||
return R.ok(revAuditService.queryLogs(userId, action, startDate, endDate, page, size));
|
||||
}
|
||||
|
||||
@Operation(summary = "按天统计")
|
||||
@GetMapping("/stats/day")
|
||||
public R<List<Map<String, Object>>> statsByDay(
|
||||
@RequestParam String startDate,
|
||||
@RequestParam String endDate) {
|
||||
return R.ok(revAuditService.statsByDay(startDate, endDate));
|
||||
}
|
||||
|
||||
@Operation(summary = "按操作类型统计")
|
||||
@GetMapping("/stats/action")
|
||||
public R<List<Map<String, Object>>> statsByAction(
|
||||
@RequestParam String startDate,
|
||||
@RequestParam String endDate) {
|
||||
return R.ok(revAuditService.statsByAction(startDate, endDate));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package com.water.revenue.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AppAccessService {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
/**
|
||||
* 注册应用
|
||||
*/
|
||||
public Map<String, Object> registerApp(String appName, String redirectUris) {
|
||||
String appId = "APP-" + System.currentTimeMillis();
|
||||
String appSecret = UUID.randomUUID().toString().replace("-", "");
|
||||
|
||||
jdbcTemplate.update(
|
||||
"INSERT INTO rev_app_registry (app_id, app_secret, app_name, redirect_uris, enabled, created_at) VALUES (?,?,?,?,?,?)",
|
||||
appId, appSecret, appName, redirectUris, 1, new java.sql.Timestamp(System.currentTimeMillis())
|
||||
);
|
||||
|
||||
log.info("App registered: {} ({})", appName, appId);
|
||||
return Map.of(
|
||||
"appId", appId,
|
||||
"appSecret", appSecret,
|
||||
"appName", appName,
|
||||
"redirectUris", redirectUris
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用/禁用应用
|
||||
*/
|
||||
public Map<String, Object> toggleApp(String appId, Boolean enabled) {
|
||||
int updated = jdbcTemplate.update(
|
||||
"UPDATE rev_app_registry SET enabled = ? WHERE app_id = ?",
|
||||
enabled ? 1 : 0, appId
|
||||
);
|
||||
|
||||
if (updated == 0) {
|
||||
throw new RuntimeException("App not found: " + appId);
|
||||
}
|
||||
|
||||
log.info("App {} {}", appId, enabled ? "enabled" : "disabled");
|
||||
return Map.of("appId", appId, "enabled", enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询应用列表
|
||||
*/
|
||||
public List<Map<String, Object>> listApps(String keyword, Integer page, Integer size) {
|
||||
StringBuilder sql = new StringBuilder("SELECT * FROM rev_app_registry WHERE 1=1");
|
||||
List<Object> params = new ArrayList<>();
|
||||
|
||||
if (keyword != null && !keyword.isEmpty()) {
|
||||
sql.append(" AND (app_name LIKE ? OR app_id LIKE ?)");
|
||||
params.add("%" + keyword + "%");
|
||||
params.add("%" + keyword + "%");
|
||||
}
|
||||
|
||||
sql.append(" ORDER BY created_at DESC LIMIT ? OFFSET ?");
|
||||
params.add(size);
|
||||
params.add((page - 1) * size);
|
||||
|
||||
return jdbcTemplate.queryForList(sql.toString(), params.toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询应用详情
|
||||
*/
|
||||
public Map<String, Object> getAppDetail(String appId) {
|
||||
List<Map<String, Object>> apps = jdbcTemplate.queryForList(
|
||||
"SELECT * FROM rev_app_registry WHERE app_id = ?",
|
||||
appId
|
||||
);
|
||||
|
||||
if (apps.isEmpty()) {
|
||||
throw new RuntimeException("App not found: " + appId);
|
||||
}
|
||||
|
||||
return apps.get(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除应用
|
||||
*/
|
||||
public void deleteApp(String appId) {
|
||||
int deleted = jdbcTemplate.update("DELETE FROM rev_app_registry WHERE app_id = ?", appId);
|
||||
if (deleted == 0) {
|
||||
throw new RuntimeException("App not found: " + appId);
|
||||
}
|
||||
log.info("App deleted: {}", appId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package com.water.revenue.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class InstallReportService {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
/**
|
||||
* 报装周期分析(平均天数、各环节耗时)
|
||||
*/
|
||||
public Map<String, Object> cycleAnalysis(String startDate, String endDate) {
|
||||
// 总体平均完成天数
|
||||
Double avgTotalDays = jdbcTemplate.queryForObject(
|
||||
"SELECT AVG(DATEDIFF(completed_at, created_at)) FROM rev_installation " +
|
||||
"WHERE status = 'completed' AND created_at >= ? AND created_at <= ?",
|
||||
Double.class,
|
||||
startDate + " 00:00:00", endDate + " 23:59:59"
|
||||
);
|
||||
|
||||
// 各环节平均耗时(预受理→派单)
|
||||
Double avgPreAcceptToDispatch = jdbcTemplate.queryForObject(
|
||||
"SELECT AVG(DATEDIFF(dispatched_at, created_at)) FROM rev_installation " +
|
||||
"WHERE dispatched_at IS NOT NULL AND created_at >= ? AND created_at <= ?",
|
||||
Double.class,
|
||||
startDate + " 00:00:00", endDate + " 23:59:59"
|
||||
);
|
||||
|
||||
// 各环节平均耗时(派单→施工)
|
||||
Double avgDispatchToConstruction = jdbcTemplate.queryForObject(
|
||||
"SELECT AVG(DATEDIFF(construction_started_at, dispatched_at)) FROM rev_installation " +
|
||||
"WHERE construction_started_at IS NOT NULL AND dispatched_at IS NOT NULL " +
|
||||
"AND created_at >= ? AND created_at <= ?",
|
||||
Double.class,
|
||||
startDate + " 00:00:00", endDate + " 23:59:59"
|
||||
);
|
||||
|
||||
// 各环节平均耗时(施工→竣工)
|
||||
Double avgConstructionToComplete = jdbcTemplate.queryForObject(
|
||||
"SELECT AVG(DATEDIFF(completed_at, construction_started_at)) FROM rev_installation " +
|
||||
"WHERE completed_at IS NOT NULL AND construction_started_at IS NOT NULL " +
|
||||
"AND created_at >= ? AND created_at <= ?",
|
||||
Double.class,
|
||||
startDate + " 00:00:00", endDate + " 23:59:59"
|
||||
);
|
||||
|
||||
return Map.of(
|
||||
"avgTotalDays", avgTotalDays != null ? Math.round(avgTotalDays * 100.0) / 100.0 : 0,
|
||||
"avgPreAcceptToDispatch", avgPreAcceptToDispatch != null ? Math.round(avgPreAcceptToDispatch * 100.0) / 100.0 : 0,
|
||||
"avgDispatchToConstruction", avgDispatchToConstruction != null ? Math.round(avgDispatchToConstruction * 100.0) / 100.0 : 0,
|
||||
"avgConstructionToComplete", avgConstructionToComplete != null ? Math.round(avgConstructionToComplete * 100.0) / 100.0 : 0
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 区域分布统计
|
||||
*/
|
||||
public List<Map<String, Object>> areaDistribution(String startDate, String endDate) {
|
||||
return jdbcTemplate.queryForList(
|
||||
"SELECT area, COUNT(*) as total, " +
|
||||
"SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed, " +
|
||||
"AVG(CASE WHEN status = 'completed' THEN DATEDIFF(completed_at, created_at) ELSE NULL END) as avg_days " +
|
||||
"FROM rev_installation " +
|
||||
"WHERE created_at >= ? AND created_at <= ? " +
|
||||
"GROUP BY area ORDER BY total DESC",
|
||||
startDate + " 00:00:00", endDate + " 23:59:59"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 月度趋势
|
||||
*/
|
||||
public List<Map<String, Object>> monthlyTrend(String year) {
|
||||
return jdbcTemplate.queryForList(
|
||||
"SELECT DATE_FORMAT(created_at, '%Y-%m') as month, " +
|
||||
"COUNT(*) as total, " +
|
||||
"SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed, " +
|
||||
"SUM(CASE WHEN status = 'cancelled' THEN 1 ELSE 0 END) as cancelled " +
|
||||
"FROM rev_installation WHERE YEAR(created_at) = ? " +
|
||||
"GROUP BY DATE_FORMAT(created_at, '%Y-%m') ORDER BY month",
|
||||
year
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户类型分布
|
||||
*/
|
||||
public List<Map<String, Object>> customerTypeDistribution(String startDate, String endDate) {
|
||||
return jdbcTemplate.queryForList(
|
||||
"SELECT customer_type, COUNT(*) as count FROM rev_installation " +
|
||||
"WHERE created_at >= ? AND created_at <= ? " +
|
||||
"GROUP BY customer_type ORDER BY count DESC",
|
||||
startDate + " 00:00:00", endDate + " 23:59:59"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package com.water.revenue.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.YearMonth;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class InstallationOverviewService {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
/**
|
||||
* 首页概览数据
|
||||
*/
|
||||
public Map<String, Object> getOverview() {
|
||||
// 总数
|
||||
Integer total = jdbcTemplate.queryForObject(
|
||||
"SELECT COUNT(*) FROM rev_installation", Integer.class
|
||||
);
|
||||
|
||||
// 待处理
|
||||
Integer pending = jdbcTemplate.queryForObject(
|
||||
"SELECT COUNT(*) FROM rev_installation WHERE status = 'pending'", Integer.class
|
||||
);
|
||||
|
||||
// 进行中
|
||||
Integer inProgress = jdbcTemplate.queryForObject(
|
||||
"SELECT COUNT(*) FROM rev_installation WHERE status IN ('dispatched', 'in_progress')", Integer.class
|
||||
);
|
||||
|
||||
// 本月完成
|
||||
String currentMonth = YearMonth.now().format(DateTimeFormatter.ofPattern("yyyy-MM"));
|
||||
Integer completedThisMonth = jdbcTemplate.queryForObject(
|
||||
"SELECT COUNT(*) FROM rev_installation WHERE status = 'completed' AND DATE_FORMAT(completed_at, '%Y-%m') = ?",
|
||||
Integer.class, currentMonth
|
||||
);
|
||||
|
||||
// 平均完成天数
|
||||
Double avgDays = jdbcTemplate.queryForObject(
|
||||
"SELECT AVG(DATEDIFF(completed_at, created_at)) FROM rev_installation WHERE status = 'completed' AND completed_at IS NOT NULL",
|
||||
Double.class
|
||||
);
|
||||
|
||||
return Map.of(
|
||||
"total", total != null ? total : 0,
|
||||
"pending", pending != null ? pending : 0,
|
||||
"inProgress", inProgress != null ? inProgress : 0,
|
||||
"completedThisMonth", completedThisMonth != null ? completedThisMonth : 0,
|
||||
"avgDays", avgDays != null ? Math.round(avgDays * 100.0) / 100.0 : 0
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按月统计
|
||||
*/
|
||||
public List<Map<String, Object>> statsByMonth(String year) {
|
||||
return jdbcTemplate.queryForList(
|
||||
"SELECT DATE_FORMAT(created_at, '%Y-%m') as month, COUNT(*) as count, " +
|
||||
"SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed " +
|
||||
"FROM rev_installation WHERE YEAR(created_at) = ? " +
|
||||
"GROUP BY DATE_FORMAT(created_at, '%Y-%m') ORDER BY month",
|
||||
year
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按区域统计
|
||||
*/
|
||||
public List<Map<String, Object>> statsByArea() {
|
||||
return jdbcTemplate.queryForList(
|
||||
"SELECT area, COUNT(*) as total, " +
|
||||
"SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed, " +
|
||||
"SUM(CASE WHEN status IN ('pending', 'dispatched', 'in_progress') THEN 1 ELSE 0 END) as in_progress " +
|
||||
"FROM rev_installation GROUP BY area ORDER BY total DESC"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转化率分析
|
||||
*/
|
||||
public Map<String, Object> conversionAnalysis() {
|
||||
Integer total = jdbcTemplate.queryForObject(
|
||||
"SELECT COUNT(*) FROM rev_installation", Integer.class
|
||||
);
|
||||
Integer completed = jdbcTemplate.queryForObject(
|
||||
"SELECT COUNT(*) FROM rev_installation WHERE status = 'completed'", Integer.class
|
||||
);
|
||||
Integer cancelled = jdbcTemplate.queryForObject(
|
||||
"SELECT COUNT(*) FROM rev_installation WHERE status = 'cancelled'", Integer.class
|
||||
);
|
||||
|
||||
double conversionRate = total != null && total > 0 && completed != null ?
|
||||
(completed * 100.0 / total) : 0;
|
||||
double cancelRate = total != null && total > 0 && cancelled != null ?
|
||||
(cancelled * 100.0 / total) : 0;
|
||||
|
||||
return Map.of(
|
||||
"total", total != null ? total : 0,
|
||||
"completed", completed != null ? completed : 0,
|
||||
"cancelled", cancelled != null ? cancelled : 0,
|
||||
"conversionRate", Math.round(conversionRate * 100.0) / 100.0,
|
||||
"cancelRate", Math.round(cancelRate * 100.0) / 100.0
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package com.water.revenue.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class InstallationQueryService {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
/**
|
||||
* 综合查询(多条件组合)
|
||||
*/
|
||||
public Map<String, Object> query(String startDate, String endDate, String status, String area,
|
||||
String customerType, String keyword, Integer page, Integer size) {
|
||||
StringBuilder sql = new StringBuilder("SELECT * FROM rev_installation WHERE 1=1");
|
||||
StringBuilder countSql = new StringBuilder("SELECT COUNT(*) FROM rev_installation WHERE 1=1");
|
||||
List<Object> params = new ArrayList<>();
|
||||
List<Object> countParams = new ArrayList<>();
|
||||
|
||||
if (startDate != null && !startDate.isEmpty()) {
|
||||
sql.append(" AND created_at >= ?");
|
||||
countSql.append(" AND created_at >= ?");
|
||||
params.add(startDate + " 00:00:00");
|
||||
countParams.add(startDate + " 00:00:00");
|
||||
}
|
||||
if (endDate != null && !endDate.isEmpty()) {
|
||||
sql.append(" AND created_at <= ?");
|
||||
countSql.append(" AND created_at <= ?");
|
||||
params.add(endDate + " 23:59:59");
|
||||
countParams.add(endDate + " 23:59:59");
|
||||
}
|
||||
if (status != null && !status.isEmpty()) {
|
||||
sql.append(" AND status = ?");
|
||||
countSql.append(" AND status = ?");
|
||||
params.add(status);
|
||||
countParams.add(status);
|
||||
}
|
||||
if (area != null && !area.isEmpty()) {
|
||||
sql.append(" AND area = ?");
|
||||
countSql.append(" AND area = ?");
|
||||
params.add(area);
|
||||
countParams.add(area);
|
||||
}
|
||||
if (customerType != null && !customerType.isEmpty()) {
|
||||
sql.append(" AND customer_type = ?");
|
||||
countSql.append(" AND customer_type = ?");
|
||||
params.add(customerType);
|
||||
countParams.add(customerType);
|
||||
}
|
||||
if (keyword != null && !keyword.isEmpty()) {
|
||||
sql.append(" AND (apply_no LIKE ? OR applicant_name LIKE ? OR address LIKE ?)");
|
||||
countSql.append(" AND (apply_no LIKE ? OR applicant_name LIKE ? OR address LIKE ?)");
|
||||
params.add("%" + keyword + "%");
|
||||
params.add("%" + keyword + "%");
|
||||
params.add("%" + keyword + "%");
|
||||
countParams.add("%" + keyword + "%");
|
||||
countParams.add("%" + keyword + "%");
|
||||
countParams.add("%" + keyword + "%");
|
||||
}
|
||||
|
||||
Integer total = jdbcTemplate.queryForObject(countSql.toString(), Integer.class, countParams.toArray());
|
||||
|
||||
sql.append(" ORDER BY created_at DESC LIMIT ? OFFSET ?");
|
||||
params.add(size);
|
||||
params.add((page - 1) * size);
|
||||
|
||||
List<Map<String, Object>> data = jdbcTemplate.queryForList(sql.toString(), params.toArray());
|
||||
|
||||
return Map.of(
|
||||
"total", total != null ? total : 0,
|
||||
"page", page,
|
||||
"size", size,
|
||||
"data", data
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出CSV(返回数据列表)
|
||||
*/
|
||||
public List<Map<String, Object>> exportCsv(String startDate, String endDate, String status,
|
||||
String area, String customerType) {
|
||||
StringBuilder sql = new StringBuilder("SELECT * FROM rev_installation WHERE 1=1");
|
||||
List<Object> params = new ArrayList<>();
|
||||
|
||||
if (startDate != null && !startDate.isEmpty()) {
|
||||
sql.append(" AND created_at >= ?");
|
||||
params.add(startDate + " 00:00:00");
|
||||
}
|
||||
if (endDate != null && !endDate.isEmpty()) {
|
||||
sql.append(" AND created_at <= ?");
|
||||
params.add(endDate + " 23:59:59");
|
||||
}
|
||||
if (status != null && !status.isEmpty()) {
|
||||
sql.append(" AND status = ?");
|
||||
params.add(status);
|
||||
}
|
||||
if (area != null && !area.isEmpty()) {
|
||||
sql.append(" AND area = ?");
|
||||
params.add(area);
|
||||
}
|
||||
if (customerType != null && !customerType.isEmpty()) {
|
||||
sql.append(" AND customer_type = ?");
|
||||
params.add(customerType);
|
||||
}
|
||||
|
||||
sql.append(" ORDER BY created_at DESC LIMIT 10000");
|
||||
|
||||
return jdbcTemplate.queryForList(sql.toString(), params.toArray());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package com.water.revenue.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class InstallationTaskService {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
/**
|
||||
* 创建任务
|
||||
*/
|
||||
public Map<String, Object> createTask(String applyNo, String taskType, Long assigneeId, String assigneeName, String description) {
|
||||
String taskId = "TASK-" + System.currentTimeMillis();
|
||||
|
||||
jdbcTemplate.update(
|
||||
"INSERT INTO rev_install_task (task_id, apply_no, task_type, assignee_id, assignee_name, description, status, created_at) " +
|
||||
"VALUES (?,?,?,?,?,?,?,?)",
|
||||
taskId, applyNo, taskType, assigneeId, assigneeName, description, "pending",
|
||||
new java.sql.Timestamp(System.currentTimeMillis())
|
||||
);
|
||||
|
||||
log.info("Task created: {} for {}", taskId, applyNo);
|
||||
return Map.of(
|
||||
"taskId", taskId,
|
||||
"applyNo", applyNo,
|
||||
"taskType", taskType,
|
||||
"assigneeName", assigneeName
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新任务状态
|
||||
*/
|
||||
public Map<String, Object> updateTaskStatus(String taskId, String status, String remark) {
|
||||
StringBuilder sql = new StringBuilder("UPDATE rev_install_task SET status = ?");
|
||||
List<Object> params = new ArrayList<>();
|
||||
params.add(status);
|
||||
|
||||
if ("completed".equals(status)) {
|
||||
sql.append(", completed_at = ?");
|
||||
params.add(new java.sql.Timestamp(System.currentTimeMillis()));
|
||||
}
|
||||
|
||||
if (remark != null && !remark.isEmpty()) {
|
||||
sql.append(", remark = ?");
|
||||
params.add(remark);
|
||||
}
|
||||
|
||||
sql.append(" WHERE task_id = ?");
|
||||
params.add(taskId);
|
||||
|
||||
int updated = jdbcTemplate.update(sql.toString(), params.toArray());
|
||||
if (updated == 0) {
|
||||
throw new RuntimeException("Task not found: " + taskId);
|
||||
}
|
||||
|
||||
log.info("Task {} status updated to {}", taskId, status);
|
||||
return Map.of("taskId", taskId, "status", status);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询任务列表
|
||||
*/
|
||||
public List<Map<String, Object>> listTasks(String applyNo, String assigneeName, String status, Integer page, Integer size) {
|
||||
StringBuilder sql = new StringBuilder("SELECT * FROM rev_install_task WHERE 1=1");
|
||||
List<Object> params = new ArrayList<>();
|
||||
|
||||
if (applyNo != null && !applyNo.isEmpty()) {
|
||||
sql.append(" AND apply_no = ?");
|
||||
params.add(applyNo);
|
||||
}
|
||||
if (assigneeName != null && !assigneeName.isEmpty()) {
|
||||
sql.append(" AND assignee_name LIKE ?");
|
||||
params.add("%" + assigneeName + "%");
|
||||
}
|
||||
if (status != null && !status.isEmpty()) {
|
||||
sql.append(" AND status = ?");
|
||||
params.add(status);
|
||||
}
|
||||
|
||||
sql.append(" ORDER BY created_at DESC LIMIT ? OFFSET ?");
|
||||
params.add(size);
|
||||
params.add((page - 1) * size);
|
||||
|
||||
return jdbcTemplate.queryForList(sql.toString(), params.toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* 任务看板(按状态分组)
|
||||
*/
|
||||
public Map<String, Object> kanban() {
|
||||
List<Map<String, Object>> pending = jdbcTemplate.queryForList(
|
||||
"SELECT * FROM rev_install_task WHERE status = 'pending' ORDER BY created_at"
|
||||
);
|
||||
List<Map<String, Object>> inProgress = jdbcTemplate.queryForList(
|
||||
"SELECT * FROM rev_install_task WHERE status = 'in_progress' ORDER BY created_at"
|
||||
);
|
||||
List<Map<String, Object>> completed = jdbcTemplate.queryForList(
|
||||
"SELECT * FROM rev_install_task WHERE status = 'completed' ORDER BY completed_at DESC LIMIT 50"
|
||||
);
|
||||
|
||||
return Map.of(
|
||||
"pending", pending,
|
||||
"inProgress", inProgress,
|
||||
"completed", completed
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 任务详情
|
||||
*/
|
||||
public Map<String, Object> getTaskDetail(String taskId) {
|
||||
List<Map<String, Object>> tasks = jdbcTemplate.queryForList(
|
||||
"SELECT * FROM rev_install_task WHERE task_id = ?",
|
||||
taskId
|
||||
);
|
||||
|
||||
if (tasks.isEmpty()) {
|
||||
throw new RuntimeException("Task not found: " + taskId);
|
||||
}
|
||||
|
||||
return tasks.get(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package com.water.revenue.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class RevAuditService {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
/**
|
||||
* 记录审计日志
|
||||
*/
|
||||
public void log(String userId, String userName, String action, String targetType, String targetId, String detail, String ip) {
|
||||
jdbcTemplate.update(
|
||||
"INSERT INTO rev_audit_log (user_id, user_name, action, target_type, target_id, detail, ip, created_at) VALUES (?,?,?,?,?,?,?,?)",
|
||||
userId, userName, action, targetType, targetId, detail, ip, new java.sql.Timestamp(System.currentTimeMillis())
|
||||
);
|
||||
log.debug("Audit log: {} {} {} {}", userName, action, targetType, targetId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询审计日志(分页+过滤)
|
||||
*/
|
||||
public Map<String, Object> queryLogs(String userId, String action, String startDate, String endDate, Integer page, Integer size) {
|
||||
StringBuilder sql = new StringBuilder("SELECT * FROM rev_audit_log WHERE 1=1");
|
||||
List<Object> params = new ArrayList<>();
|
||||
|
||||
if (userId != null && !userId.isEmpty()) {
|
||||
sql.append(" AND user_id = ?");
|
||||
params.add(userId);
|
||||
}
|
||||
if (action != null && !action.isEmpty()) {
|
||||
sql.append(" AND action = ?");
|
||||
params.add(action);
|
||||
}
|
||||
if (startDate != null && !startDate.isEmpty()) {
|
||||
sql.append(" AND created_at >= ?");
|
||||
params.add(startDate + " 00:00:00");
|
||||
}
|
||||
if (endDate != null && !endDate.isEmpty()) {
|
||||
sql.append(" AND created_at <= ?");
|
||||
params.add(endDate + " 23:59:59");
|
||||
}
|
||||
|
||||
sql.append(" ORDER BY created_at DESC LIMIT ? OFFSET ?");
|
||||
params.add(size);
|
||||
params.add((page - 1) * size);
|
||||
|
||||
List<Map<String, Object>> logs = jdbcTemplate.queryForList(sql.toString(), params.toArray());
|
||||
|
||||
// 查询总数
|
||||
StringBuilder countSql = new StringBuilder("SELECT COUNT(*) FROM rev_audit_log WHERE 1=1");
|
||||
List<Object> countParams = new ArrayList<>();
|
||||
if (userId != null && !userId.isEmpty()) {
|
||||
countSql.append(" AND user_id = ?");
|
||||
countParams.add(userId);
|
||||
}
|
||||
if (action != null && !action.isEmpty()) {
|
||||
countSql.append(" AND action = ?");
|
||||
countParams.add(action);
|
||||
}
|
||||
if (startDate != null && !startDate.isEmpty()) {
|
||||
countSql.append(" AND created_at >= ?");
|
||||
countParams.add(startDate + " 00:00:00");
|
||||
}
|
||||
if (endDate != null && !endDate.isEmpty()) {
|
||||
countSql.append(" AND created_at <= ?");
|
||||
countParams.add(endDate + " 23:59:59");
|
||||
}
|
||||
|
||||
Integer total = jdbcTemplate.queryForObject(countSql.toString(), Integer.class, countParams.toArray());
|
||||
|
||||
return Map.of(
|
||||
"total", total != null ? total : 0,
|
||||
"page", page,
|
||||
"size", size,
|
||||
"data", logs
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 审计统计(按天)
|
||||
*/
|
||||
public List<Map<String, Object>> statsByDay(String startDate, String endDate) {
|
||||
return jdbcTemplate.queryForList(
|
||||
"SELECT DATE(created_at) as day, COUNT(*) as count FROM rev_audit_log " +
|
||||
"WHERE created_at >= ? AND created_at <= ? " +
|
||||
"GROUP BY DATE(created_at) ORDER BY day DESC",
|
||||
startDate + " 00:00:00", endDate + " 23:59:59"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 审计统计(按操作类型)
|
||||
*/
|
||||
public List<Map<String, Object>> statsByAction(String startDate, String endDate) {
|
||||
return jdbcTemplate.queryForList(
|
||||
"SELECT action, COUNT(*) as count FROM rev_audit_log " +
|
||||
"WHERE created_at >= ? AND created_at <= ? " +
|
||||
"GROUP BY action ORDER BY count DESC",
|
||||
startDate + " 00:00:00", endDate + " 23:59:59"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user