diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 7c765927..aa21a34d 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -1,30 +1,30 @@ -# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time -# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven - -name: Java CI with Maven - -on: - push: - branches: [ master ] - # pull_request: - # branches: [ master ] - -jobs: - build: - - runs-on: ubuntu-latest - - strategy: - matrix: - java: [ '8', '11', '17' ] - - steps: - - uses: actions/checkout@v2 - - name: Set up JDK ${{ matrix.Java }} - uses: actions/setup-java@v2 - with: - java-version: ${{ matrix.java }} - distribution: 'temurin' - cache: maven - - name: Build with Maven - run: mvn -B package --file pom.xml -Dmaven.test.skip=true +# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time +# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven + +name: Java CI with Maven + +on: + push: + branches: [ master, release/next ] + # pull_request: + # branches: [ master ] + +jobs: + build: + + runs-on: ubuntu-latest + + strategy: + matrix: + java: [ '8', '11', '17' ] + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK ${{ matrix.Java }} + uses: actions/setup-java@v2 + with: + java-version: ${{ matrix.java }} + distribution: 'temurin' + cache: maven + - name: Build with Maven + run: mvn -B package --file pom.xml -Dmaven.test.skip=true diff --git a/Jenkinsfile b/Jenkinsfile index 82b563e6..768d0746 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -27,11 +27,15 @@ pipeline { // 服务配置 CORE_SERVICES = 'viewsh-gateway,viewsh-module-system-server,viewsh-module-infra-server,viewsh-module-iot-server,viewsh-module-iot-gateway,viewsh-module-ops-server' - // 部署配置(Prod 服务器内网地址) + // 部署配置(默认 Prod,release/next 分支会在 Initialize 阶段覆盖为 Staging) DEPLOY_HOST = '172.17.16.14' DEPLOY_PATH = '/opt/aiot-platform-cloud' SSH_KEY = '/var/jenkins_home/.ssh/id_rsa' + // Staging 服务器配置 + STAGING_DEPLOY_HOST = '172.17.16.7' + STAGING_DEPLOY_PATH = '/opt/aiot-platform-cloud' + // 性能配置 - 将动态调整 BUILD_TIMEOUT = 45 DEPLOY_TIMEOUT = 10 @@ -61,25 +65,17 @@ pipeline { echo "Build: #${env.BUILD_NUMBER}" echo "Workspace: ${env.WORKSPACE}" echo "Start Time: ${new Date()}" - if (env.BRANCH_NAME == 'master') { - env.DEPLOY_ENV = 'prod' - env.DEPLOY_HOST = '172.17.16.14' - env.DEPLOY_PATH = '/opt/aiot-platform-cloud' - env.DEPLOY_COMPOSE_ARGS = '-f docker-compose.core.yml' - } else if (env.BRANCH_NAME == 'stage/multi-tenant-isolation') { - env.DEPLOY_ENV = 'stage' - env.DEPLOY_HOST = '172.17.16.7' - env.DEPLOY_PATH = '/opt/aiot-platform-cloud-stage' - env.DEPLOY_COMPOSE_ARGS = '-f docker-compose.core.yml -f docker-compose.stage.override.yml' - } else { - env.DEPLOY_ENV = 'build-only' - env.DEPLOY_COMPOSE_ARGS = '-f docker-compose.core.yml' - } - echo "Deploy Env: ${env.DEPLOY_ENV}" - echo "Deploy Host: ${env.DEPLOY_HOST ?: 'N/A'}" - echo "Deploy Path: ${env.DEPLOY_PATH ?: 'N/A'}" echo "==========================================" + // 根据分支选择部署目标 + if (env.BRANCH_NAME == 'release/next') { + env.DEPLOY_HOST = env.STAGING_DEPLOY_HOST + env.DEPLOY_PATH = env.STAGING_DEPLOY_PATH + echo "📦 Deploy target: STAGING (${env.DEPLOY_HOST})" + } else { + echo "📦 Deploy target: PRODUCTION (${env.DEPLOY_HOST})" + } + // 【优化2】动态检测系统资源 detectSystemResources() } @@ -278,7 +274,10 @@ pipeline { when { allOf { expression { env.SERVICES_TO_BUILD != '' } - expression { env.BRANCH_NAME == 'master' || env.BRANCH_NAME == 'stage/multi-tenant-isolation' } + anyOf { + branch 'master' + branch 'release/next' + } } } steps { @@ -298,12 +297,9 @@ pipeline { backupCurrentDeployment(sortedServices) } - // 【新增】同步最新的 compose 文件到部署服务器 - echo "📂 Syncing compose files to deploy host..." + // 【新增】同步最新的 docker-compose.core.yml 到部署服务器 + echo "📂 Syncing docker-compose.core.yml to deploy host..." sh "scp -o StrictHostKeyChecking=no -i ${env.SSH_KEY} docker-compose.core.yml root@${env.DEPLOY_HOST}:${env.DEPLOY_PATH}/" - if (env.DEPLOY_ENV == 'stage') { - sh "scp -o StrictHostKeyChecking=no -i ${env.SSH_KEY} docker-compose.stage.override.yml root@${env.DEPLOY_HOST}:${env.DEPLOY_PATH}/" - } try { // 串行部署(保证依赖关系) @@ -332,7 +328,10 @@ pipeline { when { allOf { expression { env.SERVICES_TO_BUILD != '' } - expression { env.BRANCH_NAME == 'master' || env.BRANCH_NAME == 'stage/multi-tenant-isolation' } + anyOf { + branch 'master' + branch 'release/next' + } } } steps { @@ -672,7 +671,7 @@ def getPreviousImageTag() { script: """ ssh ${sshOpts} root@${env.DEPLOY_HOST} ' cd ${env.DEPLOY_PATH} - docker compose ${env.DEPLOY_COMPOSE_ARGS} images --format json | \ + docker compose -f docker-compose.core.yml images --format json | \ jq -r ".[0].Tag" | head -1 ' 2>/dev/null || echo "latest" """, @@ -696,16 +695,12 @@ def backupCurrentDeployment(def services) { sh """ ssh ${sshOpts} root@${env.DEPLOY_HOST} ' cd ${env.DEPLOY_PATH} - export IMAGE_TAG=${env.IMAGE_TAG} # 保存当前 docker-compose 配置 cp docker-compose.core.yml docker-compose.core.yml.backup-${env.BUILD_NUMBER} - if [ -f docker-compose.stage.override.yml ]; then - cp docker-compose.stage.override.yml docker-compose.stage.override.yml.backup-${env.BUILD_NUMBER} - fi # 记录当前运行的镜像 - docker compose ${env.DEPLOY_COMPOSE_ARGS} images > deployment-state-${env.BUILD_NUMBER}.txt + docker compose -f docker-compose.core.yml images > deployment-state-${env.BUILD_NUMBER}.txt echo "✅ Backup completed: deployment-state-${env.BUILD_NUMBER}.txt" ' @@ -742,10 +737,10 @@ def rollbackDeployment(def services) { export IMAGE_TAG=${env.PREVIOUS_IMAGE_TAG} # 拉取旧版本镜像 - docker compose ${env.DEPLOY_COMPOSE_ARGS} pull ${service} + docker compose -f docker-compose.core.yml pull ${service} # 重启服务 - docker compose ${env.DEPLOY_COMPOSE_ARGS} up -d ${service} + docker compose -f docker-compose.core.yml up -d ${service} echo "✅ ${service} rolled back to ${env.PREVIOUS_IMAGE_TAG}" ' @@ -816,8 +811,10 @@ def buildService(String service) { -t ${env.REGISTRY}/${service}:${env.IMAGE_TAG} \\ . - # 推送镜像 + # 推送带版本号的镜像 docker push ${env.REGISTRY}/${service}:${env.IMAGE_TAG} + + # 仅 master 分支推送 latest 标签 if [ "${env.BRANCH_NAME}" = "master" ]; then docker tag ${env.REGISTRY}/${service}:${env.IMAGE_TAG} ${env.REGISTRY}/${service}:latest docker push ${env.REGISTRY}/${service}:latest @@ -831,7 +828,7 @@ def buildService(String service) { // 获取镜像大小 def imageSize = sh( - script: "docker images ${env.REGISTRY}/${service}:${env.IMAGE_TAG} --format '{{.Size}}'", + script: "docker images ${env.REGISTRY}/${service}:latest --format '{{.Size}}'", returnStdout: true ).trim() @@ -877,12 +874,11 @@ def deployService(String service) { set -e cd ${env.DEPLOY_PATH} - export IMAGE_TAG=${env.IMAGE_TAG} echo "📥 Pulling ${service}..." - docker compose ${env.DEPLOY_COMPOSE_ARGS} pull ${service} + docker compose -f docker-compose.core.yml pull ${service} echo "🔄 Restarting ${service}..." - docker compose ${env.DEPLOY_COMPOSE_ARGS} up -d ${service} + docker compose -f docker-compose.core.yml up -d ${service} echo "⏳ Waiting for container to start..." sleep 5 diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/badge/CleanBadgeServiceImpl.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/badge/CleanBadgeServiceImpl.java index d75acd46..3c6e631b 100644 --- a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/badge/CleanBadgeServiceImpl.java +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/badge/CleanBadgeServiceImpl.java @@ -275,6 +275,7 @@ public class CleanBadgeServiceImpl implements CleanBadgeService { return BadgeStatusRespDTO.builder() .deviceId(status.getDeviceId()) .deviceKey(status.getDeviceCode()) + .nickname(status.getNickname()) .status(status.getStatusCode()) .batteryLevel(status.getBatteryLevel()) .lastHeartbeatTime(formatTimestamp(status.getLastHeartbeatTime())) diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/cleanorder/CleanWorkOrderServiceImpl.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/cleanorder/CleanWorkOrderServiceImpl.java index fd8e3d7a..5f7557e7 100644 --- a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/cleanorder/CleanWorkOrderServiceImpl.java +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/cleanorder/CleanWorkOrderServiceImpl.java @@ -347,6 +347,7 @@ public class CleanWorkOrderServiceImpl implements CleanWorkOrderService { .orderId(req.getOrderId()) .operator(OperatorContext.ofAdmin(req.getOperatorId(), resolveUserName(req.getOperatorId()))) .assigneeId(req.getAssigneeId()) + .assigneeName(req.getAssigneeName()) .reason(req.getRemark()) .build()); } diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/cleanorder/dto/CleanManualDispatchReqDTO.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/cleanorder/dto/CleanManualDispatchReqDTO.java index ca5f3fa9..902daf3c 100644 --- a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/cleanorder/dto/CleanManualDispatchReqDTO.java +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/cleanorder/dto/CleanManualDispatchReqDTO.java @@ -2,6 +2,7 @@ package com.viewsh.module.ops.environment.service.cleanorder.dto; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; import lombok.Data; /** @@ -21,6 +22,10 @@ public class CleanManualDispatchReqDTO { @NotNull(message = "目标设备ID不能为空") private Long assigneeId; + @Schema(description = "目标设备名称(昵称或设备编码)", example = "男卫-01") + @Size(max = 100, message = "设备名称不能超过100字符") + private String assigneeName; + @Schema(description = "派单备注", example = "紧急情况,指定该设备处理") private String remark; diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/manual/CleanOrderBusinessStrategy.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/manual/CleanOrderBusinessStrategy.java index 74e5aa3b..95d9b611 100644 --- a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/manual/CleanOrderBusinessStrategy.java +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/manual/CleanOrderBusinessStrategy.java @@ -55,15 +55,9 @@ public class CleanOrderBusinessStrategy implements OrderBusinessStrategy { if (!badge.isOnline()) { throw new IllegalStateException("目标保洁设备当前离线,不能手动派单"); } - if (!badge.canAcceptNewOrder()) { - throw new IllegalStateException("目标保洁设备当前不可接单"); - } - if (order.getAreaId() != null && badge.getCurrentAreaId() == null) { - throw new IllegalStateException("目标保洁设备当前未绑定区域,不能手动派单"); - } - if (order.getAreaId() != null && !order.getAreaId().equals(badge.getCurrentAreaId())) { - throw new IllegalStateException("目标保洁设备不在当前工单所属区域"); - } + // 注意:以下校验已按产品需求移除,由调度员人工判断合理性: + // 1. canAcceptNewOrder() — 允许向 BUSY/PAUSED 工牌手动派单,工单进入 QUEUED 排队 + // 2. 区域一致性校验 — 允许跨区域分配,支持灵活调度场景 } @Override @@ -87,4 +81,11 @@ public class CleanOrderBusinessStrategy implements OrderBusinessStrategy { log.info("[CleanStrategy] 升级优先级后置完成: orderId={}, newPriority={}, queueId={}", cmd.getOrderId(), newPriority, queueDTO.getId()); } + + @Override + public void afterDispatch(DispatchOrderCommand cmd, OpsOrderDO order) { + // TODO: 转派场景下(order.getAssigneeId() != null && !order.getAssigneeId().equals(cmd.getAssigneeId())), + // 应向旧工牌发送震动/语音通知告知任务已转移,避免旧工牌持有者继续前往已无效的区域。 + // 实现参考:cleanOrderNotificationService.sendReassignNotification(oldBadgeId, order.getOrderCode()) + } } diff --git a/viewsh-module-ops/viewsh-module-ops-api/src/main/java/com/viewsh/module/ops/api/clean/BadgeStatusRespDTO.java b/viewsh-module-ops/viewsh-module-ops-api/src/main/java/com/viewsh/module/ops/api/clean/BadgeStatusRespDTO.java index d4884521..dd1da956 100644 --- a/viewsh-module-ops/viewsh-module-ops-api/src/main/java/com/viewsh/module/ops/api/clean/BadgeStatusRespDTO.java +++ b/viewsh-module-ops/viewsh-module-ops-api/src/main/java/com/viewsh/module/ops/api/clean/BadgeStatusRespDTO.java @@ -26,6 +26,9 @@ public class BadgeStatusRespDTO { @Schema(description = "设备编码", example = "badge_001") private String deviceKey; + @Schema(description = "设备昵称(用户可读的显示名称)", example = "张三的工牌") + private String nickname; + @Schema(description = "状态(IDLE/BUSY/OFFLINE/PAUSED)", example = "IDLE") private String status; diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/code/OrderCodeGenerator.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/code/OrderCodeGenerator.java index 451dc179..91178ec1 100644 --- a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/code/OrderCodeGenerator.java +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/code/OrderCodeGenerator.java @@ -1,93 +1,160 @@ -package com.viewsh.module.ops.infrastructure.code; - -import com.viewsh.module.ops.infrastructure.redis.OpsRedisKeyBuilder; +package com.viewsh.module.ops.infrastructure.code; + import jakarta.annotation.Resource; -import lombok.extern.slf4j.Slf4j; -import org.springframework.data.redis.core.StringRedisTemplate; -import org.springframework.stereotype.Service; - -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; -import java.util.concurrent.TimeUnit; - -/** - * 工单编号生成器 - *
- * 格式:{业务前缀}-{日期}-{序号} - * 例如:CLEAN-20250119-0001, SECURITY-20250119-0001 - *
- * 特性: - * - 使用 Redis 保证序号唯一性 - * - 序号每日自动重置(按日期分 key) - * - 不同业务类型独立计数 - * - * @author lzh - */ -@Slf4j -@Service -public class OrderCodeGenerator { - +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.script.DefaultRedisScript; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.Collections; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +/** + * 工单编号生成器 + *
+ * 格式:{业务前缀}-{日期}-{序号} + * 例如:CLEAN-20250119-0001, SECURITY-20250119-0001 + *
+ * 特性: + * - 使用 Redis 保证序号唯一性 + * - 序号每日自动重置(按日期分 key) + * - 不同业务类型独立计数 + * - 应用启动后首次使用时自动从数据库校准,之后纯 Redis 自增 + * + * @author lzh + */ +@Slf4j +@Service +public class OrderCodeGenerator { + + private static final String KEY_PREFIX = "ops:order:code:"; private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd"); - private static final long DEFAULT_EXPIRE_DAYS = 7; // key 默认保留7天 - - @Resource - private StringRedisTemplate stringRedisTemplate; - - /** - * 生成工单编号 - * - * @param businessType 业务类型(如:CLEAN、SECURITY、FACILITIES) - * @return 工单编号,格式:{业务类型}-{日期}-{4位序号} - */ - public String generate(String businessType) { - if (businessType == null || businessType.isEmpty()) { - throw new IllegalArgumentException("Business type cannot be null or empty"); - } - - String dateStr = LocalDate.now().format(DATE_FORMATTER); - String key = OpsRedisKeyBuilder.orderCode(businessType, dateStr); - - // Redis 自增并获取新值 - Long seq = stringRedisTemplate.opsForValue().increment(key); - - // 首次创建时设置过期时间 - if (seq != null && seq == 1) { - stringRedisTemplate.expire(key, DEFAULT_EXPIRE_DAYS, TimeUnit.DAYS); - } - - if (seq == null) { - throw new RuntimeException("Failed to generate order code for business type: " + businessType); - } - - String orderCode = String.format("%s-%s-%04d", businessType, dateStr, seq); - - log.debug("生成工单编号: businessType={}, orderCode={}", businessType, orderCode); - - return orderCode; - } - - /** - * 获取指定业务类型当天的当前序号 - * - * @param businessType 业务类型 - * @return 当前序号,如果不存在返回0 - */ - public long getCurrentSeq(String businessType) { - String dateStr = LocalDate.now().format(DATE_FORMATTER); - String key = OpsRedisKeyBuilder.orderCode(businessType, dateStr); - String value = stringRedisTemplate.opsForValue().get(key); - return value == null ? 0 : Long.parseLong(value); - } - - /** - * 重置指定业务类型当天的序号(仅供测试或特殊场景使用) - * - * @param businessType 业务类型 - */ - public void reset(String businessType) { - String dateStr = LocalDate.now().format(DATE_FORMATTER); - String key = OpsRedisKeyBuilder.orderCode(businessType, dateStr); - stringRedisTemplate.delete(key); - log.warn("重置工单编号序号: businessType={}, date={}", businessType, dateStr); - } -} + private static final long DEFAULT_EXPIRE_DAYS = 7; + + /** + * 记录本次应用生命周期内已校准过的 key,避免重复查库。 + *
+ * ConcurrentHashMap: key = "CLEAN:20260413", value = dateStr(用于跨天清理)
+ */
+ private final ConcurrentHashMap