diff --git a/.gitignore b/.gitignore index 360ee96..8af7ad5 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,9 @@ *.swp *.swo .DS_Store +.classpath +.project +.settings/ # Maven **/target/ @@ -19,7 +22,12 @@ # Env .env.local .env.*.local +deploy/docker/.env +deploy/docker/.current-version +deploy/docker/.last-version # Build *.war *.jar +sql/ +docs.zip diff --git a/backend/xiaoqu-intellectual-public/src/main/java/xiaoqu/home/open/constant/BusinessRules.java b/backend/xiaoqu-intellectual-public/src/main/java/xiaoqu/home/open/constant/BusinessRules.java new file mode 100644 index 0000000..3aa62ae --- /dev/null +++ b/backend/xiaoqu-intellectual-public/src/main/java/xiaoqu/home/open/constant/BusinessRules.java @@ -0,0 +1,281 @@ +package xiaoqu.home.open.constant; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * 业务规则注册表. + * + * 所有核心业务规则在此集中定义,作为系统业务逻辑的"单一事实来源"。 + * + * [PROTECTED] 此文件为红区文件,AI 不可直接修改。 + * 修改需审批人:项目经理 + 技术负责人。 + * + * 使用方式: + * 1. 业务代码中引用此处常量,禁止硬编码业务规则值 + * 2. 系统启动时调用 validateAll() 校验规则完整性 + * 3. 新增业务规则须同步添加校验方法和单元测试 + * + * @author SmartClean Team + * @since 2026-04-15 + */ +public final class BusinessRules { + + private BusinessRules() { + throw new UnsupportedOperationException("常量类不可实例化"); + } + + // ===================================================================== + // 一、工资计算规则 (BR-SALARY) + // ===================================================================== + + /** BR-SALARY-001: 法定月计薪天数(劳动法规定) */ + public static final BigDecimal MONTHLY_WORK_DAYS = new BigDecimal("21.75"); + + /** BR-SALARY-002: 平日加班费率(1.5 倍) */ + public static final BigDecimal WEEKDAY_OVERTIME_RATE = new BigDecimal("1.5"); + + /** BR-SALARY-003: 周末加班费率(2.0 倍) */ + public static final BigDecimal WEEKEND_OVERTIME_RATE = new BigDecimal("2.0"); + + /** BR-SALARY-004: 法定假日加班费率(3.0 倍) */ + public static final BigDecimal HOLIDAY_OVERTIME_RATE = new BigDecimal("3.0"); + + /** BR-SALARY-005: 标准日工作时长(小时) */ + public static final BigDecimal STANDARD_DAILY_WORK_HOURS = new BigDecimal("8"); + + /** BR-SALARY-006: 工资金额计算精度(小数位数) */ + public static final int SALARY_SCALE = 2; + + /** BR-SALARY-007: 工资金额舍入模式(四舍五入) */ + public static final RoundingMode SALARY_ROUNDING = RoundingMode.HALF_UP; + + /** BR-SALARY-008: 最低工资标准(元/月),按当地政策设定,需定期更新 */ + public static final BigDecimal MINIMUM_MONTHLY_WAGE = new BigDecimal("2690.00"); + + // ===================================================================== + // 二、打卡考勤规则 (BR-ATTEND) + // ===================================================================== + + /** BR-ATTEND-001: 打卡地理围栏默认半径(米) */ + public static final int DEFAULT_FENCE_RADIUS_METERS = 200; + + /** BR-ATTEND-002: 防重复打卡时间窗口(分钟),同类型打卡在此时间内不可重复 */ + public static final int DUPLICATE_PUNCH_WINDOW_MINUTES = 30; + + /** BR-ATTEND-003: 迟到判定阈值(分钟),超过排班开始时间此值算迟到 */ + public static final int LATE_THRESHOLD_MINUTES = 15; + + /** BR-ATTEND-004: 早退判定阈值(分钟),早于排班结束时间此值算早退 */ + public static final int EARLY_LEAVE_THRESHOLD_MINUTES = 15; + + /** BR-ATTEND-005: 严重迟到阈值(分钟),超过此值算严重迟到 */ + public static final int SEVERE_LATE_THRESHOLD_MINUTES = 60; + + /** BR-ATTEND-006: 旷工判定阈值(分钟),超过此值未打卡算旷工 */ + public static final int ABSENT_THRESHOLD_MINUTES = 120; + + /** BR-ATTEND-007: GPS 定位超时时间(秒) */ + public static final int GPS_TIMEOUT_SECONDS = 15; + + /** BR-ATTEND-008: WiFi 辅助定位允许的最大偏差(米) */ + public static final int WIFI_LOCATION_MAX_DEVIATION_METERS = 100; + + /** BR-ATTEND-009: 只允许本人打卡,不可代打 */ + public static final boolean SELF_PUNCH_ONLY = true; + + /** BR-ATTEND-010: 打卡记录保留期限(天),超过此期限的记录可归档 */ + public static final int PUNCH_RECORD_RETENTION_DAYS = 365; + + // ===================================================================== + // 三、排班规则 (BR-SCHED) + // ===================================================================== + + /** BR-SCHED-001: 单人单日最大排班时长(小时) */ + public static final int MAX_DAILY_SCHEDULE_HOURS = 12; + + /** BR-SCHED-002: 两个排班之间最小休息时间(小时) */ + public static final int MIN_REST_HOURS_BETWEEN_SHIFTS = 8; + + /** BR-SCHED-003: 排班交接重叠容差(分钟),此范围内不算冲突 */ + public static final int SCHEDULE_HANDOVER_TOLERANCE_MINUTES = 0; + + /** BR-SCHED-004: 排班提前发布天数,排班表至少提前此天数发布 */ + public static final int SCHEDULE_PUBLISH_ADVANCE_DAYS = 3; + + /** BR-SCHED-005: 单人单周最大排班天数 */ + public static final int MAX_WEEKLY_SCHEDULE_DAYS = 6; + + /** BR-SCHED-006: 夜班定义起始时间(24 小时制) */ + public static final int NIGHT_SHIFT_START_HOUR = 22; + + /** BR-SCHED-007: 夜班定义结束时间(24 小时制) */ + public static final int NIGHT_SHIFT_END_HOUR = 6; + + // ===================================================================== + // 四、任务管理规则 (BR-TASK) + // ===================================================================== + + /** BR-TASK-001: 任务超时告警阈值(分钟),超过预计时长此值后触发告警 */ + public static final int TASK_OVERTIME_ALERT_MINUTES = 30; + + /** BR-TASK-002: 任务照片最少数量,完成任务时至少需上传此数量的照片 */ + public static final int TASK_MIN_PHOTO_COUNT = 1; + + /** BR-TASK-003: 任务照片最大数量 */ + public static final int TASK_MAX_PHOTO_COUNT = 9; + + /** BR-TASK-004: 任务评分满分 */ + public static final int TASK_MAX_SCORE = 100; + + /** BR-TASK-005: 任务合格分数线 */ + public static final int TASK_PASS_SCORE = 60; + + // ===================================================================== + // 五、巡检规则 (BR-PATROL) + // ===================================================================== + + /** BR-PATROL-001: 巡检点最小停留时间(秒),少于此时间判定为无效巡检 */ + public static final int PATROL_MIN_STAY_SECONDS = 30; + + /** BR-PATROL-002: 巡检轨迹记录间隔(秒) */ + public static final int PATROL_TRACK_INTERVAL_SECONDS = 10; + + /** BR-PATROL-003: 巡检异常自动上报,发现异常时自动生成工单 */ + public static final boolean PATROL_AUTO_REPORT_ANOMALY = true; + + // ===================================================================== + // 六、系统通用规则 (BR-SYS) + // ===================================================================== + + /** BR-SYS-001: 分页查询每页最大条数 */ + public static final int MAX_PAGE_SIZE = 100; + + /** BR-SYS-002: 分页查询默认每页条数 */ + public static final int DEFAULT_PAGE_SIZE = 20; + + /** BR-SYS-003: 数据删除方式 — 逻辑删除标记值(0=未删除, 1=已删除) */ + public static final int DELETED_FLAG = 1; + public static final int NOT_DELETED_FLAG = 0; + + /** BR-SYS-004: 批量操作最大条数 */ + public static final int MAX_BATCH_SIZE = 500; + + /** BR-SYS-005: 导出 Excel 最大行数 */ + public static final int MAX_EXPORT_ROWS = 10000; + + /** BR-SYS-006: 文件上传最大大小(MB) */ + public static final int MAX_UPLOAD_SIZE_MB = 20; + + /** BR-SYS-007: 允许上传的图片格式 */ + public static final Set ALLOWED_IMAGE_FORMATS = Collections.unmodifiableSet( + new HashSet<>(Arrays.asList("jpg", "jpeg", "png", "bmp", "gif"))); + + // ===================================================================== + // 七、龙虾助手/对话交互规则 (BR-CHAT) + // ===================================================================== + + /** BR-CHAT-001: 写操作需要二次确认 */ + public static final boolean WRITE_OPERATION_REQUIRES_CONFIRM = true; + + /** BR-CHAT-002: 闲聊不触发任何数据库操作 */ + public static final boolean CHITCHAT_NO_DB_OPERATION = true; + + /** BR-CHAT-003: 危险操作(删除/修改工资/修改权限)通过对话禁止执行 */ + public static final boolean DANGEROUS_OPERATION_BLOCKED_VIA_CHAT = true; + + // ===================================================================== + // 规则校验方法 + // ===================================================================== + + /** + * 校验所有业务规则完整性. + * 建议在系统启动时调用,确保核心常量未被篡改。 + * + * @throws AssertionError 如果任何规则被篡改 + */ + public static void validateAll() { + validateSalaryRules(); + validateAttendanceRules(); + validateScheduleRules(); + validateSystemRules(); + } + + /** + * 校验工资计算规则. + */ + public static void validateSalaryRules() { + assertRule(MONTHLY_WORK_DAYS.compareTo(new BigDecimal("21.75")) == 0, + "BR-SALARY-001", "月计薪天数", "21.75", MONTHLY_WORK_DAYS.toString()); + assertRule(WEEKDAY_OVERTIME_RATE.compareTo(new BigDecimal("1.5")) == 0, + "BR-SALARY-002", "平日加班费率", "1.5", WEEKDAY_OVERTIME_RATE.toString()); + assertRule(WEEKEND_OVERTIME_RATE.compareTo(new BigDecimal("2.0")) == 0, + "BR-SALARY-003", "周末加班费率", "2.0", WEEKEND_OVERTIME_RATE.toString()); + assertRule(HOLIDAY_OVERTIME_RATE.compareTo(new BigDecimal("3.0")) == 0, + "BR-SALARY-004", "法定假日加班费率", "3.0", HOLIDAY_OVERTIME_RATE.toString()); + assertRule(STANDARD_DAILY_WORK_HOURS.compareTo(new BigDecimal("8")) == 0, + "BR-SALARY-005", "标准日工作时长", "8", STANDARD_DAILY_WORK_HOURS.toString()); + assertRule(SALARY_SCALE == 2, + "BR-SALARY-006", "工资精度", "2", String.valueOf(SALARY_SCALE)); + assertRule(SALARY_ROUNDING == RoundingMode.HALF_UP, + "BR-SALARY-007", "舍入模式", "HALF_UP", SALARY_ROUNDING.toString()); + } + + /** + * 校验打卡考勤规则. + */ + public static void validateAttendanceRules() { + assertRule(DEFAULT_FENCE_RADIUS_METERS == 200, + "BR-ATTEND-001", "围栏半径", "200", String.valueOf(DEFAULT_FENCE_RADIUS_METERS)); + assertRule(DUPLICATE_PUNCH_WINDOW_MINUTES == 30, + "BR-ATTEND-002", "防重窗口", "30", String.valueOf(DUPLICATE_PUNCH_WINDOW_MINUTES)); + assertRule(LATE_THRESHOLD_MINUTES == 15, + "BR-ATTEND-003", "迟到阈值", "15", String.valueOf(LATE_THRESHOLD_MINUTES)); + assertRule(EARLY_LEAVE_THRESHOLD_MINUTES == 15, + "BR-ATTEND-004", "早退阈值", "15", String.valueOf(EARLY_LEAVE_THRESHOLD_MINUTES)); + assertRule(SELF_PUNCH_ONLY, + "BR-ATTEND-009", "本人打卡", "true", String.valueOf(SELF_PUNCH_ONLY)); + } + + /** + * 校验排班规则. + */ + public static void validateScheduleRules() { + assertRule(MAX_DAILY_SCHEDULE_HOURS == 12, + "BR-SCHED-001", "单日最大排班时长", "12", String.valueOf(MAX_DAILY_SCHEDULE_HOURS)); + assertRule(MIN_REST_HOURS_BETWEEN_SHIFTS == 8, + "BR-SCHED-002", "最小休息时间", "8", String.valueOf(MIN_REST_HOURS_BETWEEN_SHIFTS)); + assertRule(MAX_WEEKLY_SCHEDULE_DAYS == 6, + "BR-SCHED-005", "单周最大排班天数", "6", String.valueOf(MAX_WEEKLY_SCHEDULE_DAYS)); + } + + /** + * 校验系统通用规则. + */ + public static void validateSystemRules() { + assertRule(MAX_PAGE_SIZE == 100, + "BR-SYS-001", "最大分页条数", "100", String.valueOf(MAX_PAGE_SIZE)); + assertRule(DELETED_FLAG == 1, + "BR-SYS-003", "逻辑删除标记", "1", String.valueOf(DELETED_FLAG)); + assertRule(NOT_DELETED_FLAG == 0, + "BR-SYS-003", "未删除标记", "0", String.valueOf(NOT_DELETED_FLAG)); + assertRule(MAX_BATCH_SIZE == 500, + "BR-SYS-004", "批量操作上限", "500", String.valueOf(MAX_BATCH_SIZE)); + } + + /** + * 断言业务规则. + */ + private static void assertRule(boolean condition, String ruleId, String ruleName, + String expectedValue, String actualValue) { + if (!condition) { + throw new AssertionError(String.format( + "业务规则被篡改![%s] %s: 期望=%s, 实际=%s", + ruleId, ruleName, expectedValue, actualValue)); + } + } +} diff --git a/backend/xiaoqu-intellectual-task/src/main/resources/application-test.yml b/backend/xiaoqu-intellectual-task/src/main/resources/application-test.yml index c6c2349..1987588 100644 --- a/backend/xiaoqu-intellectual-task/src/main/resources/application-test.yml +++ b/backend/xiaoqu-intellectual-task/src/main/resources/application-test.yml @@ -2,7 +2,7 @@ server: port: 8097 #redis 多DB配置 redis: - host: 192.168.1.181 + host: 100.93.0.28 port: 6379 password: kaixinjiuhao timeout: 30000 @@ -55,13 +55,13 @@ spring: datasource: type: com.alibaba.druid.pool.DruidDataSource db1: - url: jdbc:mysql://192.168.1.181:3306/xiaoqu_comples_d?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowMultiQueries=true + url: jdbc:mysql://100.93.0.28:3306/xiaoqu_comples_d?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowMultiQueries=true username: root password: kaixinjiuhao driver-class-name: com.mysql.cj.jdbc.Driver db2: - url: jdbc:mysql://192.168.1.181:3306/xiaoqu_intellectual_d?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowMultiQueries=true + url: jdbc:mysql://100.93.0.28:3306/xiaoqu_intellectual_d?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowMultiQueries=true username: root password: kaixinjiuhao driver-class-name: com.mysql.cj.jdbc.Driver diff --git a/backend/xiaoqu-intellectual-task/src/main/resources/application.yml b/backend/xiaoqu-intellectual-task/src/main/resources/application.yml index 90385b2..027b4e3 100644 --- a/backend/xiaoqu-intellectual-task/src/main/resources/application.yml +++ b/backend/xiaoqu-intellectual-task/src/main/resources/application.yml @@ -1,3 +1,3 @@ spring: profiles: - active: prod \ No newline at end of file + active: test \ No newline at end of file diff --git a/backend/xiaoqu-intellectual-web/src/main/resources/application-test.yml b/backend/xiaoqu-intellectual-web/src/main/resources/application-test.yml index 6a1cbd0..236bffc 100644 --- a/backend/xiaoqu-intellectual-web/src/main/resources/application-test.yml +++ b/backend/xiaoqu-intellectual-web/src/main/resources/application-test.yml @@ -2,7 +2,7 @@ server: port: 8095 #redis 多DB配置 redis: - host: localhost + host: 100.93.0.28 port: 6379 password: kaixinjiuhao timeout: 30000 @@ -47,13 +47,13 @@ spring: datasource: type: com.alibaba.druid.pool.DruidDataSource db1: - url: jdbc:mysql://localhost:3306/xiaoqu_comples_d?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowMultiQueries=true&allowPublicKeyRetrieval=true + url: jdbc:mysql://100.93.0.28:3306/xiaoqu_comples_d?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowMultiQueries=true&allowPublicKeyRetrieval=true username: root password: kaixinjiuhao driver-class-name: com.mysql.cj.jdbc.Driver db2: - url: jdbc:mysql://localhost:3306/xiaoqu_intellectual_d?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowMultiQueries=true&allowPublicKeyRetrieval=true + url: jdbc:mysql://100.93.0.28:3306/xiaoqu_intellectual_d?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowMultiQueries=true&allowPublicKeyRetrieval=true username: root password: kaixinjiuhao driver-class-name: com.mysql.cj.jdbc.Driver diff --git a/backend/xiaoqu-intellectual-web/src/main/resources/application.yml b/backend/xiaoqu-intellectual-web/src/main/resources/application.yml index 90385b2..027b4e3 100644 --- a/backend/xiaoqu-intellectual-web/src/main/resources/application.yml +++ b/backend/xiaoqu-intellectual-web/src/main/resources/application.yml @@ -1,3 +1,3 @@ spring: profiles: - active: prod \ No newline at end of file + active: test \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..9297982 --- /dev/null +++ b/build.sh @@ -0,0 +1,237 @@ +#!/bin/bash +# +# SmartClean 构建打包脚本(带飞书通知) +# 用法: +# ./build.sh # 构建所有模块(public + web + task + frontend) +# ./build.sh web # 仅构建后端 web (含 public) +# ./build.sh task # 仅构建后端 task (含 public) +# ./build.sh backend # 构建后端 web + task +# ./build.sh front # 仅构建前端 +# ./build.sh front-test # 构建前端测试环境包 + +# ===== 飞书 Webhook 配置 ===== +FEISHU_WEBHOOK="https://open.feishu.cn/open-apis/bot/v2/hook/5703e8cc-6998-46a6-af9d-8c5102cc8c1e" + +# ===== 初始化 ===== +export SDKMAN_DIR="$HOME/.sdkman" +source "$SDKMAN_DIR/bin/sdkman-init.sh" +sdk use java 8.0.432-zulu > /dev/null 2>&1 + +ROOT_DIR="$(cd "$(dirname "$0")" && pwd)" +PUBLIC_DIR="$ROOT_DIR/backend/xiaoqu-intellectual-public" +WEB_DIR="$ROOT_DIR/backend/xiaoqu-intellectual-web" +TASK_DIR="$ROOT_DIR/backend/xiaoqu-intellectual-task" +FRONT_DIR="$ROOT_DIR/frontend/witcleansystem" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +BUILD_START_TIME=$(date +%s) +BUILD_RESULTS=() +HAS_ERROR=0 + +log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +log_error() { echo -e "${RED}[ERROR]${NC} $1"; } + +# ===== 飞书通知函数 ===== +send_feishu_notify() { + local title="$1" + local content="$2" + local color="$3" # green / red / yellow + + # 构建富文本消息 + local json_content + json_content=$(cat < /dev/null 2>&1 +} + +# ===== 构建函数 ===== +build_public() { + log_info "编译 public 模块..." + local start=$(date +%s) + cd "$PUBLIC_DIR" + local output + output=$(mvn clean install -q -DskipTests 2>&1) + local rc=$? + local elapsed=$(( $(date +%s) - start )) + + if [ $rc -eq 0 ]; then + log_info "public 模块编译成功 (${elapsed}s)" + BUILD_RESULTS+=("✅ **public 模块** — 编译成功 (${elapsed}s)") + else + log_error "public 模块编译失败" + HAS_ERROR=1 + BUILD_RESULTS+=("❌ **public 模块** — 编译失败 (${elapsed}s)") + BUILD_RESULTS+=("错误信息: $(echo "$output" | tail -5 | sed 's/"/\\"/g')") + return 1 + fi +} + +build_web() { + log_info "打包 Web 服务 (ROOT.war)..." + local start=$(date +%s) + cd "$WEB_DIR" + local output + output=$(mvn clean package -q -DskipTests 2>&1) + local rc=$? + local elapsed=$(( $(date +%s) - start )) + + if [ $rc -eq 0 ]; then + local war_file="$WEB_DIR/target/ROOT.war" + local war_size="" + if [ -f "$war_file" ]; then + war_size=" | 文件大小: $(du -h "$war_file" | cut -f1)" + fi + log_info "Web 服务打包成功 (${elapsed}s)$war_size" + BUILD_RESULTS+=("✅ **Web 服务 (ROOT.war)** — 打包成功 (${elapsed}s)$war_size") + else + log_error "Web 服务打包失败" + HAS_ERROR=1 + BUILD_RESULTS+=("❌ **Web 服务** — 打包失败 (${elapsed}s)") + BUILD_RESULTS+=("错误信息: $(echo "$output" | tail -5 | sed 's/"/\\"/g')") + return 1 + fi +} + +build_task() { + log_info "打包 Task 服务..." + local start=$(date +%s) + cd "$TASK_DIR" + local output + output=$(mvn clean package -q -DskipTests 2>&1) + local rc=$? + local elapsed=$(( $(date +%s) - start )) + + if [ $rc -eq 0 ]; then + log_info "Task 服务打包成功 (${elapsed}s)" + BUILD_RESULTS+=("✅ **Task 服务** — 打包成功 (${elapsed}s)") + else + log_error "Task 服务打包失败" + HAS_ERROR=1 + BUILD_RESULTS+=("❌ **Task 服务** — 打包失败 (${elapsed}s)") + BUILD_RESULTS+=("错误信息: $(echo "$output" | tail -5 | sed 's/"/\\"/g')") + return 1 + fi +} + +build_front() { + local mode="${1:-build}" # build 或 build-test + local mode_label="生产" + [ "$mode" = "build-test" ] && mode_label="测试" + + log_info "构建前端 (${mode_label}环境)..." + local start=$(date +%s) + cd "$FRONT_DIR" + + if [ ! -d "node_modules" ]; then + log_warn "未检测到 node_modules, 正在安装依赖..." + npm install + fi + + local output + output=$(npm run "$mode" 2>&1) + local rc=$? + local elapsed=$(( $(date +%s) - start )) + + if [ $rc -eq 0 ]; then + log_info "前端构建成功 (${mode_label}, ${elapsed}s)" + BUILD_RESULTS+=("✅ **前端 (${mode_label})** — 构建成功 (${elapsed}s)") + else + log_error "前端构建失败" + HAS_ERROR=1 + BUILD_RESULTS+=("❌ **前端 (${mode_label})** — 构建失败 (${elapsed}s)") + BUILD_RESULTS+=("错误信息: $(echo "$output" | tail -5 | sed 's/"/\\"/g')") + return 1 + fi +} + +# ===== 发送构建汇总通知 ===== +send_build_summary() { + local total_elapsed=$(( $(date +%s) - BUILD_START_TIME )) + local branch=$(cd "$ROOT_DIR" && git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown") + local commit=$(cd "$ROOT_DIR" && git log -1 --format='%h %s' 2>/dev/null || echo "unknown") + local build_target="${1:-all}" + local hostname=$(hostname) + local timestamp=$(date '+%Y-%m-%d %H:%M:%S') + + local status_emoji="✅" + local title_status="成功" + local color="green" + if [ $HAS_ERROR -ne 0 ]; then + status_emoji="❌" + title_status="失败" + color="red" + fi + + # 拼接结果列表 + local detail="" + for item in "${BUILD_RESULTS[@]}"; do + detail="${detail}\n${item}" + done + + local content="**构建目标:** ${build_target}\\n**分支:** ${branch}\\n**最新提交:** ${commit}\\n**构建机器:** ${hostname}\\n**时间:** ${timestamp}\\n**总耗时:** ${total_elapsed}s\\n\\n---\\n${detail}" + + send_feishu_notify "${status_emoji} SmartClean 构建${title_status}" "$content" "$color" + log_info "飞书通知已发送" +} + +# ===== 主流程 ===== +TARGET="${1:-all}" + +case "$TARGET" in + web) + build_public && build_web + ;; + task) + build_public && build_task + ;; + backend) + build_public && build_web && build_task + ;; + front) + build_front "build" + ;; + front-test) + build_front "build-test" + ;; + all) + build_public && build_web && build_task + build_front "build" + ;; + *) + echo "用法: $0 {all|web|task|backend|front|front-test}" + exit 1 + ;; +esac + +send_build_summary "$TARGET" + +if [ $HAS_ERROR -ne 0 ]; then + exit 1 +fi diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 0000000..52213d9 --- /dev/null +++ b/deploy/README.md @@ -0,0 +1,403 @@ +# SmartClean 自动化部署使用手册 + +## 概述 + +本项目提供两套独立的自动化部署方案,可同时使用、互不干扰: + +| 方案 | 目录 | 适用场景 | 依赖 | +|------|------|---------|------| +| **远程服务器部署** | `deploy/remote/` | 部署到 182 测试/生产服务器 | SSH 免密登录 | +| **Docker 本机模拟** | `deploy/docker/` | 本机模拟完整生产环境 | Docker Desktop | + +--- + +## 现状分析 + +### 当前部署方式 + +| 环节 | 现状 | 问题 | +|------|------|------| +| **构建** | 本地执行 `build.sh`,手动触发 | 已自动化,含飞书通知 | +| **上传** | 手动 SCP 或 FTP 上传到 182 服务器 | 无标准化流程,依赖个人操作习惯 | +| **部署** | SSH 登录服务器,手动替换文件、重启服务 | 步骤多、易出错、无操作记录 | +| **回滚** | 无备份机制,出问题需手动找旧版本 | 无法快速回滚,故障恢复时间长 | +| **通知** | 构建结果有飞书通知,部署结果无通知 | 团队无法感知部署状态 | +| **环境一致性** | 服务器环境手动搭建维护 | 本地与服务器环境可能不一致 | + +### 现有脚本 + +| 脚本 | 用途 | 覆盖范围 | +|------|------|---------| +| `build.sh` | 本地构建(Maven + npm),支持分模块构建,含飞书通知 | 仅构建,不含部署 | +| `start.sh` | 本地启动开发环境(mvn spring-boot:run + npm run dev) | 仅本地开发,非生产部署 | + +### 现有流程 vs 新方案对比 + +``` +现有流程(5 步手动操作,约 10-15 分钟): +┌──────────┐ 手动 ┌──────────┐ 手动 ┌──────────┐ 手动 ┌──────────┐ +│ build.sh │ ───────→ │ SCP 上传 │ ───────→ │ SSH 替换 │ ───────→ │ 手动重启 │ +│ 本地构建 │ │ 到服务器 │ │ 文件 │ │ 服务 │ +└──────────┘ └──────────┘ └──────────┘ └──────────┘ + ✅ 已自动化 ❌ 手动 ❌ 手动 ❌ 手动 + ✅ 有飞书通知 ❌ 无记录 ❌ 无备份 ❌ 无健康检查 + +方案一 — 远程部署(1 条命令,全自动): +┌──────────────────────────────────────────────────────────────────────┐ +│ deploy/remote/deploy.sh web │ +│ 构建 → 备份 → 上传 → 重启 → 健康检查 → 飞书通知(失败自动回滚) │ +└──────────────────────────────────────────────────────────────────────┘ + +方案二 — Docker 部署(1 条命令,全自动): +┌──────────────────────────────────────────────────────────────────────┐ +│ deploy/docker/deploy.sh web │ +│ 镜像构建 → 容器替换 → 健康检查 → 飞书通知(失败自动回滚) │ +└──────────────────────────────────────────────────────────────────────┘ +``` + +### 新方案解决的问题 + +| 现状痛点 | 方案一(远程部署) | 方案二(Docker) | +|----------|------------------|-----------------| +| 手动上传、易传错 | SCP 自动传输,路径固定 | 不需要传输,镜像自包含 | +| 无备份,无法回滚 | 每次部署自动备份,一键回滚 | 基于镜像 tag 回滚 | +| 部署步骤多、易遗漏 | 一条命令完成全部步骤 | 一条命令完成全部步骤 | +| 团队不知道谁部署了什么 | 飞书通知含版本/分支/操作人 | 飞书通知含版本/分支 | +| 部署后不确认是否成功 | 自动健康检查,失败自动回滚 | 自动健康检查,失败自动回滚 | +| 新人不知道怎么部署 | 看文档执行一条命令即可 | 一键启动完整环境 | + +--- + +## 落地调整工作清单 + +### 方案一(远程部署)落地前需要做的事 + +#### 一次性准备工作 + +| # | 任务 | 说明 | 负责人 | 耗时预估 | +|---|------|------|--------|---------| +| 1 | **配置 SSH 免密登录** | 本机生成密钥,`ssh-copy-id` 到 182 服务器 | 开发者 | 5 分钟 | +| 2 | **确认服务器目录结构** | 登录 182 确认 Tomcat 实际安装路径、Nginx 配置路径、Task JAR 存放路径 | 开发者 | 10 分钟 | +| 3 | **修改 config.sh** | 将 `TOMCAT_HOME`、`NGINX_HTML` 等路径改为服务器实际路径 | 开发者 | 5 分钟 | +| 4 | **执行 setup 初始化** | `deploy/remote/deploy.sh setup` 创建标准目录 + 上传重启脚本 | 开发者 | 2 分钟 | +| 5 | **调整重启脚本** | 根据服务器实际情况微调 `restart-web.sh` / `restart-task.sh`(如 Tomcat 启停方式、Task 服务的 JVM 参数) | 开发者 | 15 分钟 | +| 6 | **首次全量部署测试** | 执行 `deploy/remote/deploy.sh` 验证全流程跑通 | 开发者 | 10 分钟 | + +#### 需确认的服务器信息 + +在落地之前,请先 SSH 到 182 服务器确认以下信息,并更新到 `config.sh`: + +```bash +# 登录服务器后执行以下命令,记录输出 + +# 1. Tomcat 安装路径 +find / -name "catalina.sh" -type f 2>/dev/null +# 将结果的父目录的父目录填入 TOMCAT_HOME + +# 2. Nginx 前端文件路径 +nginx -T 2>/dev/null | grep "root " +# 将 root 路径填入 NGINX_HTML + +# 3. Task 服务当前运行方式 +ps -ef | grep "intellectual-task" +# 确认 JAR 名称、启动参数,更新 TASK_JAR_NAME 和 TASK_JVM_OPTS + +# 4. 当前 WAR 包位置 +find / -name "ROOT.war" -type f 2>/dev/null + +# 5. Java 版本 +java -version +``` + +### 方案二(Docker 本机模拟)落地前需要做的事 + +| # | 任务 | 说明 | 负责人 | 耗时预估 | +|---|------|------|--------|---------| +| 1 | **安装 Docker Desktop** | 如未安装,从官网下载安装 | 开发者 | 10 分钟 | +| 2 | **确认端口无冲突** | 确保 8180/18095/18097/3307/6380 未被占用,如有冲突在 `.env` 中调整 | 开发者 | 2 分钟 | +| 3 | **检查 SQL 初始化脚本** | 确认 `sql/` 目录下有完整的建库建表 SQL(Docker MySQL 首次启动会自动导入) | 开发者 | 5 分钟 | +| 4 | **首次全量部署测试** | 执行 `deploy/docker/deploy.sh`,首次需下载基础镜像 + 编译,耗时较长 | 开发者 | 20-30 分钟 | +| 5 | **验证前端 API 代理** | 确认 Nginx 的 `/api/` 反代规则与前端实际请求路径匹配(当前前端直接请求后端,无 `/api` 前缀,可能需要调整 `nginx.conf`) | 开发者 | 10 分钟 | + +#### 可能需要的代码调整 + +| 调整项 | 原因 | 影响范围 | +|--------|------|---------| +| **前端 .env.docker**(可选) | Docker 环境中前端通过 Nginx 反代访问后端,API 地址可能需要调整 | 新增一个 `.env.docker` 文件 | +| **nginx.conf 反代路径** | 当前前端直接调用后端接口(无 `/api` 前缀),需确认 Nginx `location /api/` 的 rewrite 规则与前端请求路径一致 | `deploy/docker/conf/nginx.conf` | +| **application-docker.yml 补全** | 当前只覆盖了数据源和 Redis,如果后端有其他外部依赖(如 OSS、短信等),需补充配置或 mock | `deploy/docker/conf/application-docker.yml` | + +--- + +## 方案一:远程服务器部署(无 Docker) + +> 本地构建 → SCP 上传 → 远程重启 → 健康检查 → 失败自动回滚 → 飞书通知 + +### 前置条件 + +1. 本机已安装 Java 8、Maven、Node.js 16+ +2. SSH 免密登录到目标服务器 + +```bash +# 配置 SSH 免密(一次性) +ssh-keygen -t ed25519 +ssh-copy-id root@192.168.1.182 + +# 验证 +ssh root@192.168.1.182 "echo ok" +``` + +### 首次使用 + +```bash +# 1. 根据实际环境修改配置 +vi deploy/remote/config.sh + +# 2. 初始化服务器目录结构 + 上传管理脚本 +deploy/remote/deploy.sh setup +``` + +### 日常使用 + +```bash +# ===== 部署 ===== + +# 全量部署(前端 + 后端全部) +deploy/remote/deploy.sh + +# 只改了后端 Web 代码 +deploy/remote/deploy.sh web + +# 只改了 Task 服务代码 +deploy/remote/deploy.sh task + +# 后端都改了 +deploy/remote/deploy.sh backend + +# 只改了前端(生产包) +deploy/remote/deploy.sh front + +# 前端测试环境包 +deploy/remote/deploy.sh front-test + +# ===== 回滚 ===== + +# 回滚到上一版本 +deploy/remote/deploy.sh rollback +# 或 +deploy/remote/rollback.sh + +# ===== 状态查看 ===== + +# 查看服务器服务状态 +deploy/remote/deploy.sh status +``` + +### 配置说明 + +编辑 `deploy/remote/config.sh`: + +```bash +# 服务器地址(根据实际修改) +DEPLOY_HOST="192.168.1.182" +DEPLOY_USER="root" + +# Tomcat 路径(根据服务器实际路径修改) +TOMCAT_HOME="/opt/smartclean/web/tomcat" + +# 备份保留数量 +MAX_BACKUPS=5 + +# 健康检查重试次数和间隔 +HEALTHCHECK_RETRIES=20 +HEALTHCHECK_INTERVAL=5 +``` + +### 部署流程 + +``` +1. 本地构建 → 调用 build.sh 生成 ROOT.war / task.jar / dist/ +2. 远程备份 → SSH 备份服务器当前版本到 backups/时间戳-hash/ +3. 上传产物 → SCP 传输到服务器对应目录 +4. 远程重启 → SSH 执行 restart-web.sh / restart-task.sh / restart-front.sh +5. 健康检查 → 轮询接口,最多重试 20 次(每次间隔 5s) +6. 结果通知 → 成功/失败/回滚 均推送飞书群 +``` + +### 服务器目录结构 + +``` +/opt/smartclean/ +├── web/tomcat/webapps/ROOT.war # Web 服务 +├── task/task.jar # Task 服务 +├── front/dist/ # 前端静态文件 +├── backups/ # 版本备份(自动保留最近 5 个) +│ ├── 20260415-153000-a1b2c3d/ +│ └── 20260415-140000-d4e5f6a/ +└── scripts/ # 服务管理脚本 + ├── restart-web.sh + ├── restart-task.sh + └── restart-front.sh +``` + +--- + +## 方案二:Docker 本机模拟 + +> Docker 镜像构建 → Compose 部署 → 健康检查 → 失败自动回滚 → 飞书通知 + +### 前置条件 + +1. 已安装 Docker Desktop 并启动 + +```bash +# 验证 +docker --version +docker compose version +``` + +### 首次使用 + +```bash +# 直接一键部署(首次会自动拉取基础镜像 + 构建 + 启动) +deploy/docker/deploy.sh +``` + +### 日常使用 + +```bash +# ===== 部署 ===== + +# 全量部署 +deploy/docker/deploy.sh + +# 只改了后端 Web 代码 +deploy/docker/deploy.sh web + +# 只改了 Task 服务代码 +deploy/docker/deploy.sh task + +# 只改了前端 +deploy/docker/deploy.sh front + +# ===== 回滚 ===== + +# 回滚到上一版本(基于镜像 tag) +deploy/docker/deploy.sh rollback +# 或 +deploy/docker/rollback.sh + +# ===== 运维 ===== + +# 查看容器状态 +deploy/docker/deploy.sh status + +# 查看所有服务日志 +deploy/docker/deploy.sh logs + +# 只看 Web 服务日志 +deploy/docker/deploy.sh logs web + +# 停止所有容器(数据保留) +deploy/docker/deploy.sh stop + +# 停止并清理数据卷(慎用,会丢数据库数据) +deploy/docker/deploy.sh clean +``` + +### 端口映射 + +| 服务 | 容器内端口 | 宿主机端口 | 访问地址 | +|------|-----------|-----------|---------| +| 前端 (Nginx) | 80 | **8180** | http://localhost:8180 | +| Web 后端 | 8095 | **18095** | http://localhost:18095 | +| Task 后端 | 8097 | **18097** | http://localhost:18097 | +| MySQL | 3306 | **3307** | localhost:3307 | +| Redis | 6379 | **6380** | localhost:6380 | + +> 端口可在 `deploy/docker/.env` 中自定义修改。 + +### 端口隔离说明 + +Docker 环境与本机开发环境完全独立: + +``` +本机开发环境 Docker 模拟环境 +────────────── ────────────── +前端 localhost:8079 前端 localhost:8180 +Web localhost:8095 Web localhost:18095 +MySQL localhost:3306 MySQL localhost:3307 +Redis localhost:6379 Redis localhost:6380 +``` + +两套可以同时运行,互不干扰。 + +### 连接 Docker 中的数据库调试 + +```bash +# 用本机 MySQL 客户端连接 Docker 中的 MySQL +mysql -h127.0.0.1 -P3307 -uroot -pkaixinjiuhao + +# 用本机 Redis 客户端连接 Docker 中的 Redis +redis-cli -h 127.0.0.1 -p 6380 -a kaixinjiuhao +``` + +--- + +## 两套方案对比 + +| 维度 | 远程服务器部署 | Docker 本机模拟 | +|------|--------------|----------------| +| **部署目标** | 192.168.1.182 远程服务器 | 本机 Docker 容器 | +| **依赖** | SSH 免密登录 | Docker Desktop | +| **构建方式** | 本机 mvn/npm 构建,上传产物 | Docker 多阶段构建(镜像内编译) | +| **部署速度** | 快(只传几十 MB 产物) | 慢(首次构建镜像,后续有缓存) | +| **环境一致性** | 依赖服务器已有环境 | 镜像自包含,完全一致 | +| **回滚方式** | 文件级(还原备份的 WAR/JAR/dist) | 镜像级(切换 Docker 镜像 tag) | +| **数据库** | 使用服务器上的 MySQL | 独立 MySQL 容器(数据隔离) | +| **适合场景** | 日常发布到测试/生产 | 本机验证部署流程、新人搭环境 | + +## 命令速查表 + +```bash +# ==================== 远程部署 ==================== +deploy/remote/deploy.sh # 全量部署到远程服务器 +deploy/remote/deploy.sh web # 只部署 Web +deploy/remote/deploy.sh task # 只部署 Task +deploy/remote/deploy.sh front # 只部署前端 +deploy/remote/deploy.sh backend # 部署后端(web + task) +deploy/remote/deploy.sh rollback # 回滚 +deploy/remote/deploy.sh status # 查看服务器状态 +deploy/remote/deploy.sh setup # 首次初始化服务器 + +# ==================== Docker 部署 ==================== +deploy/docker/deploy.sh # 全量部署到 Docker +deploy/docker/deploy.sh web # 只部署 Web +deploy/docker/deploy.sh task # 只部署 Task +deploy/docker/deploy.sh front # 只部署前端 +deploy/docker/deploy.sh rollback # 回滚 +deploy/docker/deploy.sh status # 查看容器状态 +deploy/docker/deploy.sh logs [服务] # 查看日志 +deploy/docker/deploy.sh stop # 停止容器 +deploy/docker/deploy.sh clean # 停止并清理数据 +``` + +## 飞书通知 + +两套方案都集成了飞书 Webhook 通知,部署结果会自动推送到群里: + +- ✅ **部署成功**:版本号、分支、提交信息、耗时 +- ❌ **部署失败**:自动回滚后通知,附失败原因 +- ⚠️ **手动回滚**:回滚版本号、操作人 + +Webhook 地址在各自的配置文件中修改: +- 远程版:`deploy/remote/config.sh` → `FEISHU_WEBHOOK` +- Docker 版:`deploy/docker/.env` → `FEISHU_WEBHOOK` + +## 注意事项 + +1. **deploy/docker/.env 不要提交到 git**(含密码),已在 .gitignore 中排除 +2. **首次 Docker 构建较慢**(需下载基础镜像 + Maven 依赖),后续有缓存会快很多 +3. **远程部署前确认 config.sh 中的路径**与服务器实际一致,特别是 `TOMCAT_HOME` +4. **回滚只保留最近 5 个版本**,超出自动清理(可在配置中调整 `MAX_BACKUPS`) +5. **Docker 版 Task 服务**可能因缺少 MQTT/XXL-Job 而启动异常,这是正常的,不影响 Web 和前端 diff --git a/deploy/docker/Dockerfile.front b/deploy/docker/Dockerfile.front new file mode 100644 index 0000000..d5cf6fc --- /dev/null +++ b/deploy/docker/Dockerfile.front @@ -0,0 +1,4 @@ +FROM nginx:1.24-alpine +RUN rm -rf /usr/share/nginx/html/* +COPY frontend/witcleansystem/dist /usr/share/nginx/html +EXPOSE 80 diff --git a/deploy/docker/Dockerfile.task b/deploy/docker/Dockerfile.task new file mode 100644 index 0000000..7f2db95 --- /dev/null +++ b/deploy/docker/Dockerfile.task @@ -0,0 +1,15 @@ +FROM maven:3.8-openjdk-8 AS builder +WORKDIR /src +COPY backend/xiaoqu-intellectual-public/ xiaoqu-intellectual-public/ +COPY backend/xiaoqu-intellectual-task/ xiaoqu-intellectual-task/ +RUN cd xiaoqu-intellectual-public && mvn clean install -q -DskipTests \ + && cd ../xiaoqu-intellectual-task && mvn clean package -q -DskipTests + +FROM openjdk:8-jre-slim +WORKDIR /app +RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* \ + && mkdir -p /app/config /app/logs +COPY --from=builder /src/xiaoqu-intellectual-task/target/*.jar app.jar +EXPOSE 8097 +ENTRYPOINT ["java", "-jar", "app.jar", \ + "--spring.config.additional-location=/app/config/"] diff --git a/deploy/docker/Dockerfile.web b/deploy/docker/Dockerfile.web new file mode 100644 index 0000000..46963d2 --- /dev/null +++ b/deploy/docker/Dockerfile.web @@ -0,0 +1,7 @@ +FROM tomcat:8.5-jdk8-temurin +RUN rm -rf /usr/local/tomcat/webapps/* \ + && mkdir -p /app/config /app/logs \ + && apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* +COPY backend/xiaoqu-intellectual-web/target/ROOT.war /usr/local/tomcat/webapps/ +ENV CATALINA_OPTS="-Dspring.config.additional-location=/app/config/ -Dspring.profiles.active=test,docker" +EXPOSE 8080 diff --git a/deploy/docker/conf/application-docker.yml b/deploy/docker/conf/application-docker.yml new file mode 100644 index 0000000..16e0886 --- /dev/null +++ b/deploy/docker/conf/application-docker.yml @@ -0,0 +1,35 @@ +server: + port: 8095 + +redis: + host: 100.93.0.28 + port: 6379 + password: kaixinjiuhao + timeout: 30000 + pool: + max-total: 200 + max-wait: -1 + max-idle: 8 + min-idle: 1 + timeout: 30000 + +spring: + datasource: + type: com.alibaba.druid.pool.DruidDataSource + db1: + url: jdbc:mysql://100.93.0.28:3306/xiaoqu_comples_d?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowMultiQueries=true&allowPublicKeyRetrieval=true + username: root + password: kaixinjiuhao + driver-class-name: com.mysql.cj.jdbc.Driver + db2: + url: jdbc:mysql://100.93.0.28:3306/xiaoqu_intellectual_d?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowMultiQueries=true&allowPublicKeyRetrieval=true + username: root + password: kaixinjiuhao + driver-class-name: com.mysql.cj.jdbc.Driver + +xxl: + job: + admin: + addresses: "" + executor: + appname: xiaoqu-intellectual-task diff --git a/deploy/docker/conf/application-task-docker.yml b/deploy/docker/conf/application-task-docker.yml new file mode 100644 index 0000000..2ef79fd --- /dev/null +++ b/deploy/docker/conf/application-task-docker.yml @@ -0,0 +1,35 @@ +server: + port: 8097 + +redis: + host: host.docker.internal + port: 6379 + password: kaixinjiuhao + +spring: + mqtt: + username: intellectual + password: xiaoqu2022 + host-url: tcp://mqtt:1883 + client-id: xiaoqu-intellectual-task-docker + timeout: 1000 + keepalive: 100 + datasource: + type: com.alibaba.druid.pool.DruidDataSource + db1: + url: jdbc:mysql://host.docker.internal:3306/xiaoqu_comples_d?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowMultiQueries=true&allowPublicKeyRetrieval=true + username: root + password: kaixinjiuhao + driver-class-name: com.mysql.cj.jdbc.Driver + db2: + url: jdbc:mysql://host.docker.internal:3306/xiaoqu_intellectual_d?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowMultiQueries=true&allowPublicKeyRetrieval=true + username: root + password: kaixinjiuhao + driver-class-name: com.mysql.cj.jdbc.Driver + +xxl: + job: + admin: + addresses: "" + executor: + appname: xiaoqu-intellectual-task diff --git a/deploy/docker/conf/nginx.conf b/deploy/docker/conf/nginx.conf new file mode 100644 index 0000000..3eca7f9 --- /dev/null +++ b/deploy/docker/conf/nginx.conf @@ -0,0 +1,28 @@ +server { + listen 80; + server_name localhost; + + # 静态资源缓存 + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?|map)$ { + root /usr/share/nginx/html; + expires 7d; + add_header Cache-Control "public, immutable"; + } + + # 前端 SPA — 静态文件存在则返回,不存在则转发到后端 + location / { + root /usr/share/nginx/html; + index index.html; + try_files $uri $uri/ @backend; + } + + # 后端接口反代到本机 Docker 内的 Web 容器 + location @backend { + proxy_pass http://web:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_connect_timeout 60s; + proxy_read_timeout 120s; + } +} diff --git a/deploy/docker/deploy.sh b/deploy/docker/deploy.sh new file mode 100755 index 0000000..8dbe29f --- /dev/null +++ b/deploy/docker/deploy.sh @@ -0,0 +1,355 @@ +#!/bin/bash +# +# SmartClean 一键部署脚本(Docker 版 — 本机模拟) +# +# 用法: +# ./deploy.sh # 构建并部署所有服务 +# ./deploy.sh web # 仅重建部署 Web 服务 +# ./deploy.sh task # 仅重建部署 Task 服务 +# ./deploy.sh front # 仅重建部署前端 +# ./deploy.sh rollback # 回滚到上一版本 +# ./deploy.sh status # 查看容器状态 +# ./deploy.sh logs [服务名] # 查看日志 +# ./deploy.sh stop # 停止所有容器 +# ./deploy.sh clean # 停止并清理(含数据卷) + +set -e + +DEPLOY_DIR="$(cd "$(dirname "$0")" && pwd)" +ROOT_DIR="$(dirname "$(dirname "$DEPLOY_DIR")")" +source "$DEPLOY_DIR/.env" + +# ===== 版本号 ===== +GIT_HASH=$(cd "$ROOT_DIR" && git rev-parse --short HEAD) +TIMESTAMP=$(date +%Y%m%d-%H%M%S) +VERSION="${TIMESTAMP}-${GIT_HASH}" +BRANCH=$(cd "$ROOT_DIR" && git rev-parse --abbrev-ref HEAD) +COMMIT=$(cd "$ROOT_DIR" && git log -1 --format='%h %s') + +BACKUP_FILE="$DEPLOY_DIR/.last-version" +CURRENT_FILE="$DEPLOY_DIR/.current-version" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +NC='\033[0m' + +log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +log_error() { echo -e "${RED}[ERROR]${NC} $1"; } +log_step() { echo -e "${CYAN}[STEP]${NC} $1"; } + +DC="docker compose -f $DEPLOY_DIR/docker-compose.yml" + +# ===== 飞书通知 ===== +notify_feishu() { + local title="$1" content="$2" color="$3" + curl -s -X POST "$FEISHU_WEBHOOK" \ + -H "Content-Type: application/json" \ + -d "{ + \"msg_type\": \"interactive\", + \"card\": { + \"header\": {\"title\":{\"tag\":\"plain_text\",\"content\":\"$title\"},\"template\":\"$color\"}, + \"elements\": [{\"tag\":\"markdown\",\"content\":\"$content\"}] + } + }" > /dev/null 2>&1 +} + +# ===== 检查 Docker ===== +check_docker() { + if ! command -v docker &> /dev/null; then + log_error "Docker 未安装,请先安装 Docker Desktop" + exit 1 + fi + if ! docker info > /dev/null 2>&1; then + log_error "Docker 未启动,请先启动 Docker Desktop" + exit 1 + fi +} + +# ===== 备份当前版本号 ===== +backup_version() { + if [ -f "$CURRENT_FILE" ]; then + cp "$CURRENT_FILE" "$BACKUP_FILE" + log_info "已备份当前版本: $(cat "$BACKUP_FILE")" + fi +} + +# ===== 本地构建产物 ===== +build_local() { + local target="${1:-all}" + local start_time=$(date +%s) + + cd "$ROOT_DIR" + + case "$target" in + web) + log_step "本地构建 Web 服务..." + bash build.sh web + ;; + task) + log_step "本地构建 Task 服务..." + bash build.sh task + ;; + front) + log_step "本地构建前端 (docker 模式)..." + cd "$ROOT_DIR/frontend/witcleansystem" + npm run build-docker + ;; + all) + log_step "本地构建全部..." + bash build.sh backend + cd "$ROOT_DIR/frontend/witcleansystem" + npm run build-docker + ;; + esac + + local elapsed=$(( $(date +%s) - start_time )) + log_info "本地构建完成 (${elapsed}s)" +} + +# ===== 打包 Docker 镜像 ===== +build_images() { + local target="${1:-all}" + local start_time=$(date +%s) + + cd "$DEPLOY_DIR" + + case "$target" in + web) + log_step "打包 Web 镜像 ($VERSION)..." + $DC build --no-cache web + docker tag smartclean-web:latest smartclean-web:$VERSION 2>/dev/null || true + ;; + task) + log_step "打包 Task 镜像 ($VERSION)..." + $DC build --no-cache task + docker tag smartclean-task:latest smartclean-task:$VERSION 2>/dev/null || true + ;; + front) + log_step "打包前端镜像 ($VERSION)..." + $DC build --no-cache frontend + docker tag smartclean-front:latest smartclean-front:$VERSION 2>/dev/null || true + ;; + all) + log_step "打包全部镜像 ($VERSION)..." + $DC build --no-cache web frontend + docker tag smartclean-web:latest smartclean-web:$VERSION 2>/dev/null || true + docker tag smartclean-front:latest smartclean-front:$VERSION 2>/dev/null || true + ;; + esac + + local elapsed=$(( $(date +%s) - start_time )) + log_info "镜像打包完成 (${elapsed}s)" +} + +# ===== 部署服务 ===== +deploy_services() { + local target="${1:-all}" + + cd "$DEPLOY_DIR" + + log_step "部署应用服务..." + case "$target" in + web) + $DC down 2>/dev/null || true + $DC up -d web frontend + ;; + task) + $DC up -d --no-deps task + ;; + front) + $DC down 2>/dev/null || true + $DC up -d web frontend + ;; + all) + $DC down 2>/dev/null || true + $DC up -d web frontend + ;; + esac + + log_info "容器已启动" +} + +# ===== 健康检查 ===== +healthcheck() { + local target="${1:-all}" + local max_retries=30 + local interval=3 + + log_step "健康检查..." + + # Web 服务 + if [ "$target" = "all" ] || [ "$target" = "web" ]; then + log_info " 检查 Web 服务 (http://localhost:$WEB_PORT)..." + for i in $(seq 1 $max_retries); do + if curl -sf -X POST "http://localhost:$WEB_PORT/dropDown/districtTree" > /dev/null 2>&1; then + log_info " Web 服务健康 (第${i}次检查通过)" + break + fi + if [ $i -eq $max_retries ]; then + log_error " Web 服务健康检查失败" + log_error " 查看日志: $DC logs web" + return 1 + fi + printf "." + sleep $interval + done + fi + + # 前端 + if [ "$target" = "all" ] || [ "$target" = "front" ]; then + log_info " 检查前端服务 (http://localhost:$FRONT_PORT)..." + for i in $(seq 1 15); do + if curl -sf "http://localhost:$FRONT_PORT/" > /dev/null 2>&1; then + log_info " 前端服务健康 (第${i}次检查通过)" + break + fi + if [ $i -eq 15 ]; then + log_error " 前端服务健康检查失败" + return 1 + fi + sleep 2 + done + fi + + log_info "健康检查通过" + return 0 +} + +# ===== 回滚 ===== +rollback() { + if [ ! -f "$BACKUP_FILE" ]; then + log_error "没有可回滚的版本" + exit 1 + fi + + local old_version=$(cat "$BACKUP_FILE") + log_warn "回滚到版本: $old_version" + + local has_images=true + docker image inspect smartclean-web:$old_version > /dev/null 2>&1 || has_images=false + docker image inspect smartclean-front:$old_version > /dev/null 2>&1 || has_images=false + + if [ "$has_images" = false ]; then + log_error "旧版本镜像不存在 ($old_version),无法回滚" + exit 1 + fi + + export VERSION="$old_version" + cd "$DEPLOY_DIR" + $DC down 2>/dev/null || true + $DC up -d web frontend + + echo "$old_version" > "$CURRENT_FILE" + log_info "回滚完成" + + notify_feishu "⚠️ SmartClean 已回滚 (Docker)" \ + "**回滚版本:** $old_version\\n**触发原因:** 健康检查失败" \ + "yellow" +} + +# ===== 清理旧镜像 ===== +cleanup_images() { + log_info "清理旧镜像(保留最近 5 个版本)..." + for name in smartclean-web smartclean-task smartclean-front; do + docker images "$name" --format "{{.Tag}}" | grep -v "latest" | sort -r | tail -n +6 | while read tag; do + docker rmi "$name:$tag" 2>/dev/null && echo " 删除 $name:$tag" + done + done +} + +# ===== 主流程 ===== +TARGET="${1:-all}" +DEPLOY_START=$(date +%s) + +case "$TARGET" in + status) + $DC ps + exit 0 + ;; + logs) + shift + $DC logs -f $@ + exit 0 + ;; + stop) + log_info "停止所有容器..." + $DC down + log_info "已停止" + exit 0 + ;; + clean) + log_warn "停止所有容器并清理数据卷..." + $DC down -v --rmi all + log_info "已清理" + exit 0 + ;; + rollback) + check_docker + rollback + exit 0 + ;; + all|web|task|front) + ;; + *) + echo "用法: $0 {all|web|task|front|rollback|status|logs|stop|clean}" + exit 1 + ;; +esac + +echo "" +log_info "======================================" +log_info " SmartClean 自动化部署 (Docker)" +log_info " 版本: $VERSION" +log_info " 分支: $BRANCH" +log_info " 提交: $COMMIT" +log_info " 目标: $TARGET" +log_info "======================================" +echo "" + +# 1. 检查 Docker +check_docker + +# 2. 备份当前版本号 +backup_version + +# 3. 本地构建产物(Maven + npm) +build_local "$TARGET" + +# 4. 打包 Docker 镜像(COPY 本地产物) +build_images "$TARGET" + +# 5. 部署容器 +deploy_services "$TARGET" + +# 6. 健康检查 +if healthcheck "$TARGET"; then + echo "$VERSION" > "$CURRENT_FILE" + cleanup_images + + ELAPSED=$(( $(date +%s) - DEPLOY_START )) + + echo "" + log_info "======================================" + log_info " ✅ 部署成功!" + log_info " 版本: $VERSION" + log_info " 耗时: ${ELAPSED}s" + log_info " 前端: http://localhost:$FRONT_PORT" + log_info " Web: http://localhost:$WEB_PORT" + log_info "======================================" + + notify_feishu "✅ SmartClean 部署成功 (Docker)" \ + "**版本:** $VERSION\\n**分支:** $BRANCH\\n**提交:** $COMMIT\\n**目标:** $TARGET\\n**耗时:** ${ELAPSED}s\\n**前端:** http://localhost:$FRONT_PORT\\n**Web:** http://localhost:$WEB_PORT" \ + "green" +else + log_error "健康检查失败,自动回滚..." + rollback + + ELAPSED=$(( $(date +%s) - DEPLOY_START )) + notify_feishu "❌ SmartClean 部署失败 (Docker,已回滚)" \ + "**版本:** $VERSION\\n**分支:** $BRANCH\\n**提交:** $COMMIT\\n**耗时:** ${ELAPSED}s\\n**状态:** 健康检查失败,已自动回滚" \ + "red" + exit 1 +fi diff --git a/deploy/docker/docker-compose.yml b/deploy/docker/docker-compose.yml new file mode 100644 index 0000000..8353688 --- /dev/null +++ b/deploy/docker/docker-compose.yml @@ -0,0 +1,110 @@ +services: + # ==================== 基础设施(可选,默认使用宿主机 MySQL/Redis)==================== + # 如需独立数据库,取消注释以下 mysql 和 redis 服务 + # mysql: + # image: mysql:5.7 + # platform: linux/amd64 + # container_name: smartclean-mysql + # ports: + # - "${MYSQL_PORT:-3307}:3306" + # environment: + # MYSQL_ROOT_PASSWORD: ${DB_PASSWORD:-kaixinjiuhao} + # volumes: + # - mysql_data:/var/lib/mysql + # - ./init-sql:/docker-entrypoint-initdb.d + # - ../../sql:/sql-source + # command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci + # healthcheck: + # test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + # interval: 10s + # timeout: 5s + # retries: 10 + # start_period: 30s + # networks: + # - smartclean + + # redis: + # image: redis:6-alpine + # container_name: smartclean-redis + # ports: + # - "${REDIS_PORT:-6380}:6379" + # command: redis-server --requirepass ${REDIS_PASSWORD:-kaixinjiuhao} + # volumes: + # - redis_data:/data + # healthcheck: + # test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:-kaixinjiuhao}", "ping"] + # interval: 10s + # retries: 3 + # networks: + # - smartclean + + # ==================== 应用服务 ==================== + web: + image: smartclean-web:${VERSION:-latest} + container_name: smartclean-web + build: + context: ../.. + dockerfile: deploy/docker/Dockerfile.web + ports: + - "${WEB_PORT:-18095}:8080" + extra_hosts: + - "host.docker.internal:host-gateway" + volumes: + - ./conf/application-docker.yml:/app/config/application-docker.yml + - web_logs:/app/logs + healthcheck: + test: ["CMD", "curl", "-sf", "http://localhost:8080/"] + interval: 15s + timeout: 5s + retries: 5 + start_period: 90s + restart: on-failure:3 + networks: + - smartclean + + task: + image: smartclean-task:${VERSION:-latest} + container_name: smartclean-task + build: + context: ../.. + dockerfile: deploy/docker/Dockerfile.task + ports: + - "${TASK_PORT:-18097}:8097" + extra_hosts: + - "host.docker.internal:host-gateway" + environment: + SPRING_PROFILES_ACTIVE: docker + volumes: + - ./conf/application-task-docker.yml:/app/config/application-task-docker.yml + - task_logs:/app/logs + restart: on-failure:3 + networks: + - smartclean + + frontend: + image: smartclean-front:${VERSION:-latest} + container_name: smartclean-front + build: + context: ../.. + dockerfile: deploy/docker/Dockerfile.front + ports: + - "${FRONT_PORT:-8180}:80" + depends_on: + - web + volumes: + - ./conf/nginx.conf:/etc/nginx/conf.d/default.conf + healthcheck: + test: ["CMD", "curl", "-sf", "http://localhost/"] + interval: 10s + retries: 3 + restart: on-failure:3 + networks: + - smartclean + +volumes: + web_logs: + task_logs: + +networks: + smartclean: + driver: bridge diff --git a/deploy/docker/init-sql/00-create-databases.sql b/deploy/docker/init-sql/00-create-databases.sql new file mode 100644 index 0000000..53ee896 --- /dev/null +++ b/deploy/docker/init-sql/00-create-databases.sql @@ -0,0 +1,3 @@ +-- 创建数据库(如不存在) +CREATE DATABASE IF NOT EXISTS `xiaoqu_comples_d` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +CREATE DATABASE IF NOT EXISTS `xiaoqu_intellectual_d` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; diff --git a/deploy/docker/init-sql/01-import-intellectual.sh b/deploy/docker/init-sql/01-import-intellectual.sh new file mode 100755 index 0000000..e76ad95 --- /dev/null +++ b/deploy/docker/init-sql/01-import-intellectual.sh @@ -0,0 +1,5 @@ +#!/bin/bash +# 导入 xiaoqu_intellectual_d 数据库 +echo "[INIT] Importing xiaoqu_intellectual_d..." +mysql -uroot -p"$MYSQL_ROOT_PASSWORD" xiaoqu_intellectual_d < /sql-source/xiaoqu_intellectual_d.sql +echo "[INIT] Done." diff --git a/deploy/docker/rollback.sh b/deploy/docker/rollback.sh new file mode 100755 index 0000000..f5bcd75 --- /dev/null +++ b/deploy/docker/rollback.sh @@ -0,0 +1,4 @@ +#!/bin/bash +# 快捷回滚入口 +DEPLOY_DIR="$(cd "$(dirname "$0")" && pwd)" +exec "$DEPLOY_DIR/deploy.sh" rollback diff --git a/deploy/remote/config.sh b/deploy/remote/config.sh new file mode 100644 index 0000000..c9800c7 --- /dev/null +++ b/deploy/remote/config.sh @@ -0,0 +1,44 @@ +#!/bin/bash +# +# SmartClean 部署配置(无 Docker 版) +# 根据实际服务器环境修改以下配置 + +# ===== 服务器配置 ===== +DEPLOY_HOST="192.168.1.182" +DEPLOY_USER="root" +DEPLOY_BASE="/opt/smartclean" + +# ===== 服务器目录 ===== +REMOTE_WEB_DIR="$DEPLOY_BASE/web" +REMOTE_TASK_DIR="$DEPLOY_BASE/task" +REMOTE_FRONT_DIR="$DEPLOY_BASE/front" +REMOTE_BACKUP_DIR="$DEPLOY_BASE/backups" +REMOTE_SCRIPTS_DIR="$DEPLOY_BASE/scripts" + +# ===== Tomcat 配置 ===== +TOMCAT_HOME="$REMOTE_WEB_DIR/tomcat" +TOMCAT_WEBAPPS="$TOMCAT_HOME/webapps" + +# ===== Nginx 配置 ===== +NGINX_HTML="$REMOTE_FRONT_DIR/dist" + +# ===== Task 服务配置 ===== +TASK_JAR_NAME="xiaoqu-intellectual-task-0.0.1-SNAPSHOT.jar" +TASK_PROFILE="prod" +TASK_JVM_OPTS="-Xms256m -Xmx512m" + +# ===== 本地构建产物路径 ===== +LOCAL_WAR="backend/xiaoqu-intellectual-web/target/ROOT.war" +LOCAL_TASK_JAR="backend/xiaoqu-intellectual-task/target/$TASK_JAR_NAME" +LOCAL_FRONT_DIST="frontend/witcleansystem/dist" + +# ===== 备份保留数量 ===== +MAX_BACKUPS=5 + +# ===== 健康检查 ===== +HEALTHCHECK_URL="http://$DEPLOY_HOST:8095/dropDown/districtTree" +HEALTHCHECK_RETRIES=20 +HEALTHCHECK_INTERVAL=5 + +# ===== 飞书通知 ===== +FEISHU_WEBHOOK="https://open.feishu.cn/open-apis/bot/v2/hook/5703e8cc-6998-46a6-af9d-8c5102cc8c1e" diff --git a/deploy/remote/deploy.sh b/deploy/remote/deploy.sh new file mode 100755 index 0000000..995c054 --- /dev/null +++ b/deploy/remote/deploy.sh @@ -0,0 +1,410 @@ +#!/bin/bash +# +# SmartClean 一键部署脚本(无 Docker 版 — 部署到远程服务器) +# +# 用法: +# ./deploy.sh # 构建并部署所有服务 +# ./deploy.sh web # 仅构建部署 Web 服务 +# ./deploy.sh task # 仅构建部署 Task 服务 +# ./deploy.sh front # 仅构建部署前端(生产包) +# ./deploy.sh front-test # 构建测试环境前端并部署 +# ./deploy.sh backend # 构建部署后端(web + task) +# ./deploy.sh rollback # 回滚到上一版本 +# ./deploy.sh setup # 首次初始化服务器目录 +# ./deploy.sh status # 查看服务器服务状态 + +set -e + +DEPLOY_DIR="$(cd "$(dirname "$0")" && pwd)" +ROOT_DIR="$(dirname "$(dirname "$DEPLOY_DIR")")" +source "$DEPLOY_DIR/config.sh" + +# ===== 版本号 ===== +GIT_HASH=$(cd "$ROOT_DIR" && git rev-parse --short HEAD) +TIMESTAMP=$(date +%Y%m%d-%H%M%S) +VERSION="${TIMESTAMP}-${GIT_HASH}" +BRANCH=$(cd "$ROOT_DIR" && git rev-parse --abbrev-ref HEAD) +COMMIT=$(cd "$ROOT_DIR" && git log -1 --format='%h %s') + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +NC='\033[0m' + +log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +log_error() { echo -e "${RED}[ERROR]${NC} $1"; } +log_step() { echo -e "${CYAN}[STEP]${NC} $1"; } + +SSH_CMD="ssh $DEPLOY_USER@$DEPLOY_HOST" +SCP_CMD="scp" + +# ===== 飞书通知 ===== +notify_feishu() { + local title="$1" content="$2" color="$3" + curl -s -X POST "$FEISHU_WEBHOOK" \ + -H "Content-Type: application/json" \ + -d "{ + \"msg_type\": \"interactive\", + \"card\": { + \"header\": {\"title\":{\"tag\":\"plain_text\",\"content\":\"$title\"},\"template\":\"$color\"}, + \"elements\": [{\"tag\":\"markdown\",\"content\":\"$content\"}] + } + }" > /dev/null 2>&1 +} + +# ===== 检查 SSH 连接 ===== +check_ssh() { + log_info "检查 SSH 连接 ($DEPLOY_USER@$DEPLOY_HOST)..." + if ! $SSH_CMD "echo ok" > /dev/null 2>&1; then + log_error "无法连接到 $DEPLOY_USER@$DEPLOY_HOST" + log_error "请先配置 SSH 免密登录:" + log_error " ssh-keygen -t ed25519" + log_error " ssh-copy-id $DEPLOY_USER@$DEPLOY_HOST" + exit 1 + fi + log_info "SSH 连接正常" +} + +# ===== 首次初始化服务器 ===== +setup_server() { + log_step "1/2 初始化服务器目录结构..." + $SCP_CMD "$DEPLOY_DIR/scripts/setup.sh" "$DEPLOY_USER@$DEPLOY_HOST:/tmp/smartclean-setup.sh" + $SSH_CMD "bash /tmp/smartclean-setup.sh && rm -f /tmp/smartclean-setup.sh" + + log_step "2/2 上传管理脚本..." + $SCP_CMD "$DEPLOY_DIR/scripts/restart-web.sh" "$DEPLOY_USER@$DEPLOY_HOST:$REMOTE_SCRIPTS_DIR/" + $SCP_CMD "$DEPLOY_DIR/scripts/restart-task.sh" "$DEPLOY_USER@$DEPLOY_HOST:$REMOTE_SCRIPTS_DIR/" + $SCP_CMD "$DEPLOY_DIR/scripts/restart-front.sh" "$DEPLOY_USER@$DEPLOY_HOST:$REMOTE_SCRIPTS_DIR/" + $SSH_CMD "chmod +x $REMOTE_SCRIPTS_DIR/*.sh" + + log_info "服务器初始化完成" +} + +# ===== 查看服务器状态 ===== +show_status() { + log_info "服务器状态 ($DEPLOY_HOST):" + $SSH_CMD bash <<'EOF' + echo "" + echo "===== Web 服务 (Tomcat) =====" + TOMCAT_PID=$(ps -ef | grep "[c]atalina" | awk '{print $2}') + if [ -n "$TOMCAT_PID" ]; then + echo " 状态: 运行中 (PID=$TOMCAT_PID)" + else + echo " 状态: 未运行" + fi + + echo "" + echo "===== Task 服务 =====" + if [ -f /opt/smartclean/task/task.pid ]; then + TASK_PID=$(cat /opt/smartclean/task/task.pid) + if kill -0 "$TASK_PID" 2>/dev/null; then + echo " 状态: 运行中 (PID=$TASK_PID)" + else + echo " 状态: 未运行 (PID 文件残留)" + fi + else + echo " 状态: 未运行" + fi + + echo "" + echo "===== Nginx =====" + if nginx -t 2>/dev/null; then + echo " 状态: 运行中" + else + echo " 状态: 异常" + fi + + echo "" + echo "===== 备份列表 =====" + ls -dt /opt/smartclean/backups/*/ 2>/dev/null | head -5 || echo " 无备份" + echo "" +EOF +} + +# ===== 本地构建 ===== +build_local() { + local target="$1" + local build_target="$target" + + # 映射 deploy 参数到 build.sh 参数 + case "$target" in + all) build_target="all" ;; + web) build_target="web" ;; + task) build_target="task" ;; + backend) build_target="backend" ;; + front) build_target="front" ;; + front-test) build_target="front-test" ;; + esac + + log_step "本地构建 (目标: $build_target)..." + cd "$ROOT_DIR" + bash build.sh "$build_target" + + # 验证产物 + case "$target" in + web|backend|all) + [ ! -f "$ROOT_DIR/$LOCAL_WAR" ] && log_error "产物不存在: $LOCAL_WAR" && exit 1 + log_info " ROOT.war: $(du -h "$ROOT_DIR/$LOCAL_WAR" | cut -f1)" + ;;& + task|backend|all) + [ ! -f "$ROOT_DIR/$LOCAL_TASK_JAR" ] && log_error "产物不存在: $LOCAL_TASK_JAR" && exit 1 + log_info " task.jar: $(du -h "$ROOT_DIR/$LOCAL_TASK_JAR" | cut -f1)" + ;;& + front|front-test|all) + [ ! -d "$ROOT_DIR/$LOCAL_FRONT_DIST" ] && log_error "产物不存在: $LOCAL_FRONT_DIST" && exit 1 + log_info " dist/: $(du -sh "$ROOT_DIR/$LOCAL_FRONT_DIST" | cut -f1)" + ;; + esac + + log_info "本地构建完成" +} + +# ===== 远程备份 ===== +backup_remote() { + local target="$1" + log_step "远程备份当前版本..." + + $SSH_CMD bash </dev/null && echo " 备份 ROOT.war" || echo " ROOT.war 不存在,跳过" + ;;& + task|backend|all) + cp "$REMOTE_TASK_DIR/task.jar" "\$BACKUP/" 2>/dev/null && echo " 备份 task.jar" || echo " task.jar 不存在,跳过" + ;;& + front|front-test|all) + if [ -d "$REMOTE_FRONT_DIR/dist" ]; then + cp -r "$REMOTE_FRONT_DIR/dist" "\$BACKUP/" + echo " 备份 front/dist" + fi + ;; + esac + + # 清理过期备份 + cd "$REMOTE_BACKUP_DIR" + TOTAL=\$(ls -dt */ 2>/dev/null | wc -l) + if [ \$TOTAL -gt $MAX_BACKUPS ]; then + ls -dt */ | tail -n +\$(($MAX_BACKUPS + 1)) | xargs rm -rf + echo " 已清理过期备份,保留最近 $MAX_BACKUPS 个" + fi +BKEOF + + log_info "远程备份完成" +} + +# ===== 上传产物 ===== +upload_artifacts() { + local target="$1" + log_step "上传构建产物..." + + case "$target" in + web|backend|all) + log_info " 上传 ROOT.war..." + $SCP_CMD "$ROOT_DIR/$LOCAL_WAR" "$DEPLOY_USER@$DEPLOY_HOST:$TOMCAT_WEBAPPS/ROOT.war" + ;;& + task|backend|all) + log_info " 上传 task.jar..." + $SCP_CMD "$ROOT_DIR/$LOCAL_TASK_JAR" "$DEPLOY_USER@$DEPLOY_HOST:$REMOTE_TASK_DIR/task.jar" + ;;& + front|front-test|all) + log_info " 上传前端文件..." + $SSH_CMD "rm -rf $REMOTE_FRONT_DIR/dist" + $SCP_CMD -r "$ROOT_DIR/$LOCAL_FRONT_DIST" "$DEPLOY_USER@$DEPLOY_HOST:$REMOTE_FRONT_DIR/dist" + ;; + esac + + log_info "产物上传完成" +} + +# ===== 远程重启服务 ===== +restart_services() { + local target="$1" + log_step "远程重启服务..." + + case "$target" in + web|backend|all) + log_info " 重启 Web 服务..." + $SSH_CMD "bash $REMOTE_SCRIPTS_DIR/restart-web.sh" + ;;& + task|backend|all) + log_info " 重启 Task 服务..." + $SSH_CMD "bash $REMOTE_SCRIPTS_DIR/restart-task.sh $TASK_PROFILE '$TASK_JVM_OPTS'" + ;;& + front|front-test|all) + log_info " 重新加载前端..." + $SSH_CMD "bash $REMOTE_SCRIPTS_DIR/restart-front.sh" + ;; + esac + + log_info "服务重启完成" +} + +# ===== 健康检查 ===== +healthcheck() { + local target="$1" + log_step "健康检查..." + + # Web 服务检查 + if [ "$target" = "all" ] || [ "$target" = "web" ] || [ "$target" = "backend" ]; then + log_info " 检查 Web 服务 ($HEALTHCHECK_URL)..." + for i in $(seq 1 $HEALTHCHECK_RETRIES); do + if curl -sf "$HEALTHCHECK_URL" > /dev/null 2>&1; then + log_info " Web 服务健康 (第${i}次检查通过)" + break + fi + if [ $i -eq $HEALTHCHECK_RETRIES ]; then + log_error " Web 服务健康检查失败 (${HEALTHCHECK_RETRIES}次重试)" + return 1 + fi + printf "." + sleep $HEALTHCHECK_INTERVAL + done + fi + + # 前端检查 + if [ "$target" = "all" ] || [ "$target" = "front" ] || [ "$target" = "front-test" ]; then + log_info " 检查前端服务..." + for i in $(seq 1 10); do + if curl -sf "http://$DEPLOY_HOST/" > /dev/null 2>&1; then + log_info " 前端服务健康" + break + fi + if [ $i -eq 10 ]; then + log_error " 前端服务健康检查失败" + return 1 + fi + sleep 2 + done + fi + + log_info "健康检查通过" + return 0 +} + +# ===== 回滚 ===== +rollback() { + log_warn "开始回滚..." + + LATEST_BACKUP=$($SSH_CMD "ls -dt $REMOTE_BACKUP_DIR/*/ 2>/dev/null | head -1 | xargs basename 2>/dev/null") + + if [ -z "$LATEST_BACKUP" ]; then + log_error "没有可回滚的备份" + exit 1 + fi + + log_warn "回滚到版本: $LATEST_BACKUP" + + $SSH_CMD bash <&1 +if [ $? -ne 0 ]; then + echo "[ERROR] Nginx 配置有误" + exit 1 +fi + +echo "[INFO] 重新加载 Nginx..." +nginx -s reload + +echo "[INFO] Nginx 已重新加载" diff --git a/deploy/remote/scripts/restart-task.sh b/deploy/remote/scripts/restart-task.sh new file mode 100755 index 0000000..4753e13 --- /dev/null +++ b/deploy/remote/scripts/restart-task.sh @@ -0,0 +1,37 @@ +#!/bin/bash +# +# 重启 Task 服务(Spring Boot JAR) +# +# 用法: restart-task.sh [profile] [jvm_opts] + +TASK_DIR="/opt/smartclean/task" +JAR_FILE="$TASK_DIR/task.jar" +PID_FILE="$TASK_DIR/task.pid" +LOG_FILE="$TASK_DIR/logs/task.log" +PROFILE="${1:-prod}" +JVM_OPTS="${2:--Xms256m -Xmx512m}" + +# 停止旧进程 +if [ -f "$PID_FILE" ]; then + OLD_PID=$(cat "$PID_FILE") + if kill -0 "$OLD_PID" 2>/dev/null; then + echo "[INFO] 停止旧进程 PID=$OLD_PID" + kill "$OLD_PID" + sleep 3 + if kill -0 "$OLD_PID" 2>/dev/null; then + echo "[WARN] 进程未正常退出,强制终止" + kill -9 "$OLD_PID" + fi + fi + rm -f "$PID_FILE" +fi + +# 启动新进程 +echo "[INFO] 启动 Task 服务 (profile=$PROFILE)..." +nohup java $JVM_OPTS \ + -jar "$JAR_FILE" \ + --spring.profiles.active=$PROFILE \ + > "$LOG_FILE" 2>&1 & + +echo $! > "$PID_FILE" +echo "[INFO] Task 服务已启动, PID=$(cat "$PID_FILE")" diff --git a/deploy/remote/scripts/restart-web.sh b/deploy/remote/scripts/restart-web.sh new file mode 100755 index 0000000..370ae1d --- /dev/null +++ b/deploy/remote/scripts/restart-web.sh @@ -0,0 +1,26 @@ +#!/bin/bash +# +# 重启 Web 服务(Tomcat) + +TOMCAT_HOME="/opt/smartclean/web/tomcat" + +echo "[INFO] 停止 Tomcat..." +"$TOMCAT_HOME/bin/shutdown.sh" 2>/dev/null +sleep 3 + +# 确保进程已停 +TOMCAT_PID=$(ps -ef | grep "[c]atalina" | awk '{print $2}') +if [ -n "$TOMCAT_PID" ]; then + echo "[WARN] Tomcat 未正常关闭,强制终止 PID=$TOMCAT_PID" + kill -9 $TOMCAT_PID + sleep 1 +fi + +# 清理旧的解压目录,保留 WAR +rm -rf "$TOMCAT_HOME/webapps/ROOT" +rm -rf "$TOMCAT_HOME/work/Catalina" + +echo "[INFO] 启动 Tomcat..." +"$TOMCAT_HOME/bin/startup.sh" + +echo "[INFO] Tomcat 已启动" diff --git a/deploy/remote/scripts/setup.sh b/deploy/remote/scripts/setup.sh new file mode 100755 index 0000000..8347bf4 --- /dev/null +++ b/deploy/remote/scripts/setup.sh @@ -0,0 +1,14 @@ +#!/bin/bash +# +# 服务器初始化脚本(仅需执行一次) +# 在远程服务器上创建标准目录结构 + +DEPLOY_BASE="/opt/smartclean" + +echo "[INFO] 创建目录结构..." +mkdir -p "$DEPLOY_BASE"/{web/logs,task/logs,front,backups,scripts} + +echo "[INFO] 目录结构:" +find "$DEPLOY_BASE" -maxdepth 2 -type d | sort + +echo "[INFO] 服务器初始化完成" diff --git a/docs/ai-development-guide.md b/docs/ai-development-guide.md new file mode 100644 index 0000000..78ef819 --- /dev/null +++ b/docs/ai-development-guide.md @@ -0,0 +1,1456 @@ +# AI 辅助开发九大核心问题:深度解析与解决方案 + +> SmartClean 智慧清洁 SaaS 平台 — AI 辅助开发实践指南 +> 创建日期:2026-04-15 +> 适用范围:开发团队、产品团队、项目管理 + +--- + +## 目录 + +- [问题 1:AI 辅助开发文档保存](#问题-1ai-辅助开发文档保存) +- [问题 2:选择 Java 的深层分析](#问题-2选择-java-的深层分析) +- [问题 3:避免 AI 修改业务逻辑](#问题-3避免-ai-修改业务逻辑) +- [问题 4:人工/AI 开发边界划分](#问题-4人工ai-开发边界划分) +- [问题 5:产品手册与用户培训](#问题-5产品手册与用户培训) +- [问题 6:打卡身份校验与代操作拦截](#问题-6打卡身份校验与代操作拦截) +- [问题 7:开放问题与逻辑安全](#问题-7开放问题与逻辑安全) +- [问题 8:手机端数据展示方案](#问题-8手机端数据展示方案) +- [问题 9:国内 AI 编程现状与成本管理](#问题-9国内-ai-编程现状与成本管理) +- [综合实施路线图](#综合实施路线图) + +--- + +## 问题 1:AI 辅助开发文档保存 + +### 1.1 为什么传统文档体系在 AI 时代失效 + +传统软件开发的知识链条是: + +``` +需求文档 → 设计文档 → 代码 → 代码注释 → 测试用例 +``` + +每一环都有人类参与,知识自然沉淀在参与者脑中。即使文档不全,"问写这段代码的人"就能解决。 + +AI 辅助开发打破了这个链条: + +``` +需求文档 → Prompt → AI 黑盒处理 → 代码 → 人工微调 + ↑ + 这里的决策过程消失了 +``` + +消失的信息包括: +- AI 为什么选择这个实现方案而不是另一个? +- AI 理解的需求和实际需求有没有偏差?人工微调修正了什么偏差? +- AI 生成的初始版本是什么样?人工改了哪些地方?为什么改? +- Prompt 是什么?下次类似需求能否复用这个 Prompt? +- AI 遗漏了什么?哪些边界条件是后来人工补上的? + +如果不记录这些,三个月后你看到一段代码,无法判断: +- 这是经过深思熟虑的设计,还是 AI 随机生成的? +- 改了安全吗?会不会破坏 AI 当初设计的某个巧妙之处? +- 出了 Bug,是 AI 的问题还是人工微调引入的? + +### 1.2 文档体系设计(四层架构) + +#### 第一层:代码级标注体系 + +**标注分类标准:** + +| 标注 | 含义 | 审核要求 | 示例场景 | +|------|------|----------|----------| +| `[AI-Generated]` | AI 生成,人工审核未改 | 必须审核 | CRUD 接口、UI 页面 | +| `[AI-Assisted]` | AI 辅助,人工大量修改 | 必须审核 | 业务逻辑、复杂查询 | +| `[AI-Suggested]` | AI 建议方案,人工重写 | 自审 | 算法、架构方案 | +| `[Human-Only]` | 纯人工编写 | 同行审核 | 核心业务、安全相关 | +| `[AI-Test]` | AI 生成的测试代码 | 抽查 | 单元测试、集成测试 | + +**Java 注释模板:** + +```java +/** + * [AI-Generated] 2026-04-15 + * + * @ai-model Claude Opus 4.6 + * @ai-prompt "实现保洁员月度出勤汇总查询接口,支持按项目、班组筛选, + * 返回出勤天数、迟到次数、早退次数、缺勤天数" + * @ai-confidence 高(标准 CRUD 查询,逻辑简单) + * @human-review 通过 — Tony 2026-04-15 + * @human-changes 无 + * @business-rule 出勤统计以自然月为周期,跨月打卡归属到打卡开始时间所在月 + */ +``` + +```java +/** + * [AI-Assisted] 2026-04-15 + * + * @ai-model Claude Opus 4.6 + * @ai-prompt "实现保洁员排班冲突检测算法" + * @ai-confidence 中(AI 初始版本有缺陷) + * @human-review Tony 2026-04-15 + * @human-changes + * 1. AI 原始版本未考虑夜班跨天场景(22:00-06:00), + * 人工添加了跨天时间段重叠检测 + * 2. AI 使用了简单的 start/end 比较, + * 人工改为支持"开区间"(排班结束时间=下一排班开始时间不算冲突) + * 3. AI 遗漏了"法定假日不检测冲突"的业务规则 + * @business-rule + * - 同一保洁员同一时段只能分配到一个区域 + * - 夜班跨天算一个排班,不拆分 + * - 排班结束时间 = 下一排班开始时间,不算冲突(交接班) + * - 法定假日排班不参与冲突检测 + */ +``` + +**`@ai-confidence` 说明:** +- 高置信度(标准 CRUD):未来修改时可以放心让 AI 继续处理 +- 中置信度(AI 有部分错误):未来修改时需要特别注意 AI 可能犯同样的错 +- 低置信度(AI 严重偏差):这类代码应标记为人工维护,不再让 AI 碰 + +**`@human-changes` 的核心价值:** +- 记录了 AI 的能力边界 +- 反复出现相同类型的 human-changes,说明需要优化 Prompt 或调整 AI 使用策略 +- 新人入职时读这些注释,能快速了解"AI 在哪些地方容易犯错" + +#### 第二层:Git Commit 规范体系 + +**Commit Message 格式:** + +``` +<类型>(<模块>): <描述> [] + +<详细说明> + +AI-Context: + Model: <模型名> + Prompt-Summary: <核心 prompt 摘要> + AI-Contribution: + Human-Contribution: <人工具体做了什么> + AI-Errors-Fixed: <修正了 AI 的哪些错误> + +Co-Authored-By: Claude Opus 4.6 (1M context) +``` + +**实际示例:** + +``` +feat(attendance): 实现打卡地理围栏校验 [AI-Assisted] + +基于 Haversine 公式实现经纬度距离计算,支持可配置围栏半径。 +当 GPS 信号弱时降级到 WiFi 定位辅助校验。 + +AI-Context: + Model: Claude Opus 4.6 + Prompt-Summary: 实现基于经纬度的打卡地理围栏校验,支持配置围栏半径 + AI-Contribution: 生成了基础的地理围栏校验框架、Haversine 公式实现、 + Controller/Service/Mapper 三层代码 + Human-Contribution: + - 添加 WiFi 辅助定位降级方案(AI 未考虑 GPS 不可用场景) + - 修正距离计算精度(AI 用 float,改为 double) + - 添加"围栏半径按项目可配置"功能(AI 硬编码了 200 米) + AI-Errors-Fixed: + - AI 初始版本用欧氏距离代替 Haversine,在高纬度地区误差大 + - AI 未处理经纬度为 null 的情况(GPS 获取失败) + +Co-Authored-By: Claude Opus 4.6 (1M context) +``` + +**利用 Git 进行 AI 代码追溯的命令:** + +```bash +# 查看所有 AI 参与的提交 +git log --grep="AI-Generated\|AI-Assisted" --oneline + +# 查看某个文件中 AI 贡献的部分 +git log --grep="AI-" -p -- src/service/AttendanceService.java + +# 统计 AI 参与度 +git log --grep="AI-Generated" --oneline | wc -l # AI 完全生成 +git log --grep="AI-Assisted" --oneline | wc -l # AI 辅助 + +# 查看 AI 犯过的错误 +git log --grep="AI-Errors-Fixed" --format="%H %s" +``` + +#### 第三层:需求级开发日志 + +每个需求完成后,创建一份开发日志,存在 `docs/dev-logs/` 或飞书文档中: + +```markdown +# 开发日志:REQ-042 保洁员排班管理 + +## 基本信息 +- 需求编号:REQ-042 +- 开发周期:2026-04-10 ~ 2026-04-18 +- 开发者:Tony +- AI 工具:Claude Code (Opus 4.6) + +## 开发过程时间线 + +### Day 1:数据库设计 + 基础 CRUD +- Prompt: "设计保洁员排班管理的数据库表..." +- AI 输出质量:良好 +- 人工调整: + - AI 用 VARCHAR(20) 存排班类型,改为 TINYINT + 枚举类 + - AI 未创建复合索引 (user_id + schedule_date),手动添加 + +### Day 2:排班冲突检测算法 +- Prompt: "实现排班冲突检测..." +- AI 输出质量:中等 +- AI 遗漏的关键点:夜班跨天、交接班衔接、法定假日、临时调班 +- 决策:核心冲突算法改为人工编写,让 AI 只生成测试用例 + +### Day 3-4:前端排班页面 +- AI 输出质量:良好 +- 人工调整:拖拽组件替换、CSS 与设计体系对齐、全局组件使用 + +## 效率分析 +| 阶段 | 传统开发预估 | AI 辅助实际 | 效率提升 | +|------|-------------|------------|---------| +| 数据库设计 | 4h | 1.5h | 63% | +| 后端 CRUD | 8h | 2h | 75% | +| 冲突算法 | 4h | 3.5h | 12%(AI 反而拖慢了) | +| 前端页面 | 16h | 6h | 63% | +| 联调测试 | 8h | 6h | 25% | +| **总计** | **40h** | **19h** | **52%** | + +## AI 经验总结 +1. 数据库设计:必须在 Prompt 中明确字段类型规范、索引要求 +2. 复杂算法:AI 生成初版参考,但核心逻辑人工编写更可靠 +3. 前端开发:要在 Prompt 中告知项目的全局组件、CSS 规范 +4. 前后端联调:前后端代码最好在同一个对话中生成,避免接口不一致 +5. 测试:AI 生成测试框架很快,但边界条件测试必须人工补充 +``` + +#### 第四层:AI 开发经验知识库 + +将所有开发日志中的共性问题汇总,持续积累: + +**数据库设计类常见错误:** + +| ID | 错误模式 | 出现频率 | 防范措施 | +|----|---------|---------|---------| +| DB-001 | 时间字段用 VARCHAR | 高 | Prompt 中明确"时间用 DATETIME 类型" | +| DB-002 | 缺少索引 | 高 | Prompt 中明确要求设计索引 | +| DB-003 | 物理删除 | 高 | CLAUDE.md 中写明软删除规范 | +| DB-004 | 缺少审计字段 | 中 | 要求所有表包含 create_time/update_time | +| DB-005 | 类型字段用字符串 | 中 | 要求用 TINYINT + 代码枚举 | +| DB-006 | 缺少字段注释 | 高 | 明确要求每个字段加 COMMENT | + +**Java 后端常见错误:** + +| ID | 错误模式 | 出现频率 | 防范措施 | +|----|---------|---------|---------| +| BE-001 | 忽略并发 | 高 | 涉及写操作的接口必须考虑并发 | +| BE-002 | 空指针不防御 | 中 | 要求对外部数据做空值检查 | +| BE-003 | 事务边界错误 | 中 | 涉及多表写入时提醒加 @Transactional | +| BE-004 | 硬编码配置 | 高 | 要求可配置项读取配置中心 | +| BE-005 | 异常吞没 | 中 | 要求异常必须记录日志或抛出 | +| BE-006 | 权限校验遗漏 | 高 | Prompt 中明确权限要求 | +| BE-007 | 分页不设上限 | 高 | 要求分页大小有最大值限制 | + +**Vue 前端常见错误:** + +| ID | 错误模式 | 出现频率 | 防范措施 | +|----|---------|---------|---------| +| FE-001 | 不用全局组件 | 高 | Prompt 中列出全局组件清单 | +| FE-002 | 样式不统一 | 高 | 要求使用项目 CSS 变量 | +| FE-003 | API 路径硬编码 | 中 | 要求使用 api.js 中定义的端点 | +| FE-004 | 权限控制遗漏 | 高 | 要求使用 fetchButtons/fetchFields | +| FE-005 | 内存泄漏 | 低 | 提醒在 onUnmounted 中清理 | +| FE-006 | 请求不防重 | 中 | 要求提交按钮加 loading 状态 | + +**业务逻辑常见错误(最危险):** + +| ID | 错误模式 | 出现频率 | 防范措施 | +|----|---------|---------|---------| +| BL-001 | 跨天场景遗漏 | 高 | 涉及时间的业务必须说明跨天 | +| BL-002 | 边界条件 >= 和 > 混淆 | 中 | 工资相关逻辑必须有精确测试 | +| BL-003 | 角色权限假设错误 | 中 | Prompt 中明确涉及的角色 | +| BL-004 | 状态机不完整 | 高 | 画状态图后再让 AI 编码 | +| BL-005 | 数据归属判断错 | 中 | 明确多租户隔离要求 | +| BL-006 | 计算精度丢失 | 中 | 金额计算强制使用 BigDecimal | + +### 1.3 文档维护的成本控制 + +| 文档层级 | 维护频率 | 维护方式 | 耗时 | +|---------|---------|---------|------| +| 代码注释 | 每次 AI 开发时 | 开发时随手写 | 2-3 分钟/文件 | +| Git Commit | 每次提交 | 遵循模板 | 3-5 分钟/提交 | +| 需求开发日志 | 每个需求完成后 | 让 AI 根据对话生成初稿,人工补充 | 15-20 分钟/需求 | +| 经验知识库 | 每月汇总 | 从开发日志中提取共性问题 | 1-2 小时/月 | + +**关键技巧:让 AI 帮你写文档。** 每个需求完成后,让 AI 回顾对话生成开发日志初稿: + +``` +请回顾我们这次对话,生成一份开发日志,包括: +1. 你完成了哪些部分 +2. 我人工修改了哪些地方 +3. 你犯了哪些错误,我是怎么修正的 +4. 下次类似需求的 Prompt 优化建议 +``` + +这样文档工作量下降 60-70%,人工只需审核和补充。 + +--- + +## 问题 2:选择 Java 的深层分析 + +### 2.1 编程语言在 AI 协作中的五个评估维度 + +#### 维度一:类型安全——AI 错误的拦截率 + +``` + AI 犯错后的发现时机 + ───────────────────────────────────── + 编译期 运行期 生产环境 + │ │ │ +Java ████████████ ██ █ +Go ████████████ ██ █ +TS █████████ ███ ██ +Python █ █████████ ████████ +JS █ █████████ ████████ +``` + +Java/Go(强类型):AI 传错参数类型、返回值类型不匹配、方法调用参数数量错误——全部在编译期报错。不需要运行代码就能发现 AI 的低级错误。 + +Python/JS(弱类型):AI 的类型错误会悄悄通过,直到运行时才暴露,甚至可能在特定条件下才触发。 + +**具体例子:** + +```java +// Java —— AI 写错类型,编译器立刻报错 +public BigDecimal calculateOvertime(Long userId, int hours) { + return baseSalary.multiply(new BigDecimal(hours)); // 类型正确才能编译 +} + +// 如果 AI 写成: +public BigDecimal calculateOvertime(Long userId, int hours) { + return baseSalary * hours; // 编译错误!BigDecimal 不能用 * 运算符 +} +``` + +```python +# Python —— AI 写错类型,运行时才发现 +def calculate_overtime(user_id, hours): + return base_salary * hours # 如果 base_salary 是字符串 "5000" + # Python 不报错,返回 "500050005000..."(字符串重复) + # 这个 Bug 可能到月底算工资时才被发现 +``` + +#### 维度二:框架约束力——AI 行为的规范性 + +框架越有"约定",AI 生成的代码越规范: + +``` +框架约束力排名: +Spring Boot (Java) ★★★★★ 注解驱动,结构高度规范 +NestJS (TypeScript) ★★★★☆ 借鉴 Spring,较规范 +Django (Python) ★★★★☆ MTV 模式明确 +Gin (Go) ★★★☆☆ 轻量灵活,约束少 +Express (Node.js) ★★☆☆☆ 过于灵活,AI 容易写出混乱代码 +Flask (Python) ★★☆☆☆ 同上 +``` + +Spring Boot 的约束力体现——AI 生成代码时框架约定自然引导分层: + +```java +@RestController // 框架约定:这是接口层 +@RequestMapping("/api/attendance") +public class AttendanceController { + + @Autowired // 框架约定:依赖注入 + private AttendanceService attendanceService; + + @PostMapping("/punch") // 框架约定:HTTP 方法映射 + @RequiresPermission("attendance:punch") // 框架约定:权限控制 + public Result punch( + @Valid @RequestBody PunchDTO dto) { // 框架约定:参数校验 + return Result.ok(attendanceService.punch(dto)); + } +} +``` + +AI 在 Spring Boot 项目中生成代码,即使不给额外提示,也会自然遵循 Controller → Service → Mapper 分层。而在 Express/Flask 中,AI 可能把所有逻辑塞进路由里。 + +#### 维度三:AI 训练数据量——生成质量 + +``` +AI 训练数据量(GitHub 企业级项目): +Java ★★★★★ 海量 Spring Boot 企业项目 +Python ★★★★★ 数量最多但质量参差不齐 +TypeScript ★★★★☆ 近年增长快 +Go ★★★★☆ 质量高但数量相对少 +Rust ★★★☆☆ 数量较少 +``` + +Java + Spring Boot 是 AI 生成质量最稳定的企业级组合之一。 + +#### 维度四:调试与排查能力 + +Java 的堆栈跟踪信息详细,异常类型明确,IDE 支持强大(IDEA 的断点调试、代码导航),当 AI 生成的代码出问题时,排查效率最高。 + +#### 维度五:团队交接与维护 + +AI 生成的 Java 代码,不同开发者读起来差异不大(框架约束)。AI 生成的 Python/JS 代码,可能每次风格都不同,维护成本高。 + +### 2.2 SmartClean 项目的具体选择建议 + +``` +核心业务后端:Java + Spring Boot ← 不变 +理由:类型安全、框架约束、团队熟悉、AI 生成质量高 + +前端:Vue 3 + TypeScript(建议渐进迁移) +理由:TypeScript 给前端加了类型安全,AI 生成 TS 代码的质量 > JS + 渐进迁移:新页面用 TS,老页面不动 + +NLP/AI 服务(龙虾助手):Python 微服务 ← 独立部署 +理由:ML/NLP 生态在 Python,通过 HTTP API 隔离,与主业务系统语言无关 + +脚本/工具:Python 或 Shell +理由:一次性脚本不需要类型安全 + +不建议: +- 核心业务改用 Go/Rust(团队学习成本、现有代码迁移) +- 全部用 Python(失去类型安全,长期维护风险) +- 微服务过度拆分(当前规模不需要) +``` + +### 2.3 Java 8 是否需要升级 + +当前项目用 Java 8,在 AI 辅助开发中有微妙影响:AI 模型训练数据中包含大量 Java 11/17 代码,AI 生成代码时倾向于使用 var、Records、switch 表达式等新特性,导致编译失败。 + +**当前做法:** 在 CLAUDE.md 中明确标注 Java 版本约束。 + +**长远建议:** 可以考虑升级到 Java 17(LTS),既能获得新特性带来的开发效率提升,又能减少 AI 生成代码的适配成本。 + +--- + +## 问题 3:避免 AI 修改业务逻辑 + +### 3.1 AI 篡改业务逻辑的真实风险场景 + +AI 修改业务逻辑的危险性在于:**不报错、不告警、看起来完全正常,但业务含义已经改变。** + +#### 场景 1:悄悄改变比较运算符 + +```java +// 原始代码:加班费按工作超过 8 小时(含)计算 +if (workHours >= 8) { + overtimePay = calculateOvertime(workHours - 8); +} + +// AI 在"优化"代码时,悄悄改成了: +if (workHours > 8) { // >= 变成了 > ! + overtimePay = calculateOvertime(workHours - 8); +} +// 后果:恰好工作 8 小时的员工,少算加班费 +// 金额差异不大,可能存在数月才被发现 +``` + +#### 场景 2:简化权限校验 + +```java +// 原始代码:完整的权限校验 +public Result deleteSchedule(Long scheduleId) { + Schedule schedule = scheduleMapper.selectById(scheduleId); + if (schedule == null) return Result.fail("排班记录不存在"); + if (!schedule.getProjectId().equals(getCurrentUserProjectId())) + return Result.fail("无权操作其他项目的排班"); + if (schedule.getStatus() == ScheduleStatus.ACTIVE) + return Result.fail("已生效的排班不能直接删除,请先取消"); + schedule.setDeleted(1); + scheduleMapper.updateById(schedule); + return Result.ok(); +} + +// AI "重构优化"后: +public Result deleteSchedule(Long scheduleId) { + scheduleMapper.deleteById(scheduleId); // 物理删除!权限校验全部丢失! + return Result.ok(); +} +``` + +#### 场景 3:改变数据精度 + +```java +// 原始代码 +BigDecimal hourlyRate = baseSalary.divide( + new BigDecimal("21.75"), 6, RoundingMode.HALF_UP); + +// AI 修改后("简化") +double hourlyRate = baseSalary.doubleValue() / 21.75; +// 浮点精度丢失,涉及工资这种敏感数据,1 分钱的差异都可能引起投诉 +``` + +### 3.2 五层防护体系 + +#### 防线一:代码分区制度(最重要) + +将代码库分为三个区域,写入 CLAUDE.md: + +**红区(AI 禁入)——AI 只可阅读和建议,不可直接修改:** + +``` +工资与财务: +- **/service/salary/** +- **/service/finance/** +- 所有包含 "calculate" + "salary/wage/pay" 的方法 + +权限与安全: +- **/config/security/** +- **/service/auth/** +- **/interceptor/**、**/filter/** + +打卡与考勤核心: +- AttendanceService.punch() +- AttendanceService.calculateOvertime() +- PunchRecordService 中的防重复逻辑 + +数据删除: +- 所有 DELETE 语句(物理删除) +- 所有 mapper.xml 中的 delete 标签 +``` + +修改红区文件的流程: +1. AI 提供修改建议和代码片段 +2. 开发者理解建议后手动修改 +3. 必须有对应的单元测试覆盖 +4. 需要至少一人 Code Review + +**黄区(AI 可写,必须审核):** + +``` +- **/service/**(除红区外的所有 Service) +- **/mapper/xml/**(复杂查询语句) +- **/job/**、**/task/**(定时任务) +- **/integration/**、**/client/**(第三方集成) +``` + +**绿区(AI 自主操作):** + +``` +- frontend/**(UI 组件和页面) +- **/controller/**(接口层) +- **/dto/**、**/vo/**、**/entity/**(数据传输对象) +- **/test/**(测试代码) +- docs/**(文档) +``` + +#### 防线二:业务规则断言 + +将核心业务规则集中定义为常量,系统启动时自动校验: + +```java +public final class BusinessRules { + // 工资规则 + public static final BigDecimal MONTHLY_WORK_DAYS = new BigDecimal("21.75"); + public static final BigDecimal WEEKDAY_OVERTIME_RATE = new BigDecimal("1.5"); + public static final BigDecimal WEEKEND_OVERTIME_RATE = new BigDecimal("2.0"); + public static final BigDecimal HOLIDAY_OVERTIME_RATE = new BigDecimal("3.0"); + + // 打卡规则 + public static final int DEFAULT_FENCE_RADIUS_METERS = 200; + public static final int DUPLICATE_PUNCH_WINDOW_MINUTES = 30; + public static final int LATE_THRESHOLD_MINUTES = 15; + + // 启动时校验 + public static void validateAll() { + assert WEEKDAY_OVERTIME_RATE.compareTo(new BigDecimal("1.5")) == 0 + : "BR-SALARY-002 被篡改: 加班费率=" + WEEKDAY_OVERTIME_RATE; + // ... 其他校验 + } +} +``` + +业务代码引用此处常量,禁止硬编码。任何值被篡改,系统启动时立刻报警。 + +#### 防线三:核心逻辑单元测试 + +核心业务测试必须做到:**修改任何一个运算符、任何一个常量、任何一个判断条件,至少一个测试会失败。** + +```java +@Test +public void 加班费_恰好8小时_包含边界() { + // 这个测试专门防止 >= 被改成 > + // 恰好 8 小时不算加班(正常工时) + assertEquals(BigDecimal.ZERO, result.getOvertimePay()); +} + +@Test +public void 精度_不使用浮点类型() { + // 通过反射检查 Service 中没有使用 double/float +} +``` + +#### 防线四:Git Hook 自动检查 + +提交前自动检测红区文件变更并告警: + +```bash +#!/bin/bash +# .git/hooks/pre-commit + +RED_ZONE_PATTERNS=("service/salary" "service/auth" "BusinessRules.java") +STAGED_FILES=$(git diff --cached --name-only) + +for pattern in "${RED_ZONE_PATTERNS[@]}"; do + MATCHES=$(echo "$STAGED_FILES" | grep "$pattern") + if [ -n "$MATCHES" ]; then + echo "⚠️ 红区文件变更警告:$MATCHES" + echo "需要:1.人工逐行审查 2.单元测试通过 3.至少一人 Code Review" + read -p "确认已完成以上检查?(yes/no): " confirm + [ "$confirm" != "yes" ] && exit 1 + fi +done +``` + +#### 防线五:定期业务逻辑巡检 + +每两周执行一次自动巡检脚本,检查: +- 红区文件近期变更 +- 新增的物理删除语句 +- 工资模块中的浮点类型使用 +- 缺少权限注解的写接口 + +--- + +## 问题 4:人工/AI 开发边界划分 + +### 4.1 按代码层级划分 + +``` +┌─────────────────────────────────────────────┐ +│ 前端 UI 层 │ AI 为主(80%) +│ 列表页、表单页、详情页、组件搭建 │ +├─────────────────────────────────────────────┤ +│ API 接口层(Controller) │ AI 为主(70%) +│ 参数接收、参数校验、调用 Service、返回结果 │ +├─────────────────────────────────────────────┤ +│ 业务逻辑层(Service) │ 人机协作(50/50) +│ 业务规则实现、流程编排、事务管理 │ +├─────────────────────────────────────────────┤ +│ 数据访问层(Mapper/DAO) │ AI 辅助(60%简单/40%复杂) +│ SQL 编写、数据查询、数据持久化 │ +├─────────────────────────────────────────────┤ +│ 基础设施层 │ 人工为主(80%) +│ 安全、缓存、消息队列、第三方集成 │ +└─────────────────────────────────────────────┘ +``` + +### 4.2 按业务风险划分(最关键) + +``` + 业务风险 + 高 + │ + ┌───────────────┼───────────────┐ + │ 人工编写 │ 人工编写 │ + │ AI 写测试 │ AI 禁入 │ + │ │ │ + │ 工资计算 │ 资金流转 │ + │ 排班算法 │ 权限认证 │ + │ 考勤规则 │ 数据安全 │ + 低 ──┼───────────────┼───────────────┼── 高 技术复杂度 + │ │ │ + │ AI 自主完成 │ AI 生成 │ + │ 人工抽查 │ 人工审核 │ + │ │ │ + │ CRUD 页面 │ 复杂报表 │ + │ 简单列表 │ 数据迁移 │ + │ 表单提交 │ 第三方对接 │ + └───────────────┼───────────────┘ + 低 +``` + +**原则:越接近钱和权限的代码,人工参与度越高。** + +### 4.3 按 SmartClean 业务模块的详细边界 + +#### 用户管理模块 + +- **绿区(AI 自主):** 用户列表/详情/表单页面、团队/标签/技能 CRUD、用户导入导出、Controller 接口 +- **黄区(AI 辅助):** 用户状态管理、用户与角色关联、数据权限、批量操作 +- **红区(人工):** 用户认证(登录/Token/Session)、密码加密、权限计算与缓存、敏感信息脱敏 + +#### 打卡考勤模块 + +- **绿区:** 打卡记录列表/详情页面、考勤报表页面、查询接口、导出 Excel +- **黄区:** 考勤月度汇总统计 SQL、打卡异常检测定时任务、考勤与排班关联查询 +- **红区:** 打卡核心逻辑(身份校验、地理围栏、防重、迟到/早退判定)、补卡审批、数据防篡改 + +#### 任务调度模块 + +- **绿区:** 任务列表/详情/表单页面、任务记录查询接口、数据导出 +- **黄区:** 任务分配算法、计划任务生成、状态流转、巡检路线规划 +- **红区:** 任务与工资联动计算、任务完成验证(照片/GPS)、紧急任务推送 + +#### 人效分析模块 + +- **绿区:** 统计页面、图表组件、数据导出 +- **黄区:** 统计 SQL、效率评分算法、数据缓存策略 +- **红区:** 工资积分计算公式、绩效评定规则、数据权限隔离 + +### 4.4 开发流程中的分工 + +``` +1. 需求分析 → 100% 人工(产品+开发+测试) +2. 技术方案设计 → 人工主导,AI 辅助出初稿 +3. 数据库设计 → AI 出初稿 → 人工审核字段类型、索引、关联关系 +4. API 接口设计 → AI 出初稿 → 人工审核接口粒度、参数设计 +5. 后端 CRUD 开发 → AI 为主(Controller + 简单 Service + Mapper) +6. 核心业务逻辑 → 人工为主,AI 辅助写辅助函数 +7. 前端页面开发 → AI 为主 +8. 前后端联调 → AI 辅助排查参数不匹配问题 +9. 单元测试 → AI 生成测试用例 → 人工补充边界条件 +10. Code Review → 人工审核 AI 代码 + AI 辅助审核人工代码 +11. 部署上线 → 人工操作 +12. 线上验证 → 人工验证 +``` + +--- + +## 问题 5:产品手册与用户培训 + +### 5.1 用户画像分析 + +#### 保洁员画像 + +``` +年龄分布:35-55 岁为主 +教育背景:初中到高中为主 +手机使用: + - 会发微信、看短视频 + - 偏好语音 > 打字 + - 对新 APP 有抵触心理("又要学新东西") +工作特点: + - 早班 5:00-6:00 到岗,没时间"研究"系统 + - 手可能湿/脏,不方便精细操作 + - 手机可能是低端安卓机 + - 流动性高,平均在职 6-12 个月 +核心诉求: + - 打卡不要麻烦,任务看得懂就行,工资算对就行 +``` + +#### 班组长画像 + +``` +年龄分布:30-45 岁 +手机使用:熟练使用微信、钉钉,能接受学习新工具 +核心诉求: + - 快速知道谁没来、谁没干完 + - 简单地安排和调整任务 + - 处理异常(补卡、调班) +``` + +#### 项目经理画像 + +``` +年龄分布:28-40 岁 +核心诉求: + - 数据驾驶舱(全局概览) + - 人力成本分析、月度报告数据支撑 +``` + +### 5.2 分角色产品手册设计 + +#### 保洁员手册(极简版——一页纸、大字体、有图示、零术语) + +``` +设计规格: +- A4 双面彩打,覆膜(防水防脏) +- 字体最小 16px,关键词 20px 加粗 +- 左边图/截图,右边文字说明 +- 纯口语化 + +内容: + 上班时说: "我要上班打卡" + 下班时说: "我要下班打卡" + 看任务说: "今天有什么任务" + 干完了说: "XX区域打扫完了" + 请假时说: "我要请假" + 看工资说: "查看本月工资" + 有问题说: "我有问题要反馈" + + 注意: + - 打卡必须本人操作,不能帮别人打卡 + - 打卡时要在工作区域范围内 + - 不确定怎么说,直接说你想做什么就行 + 遇到问题找:班组长 XXX(电话) +``` + +#### 班组长手册(功能更全) + +**日常管理操作:** + +| 你想做什么 | 怎么说 | 备注 | +|-----------|--------|------| +| 查看团队出勤 | "今天谁没打卡" | 可加时间,如"本周出勤情况" | +| 安排临时任务 | "给张三安排清扫3号楼大厅" | 需指定人员和区域 | +| 查看任务进度 | "今天的任务完成情况" | 可按区域查 | +| 审批请假 | "查看待审批的请假" | | +| 查看巡检结果 | "今天的巡检报告" | 可指定区域 | + +**常见问题处理:** + +- 新员工第一天:班组长花 2 分钟教"上班打卡""看任务""下班打卡"三句话 +- 龙虾听不懂:换个说法试试,不行就打开 APP 手动操作 +- GPS 不准:连工作区域 WiFi,或到室外信号好的地方 +- 临时借调:说"临时借调XX到我的班组" + +### 5.3 系统内引导设计 + +#### 新手引导流程 + +``` +第1屏(欢迎):"你好!我是龙虾助手,从今天起帮你处理工作上的事" +第2屏(打卡演示):"来,试试跟我说'上班打卡'" → 用户说后 → "太棒了!就是这么简单!" +第3屏(任务演示):"再试试说'今天有什么任务'" → 展示任务列表 → "干完了跟我说一声" +``` + +#### 智能引导与纠错 + +``` +高置信度(>90%)→ 直接执行 + "我要上班打卡" → 执行打卡 + +中置信度(60-90%)→ 确认后执行 + "打卡" → "你是要上班打卡还是下班打卡?" + +低置信度(30-60%)→ 猜测 + 引导 + "考勤" → "关于考勤,你想:上班打卡/下班打卡/查看出勤记录?" + +极低置信度(<30%)→ 通用引导 + "吃饭了吗" → "我暂时只能帮你处理工作相关的事情。你可以问我:..." + +无法识别 → 兜底 + 学习 + 同时将消息记录到"未识别日志"中,用于后续优化 +``` + +### 5.4 培训方案(零集中培训) + +``` +培训金字塔: + +运营团队(2人) ← 深度培训 1 天 + ↓ 培训 +班组长 ← 中度培训 2 小时 + ↓ 带教 +保洁员 ← 现场带教 10 分钟(只学打卡、看任务、完成任务) +``` + +**为什么不做集中培训:** +1. 保洁员分散在各项目点,集中成本高 +2. 人员流动大,每月都有新人 +3. 学习能力参差不齐,集中培训效果差 +4. 学了不马上用就忘 + +**辅助手段:** +- 微信群钉 3 个 30 秒短视频(打卡、看任务、完成任务) +- 每周微信群发一个"你知道吗?你还可以说'查看本月工资'"小贴士 +- 月度工资提醒:"本月工资已发放,跟龙虾说'看工资'查看工资条" + +**应对抵触心理:** +- "我不会用手机" → 班组长当面教,只教打卡 +- "以前不用打卡也没事" → 强调打卡是算工资的依据 +- "手机里装太多东西了" → 基于微信,不需要装新 APP + +### 5.5 持续优化闭环 + +``` +数据收集 → 分析 → 优化 → 验证 + +1. 记录所有用户输入和系统响应,特别关注: + - 系统无法识别的消息 + - 用户放弃的对话 + - 用户重复说相同内容(说明第一次没成功) + +2. 每周分析: + - Top 20 未识别消息 → 添加意图/话术映射 + - 高放弃率的对话流程 → 优化引导 + +3. 持续更新: + - 每周更新话术库 + - 每月更新引导流程 + - 每季度更新手册 +``` + +--- + +## 问题 6:打卡身份校验与代操作拦截 + +### 6.1 威胁模型分析 + +| 威胁类型 | 典型场景 | 风险级别 | +|---------|---------|---------| +| 直接代打 | "帮李四打卡" | 高 | +| 间接代打 | "李四让我帮他说一下,他今天到了" | 高 | +| 身份伪装 | "我是李四"(用张三账号说) | 高 | +| 模糊描述 | "李四也到了" | 中 | +| 多人同时 | "我和李四都打卡" | 中 | +| 位置作弊 | 使用虚拟定位 APP | 高 | +| 时间作弊 | "我7点就到了,手机没电" | 中 | +| 社会工程 | "班组长帮我打个卡吧" | 中 | + +### 6.2 身份认证方案 + +| 级别 | 方案 | 适用场景 | +|------|------|----------| +| Level 1 | 微信 OpenID 绑定 | 低安全要求(基础方案) | +| Level 2 | 微信 + 设备指纹(推荐) | 标准安全要求 | +| Level 3 | 微信 + 人脸识别 | 高安全要求(如金融物业) | + +**建议 SmartClean 采用 Level 2:微信 OpenID + 设备指纹 + 地理围栏三重验证。** + +### 6.3 代操作检测逻辑 + +**检测规则:** + +| 规则 | 模式 | 示例 | 处理 | +|------|------|------|------| +| 规则1 | 代操作介词 + 他人名字 + 动词 | "帮李四打卡" | 拦截 | +| 规则2 | 第三人称 + 操作动词 | "李四要打卡" | 拦截 | +| 规则3 | 身份伪装 | "我是李四" | 拦截,提示以登录账号为准 | +| 规则4 | 多人操作 | "我和李四一起打卡" | 拦截,只为当前用户打卡 | +| 查询例外 | 他人名字 + 疑问 | "李四打卡了吗?" | 允许(需权限),当做查询 | + +**代操作检测核心逻辑(伪代码):** + +```java +public ProxyDetectResult detect(String message, Long currentUserId) { + String currentUserName = getNameById(currentUserId); + + // 规则1:代操作介词检测 + // "帮/替/给 + 他人名字 + 操作动词" + if (包含代操作介词 && 包含他人名字 && 包含操作动词) { + return 拦截("打卡只能本人操作,请让对方用自己的手机"); + } + + // 规则2:第三人称操作请求 + // "李四要打卡"(不是"李四打卡了吗?") + if (提到他人名字 && 包含操作动词 && 不是查询语气) { + return 拦截("不能帮他人操作,每个人需要用自己的手机"); + } + + // 规则3:身份伪装 + // "我是李四" + if (包含身份声明 && 声明的名字不是当前用户) { + return 拦截("系统已通过手机识别你的身份,不需要报名字"); + } + + // 规则4:多人操作 + // "我和李四一起打卡" + if (包含"和/一起/我们" && 提到他人名字 && 包含操作动词) { + return 拦截("我先帮你打卡,其他人请用自己的手机"); + } + + return 通过(); +} +``` + +### 6.4 打卡全流程 + +``` +用户发送消息 + │ + ▼ +1. 意图识别 → "我要上班打卡" → PUNCH_IN + │ + ▼ +2. 代操作检测 → 检查是否涉及他人 → 拦截或通过 + │ + ▼ +3. 身份确认 → Session 获取 userId + 设备指纹校验 + │ + ▼ +4. 地理围栏校验 → GPS 定位(降级到 WiFi)→ 计算距离 + │ + ▼ +5. 防重复打卡 → 30 分钟内同类型已打过 → 提示 + │ + ▼ +6. 考勤判定 → 与排班时间比对 → 正常/迟到/早退 + │ + ▼ +7. 写入记录 + 审计日志 + │ + ▼ +8. 返回结果 + 正常:"上班打卡成功!08:01,全勤继续保持!" + 迟到:"上班打卡成功!08:18,迟到 3 分钟。" +``` + +### 6.5 审计日志设计 + +每次打卡请求(无论成功失败)都记录: + +```json +{ + "timestamp": "2026-04-15T08:32:15", + "userId": 10086, + "userName": "张三", + "rawMessage": "我要上班打卡", + "intent": "PUNCH_IN", + "proxyDetected": false, + "geoLocation": {"lat": 31.2304, "lng": 121.4737}, + "withinFence": true, + "result": "SUCCESS", + "attendanceStatus": "NORMAL" +} +``` + +代操作拦截记录: + +```json +{ + "timestamp": "2026-04-15T08:35:22", + "userId": 10086, + "userName": "张三", + "rawMessage": "帮李四打个卡吧", + "intent": "PUNCH_IN", + "proxyDetected": true, + "proxyTarget": "李四", + "result": "BLOCKED", + "blockReason": "代操作拦截" +} +``` + +审计日志的价值: +- 员工投诉"我打了卡但系统没记录" → 查审计日志 +- 发现疑似作弊行为 → 分析异常模式 +- 优化 NLP → 分析被拦截的消息 +- 法律纠纷 → 作为电子证据 + +--- + +## 问题 7:开放问题与逻辑安全 + +### 7.1 两个层面分析 + +#### 层面 A:AI 辅助开发中的开放问题 + +开发者和 Claude Code 对话时,问分析性问题,AI 可能"好心地"顺便改代码。 + +**防护措施(写入 CLAUDE.md):** + +``` +1. 分析性问题("你觉得""你看看""合不合理")→ 只输出文字分析,不修改文件 +2. 明确指令("修改""添加""删除""重构")→ 执行修改 +3. 模糊指令("处理一下""搞一下")→ 先确认意图,等待确认 +4. 修改红区文件 → 必须先列出完整修改方案,等待确认 +5. 不可在回答问题时顺便修改不相关的文件 +6. 不可"优化"未被要求修改的代码 +7. 不可修改测试用例中的预期值来让测试通过 +``` + +**额外安全网:Git 习惯** +- 每次让 AI 开始新任务前,先 commit 当前状态 +- AI 做完后,`git diff` 审核所有变更 +- 不满意 → `git checkout` 回退 + +#### 层面 B:终端用户对话的逻辑安全(龙虾助手) + +面向保洁员的对话系统,必须做到**绝对的读写隔离**。 + +**核心设计:意图白名单制** + +``` +所有允许的操作穷举列出: + +写操作(需二次确认): +- PUNCH_IN / PUNCH_OUT(打卡) +- LEAVE_REQUEST(请假) +- TASK_COMPLETE(完成任务) +- SUPPLEMENT_PUNCH(补卡申请) + +读操作(直接返回): +- QUERY_MY_TASKS(查看我的任务) +- QUERY_MY_ATTENDANCE(查看我的考勤) +- QUERY_MY_SALARY(查看我的工资) + +管理操作(仅班组长+): +- QUERY_TEAM_ATTENDANCE(查看团队考勤) +- ASSIGN_TEMP_TASK(安排临时任务) + +不在白名单 → 当做闲聊,只返回文字,绝不触发数据库操作 +``` + +**写操作二次确认:** + +``` +用户:"我要请假三天" +龙虾:"确认请假信息: + 请假人:张三 + 时间:2026-04-16 至 2026-04-18(3天) + 类型:事假 + 确认提交吗?[确认] [取消]" +``` + +**危险操作完全拦截:** + +``` +用户:"把我上个月的打卡记录删了" +龙虾:"抱歉,删除数据需要管理员在后台操作。 + 如果你的考勤记录有问题,可以跟班组长说明情况。" + +以下操作不能通过对话执行: +- 删除数据 +- 修改工资配置 +- 修改权限设置 +- 批量操作 +``` + +**注入防护:** +- 用户消息永远作为纯文本处理,不参与任何代码执行 +- 意图识别输出的是枚举值,不是用户原文 +- 数据库操作使用参数化查询 +- 如果用 LLM 做意图识别,用户消息放在独立标签中,与系统 prompt 严格隔离 + +--- + +## 问题 8:手机端数据展示方案 + +### 8.1 核心设计原则 + +``` +保洁员 ≠ 办公室白领 + +设计原则: +1. 首屏 = 最重要的一个数字/状态 +2. 操作 ≤ 3 次点击完成 +3. 文字大、按钮大、间距大 +4. 支持弱网/离线基础功能 +5. 绝不要求用电脑 +``` + +### 8.2 各类数据的适配方案 + +#### 工资条:卡片化 + 渐进展开 + +**第一级:摘要卡片(龙虾助手直接返回)** + +``` +2026年4月 工资条 + + 实发工资 + ¥ 5,680.00 + +基本工资 ¥4,500.00 +加班费 ¥ 750.00 +全勤奖 ¥ 200.00 +─────────────────── +社保 -¥ 520.00 +个税 -¥ 250.00 + +[查看完整明细] [有疑问] +``` + +**第二级:完整明细(H5 页面,不是 Web 后台)** +- 所有收入项 + 计算过程(如"平日加班费:10小时 × ¥28.74 × 1.5 = ¥431.03") +- 所有扣除项 +- 出勤信息汇总 +- 底部有"工资有疑问?点击反馈"入口 + +关键设计:加班费后标注计算过程,让员工看懂怎么算的。不用横向表格,全部纵向列表。 + +#### 考勤记录 + +``` +龙虾返回: +"你4月的考勤情况: + 出勤 21 天(全勤) + 迟到 0 次 / 早退 0 次 + 加班 15.22 小时 + 全勤奖 ¥200 到手!继续保持! + [查看每日打卡详情]" +``` + +#### 任务清单 + +``` +"今天你有 3 个任务: +1. A栋大厅清扫 [08:00-10:00] +2. B栋卫生间清洁 [10:00-11:30] +3. 外围道路保洁 [13:00-16:00] +干完了跟我说'XX干完了'就行!" +``` + +#### 排班表 + +``` +"这周你的排班: +周一 07:00-16:00 A区 +周二 07:00-16:00 A区 +周三 07:00-16:00 B区 ← 注意换区了 +周四 07:00-16:00 B区 +周五 07:00-16:00 A区 +周六 休息 / 周日 休息" +``` + +### 8.3 各数据类型的双端分工 + +| 数据 | 手机端展示 | Web 端附加能力 | +|------|-----------|---------------| +| 工资条 | 卡片摘要 + H5 详情 | 导出 Excel、历史对比 | +| 考勤 | 出勤天数 + 异常标记 | 完整日历视图 | +| 排班 | 我的本周排班 | 全员排班甘特图 | +| 任务统计 | 今日/本周完成数 | 多维度图表分析 | +| 巡检报告 | 最近一次结果 | 历史趋势、照片汇总 | + +### 8.4 Web 端定位 + +Web 端不是给保洁员用的,而是给管理层用的: + +``` +Web 端用户:项目经理、区域经理、公司管理层 +Web 端功能: +- 数据驾驶舱(全局概览) +- 多维度报表(可筛选、可导出) +- 排班管理(甘特图、批量操作) +- 人员管理(入职、离职、调动) +- 薪资管理(批量算薪、审核、发放) +- 系统配置(权限、围栏、规则配置) + +设计原则: +- Web 端面向"管理+分析"场景 +- 手机端面向"执行+查询"场景 +- 两端数据实时同步 +- 不强制任何人必须用 Web 端 +``` + +--- + +## 问题 9:国内 AI 编程现状与成本管理 + +### 9.1 工具能力对比(以 SmartClean 典型任务为基准) + +**任务 1:生成完整的排班 CRUD** + +| 工具 | 评分 | 说明 | +|------|------|------| +| Claude Code (Opus) | ★★★★★ | 一次生成完整代码,分层规范 | +| Cursor (Claude) | ★★★★☆ | IDE 内生成,需分步操作 | +| GitHub Copilot | ★★★☆☆ | 补全好,无法一次生成完整模块 | +| 通义灵码 | ★★★☆☆ | 能生成框架,细节需调整 | +| DeepSeek Coder | ★★★☆☆ | Java 代码质量不错,上下文理解弱 | +| 豆包 MarsCode | ★★☆☆☆ | 简单补全可以,复杂力不足 | + +**任务 2:排查跨天排班冲突 Bug** + +| 工具 | 评分 | 说明 | +|------|------|------| +| Claude Code (Opus) | ★★★★★ | 能理解业务上下文,精确定位 | +| Cursor (Claude) | ★★★★☆ | 需手动指定相关文件 | +| DeepSeek Coder | ★★★☆☆ | 推理不错但上下文窗口小 | +| 通义灵码 | ★★☆☆☆ | 简单 Bug 可以,复杂推理弱 | + +**任务 3:设计打卡防作弊方案** + +| 工具 | 评分 | 说明 | +|------|------|------| +| Claude Code (Opus) | ★★★★★ | 方案完整、考虑全面 | +| DeepSeek (Chat) | ★★★★☆ | 推理能力强,方案有深度 | +| 通义灵码 | ★★★☆☆ | 中文好,方案深度一般 | + +### 9.2 推荐的任务分流策略 + +| 任务类型 | 推荐工具 | 原因 | +|---------|---------|------| +| 日常代码补全 | 通义灵码/Copilot | 实时补全,免费/低价 | +| 简单 CRUD | DeepSeek/通义 | 够用且便宜 | +| Vue 页面开发 | DeepSeek/通义 | 前端代码质量可以 | +| 复杂业务逻辑 | Claude Code | 推理能力最强 | +| 架构设计 | Claude Code | 长上下文 + 深度推理 | +| Bug 排查 | Claude Code | 跨文件理解能力最强 | +| 代码重构 | Claude Code/Cursor | 需要理解全局上下文 | +| 文档生成 | DeepSeek/通义 | 中文好,成本低 | +| 单元测试 | 通义/DeepSeek | 模板化工作 | +| SQL 编写优化 | Claude/DeepSeek | 需要理解表关系 | + +### 9.3 Token 消耗深度分析 + +**为什么对话越长越贵:** + +``` +轮次 单轮 token 累计 token 累计成本(Opus 估算) +1 15,000 15,000 $0.30 +5 25,000 115,000 $2.10 +10 40,000 315,000 $5.70 +20 70,000 1,015,000 $17.50 +30 100,000 2,015,000 $34.00 +50 150,000 5,015,000 $82.00 + +因为每轮对话都携带之前的完整历史! +``` + +**Token 优化策略:** + +**策略 1:优化 CLAUDE.md(节省 30-40%)** +在 CLAUDE.md 中写清楚代码生成规范(Java 版本、字段类型、全局组件等),每条规范能减少一轮返工对话。 + +**策略 2:任务拆分(节省 40-60%)** + +``` +错误:一个大对话完成整个需求 → ~3,000,000-5,000,000 token +正确:拆成 6 个小对话 → ~1,000,000 token(节省 60-70%) +``` + +10 个各 5 轮的小对话,比 1 个 50 轮的大对话便宜得多。 + +**策略 3:精准 Prompt(节省 20-30%)** + +``` +错误(引发 3-5 轮追问): +"帮我写个打卡接口" + +正确(一次到位): +"在 AttendanceController 中新增 POST /api/attendance/punch 接口: + 接收 PunchDTO(type: IN/OUT, latitude, longitude), + 校验地理围栏 200 米,防重复 30 分钟, + 判定迟到/早退 15 分钟,需要权限注解" +``` + +**策略 4:善用 /compact** +对话进行到 15-20 轮时执行 `/compact`,后续每轮消耗降低 30-50%。 + +**策略 5:分流到国内模型(节省 50-70% 成本)** +60-70% 的日常任务用国内模型(免费或极低价),30-40% 的复杂任务用 Claude。 + +### 9.4 成本测算 + +``` +假设:2 个开发者,月均开发 12 个需求 + +方案 A:全部使用 Claude Code + 月成本:约 ¥9,000 + +方案 B:混合策略(推荐) + 60% 用 DeepSeek(约 ¥200/月) + 10% 用通义灵码(免费) + 30% 用 Claude Code(约 ¥1,350/月) + 月总成本:约 ¥3,100 + +方案 C:Claude Max 订阅 + $100-200/月/人 + 两个开发者:约 ¥1,400-2,800/月 + +对比参考: + 中级 Java 开发者月薪 ¥15,000-25,000 + AI 工具成本 ¥1,500-4,500/月 = 开发者工资的 3-8% + 如果 AI 能提升 30%+ 效率,ROI 非常高 +``` + +### 9.5 数据安全合规 + +**数据分级制度:** + +| 级别 | 说明 | 处理方式 | +|------|------|----------| +| Level 1 公开数据 | 开源代码、公开技术文档 | 可发送给任何 AI | +| Level 2 内部数据 | 业务代码、表结构、接口定义 | 可发送给付费 AI 服务 | +| Level 3 敏感数据 | 含用户信息的代码、业务数据 SQL | 脱敏后可发送 | +| Level 4 机密数据 | 密码、密钥、真实工资、身份证号 | 禁止发送 | + +**发送代码给 AI 前的检查清单:** + +- [ ] 代码中没有硬编码的密码/密钥/Token +- [ ] 代码中没有真实的 IP 地址/域名 +- [ ] 代码中没有真实用户的姓名/手机号 +- [ ] 配置文件中的敏感信息已替换为占位符 +- [ ] SQL 中的 WHERE 条件不包含真实数据 +- [ ] 错误截图中没有敏感信息 + +--- + +## 综合实施路线图 + +### 第一阶段:立即执行(本周) + +| 行动 | 对应问题 | 负责人 | +|------|----------|--------| +| 更新 CLAUDE.md 添加代码生成规范 | 问题 3/9 | 技术负责人 | +| 更新 CLAUDE.md 添加代码分区规则 | 问题 3 | 技术负责人 | +| 制定 Git Commit 规范 | 问题 1 | 技术负责人 | +| 制作保洁员一页纸快速指南 | 问题 5 | 产品经理 | + +### 第二阶段:两周内完成 + +| 行动 | 对应问题 | +|------|----------| +| 创建 BusinessRules.java 业务规则断言文件 | 问题 3 | +| 建立核心业务逻辑的单元测试覆盖 | 问题 3 | +| 实现打卡代操作检测逻辑 | 问题 6 | +| 实现意图白名单 + 写操作确认机制 | 问题 7 | +| 制定人工/AI 开发边界文档并团队对齐 | 问题 4 | + +### 第三阶段:一个月内完善 + +| 行动 | 对应问题 | +|------|----------| +| 建立需求级开发日志模板并执行 | 问题 1 | +| 沉淀 AI 开发经验知识库初版 | 问题 1 | +| 完成工资条/考勤移动端卡片化设计 | 问题 8 | +| 完成班组长操作手册 + 培训视频 | 问题 5 | +| 确定混合工具策略并跟踪成本 | 问题 9 | + +### 持续迭代 + +| 行动 | 频率 | +|------|------| +| 分析用户未识别消息,优化话术 | 每周 | +| 更新 AI 经验知识库 | 每月 | +| 审计红区文件变更 | 每月 | +| 评估 AI 工具效果与成本 | 每季度 | + +--- + +## 附录:高效 Prompt 模板库 + +### 新增 CRUD 功能模板 + +``` +在 {模块名} 中新增 {功能名} 的完整 CRUD: + +数据库: +- 表名:{table_name} +- 核心字段:{字段列表及类型} +- 必须包含:id(BIGINT), create_time, update_time, deleted(TINYINT) +- 类型/状态字段用 TINYINT,时间字段用 DATETIME +- 设计查询所需索引 + +后端: +- Controller: {接口路径前缀} +- 包含:分页列表、新增、修改、删除(逻辑删除)、详情 +- 分页大小上限 100,参数校验,权限注解 + +前端: +- 使用 qu-table 全局组件 +- 列和按钮通过 fetchFields/fetchButtons 动态加载 +- API 定义在 Apis/api.js,调用封装在 ApiService/ 下 +- 提交按钮加 loading 防重 +``` + +### 复杂业务逻辑模板 + +``` +实现 {功能描述} + +业务规则(必须全部遵守,不可简化): +1. {规则1} +2. {规则2} + +边界条件: +- {如跨天、并发、空值} + +角色权限: +- {角色1}可以{操作} +- {角色2}只能{有限操作} + +技术约束: +- 金额用 BigDecimal,多表写入加 @Transactional + +请先输出实现方案概述,确认后再编写代码。 +``` diff --git a/docs/architecture-flow.md b/docs/architecture-flow.md new file mode 100644 index 0000000..87c6d21 --- /dev/null +++ b/docs/architecture-flow.md @@ -0,0 +1,705 @@ +# 智慧清洁 SaaS 平台 - 系统流程架构图 + +## 一、系统整体架构 + +``` +┌─────────────────────────────────────────────────────────────────────────────────┐ +│ 客户端层 │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ Web 管理后台 │ │ 手机 APP │ │ 智能手表 │ │ +│ │ (Vue 3 SPA) │ │ (Android/iOS)│ │ (IoT 设备) │ │ +│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ +│ │ HTTP/REST │ HTTP/REST │ MQTT │ +└─────────┼───────────────────┼───────────────────┼──────────────────────────────┘ + │ │ │ + ▼ ▼ ▼ +┌─────────────────────────────────────────────────────────────────────────────────┐ +│ 服务层 │ +│ ┌─────────────────────────────────┐ ┌─────────────────────────────────┐ │ +│ │ Web 服务 (端口 8095) │ │ Task 服务 (端口 8097) │ │ +│ │ ┌───────────┐ ┌─────────────┐ │ │ ┌───────────┐ ┌────────────┐ │ │ +│ │ │ Controller│ │ Interceptor │ │ │ │ XXL-Job │ │ @Scheduled │ │ │ +│ │ │ (API) │ │ (认证签名) │ │ │ │ (定时任务) │ │ (消息队列) │ │ │ +│ │ └─────┬─────┘ └──────┬──────┘ │ │ └─────┬─────┘ └─────┬──────┘ │ │ +│ │ │ │ │ │ │ │ │ │ +│ │ ┌─────▼──────────────▼──────┐ │ │ ┌─────▼─────────────▼───────┐ │ │ +│ │ │ Service 层 │ │ │ │ Service 层 │ │ │ +│ │ └─────────────┬─────────────┘ │ │ └────────────┬──────────────┘ │ │ +│ │ │ │ │ │ │ │ +│ │ ┌─────────────▼─────────────┐ │ │ ┌────────────▼──────────────┐ │ │ +│ │ │ Mapper (MyBatis-Plus) │ │ │ │ Mapper (MyBatis-Plus) │ │ │ +│ │ └─────────────┬─────────────┘ │ │ └────────────┬──────────────┘ │ │ +│ └────────────────┼────────────────┘ └───────────────┼─────────────────┘ │ +│ │ │ │ +└───────────────────┼─────────────────────────────────────┼──────────────────────┘ + │ │ + ┌─────────▼─────────────────────────────────────▼──────────┐ + │ 数据层 │ + │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ + │ │ MySQL DB1 │ │ MySQL DB2 │ │ Redis │ │ + │ │ (基础信息) │ │ (业务数据) │ │ (DB4+DB5) │ │ + │ └──────────────┘ └──────────────┘ └──────────────┘ │ + └─────────────────────────────────────────────────────────┘ +``` + +--- + +## 二、Web 请求完整链路 + +### 2.1 前端 HTTP 请求 → 后端响应 + +``` +┌──────────────────────────────────────────────────────────────────────────────┐ +│ 前端 (Vue 3 SPA) │ +│ │ +│ 用户操作 (点击按钮/提交表单) │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ ApiService (如 taskCenterService.js) │ │ +│ │ 调用封装的 API 方法 │ │ +│ └──────────────────────────┬──────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ requestUtils.js - Axios 请求拦截器 │ │ +│ │ │ │ +│ │ ① 从 sessionStorage 读取登录信息 (UUID, phone) │ │ +│ │ ② 生成签名: MD5(MD5(uuid) + MD5(timestamp + phone)) │ │ +│ │ ③ 设置请求头: │ │ +│ │ - signature: 签名值 │ │ +│ │ - uuid: 用户UUID │ │ +│ │ - timestamp: 当前时间戳 │ │ +│ │ - webId: 当前页面ID │ │ +│ │ ④ 清除空值参数 │ │ +│ │ ⑤ 设置 withCredentials: true │ │ +│ └──────────────────────────┬──────────────────────────────────────┘ │ +│ │ │ +│ │ POST /api/xxx │ +│ │ (通过 Vite proxy 代理到 localhost:8095) │ +└─────────────────────────────┼────────────────────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────────────────────────────┐ +│ 后端 Web 服务 (Spring Boot, 端口 8095) │ +│ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ ① SystemInterceptor (请求拦截器) │ │ +│ │ │ │ +│ │ 排除路径: /login, /sms/sendCode, /doc.html, /swagger │ │ +│ │ │ │ +│ │ a. 提取请求头: uuid, signature, timestamp │ │ +│ │ b. 验证时间戳 (10秒有效窗口) │ │ +│ │ c. 从 Redis 查询登录态: │ │ +│ │ Redis GET uuid → 获取用户信息(mobile等) │ │ +│ │ d. 验证签名: │ │ +│ │ 服务端重算 MD5 签名 vs 请求头签名 │ │ +│ │ e. 验证通过 → 将用户信息存入 Request Attribute │ │ +│ │ f. 验证失败 → 返回 code 104 (需重新登录) │ │ +│ │ │ │ +│ └──────────────────────────┬──────────────────────────────────────┘ │ +│ │ 认证通过 │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ ② RepeatSubmitAspect (AOP 防重复提交) │ │ +│ │ │ │ +│ │ a. 生成锁 Key: "redis_lock_" + 请求路径 │ │ +│ │ b. Redis SETNX 尝试加锁 (30秒过期) │ │ +│ │ c. 获取锁 → 继续执行 │ │ +│ │ d. 未获取锁 → 返回 "请勿重复提交" │ │ +│ │ │ │ +│ └──────────────────────────┬──────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ ③ Controller (如 TaskController) │ │ +│ │ │ │ +│ │ a. 接收请求参数 (@RequestBody / @RequestParam) │ │ +│ │ b. 从 Request Attribute 获取当前用户信息 │ │ +│ │ c. 调用 Service 层处理业务逻辑 │ │ +│ │ │ │ +│ └──────────────────────────┬──────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ ④ Service 层 │ │ +│ │ │ │ +│ │ a. 业务逻辑处理 │ │ +│ │ b. 组合多个 Mapper 查询/更新 │ │ +│ │ c. 操作 Redis 缓存 (读/写) │ │ +│ │ d. 写入变更日志 (ChangeLog) │ │ +│ │ │ │ +│ └──────────────────────────┬──────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ ⑤ Mapper 层 (MyBatis-Plus) │ │ +│ │ │ │ +│ │ a. DB1 Mapper → MySQL xiaoqu_comples_d (基础数据) │ │ +│ │ b. DB2 Mapper → MySQL xiaoqu_intellectual_d (业务数据) │ │ +│ │ c. 支持分页查询 (IPage) │ │ +│ │ d. 支持动态条件构造 (QueryWrapper) │ │ +│ │ │ │ +│ └──────────────────────────┬──────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ ⑥ SetUserAspect (AOP 审计日志) │ │ +│ │ │ │ +│ │ a. 记录操作人 (create_id / modify_id) │ │ +│ │ b. 记录操作时间 (create_time / modify_time) │ │ +│ │ c. 记录变更日志 (ChangeLog) │ │ +│ │ │ │ +│ └──────────────────────────┬──────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ ⑦ 构造响应 (统一返回格式) │ │ +│ │ │ │ +│ │ 成功: { code: 100, data: {...}, msg: "操作成功" } │ │ +│ │ 失败: { code: 101, data: null, msg: "错误信息" } │ │ +│ │ 未登录: { code: 104, data: null, msg: "请重新登录" } │ │ +│ │ 指纹无效: { code: 113, data: null, msg: "指纹无效" } │ │ +│ │ │ │ +│ └──────────────────────────┬──────────────────────────────────────┘ │ +│ │ │ +└─────────────────────────────┼────────────────────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────────────────────────────┐ +│ 前端 (Vue 3 SPA) │ +│ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ requestUtils.js - Axios 响应拦截器 │ │ +│ │ │ │ +│ │ ① code == 100 → 返回 data 给业务组件 │ │ +│ │ ② code == 104 → 清除 sessionStorage → 跳转登录页 │ │ +│ │ ③ code == 113 → 指纹无效 → 跳转登录页 │ │ +│ │ ④ 其他 code → 显示错误提示 (ElMessage) │ │ +│ │ │ │ +│ └──────────────────────────┬──────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 页面组件渲染 │ │ +│ │ │ │ +│ │ a. qu-table 根据权限组动态渲染列 (fetchFields) │ │ +│ │ b. qu-button-group 根据权限组动态渲染按钮 (fetchButtons) │ │ +│ │ c. 数据绑定到 Vuex Store / 组件 data │ │ +│ │ │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────┘ +``` + +### 2.2 登录认证流程 + +``` +用户输入手机号 + │ + ▼ +┌──────────────┐ GET /sms/sendCode?mobile=xxx +│ 获取验证码 │ ──────────────────────────────────► SendSmsController +└──────┬───────┘ │ + │ ▼ + │ 生成验证码 → Redis SET + │ 调用短信平台发送 + │ + ▼ +┌──────────────┐ POST /login {mobile, code} +│ 提交登录 │ ──────────────────────────────────► LoginController +└──────┬───────┘ │ + │ ▼ + │ ① 验证码校验 (Redis GET) + │ ② 查询用户 (MySQL user表) + │ ③ 生成 UUID + │ ④ Redis SET uuid → 用户信息 (30天) + │ ⑤ 记录登录日志 (login_log) + │ ⑥ 返回: uuid + 用户信息 + 菜单权限 + │ + ▼ +┌──────────────┐ +│ sessionStorage│ ← 存储 loginInfo(base64), uuid, phone +│ 后续请求携带 │ → signature, uuid, timestamp 请求头 +└──────────────┘ +``` + +--- + +## 三、任务调度完整链路 + +### 3.1 计划 → 任务生成 → 派单 → 推送 + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ 阶段一:计划创建与启用 (Web 服务) │ +│ │ +│ 管理员操作 (Web后台) │ +│ │ │ +│ ▼ │ +│ POST /plan/addOrUpdatePlan │ +│ │ │ +│ ▼ │ +│ PlanController → PlanService │ +│ │ 保存计划: Plan + PlanObject + PlanOperation + PlanScenes │ +│ │ + PlanUser + PlanWorkGroup │ +│ ▼ │ +│ POST /plan/enablePlan │ +│ │ │ +│ ▼ │ +│ 创建 XXL-Job 定时任务 (planCreateTask) │ +│ 设置 cron 表达式 (根据周期配置) │ +│ Redis SET Plan:: → xxlJobId │ +│ │ +└───────────────────────────────┬─────────────────────────────────────────────┘ + │ XXL-Job 定时触发 + ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ 阶段二:任务生成 (Task 服务 - PlanTimeTask) │ +│ │ +│ planCreateTask 触发 │ +│ │ │ +│ ▼ │ +│ ① 检查节假日 (Holiday表) │ +│ │ eliminate_type=1 且为节假日 → 跳过 │ +│ ▼ │ +│ ② 检查禁用标志 │ +│ │ Redis GET Disable:d:: │ +│ │ 如果 "1" → 存入 RebuildTask 等待恢复 │ +│ ▼ │ +│ ③ 生成任务 │ +│ │ Redis LPUSH PlanTaskQueue ← planId │ +│ ▼ │ +│ ④ 计算下一次执行时间 │ +│ │ 根据 circle_type (天/周/月) + circle_no 计算 │ +│ │ 动态更新 XXL-Job cron 表达式 │ +│ ▼ │ +│ ⑤ 创建 TaskInfo 记录 (MySQL) │ +│ │ 状态: status=0 (已生成) │ +│ │ 关联: plan_id, district_id, task_type │ +│ │ 创建: TaskScenes, TaskUser │ +│ │ +└───────────────────────────────┬─────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ 阶段三:任务派单 (Task 服务 - TaskCorrelationSetTask, 每秒执行) │ +│ │ +│ Redis RPOP taskUserQueue │ +│ │ │ +│ ▼ │ +│ ① 读取派单配置 │ +│ │ Redis HMGET Dictionary: │ +│ │ UserAppointFlag (是否指定人员) │ +│ │ UserAreaFlag (是否限定区域) │ +│ │ UserDutyFlag (是否按职责) │ +│ ▼ │ +│ ② 匹配可用人员 │ +│ │ 查询: 用户状态=空闲, 所属站点, 技能匹配, 服务区域 │ +│ │ 排序: 按优先级/距离 │ +│ ▼ │ +│ ③ 分配任务 │ +│ │ 更新 TaskInfo.status = 1 (已匹配) / 2 (待抢单) / 3 (已接单) │ +│ │ 创建/更新 TaskUser 记录 │ +│ ▼ │ +│ ④ 触发推送 │ +│ │ Redis LPUSH taskPushQueue ← TaskPushInfo │ +│ │ Redis LPUSH WatchTaskUserList ← taskUserId │ +│ │ Redis LPUSH IntellectualTaskIdList ← taskId │ +│ │ +└──────────┬──────────────────────────────┬───────────────────────────────────┘ + │ │ + ▼ ▼ +┌────────────────────────┐ ┌──────────────────────────────────────────────┐ +│ 阶段四A:APP推送 │ │ 阶段四B:手表推送 │ +│ (TaskPushMessage, 每秒) │ │ (ScheduledTask, 每2秒) │ +│ │ │ │ +│ Redis RPOP │ │ Redis RPOP WatchTaskUserList │ +│ taskPushQueue │ │ │ │ +│ │ │ │ ▼ │ +│ ▼ │ │ 构建 WatchTaskMessage │ +│ 判断推送类型: │ │ (任务名/类型/位置/时间) │ +│ status=1 新任务 │ │ │ │ +│ status=2 抢单池 │ │ ▼ │ +│ status=3 已接单 │ │ Redis LPUSH MqttWatchTaskMessage │ +│ status=7/8 已取消 │ │ │ │ +│ │ │ │ ▼ │ +│ ▼ │ │ Redis RPOP MqttWatchTaskMessage │ +│ 调用个推 SDK: │ │ │ │ +│ 在线→NotificationTpl │ │ ▼ │ +│ 离线→TransmissionTpl │ │ MQTT publish → 手表 (topic=imei) │ +│ │ │ │ │ +│ ▼ │ │ │ +│ 保存 PushLog │ │ │ +│ │ │ │ │ +│ ▼ │ │ │ +│ ┌──────────┐ │ │ ┌──────────┐ │ +│ │ 手机 APP │ │ │ │ 智能手表 │ │ +│ │ 收到通知 │ │ │ │ 收到任务 │ │ +│ └──────────┘ │ │ └──────────┘ │ +└────────────────────────┘ └──────────────────────────────────────────────┘ +``` + +### 3.2 任务状态变更推送流程 + +``` +员工操作 (APP: 接单/开始/完成) + │ + ▼ +Web API → 更新 TaskInfo.status + │ + ▼ +Redis LPUSH IntellectualTaskIdList ← taskId + │ + ▼ (Task 服务, 每2秒) +┌─────────────────────────────────────────────┐ +│ TodayDynamicMessage │ +│ │ +│ ① Redis LPOP IntellectualTaskIdList │ +│ ② 查询 TaskInfo 详情 │ +│ ③ 确定推送对象: │ +│ - 代理人员 (Agent用户) │ +│ - 物业管理员 │ +│ - 任务分配人员 │ +│ - 指派管理员 │ +│ ④ 生成 TodayDynamic 记录 (MySQL) │ +│ ⑤ Redis LPUSH MqttTaskStateMessage │ +└──────────────────┬──────────────────────────┘ + │ + ▼ (每2秒) +┌─────────────────────────────────────────────┐ +│ MqttTaskStateMessage │ +│ │ +│ ① Redis RPOP MqttTaskStateMessage │ +│ ② 构建推送消息 │ +│ ③ MQTT publish → mobile_ │ +│ │ +│ ┌──────────┐ │ +│ │ 手机 APP │ ← 实时接收任务动态 │ +│ └──────────┘ │ +└─────────────────────────────────────────────┘ +``` + +--- + +## 四、设备告警 → 维护任务流程 + +``` +┌──────────────┐ +│ IoT 设备 │ (纸巾机/空气净化器/传感器) +│ 检测到异常 │ +└──────┬───────┘ + │ 告警数据 + ▼ +┌──────────────────────────────────────────────────────────────┐ +│ 设备告警 → Redis │ +│ │ +│ Redis LPUSH taskCommand ← TaskCommandInfo JSON │ +│ { │ +│ snCode: "设备SN", │ +│ taskTypeId: 23, // 空气质量/纸巾不足等 │ +│ districtId: 1, │ +│ scenesId: 5 │ +│ } │ +└──────────────────────────┬───────────────────────────────────┘ + │ + ▼ (Task 服务 - TaskCreateQueue, 每秒) +┌──────────────────────────────────────────────────────────────┐ +│ TaskCreateQueue.taskCommandHandle() │ +│ │ +│ ① Redis RPOP taskCommand (DB5) │ +│ ② 检查任务开关: │ +│ Redis GET DevTaskFlag: (DB4) │ +│ 如果 "0" → 跳过 │ +│ ③ 检查禁用标志: │ +│ Redis GET Disable:d/t/p:: (DB4) │ +│ 如果 "1" → 存入 RebuildTask 等待恢复 │ +│ ④ 去重检查: │ +│ 查询是否存在同设备未完成任务 │ +│ 如果存在 → 跳过 │ +│ ⑤ 创建 TaskInfo (MySQL): │ +│ status=0, task_type=设备维护 │ +│ 关联: sn_code, district_id, scenes_id │ +│ ⑥ 创建 TaskScenes, TaskUser │ +│ ⑦ Redis LPUSH taskPushQueue ← 推送消息 │ +│ ⑧ Redis LPUSH WatchTaskUserList ← 手表任务 │ +│ │ +└──────────────────────────┬───────────────────────────────────┘ + │ + ▼ + (走标准推送流程,同 3.1 阶段四) + │ + ┌────────────┼────────────┐ + ▼ ▼ ▼ + ┌──────────┐ ┌──────────┐ ┌──────────┐ + │ APP 通知 │ │ 手表提醒 │ │ Web 后台 │ + │ 维护员收到│ │ 震动+语音 │ │ 任务列表 │ + └──────────┘ └──────────┘ └──────────┘ +``` + +--- + +## 五、MQTT 设备通信流程 + +### 5.1 手表上下线监控 + +``` +智能手表 + │ + │ MQTT connect/disconnect + ▼ +┌──────────────────────────────────────────────┐ +│ MQTT Broker (tcp://mqtt:1883) │ +│ │ +│ Topic: $SYS/brokers/+/clients/# │ +│ 消息: {clientid:"watch_IMEI_xxx", │ +│ connected_at: 1700000000} │ +└──────────────────┬───────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────┐ +│ PushCallback (MQTT 消息回调) │ +│ │ +│ ① 过滤: clientid 包含 "watch" │ +│ ② 解析: connected_at / disconnected_at │ +│ ③ Redis LPUSH WatchConnectList ← 连接事件 │ +└──────────────────┬───────────────────────────┘ + │ + ▼ (每秒) +┌──────────────────────────────────────────────┐ +│ ScheduledTask.WatchConnectList() │ +│ │ +│ ① Redis RPOP WatchConnectList │ +│ ② 解析连接/断开: │ +│ connected_at ≠ null → 上线 (status=1) │ +│ disconnected_at ≠ null → 离线 (status=0) │ +│ ③ 创建 WatchOffLog (MySQL) │ +│ ④ 更新 Watch 表状态 (MySQL) │ +└──────────────────────────────────────────────┘ +``` + +### 5.2 蓝牙定位流程 + +``` +┌──────────────┐ +│ 智能手表 │ 持续扫描周围蓝牙信标 +│ BLE 扫描 │ +└──────┬───────┘ + │ 信标数据: {mac, rssi, imei} + ▼ +┌──────────────────────────────────────────────┐ +│ 数据上报 → Redis │ +│ Redis LPUSH BleutoothLocationQueue ← JSON │ +└──────────────────┬───────────────────────────┘ + │ + ▼ (每秒) +┌──────────────────────────────────────────────────────────────┐ +│ LocationTask.BleutoothLocationTask() │ +│ │ +│ ① Redis RPOP BleutoothLocationQueue │ +│ ② 根据信标 MAC 查询 Beacon 表 → 获取位置信息 │ +│ ③ RSSI 阈值判断: │ +│ ┌────────────────────────────────────────────────┐ │ +│ │ RSSI ≥ 阈值 (进入范围) │ │ +│ │ → 保存 UserLocation (MySQL) │ │ +│ │ → 创建 UserTrajectory (status=0, 进行中) │ │ +│ │ → Redis SET Location: ← 位置信息 │ │ +│ ├────────────────────────────────────────────────┤ │ +│ │ 同一位置停留 < 5秒 │ │ +│ │ → 更新 UserTrajectory.stay_time │ │ +│ ├────────────────────────────────────────────────┤ │ +│ │ 同一位置停留 ≥ 5秒 │ │ +│ │ → 保存当前轨迹 (status=1, 完成) │ │ +│ │ → 创建新的 UserTrajectory │ │ +│ ├────────────────────────────────────────────────┤ │ +│ │ RSSI < 阈值 (离开范围) │ │ +│ │ → 记录离开时间 │ │ +│ │ → 标记轨迹完成 (status=1) │ │ +│ └────────────────────────────────────────────────┘ │ +│ │ +│ ④ 定位数据支撑: │ +│ → Web 后台实时位置显示 │ +│ → 人效分析报表 │ +│ → 考勤打卡佐证 │ +└──────────────────────────────────────────────────────────────┘ +``` + +--- + +## 六、员工休息管理流程 + +``` +员工点击 "休息" (APP) + │ + ▼ +Web API → 创建休息请求 + │ + ▼ (Task 服务 - XXL-Job) +┌──────────────────────────────────────────────────────────────┐ +│ userRestStartTask │ +│ │ +│ ① 创建 RestRecord (punchType=0, MySQL) │ +│ ② 查询站点配置 → 获取休息时长 (默认60分钟) │ +│ ③ Redis ZADD CleaningRestLastTime │ +│ score=截止时间戳 value=userId │ +│ ④ 更新 UserDistrictAttendance.workAtStatus=3 (休息中) │ +│ ⑤ Redis SET CleaningUpWorkAtStatus: ← 原状态 │ +│ ⑥ Redis LPUSH AttendanceStatusMessage ← 考勤消息 │ +│ │ │ +│ ▼ (ScheduledTask, 每秒) │ +│ MQTT publish → 手表 (messageType=5, 关闭蓝牙) │ +│ │ +└──────────────────────────────┬───────────────────────────────┘ + │ + ┌──────────┴──────────┐ + ▼ ▼ + 正常结束休息 超时自动恢复 +┌─────────────────────────┐ ┌─────────────────────────┐ +│ userRestEndTask │ │ restCountDown (每秒检查) │ +│ (XXL-Job 定时触发) │ │ │ +│ │ │ Redis ZRANGEBYSCORE │ +│ ① RestRecord │ │ CleaningRestLastTime │ +│ (punchType=1) │ │ 0 ~ 当前时间戳 │ +│ ② 计算实际休息时长 │ │ │ │ +│ ③ 恢复 workAtStatus │ │ ▼ │ +│ (从 Redis 读取原状态) │ │ 发现超时员工 │ +│ ④ 清除 Redis 标记 │ │ → 自动创建恢复记录 │ +│ │ │ → 恢复 workAtStatus=2 │ +└─────────────────────────┘ └─────────────────────────┘ +``` + +--- + +## 七、数据导出流程 + +``` +管理员点击 "导出" (Web 后台) + │ + ▼ +POST /xxx/export + │ + ▼ +┌──────────────────────────────────────────────────────────────┐ +│ Controller.export() │ +│ │ +│ ① 接收查询条件 (同列表查询) │ +│ ② 查询权限组字段 (FieldWebpageAuthority) │ +│ → 确定可导出的列 │ +│ ③ Service 查询数据 (不分页, 全量) │ +│ ④ 构造 Excel (Apache POI / EasyExcel) │ +│ → 根据权限动态设置列 │ +│ ⑤ 设置响应头: │ +│ Content-Type: application/octet-stream │ +│ Content-Disposition: attachment; filename=xxx.xlsx │ +│ ⑥ 输出流写入 Response │ +└──────────────────────────┬───────────────────────────────────┘ + │ + ▼ + ┌──────────────┐ + │ 浏览器下载 │ + │ Excel 文件 │ + └──────────────┘ +``` + +--- + +## 八、权限动态加载流程 + +``` +页面加载 / 路由切换 + │ + ▼ +┌──────────────────────────────────────────────────────────────┐ +│ router-guard.js (路由守卫) │ +│ │ +│ ① 检查 sessionStorage 登录态 │ +│ ② 读取路由 query: wId (页面ID), pageType │ +│ ③ 触发 Vuex actions: │ +│ store.dispatch('fetchFields', {wId, pageType}) │ +│ store.dispatch('fetchButtons', {wId, pageType}) │ +└──────────────────────────┬───────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────────────┐ +│ Vuex Store │ +│ │ +│ fetchFields: │ +│ POST /webPage/getFieldsByAuthority │ +│ → 返回当前用户权限组下该页面可见字段列表 │ +│ → 存入 state.fields │ +│ │ +│ fetchButtons: │ +│ POST /webPage/getButtonsByAuthority │ +│ → 返回当前用户权限组下该页面可见按钮列表 │ +│ → 存入 state.buttons │ +└──────────────────────────┬───────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────────────┐ +│ 页面组件渲染 │ +│ │ +│ │ +│ 遍历 state.fields → 动态生成 │ +│ 字段不在权限列表 → 不渲染该列 │ +│ │ +│ │ +│ 遍历 state.buttons → 动态生成 │ +│ 按钮不在权限列表 → 不渲染该按钮 │ +└──────────────────────────────────────────────────────────────┘ +``` + +--- + +## 九、Redis 消息队列数据流全景 + +``` + ┌─────────────┐ + │ DB 4 │ + └──────┬──────┘ + │ + ┌──────────────────────────────┬┼┬──────────────────────────────┐ + │ │ ││ │ │ + ▼ ▼ ▼▼ ▼ ▼ +┌────────┐ ┌──────────┐ ┌─────────────┐ ┌──────────┐ ┌────────────┐ +│taskUser│ │taskPush │ │WatchTask │ │Mqtt │ │Bleutooth │ +│Queue │ │Queue │ │UserList │ │TaskState │ │Location │ +│ │ │ │ │ │ │Message │ │Queue │ +└───┬────┘ └────┬─────┘ └──────┬──────┘ └────┬─────┘ └─────┬──────┘ + │ │ │ │ │ + ▼ ▼ ▼ ▼ ▼ +┌────────┐ ┌──────────┐ ┌─────────────┐ ┌──────────┐ ┌────────────┐ +│派单 │ │个推推送 │ │构建手表消息 │ │MQTT推送 │ │位置计算 │ +│匹配 │ │APP通知 │ │ │ │到手机端 │ │轨迹记录 │ +└────────┘ └──────────┘ └──────┬──────┘ └──────────┘ └────────────┘ + │ + ▼ + ┌─────────────┐ + │MqttWatch │ + │TaskMessage │ + └──────┬──────┘ + │ + ▼ + ┌─────────────┐ + │MQTT推送 │ + │到手表 │ + └─────────────┘ + + ┌─────────────┐ + │ DB 5 │ + └──────┬──────┘ + │ + ┌──────────────────────┬───────┼───────┬──────────────────────┐ + │ │ │ │ │ + ▼ ▼ ▼ ▼ ▼ +┌────────┐ ┌──────────┐ ┌────┐ ┌──────────┐ ┌──────────┐ +│task │ │Finish │ │实体│ │Dictionary│ │Location │ +│Command │ │TaskQueue │ │缓存│ │ (Hash) │ │: │ +│(设备告警)│ │(完成队列)│ │ │ │(系统配置)│ │(实时位置) │ +└───┬────┘ └────┬─────┘ └────┘ └──────────┘ └──────────┘ + │ │ + ▼ ▼ +┌────────┐ ┌──────────┐ +│生成 │ │完成后 │ +│维护任务│ │处理逻辑 │ +└────────┘ └──────────┘ +``` diff --git a/docs/backend-glossary.md b/docs/backend-glossary.md new file mode 100644 index 0000000..3a7b92c --- /dev/null +++ b/docs/backend-glossary.md @@ -0,0 +1,358 @@ +# 后端技术术语详解 - 写给前端工程师 + +> 用前端类比的方式,帮助前端同学理解后端架构和术语 + +--- + +## 一、框架与运行环境 + +### Spring Boot +**类比**:相当于前端的 Vite + Vue 全家桶的脚手架。 +**解释**:Java 后端的"一站式开发框架",帮你把 Web 服务器、数据库连接、配置管理等都自动配置好,开箱即用。就像 Vite 帮你配好了 webpack、热更新、打包一样,Spring Boot 帮后端配好了 Tomcat 服务器、数据库连接池等。 + +### WAR 包 / ROOT.war +**类比**:相当于前端 `npm run build` 打出来的 `dist` 文件夹。 +**解释**:后端代码编译打包后的产物,是一个压缩包(类似 zip)。`ROOT.war` 部署到 Tomcat 服务器上运行,就像你把 `dist` 丢到 Nginx 上一样。 + +### Tomcat +**类比**:相当于前端的 Nginx。 +**解释**:Java Web 应用的运行容器/服务器。前端用 Nginx 托管静态文件,后端用 Tomcat 运行 Java 代码。 + +### Maven / pom.xml +**类比**:Maven = npm,pom.xml = package.json。 +**解释**:Java 的包管理工具。`mvn install` 类似 `npm install`,`mvn clean package` 类似 `npm run build`。pom.xml 里定义了项目依赖(类似 dependencies)和构建配置(类似 scripts)。 + +### mvn clean install +**类比**:`rm -rf node_modules && npm install && npm run build` +**解释**:清除旧的编译产物(clean),重新编译并安装到本地仓库(install),供其他模块引用。 + +--- + +## 二、项目分层架构 + +后端代码是分层的,**请求从上往下走,响应从下往上回**: + +``` +前端请求 + ↓ +Controller ← 相当于前端的路由 (Router),定义 URL 和接收参数 + ↓ +Service ← 相当于前端的 Vuex actions / 业务逻辑层 + ↓ +Mapper ← 相当于前端的 ApiService,负责跟数据库"通信" + ↓ +MySQL ← 相当于前端的后端 API(但这次是真正的数据存储) +``` + +### Controller(控制器) +**类比**:Vue Router 的路由定义 + 页面组件的入口方法。 +**解释**:定义 API 接口地址(如 `POST /task/pageList`),接收前端传来的参数,调用 Service 处理,返回结果。你在 `api.js` 里写的每个接口地址,后端都有一个 Controller 方法与之对应。 + +``` +前端: api.js 里定义 /task/pageList +后端: TaskController.java 里有个方法处理这个请求 +``` + +### Service(服务层) +**类比**:Vuex 的 actions + 复杂的业务逻辑函数。 +**解释**:处理业务逻辑的地方。比如"创建任务"不只是往数据库插一条数据,还要检查权限、分配人员、发通知等,这些逻辑都在 Service 里。 + +### Mapper(数据访问层) +**类比**:前端的 `ApiService/*.js`,但操作对象是数据库而非 HTTP 接口。 +**解释**:负责跟 MySQL 数据库交互,执行增删改查 SQL。每个 Mapper 对应一张数据库表。 + +### Entity / Model(实体类) +**类比**:TypeScript 的 `interface` 或 `type` 定义。 +**解释**:定义数据结构。比如 `TaskInfo` 实体就是定义了任务表有哪些字段(id、name、status...),类似前端定义的: +```typescript +// 前端 TypeScript 的写法 +interface TaskInfo { + id: number; + name: string; + status: number; +} +``` + +--- + +## 三、数据库相关 + +### MySQL +**类比**:一个超大的、持久化的、结构化的 JSON 数据仓库。 +**解释**:关系型数据库,数据按表(Table)存储。一张表就像一个 Excel 表格,有固定的列(字段),每一行是一条数据。跟前端的 localStorage 不同,MySQL 可以存海量数据、支持复杂查询、多人并发访问。 + +### 数据源 / DataSource +**类比**:前端 axios 实例配置不同的 `baseURL`。 +**解释**:本项目有两个数据库(DB1 基础信息、DB2 业务数据),后端配置了两个数据源,不同的 Mapper 连接不同的数据库。就像前端可能有多个 axios 实例,一个请求 `api.example.com`,另一个请求 `auth.example.com`。 + +### MyBatis-Plus +**类比**:一个帮你自动写 SQL 的工具,类似前端用 Axios 封装了 fetch。 +**解释**:Java 的 ORM 框架(对象关系映射)。不用手写 SQL,直接操作 Java 对象就能完成数据库增删改查。比如 `mapper.selectById(1)` 自动生成 `SELECT * FROM task_info WHERE id = 1`。 + +### Mapper XML +**类比**:复杂的 GraphQL 查询定义文件。 +**解释**:当自动生成的 SQL 不够用时(比如多表关联查询),在 XML 文件里手写 SQL 语句。类似前端遇到复杂请求时要手动拼接参数。 + +### QueryWrapper +**类比**:前端拼接 URL 查询参数的工具函数。 +**解释**:用代码构造 SQL 查询条件。比如: +```java +// 后端 +wrapper.eq("status", 1).like("name", "保洁") +// 等同于 SQL: WHERE status = 1 AND name LIKE '%保洁%' + +// 类比前端 +params = { status: 1, name: '保洁' } // 传给后端做筛选 +``` + +### IPage(分页) +**类比**:前端 `el-pagination` 组件的 `currentPage` 和 `pageSize`。 +**解释**:后端分页查询对象,包含页码、每页条数、总数、数据列表。前端传 `pageNum` 和 `pageSize`,后端返回对应页的数据。 + +### Druid 连接池 +**类比**:浏览器的 HTTP 连接复用(HTTP/2 多路复用)。 +**解释**:数据库连接的"缓存池"。创建数据库连接很耗时(类似 TCP 握手),连接池预先创建一批连接放着,用完归还而不是销毁,下次直接取用,提升性能。 + +### 事务 / Transaction +**类比**:前端表单提交时的"全部成功才提交,否则全部撤销"。 +**解释**:保证多个数据库操作要么全部成功,要么全部回滚。比如"创建任务"要同时插入 task_info、task_user、task_scenes 三张表,如果 task_scenes 插入失败,前面两张表的数据也要撤销,保持数据一致。 + +### 软删除 / soft_delete +**类比**:前端列表中的"隐藏"而非"删除"。 +**解释**:删除数据时不是真的从数据库删掉,而是把 `soft_delete` 字段从 0 改为 1。查询时自动过滤 `soft_delete=1` 的记录。好处是数据可恢复。 + +--- + +## 四、Redis 相关 + +### Redis +**类比**:一个超级强化版的 `sessionStorage`,速度极快,支持多种数据结构,多个服务可以共享。 +**解释**:内存数据库,数据存在内存中所以读写极快。本项目用 Redis 做三件事: +1. **缓存** - 避免频繁查数据库(类似前端用变量缓存 API 响应) +2. **消息队列** - 服务之间传递消息(类似前端的 EventBus) +3. **分布式锁** - 防止并发冲突(类似前端的防重复点击) + +### Redis DB4 / DB5 +**类比**:localStorage 和 sessionStorage 的区别,两个独立的存储空间。 +**解释**:Redis 支持 16 个独立数据库(编号 0-15),本项目用了 DB4 和 DB5,存放不同类型的数据,互不干扰。 + +### Redis 数据类型 + +| Redis 类型 | 前端类比 | 说明 | +|-----------|---------|------| +| **String** | `localStorage.setItem('key', 'value')` | 最简单的键值对,存一个字符串值 | +| **List** | JavaScript 数组 `[]`,支持 push/pop | 有序列表,常用作消息队列(先进先出) | +| **Set** | JavaScript `new Set()` | 无序集合,元素不重复 | +| **Hash** | JavaScript 对象 `{}` | 键值对的集合,类似一个小型的 JSON 对象 | +| **ZSet** | 带排序分数的 Set | 有序集合,每个元素有一个分数,按分数排序 | + +### Redis 消息队列 +**类比**:前端的 EventBus / Vuex 的 dispatch。 +**解释**:用 Redis 的 List 当消息队列: +- **生产者**用 `LPUSH` 往列表左边塞消息(类似 `eventBus.$emit`) +- **消费者**用 `RPOP` 从列表右边取消息(类似 `eventBus.$on`) +- 实现了 Web 服务和 Task 服务之间的异步通信 + +``` +// 类比前端 +// Web 服务(生产者) +eventBus.$emit('taskPushQueue', { taskId: 123, type: '新任务' }) + +// Task 服务(消费者,每秒轮询) +setInterval(() => { + const msg = eventBus.popMessage('taskPushQueue') + if (msg) sendPushNotification(msg) +}, 1000) +``` + +### Redis 分布式锁 +**类比**:前端按钮的 `loading` + `disabled` 防重复点击,但是跨服务器生效。 +**解释**:用 Redis 的 `SETNX`(Set if Not Exists)命令实现。多个请求同时到达时,只有第一个能设置成功(获得锁),其他的被拒绝。30秒后锁自动过期释放。 + +### TTL(Time To Live) +**类比**:`setTimeout(() => localStorage.removeItem('key'), 30000)` — 数据过期自动删除。 +**解释**:Redis 数据的过期时间。设置 TTL=30秒,30秒后这个 Key 自动消失。用于登录 Token 过期、临时缓存清理等。 + +### Dictionary Hash +**类比**:前端 Vuex Store 里的全局配置 state。 +**解释**:Redis 里存了一个叫 `Dictionary` 的 Hash,里面存各种系统开关和配置项(如 `UserAppointFlag`、`AutoOffWorkTime`),后端需要时直接从 Redis 读取,不用每次查数据库。 + +--- + +## 五、定时任务相关 + +### XXL-Job +**类比**:一个可视化的 `setInterval` 管理平台。 +**解释**:分布式定时任务调度中心。你可以在管理界面上创建定时任务,设置执行时间(比如每天早上8点),它会自动触发后端的方法执行。比前端的 `setInterval` 强大很多:支持 cron 表达式、失败重试、集群分发等。 + +### @Scheduled +**类比**:写在代码里的 `setInterval`。 +**解释**:Spring 框架提供的定时任务注解,直接写在方法上。比如 `@Scheduled(fixedRate = 1000)` 表示每 1 秒执行一次。与 XXL-Job 的区别是:@Scheduled 写死在代码里,XXL-Job 可以在管理界面动态调整。 + +### Cron 表达式 +**类比**:一种描述"什么时候执行"的规则字符串。 +**解释**:格式为 `秒 分 时 日 月 周`。例如: +``` +* * * * * ? → 每秒执行(类似 setInterval(fn, 1000)) +0 0 8 * * ? → 每天早上8点执行 +0 0 8 ? * MON → 每周一早上8点执行 +6 6 6 28 12 ? → 每年12月28日 6:06:06 执行 +``` + +--- + +## 六、消息通信相关 + +### MQTT +**类比**:WebSocket,但更轻量,专为物联网设备设计。 +**解释**:一种消息通信协议,用于手表等 IoT 设备和服务器之间的实时双向通信。跟 WebSocket 类似,建立长连接后可以互相推送消息。区别是 MQTT 有"主题"(Topic)概念,设备订阅特定主题接收消息。 + +``` +// 类比前端 WebSocket +// WebSocket +ws.send(JSON.stringify({ type: 'task', data: {...} })) +ws.onmessage = (msg) => { ... } + +// MQTT +mqtt.publish('watch_IMEI_123', JSON.stringify({ messageType: 1, ... })) +mqtt.subscribe('watch_IMEI_123', (msg) => { ... }) +``` + +### MQTT Topic(主题) +**类比**:WebSocket 里的消息 `type` 字段 / 前端路由路径。 +**解释**:消息的"频道"。设备订阅某个 Topic 后,只会收到发到这个 Topic 的消息。比如手表订阅自己的 IMEI 号作为 Topic,服务器往这个 Topic 发消息,只有这块手表能收到。 + +### MQTT QoS(服务质量等级) +**类比**:HTTP 请求的重试策略。 +**解释**: +- QoS 0:发了就不管了(类似 `fetch` 不处理失败) +- QoS 1:至少送达一次,可能重复(类似 `fetch` + 自动重试) +- QoS 2:恰好送达一次(最可靠但最慢) +- 本项目用 QoS 1。 + +### MQTT Broker +**类比**:WebSocket 服务器 / 消息中转站。 +**解释**:MQTT 的中心服务器,所有设备和后端都连接到 Broker,由它负责消息的转发。类似一个邮局,发件人投到邮局,邮局派送给收件人。 + +### 个推 / GeTui +**类比**:浏览器的 `Notification API` / PWA 推送通知。 +**解释**:第三方手机 APP 推送服务。后端调用个推 SDK,个推通过 APNs(苹果)/ FCM(谷歌)/ 各厂商通道把通知推送到用户手机。就像浏览器推送通知,但是走的是手机系统级通道。 + +--- + +## 七、认证与安全 + +### Interceptor(拦截器) +**类比**:Axios 的请求/响应拦截器。 +**解释**:在请求到达 Controller 之前先经过拦截器,做统一处理(如认证检查、日志记录)。跟前端 Axios 拦截器一模一样的概念,只是在后端执行。 + +``` +// 前端 Axios 拦截器 +axios.interceptors.request.use(config => { /* 加 token */ }) + +// 后端 Interceptor +public boolean preHandle(request, response) { /* 验证签名 */ } +``` + +### AOP(面向切面编程) +**类比**:Vue 的全局混入(mixin)/ 全局前置守卫。 +**解释**:在不修改业务代码的情况下,给方法"织入"额外逻辑。比如 `@RepeatSubmit` 注解加在 Controller 方法上,自动加上防重复提交逻辑,不用每个方法手动写。类似前端用装饰器或全局 mixin 给所有组件加统一行为。 + +### UUID +**类比**:前端 `sessionStorage` 里的 `token`。 +**解释**:登录成功后后端生成的唯一标识符(一串随机字符),存在 Redis 中(30天有效期)。前端每次请求都带上 UUID,后端据此识别用户身份。 + +### 签名 / Signature +**类比**:前端请求的防篡改校验。 +**解释**:前端把 UUID + 时间戳 + 手机号通过 MD5 算法生成一个签名字符串,放在请求头里。后端用同样的算法重新计算,对比一致则说明请求没被篡改。类似前端某些支付场景的签名验证。 + +### MD5 +**类比**:一种"指纹"算法。 +**解释**:把任意长度的字符串转换成固定长度(32位)的字符串。同样的输入永远得到同样的输出,但无法从输出反推输入。常用于签名验证、密码存储(但不推荐用于安全场景,因为已被破解)。 + +--- + +## 八、Java 特有概念 + +### 注解 / Annotation(@xxx) +**类比**:前端的装饰器 `@decorator` / Vue 的指令 `v-xxx`。 +**解释**:Java 用 `@` 开头的标记给代码添加元信息。常见的: +- `@Controller` → 标记这个类处理 HTTP 请求 +- `@PostMapping("/task/pageList")` → 标记这个方法处理 POST /task/pageList +- `@Autowired` → 自动注入依赖(类似 Vue 的 `inject`) +- `@TableName("task_info")` → 标记这个类对应数据库的 task_info 表 + +### Lombok +**类比**:TypeScript 的自动生成 getter/setter。 +**解释**:Java 的代码简化工具。加个 `@Data` 注解就自动生成 getter、setter、toString 等方法,不用手写。类似 TypeScript 的 `class` 可以直接访问属性。 + +### Bean / IoC / 依赖注入 +**类比**:Vue 的 `provide/inject` 或 Vuex Store 的全局共享。 +**解释**:Spring 框架帮你创建和管理对象(Bean),需要用的时候自动"注入"进来,不用手动 `new`。类似 Vue 的 `inject` 可以直接使用祖先组件 `provide` 的数据。 + +```java +// 后端:Spring 自动注入 +@Autowired +private TaskService taskService; // 不用自己 new + +// 类比前端:Vue inject +const taskStore = inject('taskStore') // 不用自己创建 +``` + +### profile(环境配置) +**类比**:前端的 `.env.develop` / `.env.test` / `.env.production`。 +**解释**:后端的环境区分。`application-test.yml` 存测试环境配置,`application-prod.yml` 存生产环境配置。启动时通过 `--spring.profiles.active=test` 指定用哪个配置,跟前端 `npm run dev` 和 `npm run build` 类似。 + +--- + +## 九、日志与监控 + +### Log4j +**类比**:前端的 `console.log`,但更强大。 +**解释**:Java 的日志框架。可以控制日志级别(DEBUG/INFO/WARN/ERROR)、输出位置(控制台/文件)、格式等。相当于 `console.log` 的高级版本,支持自动按日期分割日志文件。 + +### Swagger +**类比**:自动生成的 API 文档,可以在线调试。 +**解释**:后端加注解后自动生成 API 文档网页,能看到所有接口的地址、参数、返回值,还能直接在页面上发请求测试。访问 `/doc.html` 查看。相当于一个自动生成的 Postman。 + +--- + +## 十、部署相关 + +### 连接池 vs 直连 +**类比**: +```javascript +// 直连(每次都新建) +async function query() { + const conn = await createConnection() // 耗时操作 + const result = await conn.query(sql) + conn.close() +} + +// 连接池(复用连接) +const pool = createPool({ max: 100 }) // 预先创建 +async function query() { + const conn = pool.getConnection() // 直接取,很快 + const result = await conn.query(sql) + conn.release() // 归还,不关闭 +} +``` + +### 多模块依赖 +**类比**:npm 的 monorepo(如 lerna / pnpm workspace)。 +**解释**:本项目有 3 个子模块: +``` +public (公共模块) ← 类似前端的 @shared/utils 包 + ↑ ↑ + | | + web task ← 类似前端的 @app/admin 和 @app/worker +``` +修改 public 后需要先 `mvn install`(发布到本地),web 和 task 才能用到最新代码。类似 monorepo 里改了 shared 包要先 build。 + +### Lettuce +**类比**:Axios 之于 HTTP,Lettuce 之于 Redis。 +**解释**:Java 连接 Redis 的客户端驱动。负责跟 Redis 服务器建立连接、发送命令、接收结果。你不需要关心它,只需要知道后端通过它操作 Redis。 + +### Elasticsearch / BBoss +**类比**:一个超级强大的全文搜索引擎,类似浏览器的 Ctrl+F 但能搜索海量数据。 +**解释**:目前项目中这部分代码被注释掉了(`#`),暂未启用。如果启用,可以实现类似"模糊搜索任务名称"的功能,比数据库的 `LIKE` 查询快得多。 diff --git a/docs/requirements.md b/docs/requirements.md new file mode 100644 index 0000000..a8060db --- /dev/null +++ b/docs/requirements.md @@ -0,0 +1,970 @@ +# 智慧清洁 SaaS 平台 - 业务需求全景文档 + +> 基于后端 xiaoqu-intellectual-web 和 xiaoqu-intellectual-task 两个项目的代码分析整理 + +--- + +## 一、系统概述 + +智慧清洁 SaaS 平台面向物业/市政清洁行业,提供从计划排班、任务调度、打卡考勤、IoT设备管理到绩效分析的全链路数字化管理能力。 + +**系统架构**: +- **Web 服务**(端口 8095):提供 REST API,支撑后台管理 SPA +- **Task 服务**(端口 8097):定时任务调度 + MQTT 设备通信 + 消息推送 + +--- + +## 二、业务模块清单 + +### 模块 1:登录与认证 + +| 功能 | 端点 | 说明 | +|------|------|------| +| 短信验证码登录 | `POST /login` | 手机号+验证码登录,生成UUID(30天有效期) | +| 用户退出 | `GET /logOut` | 清除Redis登录态 | +| 发送验证码 | `GET /sms/sendCode` | 短信平台发送验证码 | + +**业务规则**: +- 单点登录控制,防止多终端同时登录 +- 请求签名机制:UUID + 时间戳 + 手机号 MD5签名 +- 签名有效期10秒窗口 +- 登录日志记录(IP、登录类型) + +--- + +### 模块 2:权限管理 + +| 功能 | 端点 | 说明 | +|------|------|------| +| 设置权限组人员 | `POST /authority/setAuthorityUser` | 批量添加用户到权限组 | +| 查询权限组成员 | `POST /authority/getAuthorityUserList` | 支持分页查询 | + +**动态权限体系**: +- **字段级权限**:按权限组控制表格列的显示/隐藏 +- **按钮级权限**:按权限组控制操作按钮的可见性 +- 权限链路:权限组 → 用户 → 页面 → 字段/按钮 + +--- + +### 模块 3:用户与组织管理 + +| 功能 | 端点 | 说明 | +|------|------|------| +| 用户分页查询 | `POST /userCenter/pageList` | 按服务区域、事项、技能、标签筛选 | +| 新增/修改用户 | `POST /userCenter/addOrUpdateUser` | | +| 删除用户 | `POST /userCenter/deleteUser` | | +| 用户导出 | `POST /userCenter/export` | Excel导出 | +| 用户菜单树 | `POST /userCenter/getUserMenuTree` | | +| 用户下拉选择 | `POST /userCenter/selectUsers` | | + +**用户属性**: +- 工作状态:作业(0) / 转场(1) / 空闲(2) / 休息(3) +- 关联:班组、技能标签、事项标签、服务区域、考勤设置 + +--- + +### 模块 4:班组管理 + +| 功能 | 端点 | 说明 | +|------|------|------| +| 班组分页查询 | `POST /workGroup/pageList` | | +| 新增/修改班组 | `POST /workGroup/addOrUpdate` | | +| 删除班组 | `POST /workGroup/deleteWorkGroup` | | +| 班组导出 | `POST /workGroup/export` | | +| 设置班组负责人 | `POST /workGroup/setWorkGroupPrincipal` | | +| 设置班组成员 | `POST /workGroup/setWorkGroupMember` | | +| 获取班组成员 | `POST /workGroup/getWorkGroupMember` | | + +--- + +### 模块 5:标签与技能管理 + +| 功能 | 端点 | 说明 | +|------|------|------| +| 标签分页查询 | `POST /label/pageList` | | +| 新增/修改标签 | `POST /label/addOrUpdate` | | +| 删除标签 | `POST /label/delete` | | +| 获取所有标签 | `POST /label/allLabels` | | +| 技能分页查询 | `POST /skill/pageList` | | +| 新增/修改技能 | `POST /skill/addOrUpdate` | | +| 删除技能 | `POST /skill/delete` | | +| 获取所有技能 | `POST /skill/allSkills` | | + +--- + +### 模块 6:基础数据管理 + +#### 6.1 网格管理 + +| 功能 | 端点 | 说明 | +|------|------|------| +| 网格查询 | `POST /grid/pageList` | 建筑/楼层/多级网格 | +| 新增/修改网格 | `POST /grid/addOrUpdateGrid` | | +| 删除网格 | `POST /grid/deleteGrid` | | +| 批量导入网格 | `POST /grid/importGrid` | | +| 网格负责人列表 | `POST /grid/gridPrincipalList` | | + +**网格级别**:建筑(1) → 楼层(2) → 多级网格(3) + +#### 6.2 场景管理 + +| 功能 | 端点 | 说明 | +|------|------|------| +| 场景查询 | `POST /scenes/pageList` | 厕所、楼道等特定场景 | +| 新增/修改场景 | `POST /scenes/addOrUpdateScenes` | | +| 删除场景 | `POST /scenes/deleteScenes` | | +| 批量导入场景 | `POST /scenes/batchInsertScenes` | | +| 导入场景模板 | `POST /scenes/importScenes` | | + +**场景级别**:一级场景(1,如卫生间) → 二级场景(2,如楼层卫生间) + +#### 6.3 场景对象关联 + +| 功能 | 端点 | 说明 | +|------|------|------| +| 场景对象查询 | `POST /sceneObject/sceneObjectPageList` | | +| 关联对象到场景 | `POST /sceneObject/addOrUpdateSceneObject` | | + +#### 6.4 对象管理 + +| 功能 | 端点 | 说明 | +|------|------|------| +| 对象分页查询 | `POST /object/objectPageList` | 支持树查询 | +| 新增/修改对象 | `POST /object/addOrUpdateObject` | | +| 删除对象 | `POST /object/deleteObject` | | +| 批量导入对象 | `POST /object/importObject` | | + +#### 6.5 事项管理 + +| 功能 | 端点 | 说明 | +|------|------|------| +| 事项分页查询 | `POST /matter/matterPageList` | | +| 批量添加事项 | `POST /matter/addMatter` | | +| 修改事项 | `POST /matter/updateMatter` | | +| 删除事项 | `POST /matter/deleteMatter` | | + +#### 6.6 作业管理 + +| 功能 | 端点 | 说明 | +|------|------|------| +| 作业分页查询 | `POST /operation/oprationPageList` | 含工具关联 | +| 新增作业 | `POST /operation/addOperation` | | +| 修改作业 | `POST /operation/updateOperation` | | +| 删除作业 | `POST /operation/deleteOperation` | | +| 作业导出 | `POST /operation/export` | | + +#### 6.7 工具管理 + +| 功能 | 端点 | 说明 | +|------|------|------| +| 工具分页查询 | `POST /tools/toolsPageList` | | +| 新增/修改工具 | `POST /tools/addOrUpdateTools` | | +| 删除工具 | `POST /tools/deleteTools` | | + +**层级关系**:对象(Object) → 事项(Matter) → 作业(Operation) + 工具(Tools) + +--- + +### 模块 7:计划管理 + +| 功能 | 端点 | 说明 | +|------|------|------| +| 计划分页查询 | `POST /plan/planPageList` | 按场景、对象、作业筛选 | +| 新增/修改计划 | `POST /plan/addOrUpdatePlan` | | +| 删除计划 | `POST /plan/deletePlan` | | +| 启用计划 | `POST /plan/enablePlan` | | +| 禁用计划 | `POST /plan/disablePlan` | | +| 计划导出 | `POST /plan/export` | 保洁/巡检/设备维护三类 | +| 执行计划 | `POST /plan/executePlan` | 立即生成任务 | +| 复制计划 | `POST /plan/copyPlan` | | +| 计划选择列表 | `POST /plan/selectPlanList` | | +| 计划详情 | `POST /plan/getPlanDetail` | | + +**计划类型**: +- 保洁计划(appointment) +- 巡检计划(inspect) +- 设备维护计划(devOps) + +**周期规则**: +- 周期类型:天(0) / 周(1,指定周几) / 月(2,指定日期) +- 周期模式:不重复(0) / 周期重复(1) / 间隔重复(2) +- 支持节假日跳过或强制执行 +- 支持人次触发(按规定人数完成后自动生成下一轮) + +**计划关联**: +- 计划对象(PlanObject)- 清洁对象清单 +- 计划作业(PlanOperation)- 作业步骤 +- 计划场景(PlanScenes)- 执行范围 +- 计划用户(PlanUser)- 分配人员 +- 计划班组(PlanWorkGroup)- 分配班组 + +--- + +### 模块 8:任务管理 + +#### 8.1 清洁/保洁任务 + +| 功能 | 端点 | 说明 | +|------|------|------| +| 任务分页查询 | `POST /task/pageList` | 按网格/场景、状态、时间筛选 | +| 任务明细导出 | `POST /task/export` | Excel | +| 人工取消任务 | `POST /task/cancel` | | +| 查看任务详情 | `POST /task/look` | 含对象、作业、进度、积分 | +| 设置任务优先级 | `POST /task/setTaskTaskOrder` | | + +#### 8.2 巡检任务 + +| 功能 | 端点 | 说明 | +|------|------|------| +| 巡检任务列表 | `POST /taskInspect/pageList` | | +| 巡检任务导出 | `POST /taskInspect/export` | | +| 巡检任务详情 | `POST /taskInspect/look` | | +| 上传巡检照片 | `POST /taskInspect/updateInspectPic` | | + +#### 8.3 用户上报任务 + +| 功能 | 端点 | 说明 | +|------|------|------| +| 上报任务列表 | `POST /taskUserUpload/pageList` | | +| 上报任务详情 | `POST /taskUserUpload/look` | | +| 人工设置时薪 | `POST /taskUserUpload/setWageByManual` | | + +**任务状态流转**: +``` +已生成(0) → 已匹配(1) → 待抢单(2) → 已接单(3) → 作业中(4) → 按时完成(5) / 超时完成(6) + ↘ 人工取消(7) / 系统取消(8) / 未完成(9) +``` + +**任务类型**: +- 0/1/2 = 保洁任务 +- 3/4 = 用户上报任务 +- 5 = 巡检任务 + +--- + +### 模块 9:打卡与考勤 + +| 功能 | 端点 | 说明 | +|------|------|------| +| 打卡记录查询 | `POST /punchRecord/pageList` | 按网格/场景、时间筛选 | +| 打卡记录导出 | `POST /punchRecord/export` | | +| 考勤记录查询 | `POST /attendance/pageList` | | +| 考勤记录导出 | `POST /attendance/export` | | +| 日打卡统计查询 | `POST /punchRecordDayStatis/pageList` | 按用户分组 | +| 日打卡统计导出 | `POST /punchRecordDayStatis/export` | | + +--- + +### 模块 10:效率与绩效分析 + +| 功能 | 端点 | 说明 | +|------|------|------| +| 任务人效查询 | `POST /efficiency/taskEfficiencyPageList` | 按员工统计 | +| 人效数据导出 | `POST /efficiency/export` | | +| 时薪分页查询 | `POST /wage/wagePageList` | | +| 新增/修改时薪 | `POST /wage/addOrUpdateWage` | | +| 删除时薪 | `POST /wage/deleteWage` | | +| 积分换算时薪 | `POST /wage/getAmount` | | + +**计价逻辑**:任务完成 → 获得积分(point) → 根据时薪表(Wage)换算金额 + +--- + +### 模块 11:用户上报管理 + +| 功能 | 端点 | 说明 | +|------|------|------| +| 上报用户列表 | `POST /escalationUser/pageList` | | +| 添加/修改上报用户 | `POST /escalationUser/addOrUpdate` | | +| 导出 | `POST /escalationUser/export` | | + +--- + +### 模块 12:系统参数配置 + +| 功能 | 端点 | 说明 | +|------|------|------| +| 站点参数查询 | `POST /taskDistrictParam/taskDistrictParamPageList` | | +| 修改站点参数 | `POST /taskDistrictParam/updateTaskDistrictParam` | 紧急/特殊任务限制数 | + +--- + +## 三、Task 服务 - 定时任务与后台处理 + +### 模块 13:任务自动调度引擎(XXL-Job) + +| 任务 | 触发方式 | 说明 | +|------|----------|------| +| planCreateTask | XXL-Job 动态创建 | 根据计划规则自动生成清洁/巡检/维护任务 | +| planDisableTask | XXL-Job 动态创建 | 按时间段禁用/启用任务生成 | +| InitWatch | XXL-Job 调度 | 手表设备初始化(震动/语音/蓝牙参数) | +| userRestStartTask | XXL-Job 调度 | 员工开始休息(记录、状态变更、蓝牙关闭) | +| userRestEndTask | XXL-Job 调度 | 员工结束休息(恢复状态) | +| restCountDown | XXL-Job 调度 | 休息倒计时监控,超时自动恢复 | + +### 模块 14:任务处理队列(Spring @Scheduled) + +| 任务 | 频率 | 说明 | +|------|------|------| +| 任务人员派单 | 每秒 | 根据计划分配任务给清洁员 | +| 任务过期处理 | 每秒 | 超时任务自动标记超期 | +| 任务完成处理 | 每秒 | 完成后计算工资、评分 | +| 任务重派发 | 每秒 | 失败/拒绝后重新分配 | +| 巡检任务派单 | 每秒 | 巡检任务分配 | +| 上报任务派单 | 每秒 | 用户上报维修任务分配 | +| 设备维护自动完成 | 每秒 | 部分设备维护任务自动标记完成 | +| 设备告警任务生成 | 每秒 | 监听设备告警,自动创建维护任务 | + +--- + +### 模块 15:消息推送系统 + +| 任务 | 频率 | 说明 | +|------|------|------| +| 个推推送 | 每秒 | 通过个推SDK推送APP通知 | +| MQTT任务状态推送 | 每2秒 | 推送任务状态变更到手机端 | +| 任务动态消息生成 | 每2秒 | 生成任务动态并存储 | + +**推送类型**: +- 新任务提醒(status=1) +- 抢单池任务(status=2) +- 已接单通知(status=3) +- 任务取消通知(status=7/8) +- 厕所保养提醒(status=-1) + +**推送方式**: +- 在线推送:NotificationTemplate,立即推送 +- 离线推送:TransmissionTemplate,24小时内推送,支持厂商通道 + +--- + +### 模块 16:IoT 设备通信(MQTT) + +| 任务 | 频率 | 说明 | +|------|------|------| +| 手表连接状态监听 | 每秒 | 监测上下线,维护设备状态 | +| 手表震动语音参数下发 | 每秒 | 实时调整手表配置 | +| 手表参数初始化 | 每2秒 | 初始化手表配置 | +| 手表绑定/解绑消息 | 每2秒 | 下发绑定/解绑指令 | +| 手表任务消息推送 | 每2秒 | 下发新任务到手表 | +| 任务-用户关联处理 | 每2秒 | 将任务消息转换为手表格式 | +| 考勤状态消息 | 每秒 | 下班提醒,关闭手表蓝牙 | +| 放大镜监控消息 | 每秒 | 实时位置追踪提醒 | +| 心跳保活 | 每20秒/298秒 | 保持手表连接 | + +--- + +### 模块 17:蓝牙定位与轨迹追踪 + +| 任务 | 频率 | 说明 | +|------|------|------| +| 蓝牙信标定位 | 每秒 | 根据RSSI信号强度计算员工位置 | +| 下班定位关闭 | 每秒 | 结束进行中的轨迹记录 | + +**定位逻辑**: +- 手表扫描周围蓝牙信标 → 上报信标MAC+RSSI → 服务端计算位置 +- RSSI ≥ 阈值 → 进入信标范围 → 记录驻留时间 +- 同一位置停留 ≥ 5秒保存轨迹 +- 生成员工轨迹记录(UserTrajectory) + +--- + +### 模块 18:员工休息管理 + +**完整流程**: +1. 员工点击休息 → 创建休息记录 → 更新状态为"休息中" +2. 存入Redis倒计时(默认60分钟) +3. 发送蓝牙关闭消息到手表 +4. 倒计时结束 → 自动生成休息完成记录 → 恢复员工状态 + +--- + +### 模块 19:节假日同步 + +| 任务 | 频率 | 说明 | +|------|------|------| +| 节假日同步 | 每年12月28日 06:06:06 | 调用外部API同步下一年节假日 | + +**节假日类型**:工作日(0) / 法定节假日(1) / 调休日(2) / 周末(3) + +--- + +## 四、核心业务流程 + +### 流程 1:清洁任务全生命周期 + +``` +创建计划(Plan) + ↓ +定义:对象 + 作业 + 场景范围 + 人员/班组 + ↓ +启用计划 → XXL-Job 定时触发 planCreateTask + ↓ +生成 TaskInfo → 推入 PlanTaskQueue + ↓ +TaskCorrelationSetTask 派单(匹配人员) + ↓ +推送通知:个推(APP) + MQTT(手表) + ↓ +员工接单 → 开始作业 → 完成作业 + ↓ +核算积分 → 换算时薪 → 绩效统计 +``` + +### 流程 2:设备告警 → 维护任务 + +``` +设备传感器告警(如纸巾不足、空气质量异常) + ↓ +告警推入 taskCommand 队列 + ↓ +TaskCreateQueue 处理:去重检查 + 创建 TaskInfo + ↓ +分配维护员 → 推送通知 + ↓ +维护员完成维护 → 标记完成 +``` + +### 流程 3:员工实时定位 + +``` +手表持续扫描蓝牙信标 + ↓ +上报信标数据 → BleutoothLocationQueue + ↓ +计算位置 → 保存 UserLocation + ↓ +生成驻留轨迹 → UserTrajectory + ↓ +支撑人效分析报表 +``` + +--- + +## 五、核心数据模型 + +| 实体 | 说明 | 所属库 | +|------|------|--------| +| TaskInfo | 任务主表 | db2 | +| Plan | 清洁/巡检/维护计划 | db2 | +| PlanObject | 计划对象清单 | db2 | +| PlanOperation | 计划作业 | db2 | +| PlanScenes | 计划执行范围 | db2 | +| TaskUser | 任务分配人员 | db2 | +| TaskScenes | 任务场景 | db2 | +| Operation | 作业定义 | db2 | +| Object | 清洁对象 | db2 | +| Matter | 事项 | db2 | +| Tools | 工具 | db2 | +| Wage | 时薪 | db2 | +| Authority | 权限组 | db2 | +| WorkGroup | 班组 | db2 | +| PunchRecord | 打卡记录 | db2 | +| AttendanceRecord | 考勤记录 | db2 | +| UserLocation | 实时定位 | db2 | +| UserTrajectory | 员工轨迹 | db2 | +| Watch | 手表设备 | db1 | +| WatchOffLog | 手表上下线日志 | db1 | +| Hardware | 硬件设备 | db1 | +| Beacon | 蓝牙信标 | db1 | +| Holiday | 节假日 | db2 | +| PushLog | 推送日志 | db2 | +| TodayDynamic | 任务动态消息 | db2 | + +--- + +## 六、关键 Redis 数据结构 + +| Key | 类型 | 用途 | +|-----|------|------| +| `PlanTaskQueue` | List | 待生成的计划任务队列 | +| `taskCommand` | List | 设备告警命令队列 | +| `taskPushQueue` | List | 待推送的通知消息队列 | +| `IntellectualTaskIdList` | List | 待处理的任务ID队列 | +| `MqttWatchTaskMessage` | List | 待推送给手表的任务消息 | +| `WatchTaskUserList` | List | 待处理的任务-用户关联 | +| `WatchConnectList` | List | 手表连接/断开事件 | +| `BleutoothLocationQueue` | List | 蓝牙定位数据队列 | +| `CleaningRestLastTime` | ZSet | 员工休息倒计时(score=截止时间戳) | +| `MQTTTOPIC` | Set | 活跃手表设备集合 | +| `Location:userId` | String | 员工最新位置 | +| `Disable:d/t/p:id:taskTypeId` | String | 任务生成禁用标记 | + +--- + +## 七、数据库详细分析 + +### 7.1 数据库架构 + +系统使用 **两个 MySQL 数据库**,通过 MyBatis-Plus 多数据源配置: + +| 数据库 | 名称 | 说明 | +|--------|------|------| +| DB1 | xiaoqu_comples_d | 基础信息库:组织、用户、硬件、设备、定位数据 | +| DB2 | xiaoqu_intellectual_d | 业务数据库:任务、计划、权限、考勤、日志 | + +**连接配置**:Druid 连接池,最大连接数100,初始10,最小空闲5 + +### 7.2 DB1 实体表清单(51张) + +#### 组织架构 + +| 表名 | 实体 | 说明 | 关键字段 | +|------|------|------|----------| +| agent | Agent | 代理商 | name, type(0区域主管/1代理/2物业), level(0总部/1一级/2二级), channel_no, parents_id | +| properyt | Properyt | 物业公司 | name, agent_id, serialnumber, district_on_count, hard_on_count, user_count | +| district | District | 区域站/站点 | district_name, property_id, agent_id, address, buildCount, punch_flag, online_state | +| building | Building | 一级网格(楼座) | name, district_id, longitude, latitude, grid_level, orderby | +| floor | Floor | 二级网格(楼层) | name, building_id, man, woman, man_floorpic, woman_floorpic, grid_level | +| multistage_grid | MultistageGrid | 多级网格 | 支持更细粒度的网格划分 | +| grid_to_principal | GridToPrincipal | 网格-负责人关联 | grid_id, principal_id | +| positions | Positions | 位置/岗位 | 厕所内具体位置(男厕、女厕等) | + +**组织层级**:Agent(代理) → Properyt(物业) → District(站点) → Building(楼座) → Floor(楼层) → MultistageGrid(多级网格) + +#### 用户管理 + +| 表名 | 实体 | 说明 | 关键字段 | +|------|------|------|----------| +| user | User | 用户表 | mobile, name, idcard, sex, role_type, work_status, work_at_status, district_id, property_id, agent_id, authority_id | +| user_detail | UserDetail | 用户详情 | 扩展用户信息 | +| user_proper | UserProper | 用户-物业关联 | user_id, proper_id | +| user_toilet | UserToilet | 用户-卫生间关联 | user_id, toilet_id | +| user_district_attendance | UserDistrictAttendance | 用户站点考勤 | user_id, district_id, status, workAtStatus | +| user_rest | UserRest | 用户休息记录 | user_id, rest_time | +| user_location | UserLocation | 用户实时位置 | user_id, longitude, latitude, beacon_mac, rssi | +| user_trajectory | UserTrajectory | 用户轨迹 | user_id, location_id, stay_time, status | +| user_menu_district | UserMenuDistrict | 用户菜单-站点 | user_id, menu_id, district_id | +| proper_user_district | ProperUserDistrict | 物业用户-站点关联 | user_id, district_id | + +**User 核心字段**: +- 工作状态:work_status, work_at_status(0作业/1转场/2空闲/3休息) +- 组织关联:district_id, property_id, agent_id +- 权限:role_id, authority_id, property_authority_id +- 认证:weixin_openid, registration_id + +#### 硬件设备 + +| 表名 | 实体 | 说明 | 关键字段 | +|------|------|------|----------| +| hardware | Hardware | 硬件设备 | sn_code, type, status, district_id | +| hardware_var | HardwareVar | 硬件变量 | hardware_id, var_name, var_value | +| watch | Watch | 手表设备 | imei, user_id, bind_status, online_status, shock_state, voice_state | +| watch_set_params | WatchSetParams | 手表参数设置 | watch_id, param_name, param_value | +| watch_bind_log | WatchBindLog | 手表绑定日志 | watch_id, user_id, bind_time | +| watch_off_log | WatchOffLog | 手表上下线日志 | watch_id, status(0离线/1在线), connected_at, disconnected_at | +| watch_test_log | WatchTestLog | 手表测试日志 | 测试数据 | +| toilet | Toilet | 卫生间/场景设备 | name, district_id, building_id, floor_id | +| toilet_airquality | ToiletAirquality | 空气质量传感器 | sn_code, toilet_id | +| heyi_dev | HeyiDev | 合一设备 | 第三方设备集成 | +| heyi_group | HeyiGroup | 合一设备分组 | 设备分组管理 | +| equipment_repair_recording | EquipmentRepairRecording | 设备维修记录 | equipment_id, repair_time, status | + +#### 蓝牙信标与定位 + +| 表名 | 实体 | 说明 | 关键字段 | +|------|------|------|----------| +| beacon | Beacon | 蓝牙信标 | mac, name, district_id, rssi_threshold | +| beacon_log | BeaconLog | 信标日志 | beacon_id, event_type | +| beacon_area_pic | BeaconAreaPic | 场景区域图 | area_id, pic_url | +| beacon_area_position | BeaconAreaPosition | 图层管理 | area_id, position_x, position_y | +| beacon_point_manage | BeaconPointManage | 点位管理 | beacon_id, point_name | +| bluetooth_communication | BluetoothCommunication | 蓝牙通信日志 | 通信记录 | +| bluetooth_connect | BluetoothConnect | 蓝牙连接数据 | 连接状态 | +| bluetooth_location | BluetoothLocation | 蓝牙定位数据 | mac, rssi, user_id | +| rfid_info | RfidInfo | RFID信息 | rfid标签数据 | + +#### 菜单与统计 + +| 表名 | 实体 | 说明 | +|------|------|------| +| app_menu | AppMenu | 物业APP菜单权限 | +| my_menu | MyMenu | 自定义菜单 | +| my_user_menu | MyUserMenu | 用户菜单 | +| district_strategy | DistrictStrategy | 区域策略 | +| monthly_statistics | MonthlyStatistics | 月度统计 | +| statistics_month_log | StatisticsMonthLog | 月度统计日志 | +| switch_day_data | SwitchDayData | 日数据统计 | +| switch_hour_data | SwitchHourData | 小时数据统计 | +| switch_month_data | SwitchMonthData | 月数据统计 | +| switch_year_data | SwitchYearData | 年数据统计 | +| airqualitylevelgrade | Airqualitylevelgrade | 分润管理明细 | +| xinbiao_record | XinbiaoRecord | 新表记录 | + +### 7.3 DB2 实体表清单(61张) + +#### 任务管理 + +| 表名 | 实体 | 说明 | 关键字段 | +|------|------|------|----------| +| task_info | TaskInfo | 任务主表 | name, no, task_type, status, plan_id, district_id, point, wage_hour | +| task_type | TaskType | 任务类型 | name, code | +| task_user | TaskUser | 任务-用户关联 | task_id, user_id, status | +| task_accept_user | TaskAcceptUser | 接单人员 | task_id, user_id | +| task_distributi_user | TaskDistributiUser | 派发人员 | task_id, user_id | +| task_pic | TaskPic | 任务照片 | task_id, pic_url, type | +| task_scenes | TaskScenes | 任务场景 | task_id, grid_level, scenes_level, building_id, floor_id | +| task_task_scenes | TaskTaskScenes | 任务-场景关联 | task_id, scenes_id | +| task_status_log | TaskStatusLog | 任务状态日志 | task_id, old_status, new_status, change_time | +| task_district_param | TaskDistrictParam | 站点任务参数 | district_id, rest_time, beacon_no | + +**TaskInfo 核心字段**: +- 状态流转:status(0已生成→1匹配→2待抢→3已接→4作业中→5按时完成/6超时/7人工取消/8系统取消/9未完成) +- 时间跟踪:create_time, send_time, confirm_time, action_time, finish_time, expire_time +- 时长统计:confirm_timing(响应), timing(作业), over_timing(超时), transfer_timing(转场) +- 绩效:point(积分), wage_hour(工时), wage_id, salary +- 巡检:should_inspect(应巡), actual_inspect(实巡), not_inspect(未巡) + +#### 计划管理 + +| 表名 | 实体 | 说明 | 关键字段 | +|------|------|------|----------| +| plan | Plan | 任务计划 | task_name, type(0例行/2指派/5巡检), circle_status, circle_type(0天/1周/2月), status | +| plan_object | PlanObject | 计划-对象关联 | plan_id, object_id | +| plan_operation | PlanOperation | 计划-作业关联 | plan_id, operation_id | +| plan_scenes | PlanScenes | 计划-场景关联 | plan_id, building_id, floor_id, multistage_grid_id, scenes_id | +| plan_user | PlanUser | 计划-用户关联 | plan_id, user_id | +| plan_work_group | PlanWorkGroup | 计划-班组关联 | plan_id, work_group_id | +| plan_logic_label | PlanLogicLabel | 计划-标签关联 | plan_id, label_id | + +**Plan 核心字段**: +- 周期配置:circle_status(0不重复/1周期/2间隔), circle_type(0天/1周/2月), circle_no +- 执行时段:action_start_time, action_end_time +- 过期设置:expire_type(0不过期/1次日/2指定), expire_timing +- 时长参数:working_timing(标准工时), transition_timing(转场), ready_timing(准备), redundance_timing(冗余) +- 跳过配置:eliminate_type(0不跳/1节假日/2自定义) + +#### 考勤打卡 + +| 表名 | 实体 | 说明 | 关键字段 | +|------|------|------|----------| +| punch_record | PunchRecord | 打卡记录 | user_id, punch_time, longitude, latitude, address, punch_type, pic_url | +| punch_record_day_statis | PunchRecordDayStatis | 打卡日统计 | user_id, date, punch_count | +| rest_record | RestRecord | 休息记录 | user_id, punch_type(0休息/1恢复), rest_duration | +| attendance_record | AttendanceRecord | 考勤记录 | user_id, date, status | + +#### 权限管理 + +| 表名 | 实体 | 说明 | 关键字段 | +|------|------|------|----------| +| authority | Authority | 权限组 | name, type | +| webpage | Webpage | 页面 | name, url, parent_id | +| webpage_authority | WebpageAuthority | 页面权限 | webpage_id, authority_id | +| page_field | PageField | 页面字段 | webpage_id, field_name, field_label | +| field_webpage_authority | FieldWebpageAuthority | 字段-权限关联 | field_id, webpage_id, authority_id | +| button | Button | 按钮 | webpage_id, name, code | +| button_field | ButtonField | 按钮字段 | button_id, field_name | +| button_webpage_authority | ButtonWebpageAuthority | 按钮-权限关联 | button_id, webpage_id, authority_id | + +**权限链路**:Authority(权限组) → WebpageAuthority(页面) → FieldWebpageAuthority(字段) + ButtonWebpageAuthority(按钮) + +#### 基础数据 + +| 表名 | 实体 | 说明 | 关键字段 | +|------|------|------|----------| +| object | Object | 清洁对象 | name, parent_id, level, type | +| object_type | ObjectType | 对象类型 | name | +| matter | Matter | 事项 | object_id, name, type | +| matter_type | MatterType | 事项类型 | name | +| operation | Operation | 作业 | name, type, tool_ids | +| tools | Tools | 工具 | name, district_id, status | +| scene_object | SceneObject | 场景-对象关联 | scene_id, object_id | +| inspect_scenes | InspectScenes | 巡检场景 | name, district_id | +| inspect_pic | InspectPic | 巡检照片 | inspect_id, pic_url | + +#### 人员管理 + +| 表名 | 实体 | 说明 | 关键字段 | +|------|------|------|----------| +| work_group | WorkGroup | 班组 | work_group_name, district_id | +| work_group_to_principal | WorkGroupToPrincipal | 班组-负责人 | work_group_id, principal_id | +| skill_manage | SkillManage | 技能 | name | +| user_to_skill | UserToSkill | 用户-技能 | user_id, skill_id | +| label_manage | LabelManage | 标签 | name | +| user_to_label | UserToLabel | 用户-标签 | user_id, label_id | +| user_to_matter | UserToMatter | 用户-事项 | user_id, matter_id | +| user_to_service_area | UserToServiceArea | 用户-服务区域 | user_id, area_id | +| escalation_user | EscalationUser | 上报用户 | user_id | +| escalation_service_area | EscalationServiceArea | 上报服务区域 | escalation_id, area_id | + +#### 绩效与薪资 + +| 表名 | 实体 | 说明 | 关键字段 | +|------|------|------|----------| +| wage | Wage | 时薪配置 | type, point, amount | +| efficiency_recording | EfficiencyRecording | 人效录入 | user_id, date | +| efficiency_detail | EfficiencyDetail | 人效详情 | recording_id, metric, value | + +#### 日志与通知 + +| 表名 | 实体 | 说明 | 关键字段 | +|------|------|------|----------| +| login_log | LoginLog | 登录日志 | user_id, login_time, ip, type(0登录/1退出) | +| change_log | ChangeLog | 变更日志 | entity, operation, old_value, new_value | +| modify_log | ModifyLog | 修改日志 | 记录数据修改历史 | +| push_log | PushLog | 推送日志 | client_id, user_id, title, content, status | +| sms_send_record | SmsSendRecord | 短信发送记录 | mobile, content, status | +| user_push | UserPush | 用户推送映射 | user_id, client_id | +| today_dynamic | TodayDynamic | 今日动态消息 | task_id, user_id, message, type | + +#### 其他 + +| 表名 | 实体 | 说明 | +|------|------|------| +| holiday | Holiday | 节假日(0工作日/1法定/2调休/3周末) | +| app_version | AppVersion | APP版本管理 | +| type_source | TypeSource | 类型来源 | + +### 7.4 通用字段规范 + +所有实体均包含以下标准字段: +- `id` (Long) - 自增主键 +- `create_id` (Long) - 创建人ID +- `create_time` (Date) - 创建时间 +- `modify_id` (Long) - 修改人ID +- `modify_time` (Date) - 修改时间 +- `soft_delete` (Integer) - 软删除标记(0正常/1已删除) + +### 7.5 核心关系映射 + +``` +组织层级: + Agent(代理) → Properyt(物业) → District(站点) → Building(楼座) → Floor(楼层) → MultistageGrid(多级网格) + +用户关系: + User ←→ Authority(权限组) + User ←→ WorkGroup(班组) + User ←→ SkillManage(技能) + User ←→ LabelManage(标签) + User ←→ District(站点) + User ←→ Watch(手表) + +任务流程: + Plan(计划) → TaskInfo(任务) → TaskUser(执行人) + Plan → PlanObject(对象) + PlanOperation(作业) + PlanScenes(场景) + TaskInfo → TaskScenes(场景) + TaskPic(照片) + TaskStatusLog(状态日志) + +基础数据: + Object(对象) → Matter(事项) → Operation(作业) + Tools(工具) + District → Beacon(信标) → UserLocation(定位) → UserTrajectory(轨迹) +``` + +--- + +## 八、Redis 详细分析 + +### 8.1 Redis 架构配置 + +| 配置项 | 值 | +|--------|-----| +| 连接驱动 | Lettuce (Spring Data Redis) | +| 数据库数量 | 2 个独立 DB (DB4 和 DB5) | +| 连接池最大连接数 | 200 | +| 最大空闲连接 | 8 | +| 最小空闲连接 | 1 | +| 序列化方式 | StringRedisSerializer (Key + Value 均为字符串) | + +**数据库分工**: +| 数据库 | 用途 | +|--------|------| +| DB 4 | 消息队列、状态标志、计数器、分布式锁 | +| DB 5 | 实体缓存、设备命令队列、字典配置、时间序列 | + +### 8.2 Redis 操作工具 + +**RedisService4 / RedisService5** - 封装以下数据类型操作: + +| 数据类型 | 操作方法 | +|---------|---------| +| String | set, get, remove, exists | +| List | lPush, rPush, leftPop, rightPop, lRange, lRangeAll | +| Set | add, setRemove, isMember, setMembers, setPop | +| Hash | hmSet, hmGet | +| ZSet | zAdd, zRemove, rangeByScore | + +**RedisDistributedLock** - 分布式锁: +- 锁前缀:`redis_lock_` +- 默认过期:30秒 +- 自旋重试:最多10次,间隔50ms + +### 8.3 DB4 Key 详细映射 + +#### 任务消息队列 + +| Key | 类型 | 内容 | 写入方 | 消费方 | +|-----|------|------|--------|--------| +| `taskUserQueue` | List | 保洁派单消息 (TaskPlanEntity JSON) | TaskCorrelationSetTask, TaskCreateQueue | TaskCorrelationSetTask | +| `taskPushQueue` | List | APP推送消息 (TaskPushInfo JSON) | TaskCorrelationSetTask, TaskCreateQueue | TaskPushMessage | +| `taskReportQueue` | List | 上报任务派单 (TaskPlanEntity JSON) | TaskCorrelationSetTask | TaskCorrelationSetTask | +| `inspectTaskUserQueue` | List | 巡检任务派单 (TaskPlanEntity JSON) | TaskCorrelationSetTask | TaskCorrelationSetTask | +| `IntellectualTaskIdList` | List | 待处理任务ID | TaskCorrelationSetTask | ScheduledTask | +| `PlanTaskQueue` | List | 待生成的计划ID | PlanTimeTask | 任务处理系统 | + +#### 手表/MQTT 消息队列 + +| Key | 类型 | 内容 | 写入方 | 消费方 | +|-----|------|------|--------|--------| +| `WatchTaskUserList` | List | 手表任务-用户ID | TaskCorrelationSetTask | ScheduledTask | +| `MqttWatchTaskMessage` | List | 手表任务消息 (JSON) | ScheduledTask | ScheduledTask (MQTT发送) | +| `MqttTaskStateMessage` | List | 任务状态变更消息 (JSON) | ScheduledTask | ScheduledTask (MQTT发送) | +| `AttendanceStatusMessage` | List | 考勤状态消息 (JSON) | UserRestTask | ScheduledTask | +| `TaskMagnifierMessage` | List | 放大镜监控消息 (JSON) | 业务系统 | ScheduledTask | +| `WatchConnectList` | List | 手表连接/断开事件 (JSON) | PushCallback | ScheduledTask | +| `WatchShockAndVoiceMessage` | List | 手表震动/语音参数 (JSON) | 业务系统 | ScheduledTask | + +#### 蓝牙定位队列 + +| Key | 类型 | 内容 | 写入方 | 消费方 | +|-----|------|------|--------|--------| +| `BleutoothLocationQueue` | List | 蓝牙定位数据 (JSON) | 设备上报 | LocationTask | +| `OffDutyBleutoothLocationQueue` | List | 下班定位数据 (JSON) | 设备上报 | LocationTask | + +#### 任务禁用/启用标志 + +| Key 模式 | 类型 | 值 | 说明 | +|---------|------|-----|------| +| `Disable:d::` | String | "0"/"1" | 站点级任务禁用 | +| `Disable:t::` | String | "0"/"1" | 场景级任务禁用 | +| `Disable:p::` | String | "0"/"1" | 位置级任务禁用 | +| `DevTaskFlag:` | String | "0"/"1" | 设备维护任务开关 | +| `workingTimeing:d::` | String | 数字 | 站点级工作时长 | +| `workingTimeing:t::` | String | 数字 | 场景级工作时长 | +| `workingTimeing:p::` | String | 数字 | 位置级工作时长 | + +#### 计划/计数器 + +| Key 模式 | 类型 | 值 | 说明 | +|---------|------|-----|------| +| `Plan::` | String | 任务ID | 计划关联的XXL-Job任务ID | +| `UserCount:p:` | Set | 计划ID集合 | 位置级计划集合 | +| `UserCount:p::` | String | 数字 | 人次触发剩余次数 | +| `PoolOrderCount::` | String | 数字 | 订单计数 | + +#### MQTT 主题管理 + +| Key | 类型 | 值 | 说明 | +|-----|------|-----|------| +| `MQTTTOPIC` | Set | MQTT Topic 集合 | 活跃手表设备(旧) | +| `MQTTTOPICNEW` | Set | MQTT Topic 集合 | 活跃手表设备(新) | + +#### 分布式锁 + +| Key 模式 | 类型 | TTL | 说明 | +|---------|------|-----|------| +| `redis_lock_` | String | 30秒 | 防重复提交 | +| `HolidayLock` | String | 1800秒 | 节假日同步锁 | + +#### 其他 + +| Key 模式 | 类型 | 说明 | +|---------|------|------| +| `AuthorityWebpage:` | String | 权限页面缓存 | +| `mobileLimit:` | String | 手机号限制标志 | +| `ToiletToUser:` | String | 厕所-用户映射 | +| `CleaningRestLastTime` | ZSet | 员工休息倒计时(score=截止时间戳) | + +### 8.4 DB5 Key 详细映射 + +#### 设备命令队列 + +| Key | 类型 | 内容 | 写入方 | 消费方 | +|-----|------|------|--------|--------| +| `taskCommand` | List | 设备告警命令 (TaskCommandInfo JSON) | PlanTimeTask | TaskCreateQueue | +| `taskFinishCommand` | List | 任务完成命令 (TaskCommandInfo JSON) | 业务系统 | TaskCorrelationSetTask | +| `FinishTaskQueue` | List | 已完成任务ID | TaskCorrelationSetTask | TaskCorrelationSetTask | +| `ConfirmTaskQueue` | List | 已确认任务ID | 业务系统 | TaskCorrelationSetTask | +| `WatchSetParamsInit` | List | 手表初始化参数 (JSON) | 业务系统 | ScheduledTask | +| `WatchShockSetParamsInit` | List | 手表震动初始化参数 (JSON) | 业务系统 | ScheduledTask | +| `RepeatTaskQueue` | Set | 重派发任务集合 | TaskCorrelationSetTask | TaskCorrelationSetTask | +| `DistrictBeaconNoChange` | List | 信标号更新队列 | 业务系统 | BeaconTask | + +#### 实体缓存 + +| Key 模式 | 类型 | 内容 | TTL | +|---------|------|------|-----| +| `entity:UserDistrictAttendance:_` | String | 用户站点考勤 JSON | 无 | +| `entity:Hardware:` | String | 硬件设备信息 JSON | 无 | +| `entity:ToiletAirQualityAirMac:` | String | 空气质量传感器 JSON | 无 | +| `entity:TaskTypeNearFlag::` | String | 任务类型临期标志 | 无 | + +#### 名称缓存 + +| Key 模式 | 类型 | 内容 | +|---------|------|------| +| `name:Agent:` | String | 代理商名称 | +| `name:Properyt:` | String | 物业名称 | +| `name:District:` | String | 站点名称 | +| `name:PropertyAuthority:` | String | 权限组名称 | + +#### 字典配置 (Hash) + +| Key | Hash 字段 | 值 | 说明 | +|-----|----------|-----|------| +| `Dictionary` | `UserAppointFlag` | "0"/"1" | 用户预约标志 | +| `Dictionary` | `UserAreaFlag` | "0"/"1" | 用户服务区域标志 | +| `Dictionary` | `UserDutyFlag` | "0"/"1" | 用户职责标志 | +| `Dictionary` | `LogicFlag` | "0"/"1" | 逻辑标志 | +| `Dictionary` | `TaskNearFlag` | "0"/"1" | 任务临期标志 | +| `Dictionary` | `OperateAssignmentTypes` | 类型列表 | 操作分配类型 | +| `Dictionary` | `AutoOffWorkTime` | 数字 | 自动下班时间 | +| `Dictionary` | `ScenesType` | 场景类型列表 | 场景类型 | + +#### 定位与状态 + +| Key 模式 | 类型 | 内容 | +|---------|------|------| +| `Location:` | String | 最新位置(格式: 时间_场景_楼栋_楼层) | +| `DistrictBeaconNo:` | String | 站点信标编号 | +| `disinfect:p:` | String | 位置消毒状态("0"/"1") | +| `CleaningUpWorkAtStatus:` | String | 员工原始工作状态 | + +#### 时间序列 + +| Key | 类型 | Score | 说明 | +|-----|------|-------|------| +| `ImpendingTime` | ZSet | 过期时间戳(ms) | 临期任务队列 | +| `TaskExprieQueue` | ZSet | 过期时间戳(ms) | 任务过期队列 | + +### 8.5 Redis 数据流转图 + +``` +[计划调度] + PlanTimeTask → PlanTaskQueue(DB4) → 任务生成 + PlanTimeTask → taskCommand(DB5) → TaskCreateQueue → taskPushQueue(DB4) + +[任务派单] + TaskCorrelationSetTask ←→ taskUserQueue(DB4) + TaskCorrelationSetTask ←→ inspectTaskUserQueue(DB4) + TaskCorrelationSetTask ←→ taskReportQueue(DB4) + TaskCorrelationSetTask → WatchTaskUserList(DB4) → ScheduledTask → MqttWatchTaskMessage(DB4) → MQTT + +[消息推送] + taskPushQueue(DB4) → TaskPushMessage → 个推SDK → APP + IntellectualTaskIdList(DB4) → ScheduledTask → MqttTaskStateMessage(DB4) → MQTT → 手机端 + +[设备通信] + WatchConnectList(DB4) → ScheduledTask → 更新Watch表 + BleutoothLocationQueue(DB4) → LocationTask → UserLocation/UserTrajectory + +[员工休息] + UserRestTask → AttendanceStatusMessage(DB4) → ScheduledTask → MQTT + CleaningRestLastTime(DB4, ZSet) → restCountDown → 自动恢复 +``` + +--- + +## 九、统计汇总 + +| 维度 | 数量 | +|------|------| +| Controller 数量 | 27 个 | +| REST API 端点 | 约 150+ 个 | +| XXL-Job 定时任务 | 6 个 | +| Spring @Scheduled 任务 | 20+ 个 | +| 业务模块 | 19 个 | +| DB1 数据表 | 51 张 | +| DB2 数据表 | 61 张 | +| 数据表总计 | 112 张 | +| Redis DB4 Key 类型 | 30+ 种 | +| Redis DB5 Key 类型 | 25+ 种 | +| Redis 消息队列 | 15+ 个 | diff --git a/docs/自动化部署方案-无Docker版.md b/docs/自动化部署方案-无Docker版.md new file mode 100644 index 0000000..9cdaedb --- /dev/null +++ b/docs/自动化部署方案-无Docker版.md @@ -0,0 +1,726 @@ +# SmartClean 自动化打包部署方案(无 Docker 版) + +## 一、目标 + +基于现有 build.sh 构建能力,实现 **一键构建 → 远程备份 → 上传部署 → 服务重启 → 健康检查 → 失败回滚 → 飞书通知** 全流程自动化。不依赖 Docker,直接操作远程服务器。 + +## 二、前提条件 + +| 条件 | 说明 | +|------|------| +| SSH 免密登录 | 本机 → 182 服务器配好 SSH Key,免输密码 | +| 服务器目录约定 | 统一部署路径,脚本按约定操作 | +| 现有构建脚本 | 复用已有的 build.sh | + +### SSH 免密配置(一次性) + +```bash +# 生成密钥(如已有可跳过) +ssh-keygen -t ed25519 + +# 将公钥复制到服务器 +ssh-copy-id root@192.168.1.182 + +# 验证 +ssh root@192.168.1.182 "echo ok" +``` + +## 三、整体架构 + +``` +本机 (Mac) 远程服务器 (192.168.1.182) +┌──────────────────────┐ ┌──────────────────────────────┐ +│ │ │ │ +│ 源码 → build.sh 构建 │ │ /opt/smartclean/ │ +│ │ SCP 上传 │ ├── web/ │ +│ 产物: │ ──────────────→ │ │ └── ROOT.war │ +│ - ROOT.war │ │ ├── task/ │ +│ - task.jar │ SSH 远程执行 │ │ └── task.jar │ +│ - dist/ │ ──────────────→ │ ├── front/ │ +│ │ │ │ └── dist/ │ +│ deploy.sh 控制全流程 │ │ ├── backups/ │ +│ │ │ │ ├── 20260415-153000/ │ +│ │ │ │ └── 20260415-140000/ │ +│ │ │ └── scripts/ │ +│ │ │ ├── restart-web.sh │ +│ │ │ ├── restart-task.sh │ +│ │ │ └── restart-front.sh │ +└──────────────────────┘ └──────────────────────────────┘ +``` + +## 四、服务器目录规划 + +``` +/opt/smartclean/ # 部署根目录 +├── web/ +│ ├── tomcat/ # Tomcat 安装目录 +│ │ └── webapps/ +│ │ └── ROOT.war # Web 服务 WAR 包 +│ └── logs/ +├── task/ +│ ├── task.jar # Task 服务 JAR 包 +│ ├── task.pid # 进程 PID 文件 +│ └── logs/ +├── front/ +│ └── dist/ # 前端静态文件(Nginx 指向此目录) +├── backups/ # 版本备份(自动保留最近 5 个) +│ ├── 20260415-153000/ +│ │ ├── ROOT.war +│ │ ├── task.jar +│ │ └── dist/ +│ └── 20260415-140000/ +│ └── ... +└── scripts/ # 服务器端管理脚本 + ├── restart-web.sh + ├── restart-task.sh + └── restart-front.sh +``` + +## 五、新增文件结构 + +``` +smartclean/ +├── build.sh # 已有,构建脚本 +└── deploy/ + ├── deploy.sh # 一键部署主脚本(本机执行) + ├── rollback.sh # 一键回滚脚本(本机执行) + ├── config.sh # 部署配置(服务器地址、路径等) + ├── .env # 敏感信息(密码等,不入 git) + └── remote/ # 服务器端脚本(首次部署时自动上传) + ├── setup.sh # 服务器初始化(创建目录结构,一次性) + ├── restart-web.sh # 重启 Web 服务(Tomcat) + ├── restart-task.sh # 重启 Task 服务(JAR) + └── restart-front.sh # 重新加载前端(Nginx reload) +``` + +## 六、配置文件 + +### 6.1 部署配置 + +```bash +# deploy/config.sh + +# ===== 服务器配置 ===== +DEPLOY_HOST="192.168.1.182" +DEPLOY_USER="root" +DEPLOY_BASE="/opt/smartclean" + +# ===== 服务器目录 ===== +REMOTE_WEB_DIR="$DEPLOY_BASE/web" +REMOTE_TASK_DIR="$DEPLOY_BASE/task" +REMOTE_FRONT_DIR="$DEPLOY_BASE/front" +REMOTE_BACKUP_DIR="$DEPLOY_BASE/backups" +REMOTE_SCRIPTS_DIR="$DEPLOY_BASE/scripts" + +# ===== Tomcat 配置 ===== +TOMCAT_HOME="/opt/smartclean/web/tomcat" +TOMCAT_WEBAPPS="$TOMCAT_HOME/webapps" + +# ===== Nginx 配置 ===== +NGINX_HTML="/opt/smartclean/front/dist" + +# ===== Task 服务配置 ===== +TASK_JAR_NAME="xiaoqu-intellectual-task-0.0.1-SNAPSHOT.jar" +TASK_PROFILE="prod" +TASK_JVM_OPTS="-Xms256m -Xmx512m" + +# ===== 本地构建产物路径 ===== +LOCAL_WAR="backend/xiaoqu-intellectual-web/target/ROOT.war" +LOCAL_TASK_JAR="backend/xiaoqu-intellectual-task/target/$TASK_JAR_NAME" +LOCAL_FRONT_DIST="frontend/witcleansystem/dist" + +# ===== 备份保留数量 ===== +MAX_BACKUPS=5 + +# ===== 健康检查 ===== +HEALTHCHECK_URL="http://$DEPLOY_HOST:8095/dropDown/districtTree" +HEALTHCHECK_RETRIES=20 +HEALTHCHECK_INTERVAL=5 + +# ===== 飞书通知 ===== +FEISHU_WEBHOOK="https://open.feishu.cn/open-apis/bot/v2/hook/5703e8cc-6998-46a6-af9d-8c5102cc8c1e" +``` + +## 七、服务器端脚本 + +### 7.1 服务器初始化脚本(一次性执行) + +```bash +# deploy/remote/setup.sh +#!/bin/bash +# 在服务器上创建标准目录结构,仅需执行一次 + +DEPLOY_BASE="/opt/smartclean" + +mkdir -p "$DEPLOY_BASE"/{web/logs,task/logs,front,backups,scripts} + +echo "目录结构创建完成:" +find "$DEPLOY_BASE" -maxdepth 2 -type d +``` + +### 7.2 重启 Web 服务 + +```bash +# deploy/remote/restart-web.sh +#!/bin/bash +# 重启 Tomcat(部署 ROOT.war) + +TOMCAT_HOME="/opt/smartclean/web/tomcat" + +echo "[INFO] 停止 Tomcat..." +"$TOMCAT_HOME/bin/shutdown.sh" 2>/dev/null +sleep 3 + +# 确保进程已停 +TOMCAT_PID=$(ps -ef | grep "catalina" | grep -v grep | awk '{print $2}') +if [ -n "$TOMCAT_PID" ]; then + echo "[WARN] Tomcat 未正常关闭,强制终止 PID=$TOMCAT_PID" + kill -9 $TOMCAT_PID + sleep 1 +fi + +# 清理旧的解压目录,保留 WAR +rm -rf "$TOMCAT_HOME/webapps/ROOT" +rm -rf "$TOMCAT_HOME/work/Catalina" + +echo "[INFO] 启动 Tomcat..." +"$TOMCAT_HOME/bin/startup.sh" + +echo "[INFO] Tomcat 已启动" +``` + +### 7.3 重启 Task 服务 + +```bash +# deploy/remote/restart-task.sh +#!/bin/bash +# 重启 Task 服务(Spring Boot JAR) + +TASK_DIR="/opt/smartclean/task" +JAR_FILE="$TASK_DIR/task.jar" +PID_FILE="$TASK_DIR/task.pid" +LOG_FILE="$TASK_DIR/logs/task.log" +PROFILE="${1:-prod}" +JVM_OPTS="${2:--Xms256m -Xmx512m}" + +# 停止旧进程 +if [ -f "$PID_FILE" ]; then + OLD_PID=$(cat "$PID_FILE") + if kill -0 "$OLD_PID" 2>/dev/null; then + echo "[INFO] 停止旧进程 PID=$OLD_PID" + kill "$OLD_PID" + sleep 3 + # 强制终止 + if kill -0 "$OLD_PID" 2>/dev/null; then + kill -9 "$OLD_PID" + fi + fi + rm -f "$PID_FILE" +fi + +# 启动新进程 +echo "[INFO] 启动 Task 服务 (profile=$PROFILE)..." +nohup java $JVM_OPTS \ + -jar "$JAR_FILE" \ + --spring.profiles.active=$PROFILE \ + > "$LOG_FILE" 2>&1 & + +echo $! > "$PID_FILE" +echo "[INFO] Task 服务已启动, PID=$(cat "$PID_FILE")" +``` + +### 7.4 重新加载前端 + +```bash +# deploy/remote/restart-front.sh +#!/bin/bash +# 重新加载 Nginx(前端静态文件已更新) + +echo "[INFO] 测试 Nginx 配置..." +nginx -t 2>&1 +if [ $? -ne 0 ]; then + echo "[ERROR] Nginx 配置有误" + exit 1 +fi + +echo "[INFO] 重新加载 Nginx..." +nginx -s reload + +echo "[INFO] Nginx 已重新加载" +``` + +## 八、一键部署主脚本 + +```bash +# deploy/deploy.sh +#!/bin/bash +# +# SmartClean 一键部署脚本(无 Docker 版) +# +# 用法: +# ./deploy.sh # 构建并部署所有服务 +# ./deploy.sh web # 仅构建部署 Web 服务 +# ./deploy.sh task # 仅构建部署 Task 服务 +# ./deploy.sh front # 仅构建部署前端 +# ./deploy.sh front-test # 构建测试环境前端并部署 +# ./deploy.sh backend # 构建部署后端(web + task) +# ./deploy.sh rollback # 回滚到上一版本 +# ./deploy.sh setup # 首次初始化服务器目录 + +set -e + +DEPLOY_DIR="$(cd "$(dirname "$0")" && pwd)" +ROOT_DIR="$(dirname "$DEPLOY_DIR")" +source "$DEPLOY_DIR/config.sh" + +# ===== 版本号 ===== +GIT_HASH=$(cd "$ROOT_DIR" && git rev-parse --short HEAD) +TIMESTAMP=$(date +%Y%m%d-%H%M%S) +VERSION="${TIMESTAMP}-${GIT_HASH}" +BRANCH=$(cd "$ROOT_DIR" && git rev-parse --abbrev-ref HEAD) +COMMIT=$(cd "$ROOT_DIR" && git log -1 --format='%h %s') + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +log_error() { echo -e "${RED}[ERROR]${NC} $1"; } + +SSH_CMD="ssh $DEPLOY_USER@$DEPLOY_HOST" +SCP_CMD="scp" + +# ===== 飞书通知 ===== +notify_feishu() { + local title="$1" content="$2" color="$3" + curl -s -X POST "$FEISHU_WEBHOOK" \ + -H "Content-Type: application/json" \ + -d "{ + \"msg_type\": \"interactive\", + \"card\": { + \"header\": {\"title\":{\"tag\":\"plain_text\",\"content\":\"$title\"},\"template\":\"$color\"}, + \"elements\": [{\"tag\":\"markdown\",\"content\":\"$content\"}] + } + }" > /dev/null 2>&1 +} + +# ===== 检查 SSH 连接 ===== +check_ssh() { + log_info "检查 SSH 连接..." + if ! $SSH_CMD "echo ok" > /dev/null 2>&1; then + log_error "无法连接到 $DEPLOY_USER@$DEPLOY_HOST" + log_error "请先配置 SSH 免密登录: ssh-copy-id $DEPLOY_USER@$DEPLOY_HOST" + exit 1 + fi + log_info "SSH 连接正常" +} + +# ===== 首次初始化服务器 ===== +setup_server() { + log_info "初始化服务器目录结构..." + $SCP_CMD "$DEPLOY_DIR/remote/setup.sh" "$DEPLOY_USER@$DEPLOY_HOST:/tmp/setup.sh" + $SSH_CMD "bash /tmp/setup.sh" + + log_info "上传服务器端管理脚本..." + $SCP_CMD "$DEPLOY_DIR/remote/restart-web.sh" "$DEPLOY_USER@$DEPLOY_HOST:$REMOTE_SCRIPTS_DIR/" + $SCP_CMD "$DEPLOY_DIR/remote/restart-task.sh" "$DEPLOY_USER@$DEPLOY_HOST:$REMOTE_SCRIPTS_DIR/" + $SCP_CMD "$DEPLOY_DIR/remote/restart-front.sh" "$DEPLOY_USER@$DEPLOY_HOST:$REMOTE_SCRIPTS_DIR/" + $SSH_CMD "chmod +x $REMOTE_SCRIPTS_DIR/*.sh" + + log_info "服务器初始化完成" +} + +# ===== 本地构建 ===== +build_local() { + local target="$1" + log_info "开始本地构建 (目标: $target)..." + + cd "$ROOT_DIR" + bash build.sh "$target" + + # 验证产物存在 + case "$target" in + web|backend|all) + if [ ! -f "$ROOT_DIR/$LOCAL_WAR" ]; then + log_error "构建产物不存在: $LOCAL_WAR" + exit 1 + fi + ;; + esac + + case "$target" in + task|backend|all) + if [ ! -f "$ROOT_DIR/$LOCAL_TASK_JAR" ]; then + log_error "构建产物不存在: $LOCAL_TASK_JAR" + exit 1 + fi + ;; + esac + + case "$target" in + front|front-test|all) + if [ ! -d "$ROOT_DIR/$LOCAL_FRONT_DIST" ]; then + log_error "构建产物不存在: $LOCAL_FRONT_DIST" + exit 1 + fi + ;; + esac + + log_info "本地构建完成" +} + +# ===== 远程备份 ===== +backup_remote() { + local target="$1" + log_info "备份服务器当前版本 ($VERSION)..." + + $SSH_CMD bash </dev/null && echo "已备份 ROOT.war" || echo "ROOT.war 不存在,跳过" + ;; + esac + + case "$target" in + task|backend|all) + cp "$REMOTE_TASK_DIR/task.jar" "\$BACKUP/" 2>/dev/null && echo "已备份 task.jar" || echo "task.jar 不存在,跳过" + ;; + esac + + case "$target" in + front|front-test|all) + if [ -d "$REMOTE_FRONT_DIR/dist" ]; then + cp -r "$REMOTE_FRONT_DIR/dist" "\$BACKUP/" + echo "已备份 front/dist" + fi + ;; + esac + + # 清理过期备份,保留最近 $MAX_BACKUPS 个 + cd "$REMOTE_BACKUP_DIR" + ls -dt */ 2>/dev/null | tail -n +\$(($MAX_BACKUPS + 1)) | xargs rm -rf 2>/dev/null + echo "当前备份列表:" + ls -dt */ 2>/dev/null | head -5 +EOF + + log_info "远程备份完成" +} + +# ===== 上传产物 ===== +upload_artifacts() { + local target="$1" + + case "$target" in + web|backend|all) + log_info "上传 ROOT.war..." + $SCP_CMD "$ROOT_DIR/$LOCAL_WAR" "$DEPLOY_USER@$DEPLOY_HOST:$TOMCAT_WEBAPPS/ROOT.war" + ;; + esac + + case "$target" in + task|backend|all) + log_info "上传 task.jar..." + $SCP_CMD "$ROOT_DIR/$LOCAL_TASK_JAR" "$DEPLOY_USER@$DEPLOY_HOST:$REMOTE_TASK_DIR/task.jar" + ;; + esac + + case "$target" in + front|front-test|all) + log_info "上传前端文件..." + # 先清空远程 dist,再上传 + $SSH_CMD "rm -rf $REMOTE_FRONT_DIR/dist" + $SCP_CMD -r "$ROOT_DIR/$LOCAL_FRONT_DIST" "$DEPLOY_USER@$DEPLOY_HOST:$REMOTE_FRONT_DIR/dist" + ;; + esac + + log_info "产物上传完成" +} + +# ===== 远程重启服务 ===== +restart_services() { + local target="$1" + + case "$target" in + web|backend|all) + log_info "重启 Web 服务..." + $SSH_CMD "bash $REMOTE_SCRIPTS_DIR/restart-web.sh" + ;; + esac + + case "$target" in + task|backend|all) + log_info "重启 Task 服务..." + $SSH_CMD "bash $REMOTE_SCRIPTS_DIR/restart-task.sh $TASK_PROFILE '$TASK_JVM_OPTS'" + ;; + esac + + case "$target" in + front|front-test|all) + log_info "重新加载前端..." + $SSH_CMD "bash $REMOTE_SCRIPTS_DIR/restart-front.sh" + ;; + esac +} + +# ===== 健康检查 ===== +healthcheck() { + local target="$1" + + # Web 服务检查 + if [ "$target" = "all" ] || [ "$target" = "web" ] || [ "$target" = "backend" ]; then + log_info "健康检查 Web 服务 ($HEALTHCHECK_URL)..." + for i in $(seq 1 $HEALTHCHECK_RETRIES); do + if curl -sf "$HEALTHCHECK_URL" > /dev/null 2>&1; then + log_info "✅ Web 服务健康 (第${i}次检查)" + return 0 + fi + echo -n "." + sleep $HEALTHCHECK_INTERVAL + done + log_error "❌ Web 服务健康检查失败 (${HEALTHCHECK_RETRIES}次重试后)" + return 1 + fi + + # 前端检查 + if [ "$target" = "front" ] || [ "$target" = "front-test" ]; then + log_info "健康检查前端..." + if curl -sf "http://$DEPLOY_HOST/" > /dev/null 2>&1; then + log_info "✅ 前端服务健康" + return 0 + else + log_error "❌ 前端服务健康检查失败" + return 1 + fi + fi + + return 0 +} + +# ===== 回滚 ===== +rollback() { + log_info "查找最近的备份..." + + # 获取最近的备份目录名 + LATEST_BACKUP=$($SSH_CMD "ls -dt $REMOTE_BACKUP_DIR/*/ 2>/dev/null | head -1 | xargs basename 2>/dev/null") + + if [ -z "$LATEST_BACKUP" ]; then + log_error "没有可回滚的备份" + exit 1 + fi + + log_warn "回滚到版本: $LATEST_BACKUP" + + $SSH_CMD bash < 端口刻意与本机开发环境错开,两套环境完全独立、互不干扰。 + +## 三、新增文件结构 + +``` +smartclean/ +└── deploy/ + ├── docker-compose.yml # 服务编排定义 + ├── .env # 环境变量(密码等敏感信息,不入 git) + │ + ├── Dockerfile.web # Web 服务镜像(Tomcat + ROOT.war) + ├── Dockerfile.task # Task 服务镜像(JRE + JAR) + ├── Dockerfile.front # 前端镜像(Nginx + 静态文件) + │ + ├── conf/ + │ ├── nginx.conf # Nginx 反代配置(前端 → 后端) + │ ├── application-docker.yml # Web 服务 Docker 专用 Spring 配置 + │ └── application-task-docker.yml # Task 服务 Docker 专用 Spring 配置 + │ + ├── init-sql/ + │ └── init.sh # 首次启动时自动建库导数据 + │ + ├── deploy.sh # 一键部署脚本(主入口) + └── rollback.sh # 一键回滚脚本 +``` + +## 四、各组件详细设计 + +### 4.1 Docker Compose 编排 + +```yaml +# deploy/docker-compose.yml +version: '3.8' + +services: + # ==================== 基础设施 ==================== + mysql: + image: mysql:5.7 + container_name: smartclean-mysql + ports: + - "3307:3306" + environment: + MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} + volumes: + - mysql_data:/var/lib/mysql + - ../sql:/docker-entrypoint-initdb.d # 首次启动自动导入 SQL + command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + interval: 10s + retries: 5 + networks: + - smartclean + + redis: + image: redis:6-alpine + container_name: smartclean-redis + ports: + - "6380:6379" + command: redis-server --requirepass ${REDIS_PASSWORD} + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"] + interval: 10s + retries: 3 + networks: + - smartclean + + # ==================== 应用服务 ==================== + web: + image: smartclean-web:${VERSION:-latest} + container_name: smartclean-web + build: + context: .. + dockerfile: deploy/Dockerfile.web + ports: + - "18095:8095" + depends_on: + mysql: + condition: service_healthy + redis: + condition: service_healthy + environment: + SPRING_PROFILES_ACTIVE: docker + volumes: + - ./conf/application-docker.yml:/app/config/application-docker.yml + - web_logs:/app/logs + healthcheck: + test: ["CMD", "curl", "-sf", "http://localhost:8095/dropDown/districtTree"] + interval: 15s + timeout: 5s + retries: 5 + start_period: 60s + restart: on-failure:3 + networks: + - smartclean + + task: + image: smartclean-task:${VERSION:-latest} + container_name: smartclean-task + build: + context: .. + dockerfile: deploy/Dockerfile.task + ports: + - "18097:8097" + depends_on: + mysql: + condition: service_healthy + redis: + condition: service_healthy + environment: + SPRING_PROFILES_ACTIVE: docker + volumes: + - ./conf/application-task-docker.yml:/app/config/application-task-docker.yml + - task_logs:/app/logs + restart: on-failure:3 + networks: + - smartclean + + frontend: + image: smartclean-front:${VERSION:-latest} + container_name: smartclean-front + build: + context: .. + dockerfile: deploy/Dockerfile.front + ports: + - "80:80" + depends_on: + - web + volumes: + - ./conf/nginx.conf:/etc/nginx/conf.d/default.conf + healthcheck: + test: ["CMD", "curl", "-sf", "http://localhost/"] + interval: 10s + retries: 3 + restart: on-failure:3 + networks: + - smartclean + +volumes: + mysql_data: + redis_data: + web_logs: + task_logs: + +networks: + smartclean: + driver: bridge +``` + +### 4.2 Dockerfile + +#### Web 服务(Tomcat + WAR) + +```dockerfile +# deploy/Dockerfile.web +FROM maven:3.8-openjdk-8 AS builder +WORKDIR /src +COPY backend/xiaoqu-intellectual-public/ xiaoqu-intellectual-public/ +COPY backend/xiaoqu-intellectual-web/ xiaoqu-intellectual-web/ +RUN cd xiaoqu-intellectual-public && mvn clean install -q -DskipTests \ + && cd ../xiaoqu-intellectual-web && mvn clean package -q -DskipTests + +FROM tomcat:8.5-jdk8-temurin +RUN rm -rf /usr/local/tomcat/webapps/* +COPY --from=builder /src/xiaoqu-intellectual-web/target/ROOT.war /usr/local/tomcat/webapps/ +# 注入外部配置目录 +ENV SPRING_CONFIG_ADDITIONAL_LOCATION=/app/config/ +RUN mkdir -p /app/config /app/logs +EXPOSE 8095 +``` + +#### Task 服务(Spring Boot JAR) + +```dockerfile +# deploy/Dockerfile.task +FROM maven:3.8-openjdk-8 AS builder +WORKDIR /src +COPY backend/xiaoqu-intellectual-public/ xiaoqu-intellectual-public/ +COPY backend/xiaoqu-intellectual-task/ xiaoqu-intellectual-task/ +RUN cd xiaoqu-intellectual-public && mvn clean install -q -DskipTests \ + && cd ../xiaoqu-intellectual-task && mvn clean package -q -DskipTests + +FROM openjdk:8-jre-slim +WORKDIR /app +RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* +COPY --from=builder /src/xiaoqu-intellectual-task/target/*.jar app.jar +RUN mkdir -p /app/config /app/logs +EXPOSE 8097 +ENTRYPOINT ["java", "-jar", "app.jar", \ + "--spring.config.additional-location=/app/config/"] +``` + +#### 前端(多阶段构建 → Nginx) + +```dockerfile +# deploy/Dockerfile.front +FROM node:16-alpine AS builder +WORKDIR /app +COPY frontend/witcleansystem/package*.json ./ +RUN npm ci --registry=https://registry.npmmirror.com +COPY frontend/witcleansystem/ ./ +RUN npm run build + +FROM nginx:1.24-alpine +RUN rm -rf /usr/share/nginx/html/* +COPY --from=builder /app/dist /usr/share/nginx/html +EXPOSE 80 +``` + +### 4.3 Nginx 反代配置 + +```nginx +# deploy/conf/nginx.conf +server { + listen 80; + server_name localhost; + + # 前端静态文件 + location / { + root /usr/share/nginx/html; + index index.html; + try_files $uri $uri/ /index.html; # SPA history fallback + } + + # API 反代到后端 Web 服务 + location /api/ { + proxy_pass http://web:8095/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + + # 静态资源缓存 + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ { + root /usr/share/nginx/html; + expires 7d; + add_header Cache-Control "public, immutable"; + } +} +``` + +### 4.4 Docker 专用 Spring 配置 + +```yaml +# deploy/conf/application-docker.yml +# 覆盖 application-test.yml 中的远程地址,指向 Docker 容器 +server: + port: 8095 + +redis: + host: redis # Docker 容器名 + port: 6379 + password: ${REDIS_PASSWORD:kaixinjiuhao} + +spring: + datasource: + db1: + url: jdbc:mysql://mysql:3306/xiaoqu_comples_d?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowMultiQueries=true&allowPublicKeyRetrieval=true + username: root + password: ${DB_PASSWORD:kaixinjiuhao} + db2: + url: jdbc:mysql://mysql:3306/xiaoqu_intellectual_d?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowMultiQueries=true&allowPublicKeyRetrieval=true + username: root + password: ${DB_PASSWORD:kaixinjiuhao} + +xxl: + job: + admin: + addresses: http://xxl-job-admin:8080/xxl-job-admin # 可选,本地模拟可留空 +``` + +### 4.5 环境变量文件 + +```bash +# deploy/.env(不入 git) +DB_PASSWORD=kaixinjiuhao +REDIS_PASSWORD=kaixinjiuhao +VERSION=latest +FEISHU_WEBHOOK=https://open.feishu.cn/open-apis/bot/v2/hook/5703e8cc-6998-46a6-af9d-8c5102cc8c1e +``` + +## 五、一键部署脚本 + +```bash +# deploy/deploy.sh +#!/bin/bash +# +# SmartClean 一键部署脚本(本地 Docker 模拟) +# +# 用法: +# ./deploy.sh # 部署所有服务 +# ./deploy.sh web # 仅重建部署 Web 服务 +# ./deploy.sh task # 仅重建部署 Task 服务 +# ./deploy.sh front # 仅重建部署前端 +# ./deploy.sh rollback # 回滚到上一版本 + +set -e + +DEPLOY_DIR="$(cd "$(dirname "$0")" && pwd)" +ROOT_DIR="$(dirname "$DEPLOY_DIR")" +source "$DEPLOY_DIR/.env" + +# ===== 版本号:日期-git短hash ===== +GIT_HASH=$(cd "$ROOT_DIR" && git rev-parse --short HEAD) +VERSION="v$(date +%Y%m%d)-${GIT_HASH}" +BACKUP_FILE="$DEPLOY_DIR/.last-version" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +log_error() { echo -e "${RED}[ERROR]${NC} $1"; } + +# ===== 飞书通知 ===== +notify_feishu() { + local title="$1" content="$2" color="$3" + curl -s -X POST "$FEISHU_WEBHOOK" \ + -H "Content-Type: application/json" \ + -d "{ + \"msg_type\": \"interactive\", + \"card\": { + \"header\": {\"title\":{\"tag\":\"plain_text\",\"content\":\"$title\"},\"template\":\"$color\"}, + \"elements\": [{\"tag\":\"markdown\",\"content\":\"$content\"}] + } + }" > /dev/null 2>&1 +} + +# ===== 备份当前版本号 ===== +backup_version() { + if [ -f "$DEPLOY_DIR/.current-version" ]; then + cp "$DEPLOY_DIR/.current-version" "$BACKUP_FILE" + log_info "已备份当前版本: $(cat "$BACKUP_FILE")" + fi +} + +# ===== 构建镜像 ===== +build_images() { + local target="${1:-all}" + local start_time=$(date +%s) + + cd "$DEPLOY_DIR" + + case "$target" in + web) + log_info "构建 Web 镜像 ($VERSION)..." + docker compose build web + ;; + task) + log_info "构建 Task 镜像 ($VERSION)..." + docker compose build task + ;; + front) + log_info "构建前端镜像 ($VERSION)..." + docker compose build frontend + ;; + all) + log_info "构建全部镜像 ($VERSION)..." + docker compose build web task frontend + ;; + esac + + local elapsed=$(( $(date +%s) - start_time )) + log_info "镜像构建完成 (${elapsed}s)" +} + +# ===== 部署服务 ===== +deploy_services() { + local target="${1:-all}" + + cd "$DEPLOY_DIR" + export VERSION + + # 确保基础设施先启动 + log_info "确保 MySQL + Redis 运行中..." + docker compose up -d mysql redis + log_info "等待数据库就绪..." + docker compose exec mysql mysqladmin ping -h localhost --wait=30 --silent 2>/dev/null + + case "$target" in + web) docker compose up -d --no-deps web ;; + task) docker compose up -d --no-deps task ;; + front) docker compose up -d --no-deps frontend ;; + all) docker compose up -d web task frontend ;; + esac + + log_info "容器已启动,开始健康检查..." +} + +# ===== 健康检查 ===== +healthcheck() { + local target="${1:-all}" + local max_retries=20 + local interval=5 + + # 检查 Web 服务 + if [ "$target" = "all" ] || [ "$target" = "web" ]; then + log_info "检查 Web 服务..." + for i in $(seq 1 $max_retries); do + if curl -sf http://localhost:18095/dropDown/districtTree > /dev/null 2>&1; then + log_info "✅ Web 服务健康 (第${i}次检查)" + break + fi + if [ $i -eq $max_retries ]; then + log_error "❌ Web 服务健康检查失败" + return 1 + fi + sleep $interval + done + fi + + # 检查前端 + if [ "$target" = "all" ] || [ "$target" = "front" ]; then + log_info "检查前端服务..." + for i in $(seq 1 $max_retries); do + if curl -sf http://localhost:80/ > /dev/null 2>&1; then + log_info "✅ 前端服务健康 (第${i}次检查)" + break + fi + if [ $i -eq $max_retries ]; then + log_error "❌ 前端服务健康检查失败" + return 1 + fi + sleep $interval + done + fi + + return 0 +} + +# ===== 回滚 ===== +rollback() { + if [ ! -f "$BACKUP_FILE" ]; then + log_error "没有可回滚的版本" + exit 1 + fi + + local old_version=$(cat "$BACKUP_FILE") + log_warn "回滚到版本: $old_version" + + export VERSION="$old_version" + cd "$DEPLOY_DIR" + docker compose up -d web task frontend + + echo "$old_version" > "$DEPLOY_DIR/.current-version" + log_info "回滚完成" + + notify_feishu "⚠️ SmartClean 已回滚" \ + "**回滚版本:** $old_version\\n**触发原因:** 健康检查失败" \ + "yellow" +} + +# ===== 主流程 ===== +TARGET="${1:-all}" +DEPLOY_START=$(date +%s) +BRANCH=$(cd "$ROOT_DIR" && git rev-parse --abbrev-ref HEAD) +COMMIT=$(cd "$ROOT_DIR" && git log -1 --format='%h %s') + +if [ "$TARGET" = "rollback" ]; then + rollback + exit 0 +fi + +log_info "==============================" +log_info " SmartClean 自动化部署" +log_info " 版本: $VERSION" +log_info " 分支: $BRANCH" +log_info " 目标: $TARGET" +log_info "==============================" + +# 1. 备份当前版本 +backup_version + +# 2. 构建镜像 +build_images "$TARGET" + +# 3. 部署 +deploy_services "$TARGET" + +# 4. 健康检查 +if healthcheck "$TARGET"; then + # 记录当前版本 + echo "$VERSION" > "$DEPLOY_DIR/.current-version" + + ELAPSED=$(( $(date +%s) - DEPLOY_START )) + log_info "==============================" + log_info " ✅ 部署成功!耗时 ${ELAPSED}s" + log_info " 前端: http://localhost" + log_info " Web: http://localhost:18095" + log_info " Task: http://localhost:18097" + log_info "==============================" + + notify_feishu "✅ SmartClean 部署成功" \ + "**版本:** $VERSION\\n**分支:** $BRANCH\\n**提交:** $COMMIT\\n**目标:** $TARGET\\n**耗时:** ${ELAPSED}s" \ + "green" +else + log_error "健康检查失败,自动回滚..." + rollback + + ELAPSED=$(( $(date +%s) - DEPLOY_START )) + notify_feishu "❌ SmartClean 部署失败(已回滚)" \ + "**版本:** $VERSION\\n**分支:** $BRANCH\\n**提交:** $COMMIT\\n**耗时:** ${ELAPSED}s\\n**状态:** 已自动回滚" \ + "red" + + exit 1 +fi +``` + +## 六、回滚脚本 + +```bash +# deploy/rollback.sh +#!/bin/bash +# 快捷回滚入口 +DEPLOY_DIR="$(cd "$(dirname "$0")" && pwd)" +exec "$DEPLOY_DIR/deploy.sh" rollback +``` + +## 七、使用方式 + +### 首次启动 + +```bash +cd deploy/ + +# 1. 创建 .env 文件(设置密码等) +cp .env.example .env + +# 2. 一键构建 + 部署全部服务 +./deploy.sh +``` + +### 日常使用 + +```bash +# 修改了后端代码,只重建部署 Web 服务 +./deploy.sh web + +# 修改了前端代码,只重建部署前端 +./deploy.sh front + +# 全量部署 +./deploy.sh + +# 出问题了,一键回滚 +./deploy.sh rollback +# 或 +./rollback.sh +``` + +### 查看日志 + +```bash +# 查看所有服务日志 +docker compose -f deploy/docker-compose.yml logs -f + +# 只看 Web 服务 +docker compose -f deploy/docker-compose.yml logs -f web + +# 查看容器状态 +docker compose -f deploy/docker-compose.yml ps +``` + +### 停止 / 清理 + +```bash +# 停止所有容器(数据保留) +docker compose -f deploy/docker-compose.yml down + +# 停止并删除数据卷(慎用,会丢数据库数据) +docker compose -f deploy/docker-compose.yml down -v +``` + +## 八、端口映射总览 + +| 服务 | 容器内端口 | 宿主机端口 | 用途 | +|------|-----------|-----------|------| +| 前端 (Nginx) | 80 | **80** | 浏览器访问入口 | +| Web 后端 | 8095 | **18095** | API 接口(也可通过 Nginx `/api` 访问) | +| Task 后端 | 8097 | **18097** | 定时任务服务 | +| MySQL | 3306 | **3307** | 本机连接调试用 | +| Redis | 6379 | **6380** | 本机连接调试用 | + +> 与本机开发环境 (8079/8095/3306/6379) 完全隔离。 + +## 九、与本机开发环境的对比 + +``` +┌─────────────────────────────┐ ┌─────────────────────────────┐ +│ 本机开发环境(已有) │ │ Docker 模拟部署(新增) │ +│ │ │ │ +│ npm run dev → :8079 │ │ Nginx → :80 │ +│ mvn spring-boot:run → :8095│ │ Tomcat 容器 → :18095 │ +│ 本机 MySQL → :3306 │ │ MySQL 容器 → :3307 │ +│ 本机 Redis → :6379 │ │ Redis 容器 → :6380 │ +│ │ │ │ +│ 用途: 开发调试、热重载 │ │ 用途: 模拟生产部署流程 │ +└─────────────────────────────┘ └─────────────────────────────┘ +``` + +## 十、后续演进路径 + +本方案在本机验证通过后,可以低成本迁移到真实环境: + +| 阶段 | 动作 | 改动量 | +|------|------|--------| +| **当前** | 本机 Docker 模拟 | 本方案 | +| **阶段 2** | 远程服务器部署 | deploy.sh 加 SSH 远程执行,或把镜像 push 到私有 Registry | +| **阶段 3** | Gitea Actions CI/CD | 把 deploy.sh 的逻辑搬到 `.gitea/workflows/deploy.yml`,push 自动触发 | +| **阶段 4** | 多环境 | docker-compose.prod.yml 覆盖生产配置,同一套镜像部署到不同环境 | + +每个阶段都是增量改动,核心的 Dockerfile 和 docker-compose.yml 不需要重写。 diff --git a/frontend/witcleansystem/.env.develop b/frontend/witcleansystem/.env.develop index 3fb5f01..00db4a2 100644 --- a/frontend/witcleansystem/.env.develop +++ b/frontend/witcleansystem/.env.develop @@ -1,4 +1,4 @@ //测试环境 NODE_ENV='development' VITE_APP_MODE='develop' -VITE_APP_API_ORIGIN = 'http://192.168.1.182:8095' +VITE_APP_API_ORIGIN = 'http://100.93.0.28:8095' diff --git a/frontend/witcleansystem/.env.docker b/frontend/witcleansystem/.env.docker new file mode 100644 index 0000000..1db9fd8 --- /dev/null +++ b/frontend/witcleansystem/.env.docker @@ -0,0 +1,4 @@ +//Docker模拟部署环境 +NODE_ENV='production' +VITE_APP_MODE='develop' +VITE_APP_API_ORIGIN = 'http://100.93.0.28:18095' diff --git a/frontend/witcleansystem/package-lock.json b/frontend/witcleansystem/package-lock.json index 8c6c537..228d377 100644 --- a/frontend/witcleansystem/package-lock.json +++ b/frontend/witcleansystem/package-lock.json @@ -1,121 +1,179 @@ { "name": "witCleanSystem", "version": "1.0.6", - "lockfileVersion": 1, + "lockfileVersion": 3, "requires": true, - "dependencies": { - "@ant-design/colors": { + "packages": { + "": { + "name": "witCleanSystem", + "version": "1.0.6", + "dependencies": { + "@ant-design/icons-vue": "^6.0.1", + "@element-plus/icons-vue": "^1.1.1", + "@vitejs/plugin-vue": "^2.2.4", + "ant-design-vue": "^2.2.8", + "axios": "^0.26.0", + "default-passive-events": "^2.0.0", + "element-plus": "^2.0.4", + "js-md5": "^0.7.3", + "vue": "^3.0.4", + "vue-loader": "^17.0.0", + "vue-router": "^4.0.13", + "vuedraggable": "^4.1.0", + "vuex": "^4.0.2" + }, + "devDependencies": { + "@vue/compiler-sfc": "^3.0.4", + "nprogress": "^0.2.0", + "sass": "^1.49.9", + "vite": "^2.8.6", + "vite-plugin-cesium": "^1.2.13" + } + }, + "node_modules/@ant-design/colors": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-5.1.1.tgz", "integrity": "sha512-Txy4KpHrp3q4XZdfgOBqLl+lkQIc3tEvHXOimRN1giX1AEC7mGtyrO9p8iRGJ3FLuVMGa2gNEzQyghVymLttKQ==", - "requires": { + "dependencies": { "@ctrl/tinycolor": "^3.3.1" } }, - "@ant-design/icons-svg": { + "node_modules/@ant-design/icons-svg": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.2.1.tgz", "integrity": "sha512-EB0iwlKDGpG93hW8f85CTJTs4SvMX7tt5ceupvhALp1IF44SeUFOMhKUOYqpsoYWQKAOuTRDMqn75rEaKDp0Xw==" }, - "@ant-design/icons-vue": { + "node_modules/@ant-design/icons-vue": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/@ant-design/icons-vue/-/icons-vue-6.0.1.tgz", "integrity": "sha512-HigIgEVV6bbcrz2A92/qDzi/aKWB5EC6b6E1mxMB6aQA7ksiKY+gi4U94TpqyEIIhR23uaDrjufJ+xCZQ+vx6Q==", - "requires": { + "dependencies": { "@ant-design/colors": "^5.0.0", "@ant-design/icons-svg": "^4.0.0", "@types/lodash": "^4.14.165", "lodash": "^4.17.15" + }, + "peerDependencies": { + "vue": ">=3.0.3" } }, - "@babel/parser": { + "node_modules/@babel/parser": { "version": "7.17.3", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.3.tgz", - "integrity": "sha512-7yJPvPV+ESz2IUTPbOL+YkIGyCqOyNIzdguKQuJGnH7bg1WTIifuM21YqokFt/THWh1AkCRn9IgoykTRCBVpzA==" + "integrity": "sha512-7yJPvPV+ESz2IUTPbOL+YkIGyCqOyNIzdguKQuJGnH7bg1WTIifuM21YqokFt/THWh1AkCRn9IgoykTRCBVpzA==", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } }, - "@babel/runtime": { + "node_modules/@babel/runtime": { "version": "7.17.2", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.2.tgz", "integrity": "sha512-hzeyJyMA1YGdJTuWU0e/j4wKXrU4OMFvY2MSlaI9B7VQb0r5cxTE3EAIS2Q7Tn2RIcDkRvTA/v2JsAEhxe99uw==", - "requires": { + "dependencies": { "regenerator-runtime": "^0.13.4" + }, + "engines": { + "node": ">=6.9.0" } }, - "@ctrl/tinycolor": { + "node_modules/@ctrl/tinycolor": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.4.0.tgz", - "integrity": "sha512-JZButFdZ1+/xAfpguQHoabIXkcqRRKpMrWKBkpEZZyxfY9C1DpADFB8PEqGSTeFr135SaTRfKqGKx5xSCLI7ZQ==" + "integrity": "sha512-JZButFdZ1+/xAfpguQHoabIXkcqRRKpMrWKBkpEZZyxfY9C1DpADFB8PEqGSTeFr135SaTRfKqGKx5xSCLI7ZQ==", + "engines": { + "node": ">=10" + } }, - "@element-plus/icons-vue": { + "node_modules/@element-plus/icons-vue": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-1.1.1.tgz", - "integrity": "sha512-kRL/cWwaynmwi/6ounJxwnj316EHqAqKrl2WTCmcwiDPVqT+Wt1pSK6nAI0zUeLfe/y8NhYMB97+NDEWgNBCqA==" + "integrity": "sha512-kRL/cWwaynmwi/6ounJxwnj316EHqAqKrl2WTCmcwiDPVqT+Wt1pSK6nAI0zUeLfe/y8NhYMB97+NDEWgNBCqA==", + "peerDependencies": { + "vue": "^3.2.0" + } }, - "@popperjs/core": { + "node_modules/@popperjs/core": { "version": "2.11.2", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.2.tgz", - "integrity": "sha512-92FRmppjjqz29VMJ2dn+xdyXZBrMlE42AV6Kq6BwjWV7CNUW1hs2FtxSNLQE+gJhaZ6AAmYuO9y8dshhcBl7vA==" + "integrity": "sha512-92FRmppjjqz29VMJ2dn+xdyXZBrMlE42AV6Kq6BwjWV7CNUW1hs2FtxSNLQE+gJhaZ6AAmYuO9y8dshhcBl7vA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } }, - "@rollup/pluginutils": { + "node_modules/@rollup/pluginutils": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.1.2.tgz", "integrity": "sha512-ROn4qvkxP9SyPeHaf7uQC/GPFY6L/OWy9+bd9AwcjOAWQwxRscoEyAUD8qCY5o5iL4jqQwoLk2kaTKJPb/HwzQ==", "dev": true, - "requires": { + "dependencies": { "estree-walker": "^2.0.1", "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" } }, - "@simonwep/pickr": { + "node_modules/@simonwep/pickr": { "version": "1.8.2", "resolved": "https://registry.npmjs.org/@simonwep/pickr/-/pickr-1.8.2.tgz", "integrity": "sha512-/l5w8BIkrpP6n1xsetx9MWPWlU6OblN5YgZZphxan0Tq4BByTCETL6lyIeY8lagalS2Nbt4F2W034KHLIiunKA==", - "requires": { + "dependencies": { "core-js": "^3.15.1", "nanopop": "^2.1.0" } }, - "@types/estree": { + "node_modules/@types/estree": { "version": "0.0.51", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==", "dev": true }, - "@types/lodash": { + "node_modules/@types/lodash": { "version": "4.14.179", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.179.tgz", "integrity": "sha512-uwc1x90yCKqGcIOAT6DwOSuxnrAbpkdPsUOZtwrXb4D/6wZs+6qG7QnIawDuZWg0sWpxl+ltIKCaLoMlna678w==" }, - "@vitejs/plugin-vue": { + "node_modules/@vitejs/plugin-vue": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-2.2.4.tgz", - "integrity": "sha512-ev9AOlp0ljCaDkFZF3JwC/pD2N4Hh+r5srl5JHM6BKg5+99jiiK0rE/XaRs3pVm1wzyKkjUy/StBSoXX5fFzcw==" + "integrity": "sha512-ev9AOlp0ljCaDkFZF3JwC/pD2N4Hh+r5srl5JHM6BKg5+99jiiK0rE/XaRs3pVm1wzyKkjUy/StBSoXX5fFzcw==", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "vite": "^2.5.10", + "vue": "^3.2.25" + } }, - "@vue/compiler-core": { + "node_modules/@vue/compiler-core": { "version": "3.2.31", "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.31.tgz", "integrity": "sha512-aKno00qoA4o+V/kR6i/pE+aP+esng5siNAVQ422TkBNM6qA4veXiZbSe8OTXHXquEi/f6Akc+nLfB4JGfe4/WQ==", - "requires": { + "dependencies": { "@babel/parser": "^7.16.4", "@vue/shared": "3.2.31", "estree-walker": "^2.0.2", "source-map": "^0.6.1" } }, - "@vue/compiler-dom": { + "node_modules/@vue/compiler-dom": { "version": "3.2.31", "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.31.tgz", "integrity": "sha512-60zIlFfzIDf3u91cqfqy9KhCKIJgPeqxgveH2L+87RcGU/alT6BRrk5JtUso0OibH3O7NXuNOQ0cDc9beT0wrg==", - "requires": { + "dependencies": { "@vue/compiler-core": "3.2.31", "@vue/shared": "3.2.31" } }, - "@vue/compiler-sfc": { + "node_modules/@vue/compiler-sfc": { "version": "3.2.31", "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.31.tgz", "integrity": "sha512-748adc9msSPGzXgibHiO6T7RWgfnDcVQD+VVwYgSsyyY8Ans64tALHZANrKtOzvkwznV/F4H7OAod/jIlp/dkQ==", - "requires": { + "dependencies": { "@babel/parser": "^7.16.4", "@vue/compiler-core": "3.2.31", "@vue/compiler-dom": "3.2.31", @@ -128,33 +186,33 @@ "source-map": "^0.6.1" } }, - "@vue/compiler-ssr": { + "node_modules/@vue/compiler-ssr": { "version": "3.2.31", "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.31.tgz", "integrity": "sha512-mjN0rqig+A8TVDnsGPYJM5dpbjlXeHUm2oZHZwGyMYiGT/F4fhJf/cXy8QpjnLQK4Y9Et4GWzHn9PS8AHUnSkw==", - "requires": { + "dependencies": { "@vue/compiler-dom": "3.2.31", "@vue/shared": "3.2.31" } }, - "@vue/devtools-api": { + "node_modules/@vue/devtools-api": { "version": "6.0.12", "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.0.12.tgz", "integrity": "sha512-iO/4FIezHKXhiDBdKySCvJVh8/mZPxHpiQrTy+PXVqJZgpTPTdHy4q8GXulaY+UKEagdkBb0onxNQZ0LNiqVhw==" }, - "@vue/reactivity": { + "node_modules/@vue/reactivity": { "version": "3.2.31", "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.2.31.tgz", "integrity": "sha512-HVr0l211gbhpEKYr2hYe7hRsV91uIVGFYNHj73njbARVGHQvIojkImKMaZNDdoDZOIkMsBc9a1sMqR+WZwfSCw==", - "requires": { + "dependencies": { "@vue/shared": "3.2.31" } }, - "@vue/reactivity-transform": { + "node_modules/@vue/reactivity-transform": { "version": "3.2.31", "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.2.31.tgz", "integrity": "sha512-uS4l4z/W7wXdI+Va5pgVxBJ345wyGFKvpPYtdSgvfJfX/x2Ymm6ophQlXXB6acqGHtXuBqNyyO3zVp9b1r0MOA==", - "requires": { + "dependencies": { "@babel/parser": "^7.16.4", "@vue/compiler-core": "3.2.31", "@vue/shared": "3.2.31", @@ -162,69 +220,108 @@ "magic-string": "^0.25.7" } }, - "@vue/runtime-core": { + "node_modules/@vue/runtime-core": { "version": "3.2.31", "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.2.31.tgz", "integrity": "sha512-Kcog5XmSY7VHFEMuk4+Gap8gUssYMZ2+w+cmGI6OpZWYOEIcbE0TPzzPHi+8XTzAgx1w/ZxDFcXhZeXN5eKWsA==", - "requires": { + "dependencies": { "@vue/reactivity": "3.2.31", "@vue/shared": "3.2.31" } }, - "@vue/runtime-dom": { + "node_modules/@vue/runtime-dom": { "version": "3.2.31", "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.2.31.tgz", "integrity": "sha512-N+o0sICVLScUjfLG7u9u5XCjvmsexAiPt17GNnaWHJUfsKed5e85/A3SWgKxzlxx2SW/Hw7RQxzxbXez9PtY3g==", - "requires": { + "dependencies": { "@vue/runtime-core": "3.2.31", "@vue/shared": "3.2.31", "csstype": "^2.6.8" } }, - "@vue/server-renderer": { + "node_modules/@vue/server-renderer": { "version": "3.2.31", "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.2.31.tgz", "integrity": "sha512-8CN3Zj2HyR2LQQBHZ61HexF5NReqngLT3oahyiVRfSSvak+oAvVmu8iNLSu6XR77Ili2AOpnAt1y8ywjjqtmkg==", - "requires": { + "dependencies": { "@vue/compiler-ssr": "3.2.31", "@vue/shared": "3.2.31" + }, + "peerDependencies": { + "vue": "3.2.31" } }, - "@vue/shared": { + "node_modules/@vue/shared": { "version": "3.2.31", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.31.tgz", "integrity": "sha512-ymN2pj6zEjiKJZbrf98UM2pfDd6F2H7ksKw7NDt/ZZ1fh5Ei39X5tABugtT03ZRlWd9imccoK0hE8hpjpU7irQ==" }, - "@vueuse/core": { + "node_modules/@vueuse/core": { "version": "7.7.0", "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-7.7.0.tgz", "integrity": "sha512-DS8+dg758CiWnswebYHjS05PqTtc1ZLomsDlkFjG/KC0iFRgFIsGC66AAGuSXLqWCoirp2xN6N2mkrp1aHdI7A==", - "requires": { + "dependencies": { "@vueuse/shared": "7.7.0", "vue-demi": "*" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.1.0", + "vue": "^2.6.0 || ^3.2.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + }, + "vue": { + "optional": true + } } }, - "@vueuse/shared": { + "node_modules/@vueuse/shared": { "version": "7.7.0", "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-7.7.0.tgz", "integrity": "sha512-ANzMcUnjuUPJ9nWqMAqYt8p0qon6AH5pP5/V/0RSWkwCIWZwi57ujIaxizzMwnJECUF/73BmsRmpvvtokCIrKw==", - "requires": { + "dependencies": { "vue-demi": "*" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.1.0", + "vue": "^2.6.0 || ^3.2.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + }, + "vue": { + "optional": true + } } }, - "ansi-styles": { + "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "requires": { + "dependencies": { "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "ant-design-vue": { + "node_modules/ant-design-vue": { "version": "2.2.8", "resolved": "https://registry.npmjs.org/ant-design-vue/-/ant-design-vue-2.2.8.tgz", "integrity": "sha512-3graq9/gCfJQs6hznrHV6sa9oDmk/D1H3Oo0vLdVpPS/I61fZPk8NEyNKCHpNA6fT2cx6xx9U3QS63uuyikg/Q==", - "requires": { + "dependencies": { "@ant-design/icons-vue": "^6.0.0", "@babel/runtime": "^7.10.5", "@simonwep/pickr": "~1.8.0", @@ -242,181 +339,229 @@ "vue-types": "^3.0.0", "warning": "^4.0.0" }, - "dependencies": { - "async-validator": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-3.5.2.tgz", - "integrity": "sha512-8eLCg00W9pIRZSB781UUX/H6Oskmm8xloZfr09lz5bikRpBVDlJ3hRVuxxP1SxcwsEYfJ4IU8Q19Y8/893r3rQ==" - } + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ant-design-vue" + }, + "peerDependencies": { + "@vue/compiler-sfc": ">=3.1.0", + "vue": ">=3.1.0" } }, - "anymatch": { + "node_modules/ant-design-vue/node_modules/async-validator": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-3.5.2.tgz", + "integrity": "sha512-8eLCg00W9pIRZSB781UUX/H6Oskmm8xloZfr09lz5bikRpBVDlJ3hRVuxxP1SxcwsEYfJ4IU8Q19Y8/893r3rQ==" + }, + "node_modules/anymatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", "dev": true, - "requires": { + "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" } }, - "array-tree-filter": { + "node_modules/array-tree-filter": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-tree-filter/-/array-tree-filter-2.1.0.tgz", "integrity": "sha512-4ROwICNlNw/Hqa9v+rk5h22KjmzB1JGTMVKP2AKJBOCgb0yL0ASf0+YvCcLNNwquOHNX48jkeZIJ3a+oOQqKcw==" }, - "async-validator": { + "node_modules/async-validator": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.0.7.tgz", "integrity": "sha512-Pj2IR7u8hmUEDOwB++su6baaRi+QvsgajuFB9j95foM1N2gy5HM4z60hfusIO0fBPG5uLAEl6yCJr1jNSVugEQ==" }, - "at-least-node": { + "node_modules/at-least-node": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", - "dev": true + "dev": true, + "engines": { + "node": ">= 4.0.0" + } }, - "axios": { + "node_modules/axios": { "version": "0.26.0", "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.0.tgz", "integrity": "sha512-lKoGLMYtHvFrPVt3r+RBMp9nh34N0M8zEfCWqdWZx6phynIEhQqAdydpyBAAG211zlhX9Rgu08cOamy6XjE5Og==", - "requires": { + "dependencies": { "follow-redirects": "^1.14.8" } }, - "big.js": { + "node_modules/big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==" + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "engines": { + "node": "*" + } }, - "binary-extensions": { + "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true + "dev": true, + "engines": { + "node": ">=8" + } }, - "braces": { + "node_modules/braces": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", "dev": true, - "requires": { + "dependencies": { "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" } }, - "chalk": { + "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "requires": { + "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "chokidar": { + "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", "dev": true, - "requires": { + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", - "fsevents": "~2.3.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" } }, - "color-convert": { + "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "requires": { + "dependencies": { "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" } }, - "color-name": { + "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, - "compute-scroll-into-view": { + "node_modules/compute-scroll-into-view": { "version": "1.0.17", "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.17.tgz", "integrity": "sha512-j4dx+Fb0URmzbwwMUrhqWM2BEWHdFGx+qZ9qqASHRPqvTYdqvWnHg0H1hIbcyLnvgnoNAVMlwkepyqM3DaIFUg==" }, - "core-js": { + "node_modules/core-js": { "version": "3.21.1", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.21.1.tgz", - "integrity": "sha512-FRq5b/VMrWlrmCzwRrpDYNxyHP9BcAZC+xHJaqTgIE5091ZV1NTmyh0sGOg5XqpnHvR0svdy0sv1gWA1zmhxig==" + "integrity": "sha512-FRq5b/VMrWlrmCzwRrpDYNxyHP9BcAZC+xHJaqTgIE5091ZV1NTmyh0sGOg5XqpnHvR0svdy0sv1gWA1zmhxig==", + "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.", + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } }, - "csstype": { + "node_modules/csstype": { "version": "2.6.20", "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.20.tgz", "integrity": "sha512-/WwNkdXfckNgw6S5R125rrW8ez139lBHWouiBvX8dfMFtcn6V81REDqnH7+CRpRipfYlyU1CmOnOxrmGcFOjeA==" }, - "dayjs": { + "node_modules/dayjs": { "version": "1.10.8", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.8.tgz", "integrity": "sha512-wbNwDfBHHur9UOzNUjeKUOJ0fCb0a52Wx0xInmQ7Y8FstyajiV1NmK1e00cxsr9YrE9r7yAChE0VvpuY5Rnlow==" }, - "debug": { + "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, - "requires": { - "ms": "2.0.0" - }, "dependencies": { - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } + "ms": "2.0.0" } }, - "default-passive-events": { + "node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "node_modules/default-passive-events": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/default-passive-events/-/default-passive-events-2.0.0.tgz", "integrity": "sha512-eMtt76GpDVngZQ3ocgvRcNCklUMwID1PaNbCNxfpDXuiOXttSh0HzBbda1HU9SIUsDc02vb7g9+3I5tlqe/qMQ==" }, - "depd": { + "node_modules/depd": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", - "dev": true + "dev": true, + "engines": { + "node": ">= 0.6" + } }, - "destroy": { + "node_modules/destroy": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", "dev": true }, - "dom-align": { + "node_modules/dom-align": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/dom-align/-/dom-align-1.12.2.tgz", "integrity": "sha512-pHuazgqrsTFrGU2WLDdXxCFabkdQDx72ddkraZNih1KsMcN5qsRSTR9O4VJRlwTPCPb5COYg3LOfiMHHcPInHg==" }, - "dom-scroll-into-view": { + "node_modules/dom-scroll-into-view": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/dom-scroll-into-view/-/dom-scroll-into-view-2.0.1.tgz", "integrity": "sha512-bvVTQe1lfaUr1oFzZX80ce9KLDlZ3iU+XGNE/bz9HnGdklTieqsbmsLHe+rT2XWqopvL0PckkYqN7ksmm5pe3w==" }, - "ee-first": { + "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", "dev": true }, - "element-plus": { + "node_modules/element-plus": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.0.4.tgz", "integrity": "sha512-g/YT/uY6zbNTAl9jL4zT8WRSf/4KG6GB3Y89MCO/lbglHsps5sdDFohcuOb8699BtIXkXsbOzdwmbk9kDHqHxA==", - "requires": { + "dependencies": { "@ctrl/tinycolor": "^3.4.0", "@element-plus/icons-vue": "^1.0.0", "@popperjs/core": "^2.11.2", @@ -428,25 +573,40 @@ "lodash-unified": "^1.0.2", "memoize-one": "^6.0.0", "normalize-wheel-es": "^1.1.1" + }, + "peerDependencies": { + "vue": "^3.2.0" } }, - "emojis-list": { + "node_modules/emojis-list": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==" + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "engines": { + "node": ">= 4" + } }, - "encodeurl": { + "node_modules/encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", - "dev": true + "dev": true, + "engines": { + "node": ">= 0.8" + } }, - "esbuild": { + "node_modules/esbuild": { "version": "0.14.25", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.25.tgz", "integrity": "sha512-4JHEIOMNFvK09ziiL+iVmldIhLbn49V4NAVo888tcGFKedEZY/Y8YapfStJ6zSE23tzYPKxqKwQBnQoIO0BI/Q==", - "dev": true, - "requires": { + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { "esbuild-android-64": "0.14.25", "esbuild-android-arm64": "0.14.25", "esbuild-darwin-64": "0.14.25", @@ -469,572 +629,892 @@ "esbuild-windows-arm64": "0.14.25" } }, - "esbuild-android-64": { + "node_modules/esbuild-android-64": { "version": "0.14.25", "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.25.tgz", "integrity": "sha512-L5vCUk7TzFbBnoESNoXjU3x9+/+7TDIE/1mTfy/erAfvZAqC+S3sp/Qa9wkypFMcFvN9FzvESkTlpeQDolREtQ==", + "cpu": [ + "x64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } }, - "esbuild-android-arm64": { + "node_modules/esbuild-android-arm64": { "version": "0.14.25", "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.25.tgz", "integrity": "sha512-4jv5xPjM/qNm27T5j3ZEck0PvjgQtoMHnz4FzwF5zNP56PvY2CT0WStcAIl6jNlsuDdN63rk2HRBIsO6xFbcFw==", + "cpu": [ + "arm64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } }, - "esbuild-darwin-64": { + "node_modules/esbuild-darwin-64": { "version": "0.14.25", "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.25.tgz", "integrity": "sha512-TGp8tuudIxOyWd1+8aYPxQmC1ZQyvij/AfNBa35RubixD0zJ1vkKHVAzo0Zao1zcG6pNqiSyzfPto8vmg0s7oA==", + "cpu": [ + "x64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } }, - "esbuild-darwin-arm64": { + "node_modules/esbuild-darwin-arm64": { "version": "0.14.25", "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.25.tgz", "integrity": "sha512-oTcDgdm0MDVEmw2DWu8BV68pYuImpFgvWREPErBZmNA4MYKGuBRaCiJqq6jZmBR1x+3y1DWCjez+5uLtuAm6mw==", + "cpu": [ + "arm64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } }, - "esbuild-freebsd-64": { + "node_modules/esbuild-freebsd-64": { "version": "0.14.25", "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.25.tgz", "integrity": "sha512-ueAqbnMZ8arnuLH8tHwTCQYeptnHOUV7vA6px6j4zjjQwDx7TdP7kACPf3TLZLdJQ3CAD1XCvQ2sPhX+8tacvQ==", + "cpu": [ + "x64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } }, - "esbuild-freebsd-arm64": { + "node_modules/esbuild-freebsd-arm64": { "version": "0.14.25", "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.25.tgz", "integrity": "sha512-+ZVWud2HKh+Ob6k/qiJWjBtUg4KmJGGmbvEXXW1SNKS7hW7HU+Zq2ZCcE1akFxOPkVB+EhOty/sSek30tkCYug==", + "cpu": [ + "arm64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } }, - "esbuild-linux-32": { + "node_modules/esbuild-linux-32": { "version": "0.14.25", "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.25.tgz", "integrity": "sha512-3OP/lwV3kCzEz45tobH9nj+uE4ubhGsfx+tn0L26WAGtUbmmcRpqy7XRG/qK7h1mClZ+eguIANcQntYMdYklfw==", + "cpu": [ + "ia32" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } }, - "esbuild-linux-64": { + "node_modules/esbuild-linux-64": { "version": "0.14.25", "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.25.tgz", "integrity": "sha512-+aKHdHZmX9qwVlQmu5xYXh7GsBFf4TWrePgeJTalhXHOG7NNuUwoHmketGiZEoNsWyyqwH9rE5BC+iwcLY30Ug==", + "cpu": [ + "x64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } }, - "esbuild-linux-arm": { + "node_modules/esbuild-linux-arm": { "version": "0.14.25", "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.25.tgz", "integrity": "sha512-aTLcE2VBoLydL943REcAcgnDi3bHtmULSXWLbjtBdtykRatJVSxKMjK9YlBXUZC4/YcNQfH7AxwVeQr9fNxPhw==", + "cpu": [ + "arm" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } }, - "esbuild-linux-arm64": { + "node_modules/esbuild-linux-arm64": { "version": "0.14.25", "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.25.tgz", "integrity": "sha512-UxfenPx/wSZx55gScCImPtXekvZQLI2GW3qe5dtlmU7luiqhp5GWPzGeQEbD3yN3xg/pHc671m5bma5Ns7lBHw==", + "cpu": [ + "arm64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } }, - "esbuild-linux-mips64le": { + "node_modules/esbuild-linux-mips64le": { "version": "0.14.25", "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.25.tgz", "integrity": "sha512-wLWYyqVfYx9Ur6eU5RT92yJVsaBGi5RdkoWqRHOqcJ38Kn60QMlcghsKeWfe9jcYut8LangYZ98xO1LxIoSXrQ==", + "cpu": [ + "mips64el" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } }, - "esbuild-linux-ppc64le": { + "node_modules/esbuild-linux-ppc64le": { "version": "0.14.25", "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.25.tgz", "integrity": "sha512-0dR6Csl6Zas3g4p9ULckEl8Mo8IInJh33VCJ3eaV1hj9+MHGdmDOakYMN8MZP9/5nl+NU/0ygpd14cWgy8uqRw==", + "cpu": [ + "ppc64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } }, - "esbuild-linux-riscv64": { + "node_modules/esbuild-linux-riscv64": { "version": "0.14.25", "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.25.tgz", "integrity": "sha512-J4d20HDmTrgvhR0bdkDhvvJGaikH3LzXQnNaseo8rcw9Yqby9A90gKUmWpfwqLVNRILvNnAmKLfBjCKU9ajg8w==", + "cpu": [ + "riscv64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } }, - "esbuild-linux-s390x": { + "node_modules/esbuild-linux-s390x": { "version": "0.14.25", "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.25.tgz", "integrity": "sha512-YI2d5V6nTE73ZnhEKQD7MtsPs1EtUZJ3obS21oxQxGbbRw1G+PtJKjNyur+3t6nzHP9oTg6GHQ3S3hOLLmbDIQ==", + "cpu": [ + "s390x" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } }, - "esbuild-netbsd-64": { + "node_modules/esbuild-netbsd-64": { "version": "0.14.25", "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.25.tgz", "integrity": "sha512-TKIVgNWLUOkr+Exrye70XTEE1lJjdQXdM4tAXRzfHE9iBA7LXWcNtVIuSnphTqpanPzTDFarF0yqq4kpbC6miA==", + "cpu": [ + "x64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } }, - "esbuild-openbsd-64": { + "node_modules/esbuild-openbsd-64": { "version": "0.14.25", "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.25.tgz", "integrity": "sha512-QgFJ37A15D7NIXBTYEqz29+uw3nNBOIyog+3kFidANn6kjw0GHZ0lEYQn+cwjyzu94WobR+fes7cTl/ZYlHb1A==", + "cpu": [ + "x64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } }, - "esbuild-sunos-64": { + "node_modules/esbuild-sunos-64": { "version": "0.14.25", "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.25.tgz", "integrity": "sha512-rmWfjUItYIVlqr5EnTH1+GCxXiBOC42WBZ3w++qh7n2cS9Xo0lO5pGSG2N+huOU2fX5L+6YUuJ78/vOYvefeFw==", + "cpu": [ + "x64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } }, - "esbuild-windows-32": { + "node_modules/esbuild-windows-32": { "version": "0.14.25", "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.25.tgz", "integrity": "sha512-HGAxVUofl3iUIz9W10Y9XKtD0bNsK9fBXv1D55N/ljNvkrAYcGB8YCm0v7DjlwtyS6ws3dkdQyXadbxkbzaKOA==", + "cpu": [ + "ia32" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } }, - "esbuild-windows-64": { + "node_modules/esbuild-windows-64": { "version": "0.14.25", "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.25.tgz", "integrity": "sha512-TirEohRkfWU9hXLgoDxzhMQD1g8I2mOqvdQF2RS9E/wbkORTAqJHyh7wqGRCQAwNzdNXdg3JAyhQ9/177AadWA==", + "cpu": [ + "x64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } }, - "esbuild-windows-arm64": { + "node_modules/esbuild-windows-arm64": { "version": "0.14.25", "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.25.tgz", "integrity": "sha512-4ype9ERiI45rSh+R8qUoBtaj6kJvUOI7oVLhKqPEpcF4Pa5PpT3hm/mXAyotJHREkHpM87PAJcA442mLnbtlNA==", + "cpu": [ + "arm64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } }, - "escape-html": { + "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", "dev": true }, - "estree-walker": { + "node_modules/estree-walker": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" }, - "etag": { + "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", - "dev": true + "dev": true, + "engines": { + "node": ">= 0.6" + } }, - "fill-range": { + "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", "dev": true, - "requires": { + "dependencies": { "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" } }, - "follow-redirects": { + "node_modules/follow-redirects": { "version": "1.14.9", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz", - "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==" + "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } }, - "fresh": { + "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", - "dev": true + "dev": true, + "engines": { + "node": ">= 0.6" + } }, - "fs-extra": { + "node_modules/fs-extra": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", "dev": true, - "requires": { + "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" } }, - "fsevents": { + "node_modules/fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "dev": true, - "optional": true + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } }, - "function-bind": { + "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, - "glob-parent": { + "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, - "requires": { + "dependencies": { "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" } }, - "graceful-fs": { + "node_modules/graceful-fs": { "version": "4.2.9", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==", "dev": true }, - "has": { + "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "requires": { + "dependencies": { "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" } }, - "has-flag": { + "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } }, - "hash-sum": { + "node_modules/hash-sum": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-2.0.0.tgz", "integrity": "sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==" }, - "http-errors": { + "node_modules/http-errors": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", "dev": true, - "requires": { + "dependencies": { "depd": "~1.1.2", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": ">= 1.5.0 < 2", "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.6" } }, - "immutable": { + "node_modules/immutable": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.0.0.tgz", "integrity": "sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw==", "dev": true }, - "inherits": { + "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, - "is-binary-path": { + "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dev": true, - "requires": { + "dependencies": { "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" } }, - "is-core-module": { + "node_modules/is-core-module": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz", "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==", - "dev": true, - "requires": { + "dependencies": { "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "is-extglob": { + "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, - "is-glob": { + "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, - "requires": { + "dependencies": { "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" } }, - "is-number": { + "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true + "dev": true, + "engines": { + "node": ">=0.12.0" + } }, - "is-plain-object": { + "node_modules/is-plain-object": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.1.tgz", - "integrity": "sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==" + "integrity": "sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==", + "engines": { + "node": ">=0.10.0" + } }, - "is-reference": { + "node_modules/is-reference": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", "dev": true, - "requires": { + "dependencies": { "@types/estree": "*" } }, - "js-md5": { + "node_modules/js-md5": { "version": "0.7.3", "resolved": "https://registry.npmjs.org/js-md5/-/js-md5-0.7.3.tgz", "integrity": "sha512-ZC41vPSTLKGwIRjqDh8DfXoCrdQIyBgspJVPXHBGu4nZlAEvG3nf+jO9avM9RmLiGakg7vz974ms99nEV0tmTQ==" }, - "js-tokens": { + "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, - "json5": { + "node_modules/json5": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", - "requires": { + "dependencies": { "minimist": "^1.2.5" + }, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" } }, - "jsonfile": { + "node_modules/jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dev": true, - "requires": { - "graceful-fs": "^4.1.6", + "dependencies": { "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" } }, - "loader-utils": { + "node_modules/loader-utils": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz", "integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==", - "requires": { + "dependencies": { "big.js": "^5.2.2", "emojis-list": "^3.0.0", "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" } }, - "lodash": { + "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, - "lodash-es": { + "node_modules/lodash-es": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" }, - "lodash-unified": { + "node_modules/lodash-unified": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/lodash-unified/-/lodash-unified-1.0.2.tgz", - "integrity": "sha512-OGbEy+1P+UT26CYi4opY4gebD8cWRDxAT6MAObIVQMiqYdxZr1g3QHWCToVsm31x2NkLS4K3+MC2qInaRMa39g==" + "integrity": "sha512-OGbEy+1P+UT26CYi4opY4gebD8cWRDxAT6MAObIVQMiqYdxZr1g3QHWCToVsm31x2NkLS4K3+MC2qInaRMa39g==", + "peerDependencies": { + "@types/lodash-es": "*", + "lodash": "*", + "lodash-es": "*" + } }, - "loose-envify": { + "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "requires": { + "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" } }, - "magic-string": { + "node_modules/magic-string": { "version": "0.25.9", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", - "requires": { + "dependencies": { "sourcemap-codec": "^1.4.8" } }, - "memoize-one": { + "node_modules/memoize-one": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==" }, - "mime": { + "node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } }, - "minimist": { + "node_modules/minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, - "moment": { + "node_modules/moment": { "version": "2.29.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", - "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==" + "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==", + "engines": { + "node": "*" + } }, - "ms": { + "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, - "nanoid": { + "node_modules/nanoid": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz", - "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==" + "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } }, - "nanopop": { + "node_modules/nanopop": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/nanopop/-/nanopop-2.1.0.tgz", "integrity": "sha512-jGTwpFRexSH+fxappnGQtN9dspgE2ipa1aOjtR24igG0pv6JCxImIAmrLRHX+zUF5+1wtsFVbKyfP51kIGAVNw==" }, - "normalize-path": { + "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, - "normalize-wheel-es": { + "node_modules/normalize-wheel-es": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.1.1.tgz", "integrity": "sha512-157VNH4CngrcsvF8xOVOe22cwniIR3nxSltdctvQeHZj8JttEeOXffK28jucWfWBXs0QNetAumjc1GiInnwX4w==" }, - "nprogress": { + "node_modules/nprogress": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz", "integrity": "sha1-y480xTIT2JVyP8urkH6UIq28r7E=", "dev": true }, - "omit.js": { + "node_modules/omit.js": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/omit.js/-/omit.js-2.0.2.tgz", "integrity": "sha512-hJmu9D+bNB40YpL9jYebQl4lsTW6yEHRTroJzNLqQJYHm7c+NQnJGfZmIWh8S3q3KoaxV1aLhV6B3+0N0/kyJg==" }, - "on-finished": { + "node_modules/on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", "dev": true, - "requires": { + "dependencies": { "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" } }, - "parseurl": { + "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true + "dev": true, + "engines": { + "node": ">= 0.8" + } }, - "path-parse": { + "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, - "picocolors": { + "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" }, - "picomatch": { + "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } }, - "postcss": { + "node_modules/postcss": { "version": "8.4.7", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.7.tgz", "integrity": "sha512-L9Ye3r6hkkCeOETQX6iOaWZgjp3LL6Lpqm6EtgbKrgqGGteRMNb9vzBfRL96YOSu8o7x3MfIH9Mo5cPJFGrW6A==", - "requires": { + "dependencies": { "nanoid": "^3.3.1", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" } }, - "range-parser": { + "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "dev": true + "dev": true, + "engines": { + "node": ">= 0.6" + } }, - "readdirp": { + "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, - "requires": { + "dependencies": { "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" } }, - "regenerator-runtime": { + "node_modules/regenerator-runtime": { "version": "0.13.9", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" }, - "resize-observer-polyfill": { + "node_modules/resize-observer-polyfill": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==" }, - "resolve": { + "node_modules/resolve": { "version": "1.22.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", - "dev": true, - "requires": { + "dependencies": { "is-core-module": "^2.8.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "rollup": { + "node_modules/rollup": { "version": "2.69.2", "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.69.2.tgz", "integrity": "sha512-KghktpWg3Wd+nYCsx3Griidm2/CKIJYG2yyaaKspo0TXSoGdW+0duwzKl4wWIu62oN3mFg3zCDbwVRPwuNPPlA==", - "dev": true, - "requires": { + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { "fsevents": "~2.3.2" } }, - "rollup-plugin-external-globals": { + "node_modules/rollup-plugin-external-globals": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/rollup-plugin-external-globals/-/rollup-plugin-external-globals-0.6.1.tgz", "integrity": "sha512-mlp3KNa5sE4Sp9UUR2rjBrxjG79OyZAh/QC18RHIjM+iYkbBwNXSo8DHRMZWtzJTrH8GxQ+SJvCTN3i14uMXIA==", "dev": true, - "requires": { + "dependencies": { "@rollup/pluginutils": "^4.0.0", "estree-walker": "^2.0.1", "is-reference": "^1.2.1", "magic-string": "^0.25.7" + }, + "peerDependencies": { + "rollup": "^2.25.0" } }, - "sass": { + "node_modules/sass": { "version": "1.49.9", "resolved": "https://registry.npmjs.org/sass/-/sass-1.49.9.tgz", "integrity": "sha512-YlYWkkHP9fbwaFRZQRXgDi3mXZShslVmmo+FVK3kHLUELHHEYrCmL1x6IUjC7wLS6VuJSAFXRQS/DxdsC4xL1A==", "dev": true, - "requires": { + "dependencies": { "chokidar": ">=3.0.0 <4.0.0", "immutable": "^4.0.0", "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=12.0.0" } }, - "scroll-into-view-if-needed": { + "node_modules/scroll-into-view-if-needed": { "version": "2.2.29", "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.29.tgz", "integrity": "sha512-hxpAR6AN+Gh53AdAimHM6C8oTN1ppwVZITihix+WqalywBeFcQ6LdQP5ABNl26nX8GTEL7VT+b8lKpdqq65wXg==", - "requires": { + "dependencies": { "compute-scroll-into-view": "^1.0.17" } }, - "send": { + "node_modules/send": { "version": "0.17.2", "resolved": "https://registry.npmjs.org/send/-/send-0.17.2.tgz", "integrity": "sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww==", "dev": true, - "requires": { + "dependencies": { "debug": "2.6.9", "depd": "~1.1.2", "destroy": "~1.0.4", @@ -1048,121 +1528,180 @@ "on-finished": "~2.3.0", "range-parser": "~1.2.1", "statuses": "~1.5.0" + }, + "engines": { + "node": ">= 0.8.0" } }, - "serve-static": { + "node_modules/serve-static": { "version": "1.14.2", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.2.tgz", "integrity": "sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ==", "dev": true, - "requires": { + "dependencies": { "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "0.17.2" + }, + "engines": { + "node": ">= 0.8.0" } }, - "setprototypeof": { + "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "dev": true }, - "shallow-equal": { + "node_modules/shallow-equal": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.2.1.tgz", "integrity": "sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==" }, - "sortablejs": { + "node_modules/sortablejs": { "version": "1.14.0", "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.14.0.tgz", "integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==" }, - "source-map": { + "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } }, - "source-map-js": { + "node_modules/source-map-js": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==" + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "engines": { + "node": ">=0.10.0" + } }, - "sourcemap-codec": { + "node_modules/sourcemap-codec": { "version": "1.4.8", "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", - "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==" + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "deprecated": "Please use @jridgewell/sourcemap-codec instead" }, - "statuses": { + "node_modules/statuses": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", - "dev": true + "dev": true, + "engines": { + "node": ">= 0.6" + } }, - "supports-color": { + "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "requires": { + "dependencies": { "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "supports-preserve-symlinks-flag": { + "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "to-regex-range": { + "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, - "requires": { + "dependencies": { "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" } }, - "toidentifier": { + "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "dev": true + "dev": true, + "engines": { + "node": ">=0.6" + } }, - "universalify": { + "node_modules/universalify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", - "dev": true + "dev": true, + "engines": { + "node": ">= 10.0.0" + } }, - "vite": { + "node_modules/vite": { "version": "2.8.6", "resolved": "https://registry.npmjs.org/vite/-/vite-2.8.6.tgz", "integrity": "sha512-e4H0QpludOVKkmOsRyqQ7LTcMUDF3mcgyNU4lmi0B5JUbe0ZxeBBl8VoZ8Y6Rfn9eFKYtdXNPcYK97ZwH+K2ug==", - "dev": true, - "requires": { + "dependencies": { "esbuild": "^0.14.14", - "fsevents": "~2.3.2", "postcss": "^8.4.6", "resolve": "^1.22.0", "rollup": "^2.59.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": ">=12.2.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "less": "*", + "sass": "*", + "stylus": "*" + }, + "peerDependenciesMeta": { + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + } } }, - "vite-plugin-cesium": { + "node_modules/vite-plugin-cesium": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/vite-plugin-cesium/-/vite-plugin-cesium-1.2.13.tgz", "integrity": "sha512-p7M+df+hVxPt7C8WQFg73SrHsJFK6pdb6jSvPiRiWnHzzNU+zLL0moztkBpfb2im6HnES7q3nEWHyjruZ/3o7g==", "dev": true, - "requires": { + "dependencies": { "fs-extra": "^9.1.0", "rollup-plugin-external-globals": "^0.6.1", "serve-static": "^1.14.1" + }, + "peerDependencies": { + "cesium": "^1.88.0" } }, - "vue": { + "node_modules/vue": { "version": "3.2.31", "resolved": "https://registry.npmjs.org/vue/-/vue-3.2.31.tgz", "integrity": "sha512-odT3W2tcffTiQCy57nOT93INw1auq5lYLLYtWpPYQQYQOOdHiqFct9Xhna6GJ+pJQaF67yZABraH47oywkJgFw==", - "requires": { + "dependencies": { "@vue/compiler-dom": "3.2.31", "@vue/compiler-sfc": "3.2.31", "@vue/runtime-dom": "3.2.31", @@ -1170,58 +1709,99 @@ "@vue/shared": "3.2.31" } }, - "vue-demi": { + "node_modules/vue-demi": { "version": "0.12.1", "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.12.1.tgz", - "integrity": "sha512-QL3ny+wX8c6Xm1/EZylbgzdoDolye+VpCXRhI2hug9dJTP3OUJ3lmiKN3CsVV3mOJKwFi0nsstbgob0vG7aoIw==" + "integrity": "sha512-QL3ny+wX8c6Xm1/EZylbgzdoDolye+VpCXRhI2hug9dJTP3OUJ3lmiKN3CsVV3mOJKwFi0nsstbgob0vG7aoIw==", + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } }, - "vue-loader": { + "node_modules/vue-loader": { "version": "17.0.0", "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-17.0.0.tgz", "integrity": "sha512-OWSXjrzIvbF2LtOUmxT3HYgwwubbfFelN8PAP9R9dwpIkj48TVioHhWWSx7W7fk+iF5cgg3CBJRxwTdtLU4Ecg==", - "requires": { + "dependencies": { "chalk": "^4.1.0", "hash-sum": "^2.0.0", "loader-utils": "^2.0.0" + }, + "peerDependencies": { + "webpack": "^4.1.0 || ^5.0.0-0" } }, - "vue-router": { + "node_modules/vue-router": { "version": "4.0.13", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.0.13.tgz", "integrity": "sha512-LmXrC+BkDRLak+d5xTMgUYraT3Nj0H/vCbP+7usGvIl9Viqd1UP6AsP0i69pSbn9O0dXK/xCdp4yPw21HqV9Jw==", - "requires": { + "dependencies": { "@vue/devtools-api": "^6.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.2.0" } }, - "vue-types": { + "node_modules/vue-types": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/vue-types/-/vue-types-3.0.2.tgz", "integrity": "sha512-IwUC0Aq2zwaXqy74h4WCvFCUtoV0iSWr0snWnE9TnU18S66GAQyqQbRf2qfJtUuiFsBf6qp0MEwdonlwznlcrw==", - "requires": { + "dependencies": { "is-plain-object": "3.0.1" + }, + "engines": { + "node": ">=10.15.0" + }, + "peerDependencies": { + "vue": "^3.0.0" } }, - "vuedraggable": { + "node_modules/vuedraggable": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-4.1.0.tgz", "integrity": "sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==", - "requires": { + "dependencies": { "sortablejs": "1.14.0" + }, + "peerDependencies": { + "vue": "^3.0.1" } }, - "vuex": { + "node_modules/vuex": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/vuex/-/vuex-4.0.2.tgz", "integrity": "sha512-M6r8uxELjZIK8kTKDGgZTYX/ahzblnzC4isU1tpmEuOIIKmV+TRdc+H4s8ds2NuZ7wpUTdGRzJRtoj+lI+pc0Q==", - "requires": { + "dependencies": { "@vue/devtools-api": "^6.0.0-beta.11" + }, + "peerDependencies": { + "vue": "^3.0.2" } }, - "warning": { + "node_modules/warning": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", - "requires": { + "dependencies": { "loose-envify": "^1.0.0" } } diff --git a/frontend/witcleansystem/package.json b/frontend/witcleansystem/package.json index 3e5cb7b..6c1537a 100644 --- a/frontend/witcleansystem/package.json +++ b/frontend/witcleansystem/package.json @@ -2,8 +2,9 @@ "name": "witCleanSystem", "version": "1.0.6", "scripts": { - "dev": "vite --mode develop --open", + "dev": "vite --mode develop --open -- --host", "build-test": "vite build --mode test", + "build-docker": "vite build --mode docker", "build": "vite build --mode production" }, "dependencies": { diff --git a/frontend/witcleansystem/src/Page/BasicInfo/ObjectManage/ObjectDataManage.vue b/frontend/witcleansystem/src/Page/BasicInfo/ObjectManage/ObjectDataManage.vue index b372ee7..95ef2e6 100644 --- a/frontend/witcleansystem/src/Page/BasicInfo/ObjectManage/ObjectDataManage.vue +++ b/frontend/witcleansystem/src/Page/BasicInfo/ObjectManage/ObjectDataManage.vue @@ -1,5 +1,5 @@