| | |
| | | arrTime: "arrTime", |
| | | rleStatus: "rleStatus", |
| | | ntyStatus: "ntyStatus", |
| | | cloudWmsResend: "Resend cloud WMS feedback", |
| | | cloudWmsResendConfirm: "Re-queue in/out completion feedback to cloud WMS for this order?", |
| | | }, |
| | | asnOrderItemLog: { |
| | | asnId: "asnId", |
| | |
| | | arrTime: "预计到达时间", |
| | | rleStatus: "释放状态", |
| | | ntyStatus: "质检上报状态", |
| | | cloudWmsResend: "云仓反馈重发", |
| | | cloudWmsResendConfirm: "确认将该历史单的云仓入出库反馈重新加入发送队列?", |
| | | }, |
| | | asnOrderItemLog: { |
| | | orderId: "主单标识", |
| New file |
| | |
| | | import { useState } from "react"; |
| | | import { useRecordContext, useTranslate } from "react-admin"; |
| | | import { Button } from "@mui/material"; |
| | | import request from "@/utils/request"; |
| | | |
| | | /** 出入库历史单:云仓 ERP 反馈重发(权限 manager:asnOrderLog:cloudWmsResend) */ |
| | | export default function AsnOrderLogCloudWmsResendButton() { |
| | | const record = useRecordContext(); |
| | | const translate = useTranslate(); |
| | | const [loading, setLoading] = useState(false); |
| | | |
| | | if (!record?.id) { |
| | | return null; |
| | | } |
| | | |
| | | const onClick = async () => { |
| | | const ok = window.confirm( |
| | | translate("resources.asnOrderLog.cloudWmsResendConfirm", { |
| | | _: "确认将该历史单的云仓入出库反馈重新加入发送队列?", |
| | | }) |
| | | ); |
| | | if (!ok) return; |
| | | setLoading(true); |
| | | try { |
| | | const res = await request.post(`/asnOrderLog/cloudWms/resendFeedback/${record.id}`); |
| | | const { code, msg, data } = res.data || {}; |
| | | if (code === 200) { |
| | | window.alert(msg || "OK"); |
| | | } else { |
| | | window.alert(msg || "Error"); |
| | | } |
| | | } catch (e) { |
| | | window.alert(e?.message || String(e)); |
| | | } finally { |
| | | setLoading(false); |
| | | } |
| | | }; |
| | | |
| | | return ( |
| | | <Button size="small" variant="outlined" disabled={loading} onClick={onClick}> |
| | | {translate("resources.asnOrderLog.cloudWmsResend", { _: "云仓反馈重发" })} |
| | | </Button> |
| | | ); |
| | | } |
| | |
| | | import { Box } from '@mui/material'; |
| | | import PageDrawer from "../../components/PageDrawer"; |
| | | import AsnOrderLogCreate from "./AsnOrderLogCreate"; |
| | | import AsnOrderLogCloudWmsResendButton from "./AsnOrderLogCloudWmsResendButton"; |
| | | import { styled } from '@mui/material/styles'; |
| | | |
| | | const StyledDatagrid = styled(DatagridConfigurable)(({ theme }) => ({ |
| | |
| | | <TextField source="memo" label="common.field.memo" sortable={false} /> |
| | | <WrapperField cellClassName="opt" label="common.field.opt"> |
| | | <ShowButton label="toolbar.detail" /> |
| | | <AsnOrderLogCloudWmsResendButton /> |
| | | </WrapperField> |
| | | </StyledDatagrid> |
| | | </List> |
| | |
| | | |
| | | import org.springframework.boot.SpringApplication; |
| | | import org.springframework.boot.autoconfigure.SpringBootApplication; |
| | | import org.springframework.boot.autoconfigure.data.redis.RedisReactiveAutoConfiguration; |
| | | import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; |
| | | import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration; |
| | | import org.springframework.cloud.openfeign.EnableFeignClients; |
| | | import org.springframework.scheduling.annotation.EnableScheduling; |
| | | |
| | | @SpringBootApplication(exclude = {SecurityAutoConfiguration.class, UserDetailsServiceAutoConfiguration.class }) |
| | | @SpringBootApplication(exclude = { |
| | | SecurityAutoConfiguration.class, |
| | | UserDetailsServiceAutoConfiguration.class, |
| | | RedisReactiveAutoConfiguration.class |
| | | }) |
| | | @EnableFeignClients(basePackages = "com.vincent.rsf.openApi.feign") |
| | | @EnableScheduling |
| | | public class OpenApi { |
| New file |
| | |
| | | package com.vincent.rsf.openApi.config; |
| | | |
| | | import org.springframework.beans.factory.annotation.Qualifier; |
| | | import org.springframework.beans.factory.annotation.Value; |
| | | import org.springframework.context.annotation.Bean; |
| | | import org.springframework.context.annotation.Configuration; |
| | | import org.springframework.data.redis.connection.RedisStandaloneConfiguration; |
| | | import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; |
| | | import org.springframework.data.redis.core.RedisTemplate; |
| | | |
| | | /** |
| | | * 业务 Redis({@code redis.index})与默认 {@code redisTemplate};与电视机侧 {@code tv-monitor.redis.database} 分离 |
| | | */ |
| | | @Configuration |
| | | public class OpenApiTokenRedisConfig { |
| | | |
| | | @Value("${redis.host:127.0.0.1}") |
| | | private String host; |
| | | @Value("${redis.port:6379}") |
| | | private int port; |
| | | @Value("${redis.password:}") |
| | | private String password; |
| | | @Value("${redis.index:0}") |
| | | private int database; |
| | | |
| | | @Bean(name = "openApiTokenRedisConnectionFactory") |
| | | public LettuceConnectionFactory openApiTokenRedisConnectionFactory() { |
| | | RedisStandaloneConfiguration cfg = new RedisStandaloneConfiguration(host, port); |
| | | cfg.setDatabase(database); |
| | | if (password != null && !password.isEmpty()) { |
| | | cfg.setPassword(password); |
| | | } |
| | | return new LettuceConnectionFactory(cfg); |
| | | } |
| | | |
| | | /** |
| | | * 存在多个 ConnectionFactory 时 Boot 不会装配默认 redisTemplate |
| | | */ |
| | | @Bean(name = "redisTemplate") |
| | | public RedisTemplate<Object, Object> redisTemplate( |
| | | @Qualifier("openApiTokenRedisConnectionFactory") LettuceConnectionFactory openApiTokenRedisConnectionFactory) { |
| | | RedisTemplate<Object, Object> template = new RedisTemplate<>(); |
| | | template.setConnectionFactory(openApiTokenRedisConnectionFactory); |
| | | return template; |
| | | } |
| | | } |
| | |
| | | package com.vincent.rsf.openApi.controller; |
| | | |
| | | import com.vincent.rsf.framework.common.Cools; |
| | | import com.vincent.rsf.openApi.entity.constant.Constants; |
| | | import com.vincent.rsf.openApi.entity.dto.CommonResponse; |
| | | import com.vincent.rsf.openApi.entity.params.GetTokenParam; |
| | | import com.vincent.rsf.openApi.security.service.AppAuthService; |
| | | import com.vincent.rsf.openApi.security.utils.TokenUtils; |
| | | import com.vincent.rsf.openApi.service.TokenService; |
| | | import io.swagger.annotations.Api; |
| | | import io.swagger.annotations.ApiOperation; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | |
| | | public class AuthController { |
| | | |
| | | @Autowired |
| | | private AppAuthService appAuthService; |
| | | private TokenService tokenService; |
| | | |
| | | @ApiOperation("获取App认证Token") |
| | | @PostMapping("/getToken") |
| | |
| | | if (Cools.isEmpty(appId, appSecret)) { |
| | | return CommonResponse.error("AppId和AppSecret不能为空"); |
| | | } |
| | | if (!appAuthService.validateApp(appId, appSecret)) { |
| | | String token = tokenService.issueToken(appId, appSecret); |
| | | if (token == null) { |
| | | return CommonResponse.error("AppId或AppSecret无效"); |
| | | } |
| | | String token = Constants.TOKEN_PREFIX + TokenUtils.generateToken(appId, appSecret); |
| | | return CommonResponse.ok().setMsg("获取Token成功").setData(token); |
| | | } |
| | | } |
| | |
| | | package com.vincent.rsf.openApi.security.filter; |
| | | |
| | | import com.vincent.rsf.openApi.entity.app.App; |
| | | import com.vincent.rsf.openApi.entity.constant.Constants; |
| | | import com.vincent.rsf.openApi.security.service.AppAuthService; |
| | | import com.vincent.rsf.openApi.security.utils.TokenUtils; |
| | | import com.vincent.rsf.openApi.service.TokenService; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.core.annotation.Order; |
| | | import org.springframework.stereotype.Component; |
| | |
| | | |
| | | @Resource |
| | | private AppAuthService appAuthService; |
| | | @Resource |
| | | private TokenService tokenService; |
| | | |
| | | @Override |
| | | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) |
| | |
| | | String authHeader = request.getHeader(Constants.HEADER_AUTHORIZATION); |
| | | if (authHeader != null) { |
| | | String token = TokenUtils.extractTokenFromHeader(authHeader); |
| | | if (token != null && TokenUtils.validateTokenTime(token)) { |
| | | String tokenAppId = TokenUtils.getAppIdFromToken(token); |
| | | String tokenAppSecret = TokenUtils.getSecretFromToken(token); |
| | | if (!StringUtils.hasText(tokenAppId) || !StringUtils.hasText(tokenAppSecret) |
| | | || !appAuthService.validateApp(tokenAppId, tokenAppSecret)) { |
| | | log.warn("Token验证失败"); |
| | | sendErrorResponse(response, Constants.UNAUTHENTICATED_CODE, "认证失败,请提供有效的Token"); |
| | | return; |
| | | } |
| | | request.setAttribute(Constants.REQUEST_ATTR_APP_ID, tokenAppId); |
| | | } else { |
| | | String tokenAppId = token != null ? tokenService.getAppIdIfValid(token) : null; |
| | | if (!StringUtils.hasText(tokenAppId)) { |
| | | log.warn("Token验证失败或缺失"); |
| | | sendErrorResponse(response, Constants.UNAUTHENTICATED_CODE, "认证失败,请提供有效的Token"); |
| | | return; |
| | | } |
| | | App app = appAuthService.getAppInfo(tokenAppId); |
| | | if (app == null || app.getEnable() == null || app.getEnable() != 1) { |
| | | log.warn("Token对应应用无效或已禁用"); |
| | | sendErrorResponse(response, Constants.UNAUTHENTICATED_CODE, "认证失败,请提供有效的Token"); |
| | | return; |
| | | } |
| | | request.setAttribute(Constants.REQUEST_ATTR_APP_ID, tokenAppId); |
| | | } else { |
| | | log.warn("缺少Token认证信息"); |
| | | sendErrorResponse(response, Constants.UNAUTHENTICATED_CODE, "认证失败,请提供有效的Token"); |
| | |
| | | package com.vincent.rsf.openApi.security.utils; |
| | | |
| | | import com.vincent.rsf.openApi.entity.constant.Constants; |
| | | import io.jsonwebtoken.Claims; |
| | | import io.jsonwebtoken.JwtException; |
| | | import io.jsonwebtoken.Jwts; |
| | | import io.jsonwebtoken.SignatureAlgorithm; |
| | | import io.jsonwebtoken.security.Keys; |
| | | import org.slf4j.Logger; |
| | | import org.slf4j.LoggerFactory; |
| | | |
| | | import javax.crypto.SecretKey; |
| | | import java.util.Date; |
| | | import java.util.HashMap; |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * JWT Token 工具类 |
| | | * Authorization 头解析;JWT 签发与校验在 {@link com.vincent.rsf.openApi.service.impl.TokenServiceImpl} |
| | | */ |
| | | public class TokenUtils { |
| | | private static final Logger log = LoggerFactory.getLogger(TokenUtils.class); |
| | | public final class TokenUtils { |
| | | |
| | | private static final SecretKey SECRET_KEY = Keys.secretKeyFor(SignatureAlgorithm.HS256); |
| | | private static final long TOKEN_EXPIRATION = 60 * 60 * 1000L; |
| | | |
| | | public static String generateToken(Map<String, Object> claims) { |
| | | long now = System.currentTimeMillis(); |
| | | Date expiration = new Date(now + TOKEN_EXPIRATION); |
| | | return Jwts.builder() |
| | | .setClaims(claims) |
| | | .setExpiration(expiration) |
| | | .signWith(SECRET_KEY, SignatureAlgorithm.HS256) |
| | | .compact(); |
| | | } |
| | | |
| | | public static String generateToken(String appId, String appSecret) { |
| | | Map<String, Object> claims = new HashMap<>(); |
| | | claims.put("appId", appId); |
| | | claims.put("appSecret", appSecret); |
| | | claims.put("created", System.currentTimeMillis()); |
| | | return generateToken(claims); |
| | | } |
| | | |
| | | public static Claims parseToken(String token) { |
| | | try { |
| | | return Jwts.parserBuilder() |
| | | .setSigningKey(SECRET_KEY) |
| | | .build() |
| | | .parseClaimsJws(token) |
| | | .getBody(); |
| | | } catch (JwtException e) { |
| | | log.error("解析Token失败: {}", e.getMessage()); |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | public static boolean validateTokenTime(String token) { |
| | | try { |
| | | Claims claims = parseToken(token); |
| | | if (claims == null) { |
| | | return false; |
| | | } |
| | | Date expiration = claims.getExpiration(); |
| | | return expiration != null && expiration.after(new Date()); |
| | | } catch (JwtException e) { |
| | | log.error("验证Token失败: {}", e.getMessage()); |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | public static String getAppIdFromToken(String token) { |
| | | Claims claims = parseToken(token); |
| | | return claims != null ? (String) claims.get("appId") : null; |
| | | } |
| | | |
| | | public static String getSecretFromToken(String token) { |
| | | Claims claims = parseToken(token); |
| | | return claims != null ? (String) claims.get("appSecret") : null; |
| | | private TokenUtils() { |
| | | } |
| | | |
| | | public static String extractTokenFromHeader(String authHeader) { |
| | |
| | | package com.vincent.rsf.openApi.service; |
| | | |
| | | /** |
| | | * 对接协议 8.1 Token 签发与校验 |
| | | * 对接协议 8.1 Token 签发与校验(JWT,密钥 open-api.jwt.secret) |
| | | */ |
| | | public interface TokenService { |
| | | |
| | |
| | | * 校验 token 是否有效 |
| | | */ |
| | | boolean validateToken(String token); |
| | | |
| | | /** |
| | | * 校验 token 并返回绑定的 appId(不含 Bearer 前缀的 token 串) |
| | | */ |
| | | String getAppIdIfValid(String rawToken); |
| | | } |
| | |
| | | package com.vincent.rsf.openApi.service.impl; |
| | | |
| | | import com.vincent.rsf.openApi.entity.constant.Constants; |
| | | import com.vincent.rsf.openApi.security.service.AppAuthService; |
| | | import com.vincent.rsf.openApi.service.TokenService; |
| | | import io.jsonwebtoken.Claims; |
| | | import io.jsonwebtoken.JwtException; |
| | | import io.jsonwebtoken.Jwts; |
| | | import io.jsonwebtoken.SignatureAlgorithm; |
| | | import io.jsonwebtoken.security.Keys; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.beans.factory.annotation.Value; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.util.StringUtils; |
| | | |
| | | import javax.crypto.SecretKey; |
| | | import java.nio.charset.StandardCharsets; |
| | | import java.security.MessageDigest; |
| | | import java.util.Date; |
| | | import java.util.HashMap; |
| | | import java.util.Map; |
| | | import java.util.UUID; |
| | | import java.util.concurrent.ConcurrentHashMap; |
| | | |
| | | /** |
| | | * 对接 Token:JWT(密钥来自配置,重启后同一密钥仍可校验未过期 token;claims 仅含 appId) |
| | | */ |
| | | @Slf4j |
| | | @Service |
| | | public class TokenServiceImpl implements TokenService { |
| | | |
| | | private static final long EXPIRE_MS = 60 * 60 * 1000L; |
| | | private static final String CLAIM_APP_ID = "appId"; |
| | | |
| | | private final AppAuthService appAuthService; |
| | | private final SecretKey secretKey; |
| | | private final long expireMs; |
| | | |
| | | @Autowired |
| | | private AppAuthService appAuthService; |
| | | public TokenServiceImpl( |
| | | AppAuthService appAuthService, |
| | | @Value("${open-api.jwt.secret:}") String jwtSecret, |
| | | @Value("${open-api.jwt.expire-seconds:3600}") long expireSeconds) { |
| | | this.appAuthService = appAuthService; |
| | | if (!StringUtils.hasText(jwtSecret)) { |
| | | throw new IllegalStateException("请配置 open-api.jwt.secret(HS256,建议至少 32 字符或使用环境变量覆盖)"); |
| | | } |
| | | this.secretKey = hmacSha256Key(jwtSecret.trim()); |
| | | this.expireMs = expireSeconds * 1000L; |
| | | } |
| | | |
| | | private final Map<String, Long> tokenExpire = new ConcurrentHashMap<>(); |
| | | /** 短字符串用 SHA-256 派生 32 字节以满足 HS256 */ |
| | | private static SecretKey hmacSha256Key(String secret) { |
| | | byte[] bytes = secret.getBytes(StandardCharsets.UTF_8); |
| | | if (bytes.length < 32) { |
| | | try { |
| | | MessageDigest md = MessageDigest.getInstance("SHA-256"); |
| | | bytes = md.digest(bytes); |
| | | } catch (Exception e) { |
| | | throw new IllegalStateException(e); |
| | | } |
| | | } |
| | | return Keys.hmacShaKeyFor(bytes); |
| | | } |
| | | |
| | | @Override |
| | | public String issueToken(String appId, String appSecret) { |
| | |
| | | if (!appAuthService.validateApp(appId, appSecret)) { |
| | | return null; |
| | | } |
| | | String token = UUID.randomUUID().toString().replace("-", ""); |
| | | tokenExpire.put(token, System.currentTimeMillis() + EXPIRE_MS); |
| | | evictExpired(); |
| | | return token; |
| | | long now = System.currentTimeMillis(); |
| | | Map<String, Object> claims = new HashMap<>(); |
| | | claims.put(CLAIM_APP_ID, appId); |
| | | String compact = Jwts.builder() |
| | | .setClaims(claims) |
| | | .setIssuedAt(new Date(now)) |
| | | .setExpiration(new Date(now + expireMs)) |
| | | .signWith(secretKey, SignatureAlgorithm.HS256) |
| | | .compact(); |
| | | return Constants.TOKEN_PREFIX + compact; |
| | | } |
| | | |
| | | @Override |
| | | public boolean validateToken(String token) { |
| | | if (!StringUtils.hasText(token)) { |
| | | return false; |
| | | } |
| | | Long expire = tokenExpire.get(token); |
| | | if (expire == null || System.currentTimeMillis() > expire) { |
| | | tokenExpire.remove(token); |
| | | return false; |
| | | } |
| | | return true; |
| | | return getAppIdIfValid(token) != null; |
| | | } |
| | | |
| | | private void evictExpired() { |
| | | long now = System.currentTimeMillis(); |
| | | tokenExpire.entrySet().removeIf(e -> e.getValue() < now); |
| | | @Override |
| | | public String getAppIdIfValid(String rawToken) { |
| | | if (!StringUtils.hasText(rawToken)) { |
| | | return null; |
| | | } |
| | | try { |
| | | Claims claims = Jwts.parserBuilder() |
| | | .setSigningKey(secretKey) |
| | | .build() |
| | | .parseClaimsJws(rawToken) |
| | | .getBody(); |
| | | Date exp = claims.getExpiration(); |
| | | if (exp == null || exp.before(new Date())) { |
| | | return null; |
| | | } |
| | | Object appId = claims.get(CLAIM_APP_ID); |
| | | return appId != null ? appId.toString() : null; |
| | | } catch (JwtException e) { |
| | | log.debug("JWT 校验失败: {}", e.getMessage()); |
| | | return null; |
| | | } |
| | | } |
| | | } |
| | |
| | | timeout: 5000 |
| | | index: 15 |
| | | |
| | | open-api: |
| | | jwt: |
| | | secret: "rsf-open-api-jwt-dev-key-change-in-prod-min-32chars" |
| | | expire-seconds: 3600 |
| | | |
| | | # 电视机 Redis:与 zy-monitor-admin 使用同一 database(默认 0),否则读不到报警 |
| | | tv-monitor: |
| | | redis: |
| | |
| | | database: 3 |
| | | index: 3 |
| | | |
| | | open-api: |
| | | jwt: |
| | | secret: ${OPEN_API_JWT_SECRET:REPLACE-WITH-STRONG-SECRET-AT-LEAST-32-CHARS} |
| | | expire-seconds: 3600 |
| | | |
| | | # 电视机 Redis:与 zy-monitor-admin 使用同一 database(默认 0),否则读不到报警 |
| | | tv-monitor: |
| | | redis: |
| | |
| | | spring: |
| | | profiles: |
| | | active: dev |
| | | data: |
| | | redis: |
| | | repositories: |
| | | enabled: false |
| | | |
| | | mybatis-plus: |
| | | mapper-locations: classpath:mapper/*/*.xml |
| | |
| | | import com.vincent.rsf.server.common.domain.PageParam; |
| | | import com.vincent.rsf.server.manager.entity.AsnOrderLog; |
| | | import com.vincent.rsf.server.manager.service.AsnOrderLogService; |
| | | import com.vincent.rsf.server.manager.service.CloudWmsFeedbackResendService; |
| | | import com.vincent.rsf.server.system.controller.BaseController; |
| | | import io.swagger.annotations.ApiOperation; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | |
| | | |
| | | @Autowired |
| | | private AsnOrderLogService asnOrderLogService; |
| | | @Autowired |
| | | private CloudWmsFeedbackResendService cloudWmsFeedbackResendService; |
| | | |
| | | @ApiOperation("云仓入出库反馈重发(写入上报待办队列)") |
| | | @OperationLog("CloudWMS feedback resend") |
| | | @PreAuthorize("hasAuthority('manager:asnOrderLog:cloudWmsResend')") |
| | | @PostMapping("/asnOrderLog/cloudWms/resendFeedback/{id}") |
| | | public R resendCloudWmsFeedback(@PathVariable Long id) { |
| | | return cloudWmsFeedbackResendService.resendInOutFeedbackByAsnOrderLogId(id); |
| | | } |
| | | |
| | | @PreAuthorize("hasAuthority('manager:asnOrderLog:list')") |
| | | @PostMapping("/asnOrderLog/page") |
| New file |
| | |
| | | package com.vincent.rsf.server.manager.service; |
| | | |
| | | import com.vincent.rsf.framework.common.R; |
| | | |
| | | /** 按出入库历史单重发云仓入出库反馈(写入上报待办队列) */ |
| | | public interface CloudWmsFeedbackResendService { |
| | | |
| | | /** |
| | | * @param asnOrderLogId man_asn_order_log / AsnOrderLog 主键 |
| | | */ |
| | | R resendInOutFeedbackByAsnOrderLogId(Long asnOrderLogId); |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.server.manager.service.impl; |
| | | |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.fasterxml.jackson.databind.ObjectMapper; |
| | | import com.vincent.rsf.framework.common.R; |
| | | import com.vincent.rsf.server.api.controller.erp.params.InOutResultReportParam; |
| | | import com.vincent.rsf.server.manager.entity.AsnOrderLog; |
| | | import com.vincent.rsf.server.manager.entity.CloudWmsNotifyLog; |
| | | import com.vincent.rsf.server.manager.service.AsnOrderLogService; |
| | | import com.vincent.rsf.server.manager.service.CloudWmsFeedbackResendService; |
| | | import com.vincent.rsf.server.manager.service.CloudWmsNotifyLogService; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.apache.commons.lang3.StringUtils; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | import java.util.*; |
| | | |
| | | /** |
| | | * 从历史云仓上报记录中取「最新一条」按明细维度去重后,复制请求体写入待发送队列。 |
| | | */ |
| | | @Slf4j |
| | | @Service |
| | | public class CloudWmsFeedbackResendServiceImpl implements CloudWmsFeedbackResendService { |
| | | |
| | | private static final int MAX_SCAN = 500; |
| | | |
| | | @Autowired |
| | | private AsnOrderLogService asnOrderLogService; |
| | | @Autowired |
| | | private CloudWmsNotifyLogService cloudWmsNotifyLogService; |
| | | @Autowired |
| | | private ObjectMapper objectMapper; |
| | | |
| | | @Override |
| | | public R resendInOutFeedbackByAsnOrderLogId(Long asnOrderLogId) { |
| | | if (asnOrderLogId == null) { |
| | | return R.error("参数不能为空"); |
| | | } |
| | | AsnOrderLog orderLog = asnOrderLogService.getById(asnOrderLogId); |
| | | if (orderLog == null) { |
| | | return R.error("历史单据不存在"); |
| | | } |
| | | String code = orderLog.getCode(); |
| | | if (StringUtils.isBlank(code)) { |
| | | return R.error("单据号为空,无法重发"); |
| | | } |
| | | String reportType = cloudWmsNotifyLogService.getReportTypeInOutResult(); |
| | | LambdaQueryWrapper<CloudWmsNotifyLog> qw = new LambdaQueryWrapper<CloudWmsNotifyLog>() |
| | | .eq(CloudWmsNotifyLog::getReportType, reportType) |
| | | .and(w -> w.like(CloudWmsNotifyLog::getRequestBody, "\"orderNo\":\"" + code + "\"") |
| | | .or() |
| | | .like(CloudWmsNotifyLog::getBizRef, "orderNo=" + code)) |
| | | .orderByDesc(CloudWmsNotifyLog::getId) |
| | | .last("LIMIT " + MAX_SCAN); |
| | | List<CloudWmsNotifyLog> candidates = cloudWmsNotifyLogService.list(qw); |
| | | Map<String, CloudWmsNotifyLog> latestByLine = new LinkedHashMap<>(); |
| | | for (CloudWmsNotifyLog row : candidates) { |
| | | if (StringUtils.isBlank(row.getRequestBody())) { |
| | | continue; |
| | | } |
| | | InOutResultReportParam p; |
| | | try { |
| | | p = objectMapper.readValue(row.getRequestBody(), InOutResultReportParam.class); |
| | | } catch (Exception e) { |
| | | continue; |
| | | } |
| | | if (p == null || !code.equals(p.getOrderNo())) { |
| | | continue; |
| | | } |
| | | if (!matchOrderType(orderLog.getType(), p.getInbound())) { |
| | | continue; |
| | | } |
| | | String sig = lineSignature(p); |
| | | latestByLine.putIfAbsent(sig, row); |
| | | } |
| | | if (latestByLine.isEmpty()) { |
| | | return R.error("未找到该单号对应的云仓入出库上报记录,无法重发"); |
| | | } |
| | | Date now = new Date(); |
| | | int n = 0; |
| | | for (CloudWmsNotifyLog src : latestByLine.values()) { |
| | | CloudWmsNotifyLog copy = new CloudWmsNotifyLog() |
| | | .setReportType(reportType) |
| | | .setRequestBody(src.getRequestBody()) |
| | | .setNotifyStatus(cloudWmsNotifyLogService.getNotifyStatusPending()) |
| | | .setRetryCount(0) |
| | | .setBizRef("manualResend,asnOrderLogId=" + asnOrderLogId + ",fromNotifyLogId=" + src.getId() + ",orderNo=" + code) |
| | | .setCreateTime(now) |
| | | .setUpdateTime(now); |
| | | cloudWmsNotifyLogService.fillFromConfig(copy); |
| | | if (cloudWmsNotifyLogService.save(copy)) { |
| | | n++; |
| | | } |
| | | } |
| | | return R.ok("已加入云仓重发队列 " + n + " 条,将由定时任务发送").add(n); |
| | | } |
| | | |
| | | private static boolean matchOrderType(String asnType, Boolean inbound) { |
| | | if (StringUtils.isBlank(asnType)) { |
| | | return true; |
| | | } |
| | | boolean wantIn = "in".equalsIgnoreCase(asnType.trim()); |
| | | if (inbound == null) { |
| | | return true; |
| | | } |
| | | return wantIn == inbound; |
| | | } |
| | | |
| | | private static String lineSignature(InOutResultReportParam p) { |
| | | return String.join("|", |
| | | String.valueOf(p.getOrderNo()), |
| | | String.valueOf(p.getLineId()), |
| | | String.valueOf(p.getMatNr()), |
| | | String.valueOf(p.getLocId()), |
| | | String.valueOf(p.getWareHouseId()), |
| | | String.valueOf(p.getInbound()), |
| | | String.valueOf(p.getQty()), |
| | | String.valueOf(p.getBatch())); |
| | | } |
| | | } |
| New file |
| | |
| | | -- 出入库历史单:云仓 ERP 反馈重发按钮权限(需在角色中勾选后才可使用) |
| | | -- 执行一次即可;若提示重复 authority 可忽略或先删除同 authority 记录 |
| | | INSERT INTO `sys_menu` (`name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`) |
| | | SELECT 'CloudWMS feedback resend', |
| | | m.id, |
| | | NULL, |
| | | CONCAT(IFNULL(m.path, ''), ',cloudWmsResend'), |
| | | NULL, |
| | | NULL, |
| | | NULL, |
| | | NULL, |
| | | NULL, |
| | | 1, |
| | | 'manager:asnOrderLog:cloudWmsResend', |
| | | NULL, |
| | | 98, |
| | | NULL, |
| | | m.tenant_id, |
| | | 1, |
| | | 0, |
| | | NULL, |
| | | NULL, |
| | | NULL, |
| | | NULL, |
| | | '入库/出库历史单:云仓反馈重发' |
| | | FROM `sys_menu` m |
| | | WHERE m.`authority` = 'manager:asnOrderLog:list' |
| | | AND m.`type` = 1 |
| | | AND NOT EXISTS (SELECT 1 FROM `sys_menu` x WHERE x.`authority` = 'manager:asnOrderLog:cloudWmsResend') |
| | | LIMIT 1; |