From bae1762dba23ad77c5deb5d9bc1b3297de61a1ef Mon Sep 17 00:00:00 2001 From: wangw <1594593906@qq.com> Date: Thu, 25 Dec 2025 11:18:14 +0800 Subject: [PATCH] =?UTF-8?q?ocr=20=E6=95=B0=E7=AD=BE=E5=AD=90=20=E7=99=BB?= =?UTF-8?q?=E5=BD=95=E6=9B=B4=E6=96=B0=E4=BA=86=E7=94=A8=E6=88=B7=E5=90=8D?= =?UTF-8?q?=E7=A7=B0=E7=9A=84=E9=97=AE=E9=A2=98=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E4=BF=A1=E6=81=AF=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/ShopRechargeController.java | 7 - .../admin/ConsStockFlowController.java | 8 +- .../admin/StickCountController.java | 38 ++ .../czg/product/entity/MkOcrCountStick.java | 58 ++ .../com/czg/product/entity/MkOcrService.java | 6 + .../product/service/ConsStockFlowService.java | 6 - .../service/MkOcrCountStickService.java | 15 + .../main/java/com/czg/utils/AliOcrUtil.java | 8 +- .../service/impl/ShopConfigServiceImpl.java | 17 +- .../impl/UserAuthorizationServiceImpl.java | 11 +- .../service/impl/UserInfoServiceImpl.java | 8 +- .../product/mapper/MkOcrCountStickMapper.java | 14 + .../impl/ConsStockFlowServiceImpl.java | 593 ++++++++---------- .../impl/MkOcrCountStickServiceImpl.java | 72 +++ .../service/impl/MkOcrServiceImpl.java | 132 +++- .../product/util/AISmartCountUtils.java | 132 ++++ .../product/util/MuSmartCountUtils.java | 122 ++++ .../mapper/MkOcrCountStickMapper.xml | 7 + 18 files changed, 866 insertions(+), 388 deletions(-) create mode 100644 cash-api/product-server/src/main/java/com/czg/controller/admin/StickCountController.java create mode 100644 cash-common/cash-common-service/src/main/java/com/czg/product/entity/MkOcrCountStick.java create mode 100644 cash-common/cash-common-service/src/main/java/com/czg/product/service/MkOcrCountStickService.java create mode 100644 cash-service/product-service/src/main/java/com/czg/service/product/mapper/MkOcrCountStickMapper.java create mode 100644 cash-service/product-service/src/main/java/com/czg/service/product/service/impl/MkOcrCountStickServiceImpl.java create mode 100644 cash-service/product-service/src/main/java/com/czg/service/product/util/AISmartCountUtils.java create mode 100644 cash-service/product-service/src/main/java/com/czg/service/product/util/MuSmartCountUtils.java create mode 100644 cash-service/product-service/src/main/resources/mapper/MkOcrCountStickMapper.xml diff --git a/cash-api/market-server/src/main/java/com/czg/controller/admin/ShopRechargeController.java b/cash-api/market-server/src/main/java/com/czg/controller/admin/ShopRechargeController.java index 3b3b83842..c70072bee 100644 --- a/cash-api/market-server/src/main/java/com/czg/controller/admin/ShopRechargeController.java +++ b/cash-api/market-server/src/main/java/com/czg/controller/admin/ShopRechargeController.java @@ -1,7 +1,6 @@ package com.czg.controller.admin; import com.czg.market.dto.MkShopRechargeDTO; -import com.czg.market.entity.MkShopConsumeDiscountRecord; import com.czg.market.entity.MkShopRechargeFlow; import com.czg.market.service.MkRechargeFlowService; import com.czg.market.service.MkShopConsumeDiscountRecordService; @@ -30,12 +29,6 @@ public class ShopRechargeController { @Resource private MkShopConsumeDiscountRecordService shopConsumeDiscountRecordService; - @GetMapping("/test") - public CzgResult get(@RequestParam Long shopId) { -// return CzgResult.success(shopConsumeDiscountRecordService.get(shopId)); - return null; - } - /** * 配置信息获取 * 权限标识: activate:list diff --git a/cash-api/product-server/src/main/java/com/czg/controller/admin/ConsStockFlowController.java b/cash-api/product-server/src/main/java/com/czg/controller/admin/ConsStockFlowController.java index c529c9027..a8be19364 100644 --- a/cash-api/product-server/src/main/java/com/czg/controller/admin/ConsStockFlowController.java +++ b/cash-api/product-server/src/main/java/com/czg/controller/admin/ConsStockFlowController.java @@ -5,6 +5,7 @@ import cn.hutool.core.util.URLUtil; import com.czg.log.annotation.OperationLog; import com.czg.product.dto.ConsStockFlowDTO; import com.czg.product.dto.OcrDTO; +import com.czg.product.entity.MkOcrService; import com.czg.product.param.ConsCheckStockParam; import com.czg.product.param.ConsInOutStockHeadParam; import com.czg.product.param.ConsReportDamageParam; @@ -37,6 +38,8 @@ public class ConsStockFlowController { @Resource private ConsStockFlowService consStockFlowService; + @Resource + private MkOcrService ocrService; /** * 入库单识别 @@ -46,17 +49,18 @@ public class ConsStockFlowController { URI uri = new URI(ocrDTO.getUrl()); URL url = uri.toURL(); InputStream stream = URLUtil.getStream(url); - return CzgResult.success(consStockFlowService.ocr(FileUtil.getName(ocrDTO.getUrl()), stream)); + return CzgResult.success(ocrService.ocr(FileUtil.getName(ocrDTO.getUrl()), stream, "cons")); } /** * ocr识别结果 + * * @param id ocrId * @return 识别结果 */ @GetMapping("/ocrResult") public CzgResult ocrResult(@RequestParam Long id) { - return CzgResult.success(consStockFlowService.ocrDetail(id)); + return CzgResult.success(ocrService.ocrDetail(id)); } diff --git a/cash-api/product-server/src/main/java/com/czg/controller/admin/StickCountController.java b/cash-api/product-server/src/main/java/com/czg/controller/admin/StickCountController.java new file mode 100644 index 000000000..215332b0d --- /dev/null +++ b/cash-api/product-server/src/main/java/com/czg/controller/admin/StickCountController.java @@ -0,0 +1,38 @@ +package com.czg.controller.admin; + +import com.czg.product.service.MkOcrCountStickService; +import com.czg.resp.CzgResult; +import com.czg.utils.AssertUtil; +import jakarta.annotation.Resource; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; + + +/** + * 拍照数签子 + * @author ww + */ +@RestController +@RequestMapping("/admin/stick") +public class StickCountController { + + @Resource + private MkOcrCountStickService stickCountService; + + /** + * 文件上传并返回点数统计结果 + * + * @param file 上传的图片文件 + * @return 点数统计结果 + */ + @PostMapping("/count") + public CzgResult uploadAndCount(MultipartFile file) throws IOException { + AssertUtil.isNull(file, "上传文件不能为空"); + // 9. 返回成功结果 + return CzgResult.success(stickCountService.getCountStick(file.getBytes(), file.getOriginalFilename())); + } +} diff --git a/cash-common/cash-common-service/src/main/java/com/czg/product/entity/MkOcrCountStick.java b/cash-common/cash-common-service/src/main/java/com/czg/product/entity/MkOcrCountStick.java new file mode 100644 index 000000000..e1424237e --- /dev/null +++ b/cash-common/cash-common-service/src/main/java/com/czg/product/entity/MkOcrCountStick.java @@ -0,0 +1,58 @@ +package com.czg.product.entity; + +import com.mybatisflex.annotation.Id; +import com.mybatisflex.annotation.Table; +import java.io.Serializable; + +import java.io.Serial; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 数签子的外链渠道 实体类。 + * + * @author ww + * @since 2025-12-24 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Table("mk_ocr_count_stick") +public class MkOcrCountStick implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Id + private Long id; + + /** + * 渠道名称 + */ + private String name; + /** + * 标记 + */ + private String mark; + + /** + * 地址 + */ + private String url; + + /** + * 状态 0/1 + */ + private Integer status; + + private Integer sort; + + private String token; + private String account; + private String pwd; + +} diff --git a/cash-common/cash-common-service/src/main/java/com/czg/product/entity/MkOcrService.java b/cash-common/cash-common-service/src/main/java/com/czg/product/entity/MkOcrService.java index 4ee414e72..c60acc823 100644 --- a/cash-common/cash-common-service/src/main/java/com/czg/product/entity/MkOcrService.java +++ b/cash-common/cash-common-service/src/main/java/com/czg/product/entity/MkOcrService.java @@ -1,8 +1,11 @@ package com.czg.product.entity; +import com.czg.product.param.ConsInOutStockHeadParam; import com.mybatisflex.core.service.IService; import com.czg.market.entity.MkOcr; +import java.io.InputStream; + /** * ocr识别结果 服务层。 * @@ -11,4 +14,7 @@ import com.czg.market.entity.MkOcr; */ public interface MkOcrService extends IService { + ConsInOutStockHeadParam ocrDetail(Long id); + + Integer ocr(String originalFilename, InputStream inputStream, String type); } diff --git a/cash-common/cash-common-service/src/main/java/com/czg/product/service/ConsStockFlowService.java b/cash-common/cash-common-service/src/main/java/com/czg/product/service/ConsStockFlowService.java index c039a83ef..a2e308d43 100644 --- a/cash-common/cash-common-service/src/main/java/com/czg/product/service/ConsStockFlowService.java +++ b/cash-common/cash-common-service/src/main/java/com/czg/product/service/ConsStockFlowService.java @@ -10,7 +10,6 @@ import com.czg.product.vo.ConsCheckStockRecordVo; import com.mybatisflex.core.paginate.Page; import com.mybatisflex.core.service.IService; -import java.io.InputStream; import java.util.List; /** @@ -25,7 +24,6 @@ public interface ConsStockFlowService extends IService { * 手动入库 * * @param param 手动出库入参 - * @return */ ConsInOutStockHeadParam inStock(ConsInOutStockHeadParam param); @@ -78,8 +76,4 @@ public interface ConsStockFlowService extends IService { * @param entity 库存变动记录实体 */ void saveFlow(ConsStockFlow entity); - - Integer ocr(String originalFilename, InputStream inputStream); - - ConsInOutStockHeadParam ocrDetail(Long id); } diff --git a/cash-common/cash-common-service/src/main/java/com/czg/product/service/MkOcrCountStickService.java b/cash-common/cash-common-service/src/main/java/com/czg/product/service/MkOcrCountStickService.java new file mode 100644 index 000000000..0088c4dfb --- /dev/null +++ b/cash-common/cash-common-service/src/main/java/com/czg/product/service/MkOcrCountStickService.java @@ -0,0 +1,15 @@ +package com.czg.product.service; + +import com.czg.product.entity.MkOcrCountStick; +import com.mybatisflex.core.service.IService; + +/** + * 数签子的外链渠道 服务层。 + * + * @author ww + * @since 2025-12-24 + */ +public interface MkOcrCountStickService extends IService { + + int getCountStick(byte[] stream, String fileName); +} diff --git a/cash-common/cash-common-tools/src/main/java/com/czg/utils/AliOcrUtil.java b/cash-common/cash-common-tools/src/main/java/com/czg/utils/AliOcrUtil.java index 9c0ad4583..9d75162bc 100644 --- a/cash-common/cash-common-tools/src/main/java/com/czg/utils/AliOcrUtil.java +++ b/cash-common/cash-common-tools/src/main/java/com/czg/utils/AliOcrUtil.java @@ -34,7 +34,6 @@ public class AliOcrUtil { *

使用凭据初始化账号Client

* * @return Client - * @throws Exception */ public static com.aliyun.bailian20231229.Client createClient() { @@ -114,7 +113,7 @@ public class AliOcrUtil { // 复制代码运行请自行打印 API 的返回值 DescribeFileResponse describeFileResponse = client.describeFileWithOptions("llm-9zg04s7wlbvi32tq", fileId, headers, runtime); - log.info("file status: {}", describeFileResponse.getBody()); + log.info("file status: {}", describeFileResponse.getStatusCode()); return describeFileResponse.getBody().getData() != null && "FILE_IS_READY".equals(describeFileResponse.getBody().getData().getStatus()); } catch (Exception error) { throw new RuntimeException(error); @@ -154,7 +153,8 @@ public class AliOcrUtil { } - public static String appCall(byte[] bytes, String fileName) { + //地址 https://bailian.console.aliyun.com + public static String appCall(byte[] bytes, String fileName, String detail) { String id; try { id = getSessionId(bytes, fileName); @@ -164,7 +164,7 @@ public class AliOcrUtil { ApplicationParam param = ApplicationParam.builder() .apiKey("sk-2343af4413834ad1ab43b036e3a903de") .appId("cd612ac509a4499f8ac68a656532d4ae") - .prompt("你是一名票据OCR结构化专家,请从我提供的票据图片中智能提取信息并只输出JSON,不得添加解释、不补充不存在内容、不得返回空字符串、字段缺失填null、字段不可省略、数字一律用字符串,使用以下固定JSON结构:{\"documentType\":\"\",\"orderNumber\":\"\",\"date\":\"\",\"customerName\":\"\",\"operator\":\"\",\"items\":[{\"conName\":\"\",\"spec\":\"\",\"unitName\":\"\",\"inOutNumber\":\"\",\"purchasePrice\":\"\",\"subTotal\":\"\"}],\"totalAmount\":\"\",\"remark\":\"\"}。字段映射规则:documentType对应单据类型/销售单/采购单/出货单;orderNumber对应单号/编号/No;date对应日期/开单日期;customerName对应客户名称/收货单位/供应商;operator对应业务员/经办人/制单人/操作员;items.conName对应品名/名称;items.spec对应规格/型号;items.unitName对应单位;items.inOutNumber对应数量;items.purchasePrice对应单价;items.subTotal对应金额/小计;totalAmount对应总金额/合计金额;remark对应备注。严禁生成图片中不存在的字段内容,看不清或未出现的字段必须为null,不允许推测或补全,不得生成多余字段;items只能根据识别到的行生成,不得虚构。必须能识别旋转、倾斜、模糊、撕裂、光照差异、列顺序混乱、无表格线等情况并尽量恢复信息。最终输出必须是纯JSON,不得包含任何非JSON字符。") + .prompt(detail) .ragOptions(RagOptions.builder() .sessionFileIds(List.of(id)) .build()) diff --git a/cash-service/account-service/src/main/java/com/czg/service/account/service/impl/ShopConfigServiceImpl.java b/cash-service/account-service/src/main/java/com/czg/service/account/service/impl/ShopConfigServiceImpl.java index 1d1917f10..d74b8b2be 100644 --- a/cash-service/account-service/src/main/java/com/czg/service/account/service/impl/ShopConfigServiceImpl.java +++ b/cash-service/account-service/src/main/java/com/czg/service/account/service/impl/ShopConfigServiceImpl.java @@ -1,16 +1,13 @@ package com.czg.service.account.service.impl; import cn.hutool.core.bean.BeanUtil; -import com.czg.account.dto.ShopConfigDTO; import com.czg.account.entity.ShopConfig; import com.czg.account.entity.ShopInfo; import com.czg.account.service.ShopConfigService; import com.czg.exception.CzgException; -import com.czg.sa.StpKit; import com.czg.service.RedisService; import com.czg.service.account.mapper.ShopConfigMapper; import com.czg.service.account.mapper.ShopInfoMapper; -import com.czg.utils.PageUtil; import com.mybatisflex.core.query.QueryWrapper; import com.mybatisflex.spring.service.impl.ServiceImpl; import jakarta.annotation.Resource; @@ -90,19 +87,10 @@ public class ShopConfigServiceImpl extends ServiceImpl { - or.eq(ShopConfig::getId, mainShopId); - }).or(or -> { - or.in(ShopConfig::getId, childShopIdList); - }).set(property, 0).update(); + updateChain().in(ShopConfig::getId, childShopIdList).set(property, 0).update(); }else { if ("all".equals(useShopType)) { - updateChain().or(or -> { - or.eq(ShopConfig::getId, mainShopId); - }).or(or -> { - or.in(ShopConfig::getId, childShopIdList); - }).set(property, 1).update(); + updateChain().in(ShopConfig::getId, childShopIdList).set(property, 1).update(); }else { if (shopIdList.isEmpty()) { updateChain().eq(ShopConfig::getId, mainShopId).set(property, 1).update(); @@ -115,7 +103,6 @@ public class ShopConfigServiceImpl extends ServiceImpl redisService.del("shopInfo::" + item)); redisService.del("shopInfo::" + mainShopId); } diff --git a/cash-service/account-service/src/main/java/com/czg/service/account/service/impl/UserAuthorizationServiceImpl.java b/cash-service/account-service/src/main/java/com/czg/service/account/service/impl/UserAuthorizationServiceImpl.java index ab6611451..9869b1c26 100644 --- a/cash-service/account-service/src/main/java/com/czg/service/account/service/impl/UserAuthorizationServiceImpl.java +++ b/cash-service/account-service/src/main/java/com/czg/service/account/service/impl/UserAuthorizationServiceImpl.java @@ -6,7 +6,6 @@ import com.alibaba.fastjson2.JSONObject; import com.czg.account.dto.auth.GetPhoneDTO; import com.czg.account.dto.auth.LoginTokenDTO; import com.czg.account.dto.auth.UserAuthorizationLoginDTO; -import com.czg.account.dto.auth.WechatRawDataDTO; import com.czg.account.entity.UserInfo; import com.czg.account.service.UserAuthorizationService; import com.czg.account.service.UserInfoService; @@ -95,19 +94,13 @@ public class UserAuthorizationServiceImpl implements UserAuthorizationService { openId = wechatAuthUtil.getSessionKeyOrOpenId(userAuthorizationLoginDTO.getCode(), false); userInfo = userInfoService.queryChain().eq(UserInfo::getWechatOpenId, openId).one(); userInfo = userInfo == null ? new UserInfo() : userInfo; - if (StrUtil.isNotBlank(userAuthorizationLoginDTO.getRawData())) { - WechatRawDataDTO wechatRawDataDTO = JSONObject.parseObject(userAuthorizationLoginDTO.getRawData(), WechatRawDataDTO.class); - userInfo.setHeadImg(wechatRawDataDTO.getAvatarUrl()); - userInfo.setNickName(StrUtil.isNotBlank(wechatRawDataDTO.getNickName()) ? wechatRawDataDTO.getNickName() : "微信用户"); - } else { - userInfo.setNickName("微信用户"); - } + userInfo.setNickName(StrUtil.isNotBlank(userInfo.getNickName()) ? userInfo.getNickName() : "微信用户"); userInfo.setWechatOpenId(openId); } else { openId = alipayUtil.getOpenId(userAuthorizationLoginDTO.getCode(), false); userInfo = userInfoService.queryChain().eq(UserInfo::getAlipayOpenId, openId).one(); userInfo = userInfo == null ? new UserInfo() : userInfo; - userInfo.setNickName("支付宝用户"); + userInfo.setNickName(StrUtil.isNotBlank(userInfo.getNickName()) ? userInfo.getNickName() : "支付宝用户"); userInfo.setAlipayOpenId(openId); } diff --git a/cash-service/account-service/src/main/java/com/czg/service/account/service/impl/UserInfoServiceImpl.java b/cash-service/account-service/src/main/java/com/czg/service/account/service/impl/UserInfoServiceImpl.java index 2b73fb7d0..70fc3679e 100644 --- a/cash-service/account-service/src/main/java/com/czg/service/account/service/impl/UserInfoServiceImpl.java +++ b/cash-service/account-service/src/main/java/com/czg/service/account/service/impl/UserInfoServiceImpl.java @@ -10,6 +10,7 @@ import com.czg.account.dto.user.userinfo.UserInfoEditDTO; import com.czg.account.dto.user.userinfo.UserInfoPwdEditDTO; import com.czg.account.entity.ShopUser; import com.czg.account.entity.UserInfo; +import com.czg.account.service.ShopInfoService; import com.czg.account.service.UserInfoService; import com.czg.config.RedisCst; import com.czg.exception.CzgException; @@ -38,6 +39,8 @@ public class UserInfoServiceImpl extends ServiceImpl i @Resource private ShopUserMapper shopUserMapper; @Resource + private ShopInfoService shopInfoService; + @Resource private RedisService redisService; @Resource private AcAccountUtil acAccountUtil; @@ -62,9 +65,10 @@ public class UserInfoServiceImpl extends ServiceImpl i UserInfo userInfo = getById(userId); BeanUtil.copyProperties(userInfoEditDTO, userInfo); if (updateById(userInfo)) { - if (shopId != -1L) { + if (shopId != 0L) { ShopUser shopUser = BeanUtil.copyProperties(userInfo, ShopUser.class); - return shopUserMapper.updateByQuery(shopUser, new QueryWrapper().eq(ShopUser::getSourceShopId, shopId).eq(ShopUser::getUserId, userId)) > 0; + Long mainIdByShopId = shopInfoService.getMainIdByShopId(shopId); + return shopUserMapper.updateByQuery(shopUser, new QueryWrapper().eq(ShopUser::getMainShopId, mainIdByShopId).eq(ShopUser::getUserId, userId)) > 0; } return true; } diff --git a/cash-service/product-service/src/main/java/com/czg/service/product/mapper/MkOcrCountStickMapper.java b/cash-service/product-service/src/main/java/com/czg/service/product/mapper/MkOcrCountStickMapper.java new file mode 100644 index 000000000..345d2aa26 --- /dev/null +++ b/cash-service/product-service/src/main/java/com/czg/service/product/mapper/MkOcrCountStickMapper.java @@ -0,0 +1,14 @@ +package com.czg.service.product.mapper; + +import com.mybatisflex.core.BaseMapper; +import com.czg.product.entity.MkOcrCountStick; + +/** + * 数签子的外链渠道 映射层。 + * + * @author ww + * @since 2025-12-24 + */ +public interface MkOcrCountStickMapper extends BaseMapper { + +} diff --git a/cash-service/product-service/src/main/java/com/czg/service/product/service/impl/ConsStockFlowServiceImpl.java b/cash-service/product-service/src/main/java/com/czg/service/product/service/impl/ConsStockFlowServiceImpl.java index b9a41b4ae..810521a0c 100644 --- a/cash-service/product-service/src/main/java/com/czg/service/product/service/impl/ConsStockFlowServiceImpl.java +++ b/cash-service/product-service/src/main/java/com/czg/service/product/service/impl/ConsStockFlowServiceImpl.java @@ -1,370 +1,279 @@ - package com.czg.service.product.service.impl; +package com.czg.service.product.service.impl; - import cn.hutool.core.bean.BeanUtil; - import cn.hutool.core.bean.copier.CopyOptions; - import cn.hutool.core.collection.CollUtil; - import cn.hutool.core.thread.ThreadUtil; - import cn.hutool.core.util.NumberUtil; - import cn.hutool.core.util.StrUtil; - import cn.hutool.crypto.digest.DigestUtil; - import com.alibaba.fastjson2.JSON; - import com.alibaba.fastjson2.JSONObject; - import com.czg.exception.CzgException; - import com.czg.market.entity.MkOcr; - import com.czg.product.entity.MkOcrService; - import com.czg.product.dto.ConsStockFlowDTO; - import com.czg.product.dto.SaleOrderDTO; - import com.czg.product.entity.ConsInfo; - import com.czg.product.entity.ConsStockFlow; - import com.czg.product.enums.InOutItemEnum; - import com.czg.product.enums.InOutTypeEnum; - import com.czg.product.param.*; - import com.czg.product.service.ConsStockFlowService; - import com.czg.product.vo.ConsCheckStockRecordVo; - import com.czg.sa.StpKit; - import com.czg.service.product.mapper.ConsInfoMapper; - import com.czg.service.product.mapper.ConsStockFlowMapper; - import com.czg.service.product.mapper.ProductMapper; - import com.czg.service.product.util.WxAccountUtil; - import com.czg.utils.AliOcrUtil; - import com.czg.utils.PageUtil; - import com.github.pagehelper.PageHelper; - import com.github.pagehelper.PageInfo; - import com.mybatisflex.core.paginate.Page; - import com.mybatisflex.core.query.QueryWrapper; - import com.mybatisflex.spring.service.impl.ServiceImpl; - import jakarta.annotation.Resource; - import lombok.AllArgsConstructor; - import lombok.extern.slf4j.Slf4j; - import org.springframework.stereotype.Service; - import org.springframework.transaction.annotation.Transactional; +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.bean.copier.CopyOptions; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.thread.ThreadUtil; +import cn.hutool.core.util.NumberUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson2.JSON; +import com.czg.exception.CzgException; +import com.czg.product.dto.ConsStockFlowDTO; +import com.czg.product.entity.ConsInfo; +import com.czg.product.entity.ConsStockFlow; +import com.czg.product.enums.InOutItemEnum; +import com.czg.product.enums.InOutTypeEnum; +import com.czg.product.param.*; +import com.czg.product.service.ConsStockFlowService; +import com.czg.product.vo.ConsCheckStockRecordVo; +import com.czg.sa.StpKit; +import com.czg.service.product.mapper.ConsInfoMapper; +import com.czg.service.product.mapper.ConsStockFlowMapper; +import com.czg.service.product.mapper.ProductMapper; +import com.czg.service.product.util.WxAccountUtil; +import com.czg.utils.PageUtil; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import com.mybatisflex.core.paginate.Page; +import com.mybatisflex.core.query.QueryWrapper; +import com.mybatisflex.spring.service.impl.ServiceImpl; +import jakarta.annotation.Resource; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; - import java.io.IOException; - import java.io.InputStream; - import java.math.BigDecimal; - import java.time.LocalDate; - import java.util.*; - import java.util.stream.Collectors; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; - /** - * 耗材库存变动记录 - * - * @author Tankaikai tankaikai@aliyun.com - * @since 1.0 2025-02-21 - */ - @AllArgsConstructor - @Service - @Slf4j - public class ConsStockFlowServiceImpl extends ServiceImpl implements ConsStockFlowService { +/** + * 耗材库存变动记录 + * + * @author Tankaikai tankaikai@aliyun.com + * @since 1.0 2025-02-21 + */ +@AllArgsConstructor +@Service +@Slf4j +public class ConsStockFlowServiceImpl extends ServiceImpl implements ConsStockFlowService { - private final ConsInfoMapper consInfoMapper; - private final ProductMapper productMapper; - @Resource - private WxAccountUtil wxAccountUtil; - @Resource - private MkOcrService ocrService; + private final ConsInfoMapper consInfoMapper; + private final ProductMapper productMapper; + @Resource + private WxAccountUtil wxAccountUtil; - private QueryWrapper buildQueryWrapper(ConsStockFlowDTO param) { - QueryWrapper queryWrapper = PageUtil.buildSortQueryWrapper(); + private QueryWrapper buildQueryWrapper(ConsStockFlowDTO param) { + QueryWrapper queryWrapper = PageUtil.buildSortQueryWrapper(); /*if (StrUtil.isNotEmpty(param.getName())) { queryWrapper.like(ConsStockFlow::getName, param.getName()); }*/ - Long shopId = StpKit.USER.getShopId(0L); - queryWrapper.eq(ConsStockFlow::getShopId, shopId); - queryWrapper.orderBy(ConsStockFlow::getId, false); - return queryWrapper; - } + Long shopId = StpKit.USER.getShopId(0L); + queryWrapper.eq(ConsStockFlow::getShopId, shopId); + queryWrapper.orderBy(ConsStockFlow::getId, false); + return queryWrapper; + } - @Override - @Transactional(rollbackFor = Exception.class) - public ConsInOutStockHeadParam inStock(ConsInOutStockHeadParam param) { - Long shopId = StpKit.USER.getShopId(0L); - Long createUserId = StpKit.USER.getLoginIdAsLong(); - String createUserName = StpKit.USER.getAccount(); - ConsStockFlow head = BeanUtil.copyProperties(param, ConsStockFlow.class); - List entityList = BeanUtil.copyToList(param.getBodyList(), ConsStockFlow.class); - List insertList = new ArrayList<>(); - List updateStockList = new ArrayList<>(); - for (ConsInOutStockBodyParam entity : param.getBodyList()) { + @Override + @Transactional(rollbackFor = Exception.class) + public ConsInOutStockHeadParam inStock(ConsInOutStockHeadParam param) { + Long shopId = StpKit.USER.getShopId(0L); + Long createUserId = StpKit.USER.getLoginIdAsLong(); + String createUserName = StpKit.USER.getAccount(); + ConsStockFlow head = BeanUtil.copyProperties(param, ConsStockFlow.class); + List entityList = BeanUtil.copyToList(param.getBodyList(), ConsStockFlow.class); + List insertList = new ArrayList<>(); + List updateStockList = new ArrayList<>(); + for (ConsInOutStockBodyParam entity : param.getBodyList()) { - ConsStockFlow consStockFlow = BeanUtil.copyProperties(entity, ConsStockFlow.class); - BeanUtil.copyProperties(head, entity, CopyOptions.create().ignoreNullValue()); - consStockFlow.setShopId(shopId); - consStockFlow.setInOutType(InOutTypeEnum.IN.value()); - consStockFlow.setInOutItem(InOutItemEnum.MANUAL_IN.value()); - consStockFlow.setCreateUserId(createUserId); - consStockFlow.setCreateUserName(createUserName); - consStockFlow.setVendorId(param.getVendorId()); - String conId = entity.getConId(); - ConsInfo consInfo; - if (StrUtil.isBlank(entity.getConId())) { - consInfo = consInfoMapper.selectOneByQuery(new QueryWrapper().like(ConsInfo::getConName, entity.getConName()) - .eq(ConsInfo::getShopId, shopId).limit(1)); - if (consInfo == null) { - entity.setFailReason("耗材不存在"); - }else if (!consInfo.getConUnit().equals(entity.getUnitName())) { - entity.setFailReason("耗材单位不匹配"); - consInfo = null; - } - - entity.setConId(consInfo == null ? null : consInfo.getId().toString()); - consStockFlow.setConId(consInfo == null ? null : consInfo.getId()); - }else { - consInfo = consInfoMapper.selectOneById(conId); - } + ConsStockFlow consStockFlow = BeanUtil.copyProperties(entity, ConsStockFlow.class); + BeanUtil.copyProperties(head, entity, CopyOptions.create().ignoreNullValue()); + consStockFlow.setShopId(shopId); + consStockFlow.setInOutType(InOutTypeEnum.IN.value()); + consStockFlow.setInOutItem(InOutItemEnum.MANUAL_IN.value()); + consStockFlow.setCreateUserId(createUserId); + consStockFlow.setCreateUserName(createUserName); + consStockFlow.setVendorId(param.getVendorId()); + String conId = entity.getConId(); + ConsInfo consInfo; + if (StrUtil.isBlank(entity.getConId())) { + consInfo = consInfoMapper.selectOneByQuery(new QueryWrapper().like(ConsInfo::getConName, entity.getConName()) + .eq(ConsInfo::getShopId, shopId).limit(1)); if (consInfo == null) { - continue; + entity.setFailReason("耗材不存在"); + } else if (!consInfo.getConUnit().equals(entity.getUnitName())) { + entity.setFailReason("耗材单位不匹配"); + consInfo = null; } - consStockFlow.setBeforeNumber(consInfo.getStockNumber()); - consStockFlow.setAfterNumber(NumberUtil.add(consStockFlow.getBeforeNumber(), entity.getInOutNumber())); - insertList.add(consStockFlow); - consInfo.setStockNumber(consStockFlow.getAfterNumber()); - updateStockList.add(consInfo); - } - if (!insertList.isEmpty()) { - mapper.insertBatchSelective(insertList, 50); - } - for (ConsInfo consInfo : updateStockList) { - consInfoMapper.update(consInfo); - } - return param; + entity.setConId(consInfo == null ? null : consInfo.getId().toString()); + consStockFlow.setConId(consInfo == null ? null : consInfo.getId()); + } else { + consInfo = consInfoMapper.selectOneById(conId); + } + if (consInfo == null) { + continue; + } + consStockFlow.setBeforeNumber(consInfo.getStockNumber()); + consStockFlow.setAfterNumber(NumberUtil.add(consStockFlow.getBeforeNumber(), entity.getInOutNumber())); + insertList.add(consStockFlow); + consInfo.setStockNumber(consStockFlow.getAfterNumber()); + updateStockList.add(consInfo); + } + if (!insertList.isEmpty()) { + mapper.insertBatchSelective(insertList, 50); + } + for (ConsInfo consInfo : updateStockList) { + consInfoMapper.update(consInfo); } - @Override - @Transactional(rollbackFor = Exception.class) - public void outStock(ConsInOutStockHeadParam param) { - Long shopId = StpKit.USER.getShopId(0L); - Long createUserId = StpKit.USER.getLoginIdAsLong(); - String createUserName = StpKit.USER.getAccount(); - ConsStockFlow head = BeanUtil.copyProperties(param, ConsStockFlow.class); - List entityList = BeanUtil.copyToList(param.getBodyList(), ConsStockFlow.class); - List insertList = new ArrayList<>(); - List updateStockList = new ArrayList<>(); - for (ConsStockFlow entity : entityList) { - BeanUtil.copyProperties(head, entity, CopyOptions.create().ignoreNullValue()); - entity.setInOutNumber(NumberUtil.sub(BigDecimal.ZERO, entity.getInOutNumber())); - entity.setShopId(shopId); - entity.setInOutType(InOutTypeEnum.OUT.value()); - entity.setInOutItem(InOutItemEnum.MANUAL_OUT.value()); - entity.setCreateUserId(createUserId); - entity.setCreateUserName(createUserName); - Long conId = entity.getConId(); - ConsInfo consInfo = consInfoMapper.selectOneById(conId); - if (consInfo == null) { - throw new CzgException(StrUtil.format("耗材{}不存在", entity.getConName())); - } - entity.setBeforeNumber(consInfo.getStockNumber()); - entity.setAfterNumber(NumberUtil.add(entity.getBeforeNumber(), entity.getInOutNumber())); - insertList.add(entity); - consInfo.setStockNumber(entity.getAfterNumber()); - updateStockList.add(consInfo); - } - mapper.insertBatchSelective(insertList, 50); - for (ConsInfo consInfo : updateStockList) { - consInfoMapper.update(consInfo); - } - } + return param; + } - @Override - @Transactional(rollbackFor = Exception.class) - public void checkStock(ConsCheckStockParam param) { - Long shopId = StpKit.USER.getShopId(0L); - Long createUserId = StpKit.USER.getLoginIdAsLong(); - String createUserName = StpKit.USER.getAccount(); - ConsStockFlow entity = new ConsStockFlow(); + @Override + @Transactional(rollbackFor = Exception.class) + public void outStock(ConsInOutStockHeadParam param) { + Long shopId = StpKit.USER.getShopId(0L); + Long createUserId = StpKit.USER.getLoginIdAsLong(); + String createUserName = StpKit.USER.getAccount(); + ConsStockFlow head = BeanUtil.copyProperties(param, ConsStockFlow.class); + List entityList = BeanUtil.copyToList(param.getBodyList(), ConsStockFlow.class); + List insertList = new ArrayList<>(); + List updateStockList = new ArrayList<>(); + for (ConsStockFlow entity : entityList) { + BeanUtil.copyProperties(head, entity, CopyOptions.create().ignoreNullValue()); + entity.setInOutNumber(NumberUtil.sub(BigDecimal.ZERO, entity.getInOutNumber())); + entity.setShopId(shopId); + entity.setInOutType(InOutTypeEnum.OUT.value()); + entity.setInOutItem(InOutItemEnum.MANUAL_OUT.value()); entity.setCreateUserId(createUserId); entity.setCreateUserName(createUserName); - entity.setShopId(shopId); - entity.setConId(param.getConId()); - entity.setConName(param.getConName()); - entity.setPurchasePrice(param.getPrice()); - ConsInfo consInfo = consInfoMapper.selectOneById(param.getConId()); + Long conId = entity.getConId(); + ConsInfo consInfo = consInfoMapper.selectOneById(conId); if (consInfo == null) { throw new CzgException(StrUtil.format("耗材{}不存在", entity.getConName())); } - BigDecimal winLossNumber = NumberUtil.sub(param.getActualNumber(), param.getStockNumber()); - if (!NumberUtil.equals(winLossNumber, param.getWinLossNumber())) { - throw new CzgException(StrUtil.format("耗材{}库存在发生变动,请刷新后重试", entity.getConName())); - } entity.setBeforeNumber(consInfo.getStockNumber()); - entity.setInOutNumber(winLossNumber); entity.setAfterNumber(NumberUtil.add(entity.getBeforeNumber(), entity.getInOutNumber())); - if (NumberUtil.isLess(winLossNumber, BigDecimal.ZERO)) { - entity.setInOutType(InOutTypeEnum.OUT.value()); - entity.setInOutItem(InOutItemEnum.LOSS_OUT.value()); - } else { - entity.setInOutType(InOutTypeEnum.IN.value()); - entity.setInOutItem(InOutItemEnum.WIN_IN.value()); - } - entity.setSubTotal(NumberUtil.mul(winLossNumber, param.getPrice())); - entity.setRemark(param.getRemark()); - saveFlow(entity); + insertList.add(entity); consInfo.setStockNumber(entity.getAfterNumber()); + updateStockList.add(consInfo); + } + mapper.insertBatchSelective(insertList, 50); + for (ConsInfo consInfo : updateStockList) { consInfoMapper.update(consInfo); } - - @Override - public Page getCheckStockRecordPage(Long conId) { - Long shopId = StpKit.USER.getShopId(0L); - return super.pageAs(PageUtil.buildPage(), query().eq(ConsStockFlow::getShopId, shopId).eq(ConsStockFlow::getConId, conId).orderBy(ConsStockFlow::getId, false), ConsCheckStockRecordVo.class); - } - - @Override - public List getCheckStockRecordList(Long conId) { - Long shopId = StpKit.USER.getShopId(0L); - return super.mapper.selectListByQueryAs(query().eq(ConsStockFlow::getShopId, shopId).eq(ConsStockFlow::getConId, conId).orderBy(ConsStockFlow::getId, false), ConsCheckStockRecordVo.class); - } - - @Override - public void reportDamage(ConsReportDamageParam param) { - Long shopId = StpKit.USER.getShopId(0L); - Long createUserId = StpKit.USER.getLoginIdAsLong(); - String createUserName = StpKit.USER.getAccount(); - ConsInfo consInfo = consInfoMapper.selectOneById(param.getConId()); - if (consInfo == null) { - throw new CzgException("耗材不存在"); - } - ConsStockFlow entity = new ConsStockFlow(); - entity.setCreateUserId(createUserId); - entity.setCreateUserName(createUserName); - entity.setShopId(shopId); - entity.setConId(param.getConId()); - entity.setConName(consInfo.getConName()); - entity.setPurchasePrice(consInfo.getPrice()); - BigDecimal balance = NumberUtil.sub(consInfo.getStockNumber(), param.getNumber()); - if (NumberUtil.isLess(balance, BigDecimal.ZERO)) { - throw new CzgException(StrUtil.format("耗材{}报损数量不能大于当前库存{}", entity.getConName(), consInfo.getStockNumber())); - } - entity.setBeforeNumber(consInfo.getStockNumber()); - entity.setInOutNumber(NumberUtil.sub(BigDecimal.ZERO, param.getNumber())); - entity.setAfterNumber(balance); - entity.setInOutType(InOutTypeEnum.OUT.value()); - entity.setInOutItem(InOutItemEnum.DAMAGE_OUT.value()); - entity.setSubTotal(NumberUtil.mul(param.getNumber(), consInfo.getPrice())); - entity.setImgUrls(JSON.toJSONString(param.getImgUrls())); - saveFlow(entity); - consInfo.setStockNumber(entity.getAfterNumber()); - consInfoMapper.update(consInfo); - } - - @Override - public Page findConsStockFlowPage(ConsStockFlowParam param) { - Long shopId = StpKit.USER.getShopId(0L); - PageHelper.startPage(PageUtil.buildPageHelp()); - return PageUtil.convert(new PageInfo<>(mapper.findConsStockFlowPage(shopId, param.getInOutType(), param.getInOutItem(), param.getConId()))); - } - - @Override - public void saveFlow(ConsStockFlow entity) { - super.save(entity); - Long shopId = entity.getShopId(); - BigDecimal afterNumber = entity.getAfterNumber(); - ConsInfo consInfo = consInfoMapper.selectOneById(entity.getConId()); - String shopName = ""; - try { - shopName = StpKit.USER.getShopName(); - } catch (Exception e) { - log.error("获取店铺名称失败"); - } - if (StrUtil.isEmpty(shopName)) { - shopName = productMapper.getShopName(shopId); - } - BigDecimal conWarning = consInfo.getConWarning(); - // 库存小于警告值,发送消息提醒 - if (NumberUtil.isLess(afterNumber, conWarning)) { - List openIdList = consInfoMapper.findOpenIdList(shopId, "con"); - if (CollUtil.isEmpty(openIdList)) { - return; - } - String conName = StrUtil.format("{}数量<预警值{}", consInfo.getConName(), conWarning); - String finalShopName = shopName; - ThreadUtil.execAsync(() -> { - openIdList.parallelStream().forEach(openId -> { - wxAccountUtil.sendStockMsg(finalShopName, conName, afterNumber, openId); - }); - }); - } - } - - @Override - public Integer ocr(String originalFilename, InputStream inputStream) { - Long shopId = StpKit.USER.getShopId(); - byte[] readAllBytes = null; - try { - readAllBytes = inputStream.readAllBytes(); - } catch (IOException e) { - throw new RuntimeException(e); - } - String md5 = DigestUtil.md5Hex(readAllBytes); - MkOcr ocr = ocrService.getOne(new QueryWrapper().eq(MkOcr::getShopId, shopId).eq(MkOcr::getMd5, md5)); - if (ocr != null) { - return ocr.getId(); - } - MkOcr mkOcr = new MkOcr(); - mkOcr.setShopId(shopId); - mkOcr.setMd5(md5); - ocrService.save(mkOcr); - byte[] finalReadAllBytes1 = readAllBytes; - ThreadUtil.execAsync(() -> { - try { - String infoStr = AliOcrUtil.appCall(finalReadAllBytes1, originalFilename); - SaleOrderDTO saleOrderDTO = JSONObject.parseObject(infoStr, SaleOrderDTO.class); - - ArrayList bodyList = new ArrayList<>(); - Set nameList = saleOrderDTO.getItems().stream().map(SaleOrderDTO.Item::getConName).collect(Collectors.toSet()); - Map consInfoMap = new HashMap<>(); - if (!nameList.isEmpty()) { - consInfoMap = consInfoMapper.selectListByQuery(new QueryWrapper().in(ConsInfo::getConName, nameList).eq(ConsInfo::getShopId, shopId)) - .stream().collect(Collectors.toMap(ConsInfo::getConName, consInfo -> consInfo)); - } - ArrayList unInCons = new ArrayList<>(); - for (SaleOrderDTO.Item item : saleOrderDTO.getItems()) { - ConsInOutStockBodyParam bodyParam = new ConsInOutStockBodyParam() - .setConName(item.getConName()) - .setPurchasePrice(new BigDecimal(item.getPurchasePrice())) - .setUnitName(item.getUnitName()) - .setSubTotal(new BigDecimal(item.getSubTotal())) - .setInOutNumber(new BigDecimal(item.getInOutNumber())); - ConsInfo consInfo = consInfoMap.get(item.getConName()); - if (consInfo != null) { - unInCons.add(item); - bodyParam.setConId(consInfo.getId().toString()); - } - bodyList.add(bodyParam); - } - - ConsInOutStockHeadParam headParam = new ConsInOutStockHeadParam(); - headParam.setBatchNo(saleOrderDTO.getOrderNumber()) - .setInOutDate(LocalDate.parse(saleOrderDTO.getDate())) - .setAmountPayable(new BigDecimal(saleOrderDTO.getTotalAmount())) - .setBodyList(bodyList) - .setUnInCons(unInCons) - .setOcrSaleOrder(saleOrderDTO); - - - mkOcr.setStatus("SUCCESS"); - mkOcr.setResp(JSON.toJSONString(headParam)); - }catch (Exception e) { - mkOcr.setErr(e.getMessage()); - mkOcr.setStatus("FAILED"); - log.error("ocr失败:", e); - }finally { - ocrService.updateById(mkOcr); - } - - }); - - return mkOcr.getId(); - } - - @Override - public ConsInOutStockHeadParam ocrDetail(Long id) { - MkOcr mkOcr = ocrService.getOne(new QueryWrapper().eq(MkOcr::getShopId, StpKit.USER.getShopId()).eq(MkOcr::getId, id)); - if (StrUtil.isNotBlank(mkOcr.getResp())) { - return JSONObject.parseObject(mkOcr.getResp(), ConsInOutStockHeadParam.class); - } - return null; - } } + + @Override + @Transactional(rollbackFor = Exception.class) + public void checkStock(ConsCheckStockParam param) { + Long shopId = StpKit.USER.getShopId(0L); + Long createUserId = StpKit.USER.getLoginIdAsLong(); + String createUserName = StpKit.USER.getAccount(); + ConsStockFlow entity = new ConsStockFlow(); + entity.setCreateUserId(createUserId); + entity.setCreateUserName(createUserName); + entity.setShopId(shopId); + entity.setConId(param.getConId()); + entity.setConName(param.getConName()); + entity.setPurchasePrice(param.getPrice()); + ConsInfo consInfo = consInfoMapper.selectOneById(param.getConId()); + if (consInfo == null) { + throw new CzgException(StrUtil.format("耗材{}不存在", entity.getConName())); + } + BigDecimal winLossNumber = NumberUtil.sub(param.getActualNumber(), param.getStockNumber()); + if (!NumberUtil.equals(winLossNumber, param.getWinLossNumber())) { + throw new CzgException(StrUtil.format("耗材{}库存在发生变动,请刷新后重试", entity.getConName())); + } + entity.setBeforeNumber(consInfo.getStockNumber()); + entity.setInOutNumber(winLossNumber); + entity.setAfterNumber(NumberUtil.add(entity.getBeforeNumber(), entity.getInOutNumber())); + if (NumberUtil.isLess(winLossNumber, BigDecimal.ZERO)) { + entity.setInOutType(InOutTypeEnum.OUT.value()); + entity.setInOutItem(InOutItemEnum.LOSS_OUT.value()); + } else { + entity.setInOutType(InOutTypeEnum.IN.value()); + entity.setInOutItem(InOutItemEnum.WIN_IN.value()); + } + entity.setSubTotal(NumberUtil.mul(winLossNumber, param.getPrice())); + entity.setRemark(param.getRemark()); + saveFlow(entity); + consInfo.setStockNumber(entity.getAfterNumber()); + consInfoMapper.update(consInfo); + } + + @Override + public Page getCheckStockRecordPage(Long conId) { + Long shopId = StpKit.USER.getShopId(0L); + return super.pageAs(PageUtil.buildPage(), query().eq(ConsStockFlow::getShopId, shopId).eq(ConsStockFlow::getConId, conId).orderBy(ConsStockFlow::getId, false), ConsCheckStockRecordVo.class); + } + + @Override + public List getCheckStockRecordList(Long conId) { + Long shopId = StpKit.USER.getShopId(0L); + return super.mapper.selectListByQueryAs(query().eq(ConsStockFlow::getShopId, shopId).eq(ConsStockFlow::getConId, conId).orderBy(ConsStockFlow::getId, false), ConsCheckStockRecordVo.class); + } + + @Override + public void reportDamage(ConsReportDamageParam param) { + Long shopId = StpKit.USER.getShopId(0L); + Long createUserId = StpKit.USER.getLoginIdAsLong(); + String createUserName = StpKit.USER.getAccount(); + ConsInfo consInfo = consInfoMapper.selectOneById(param.getConId()); + if (consInfo == null) { + throw new CzgException("耗材不存在"); + } + ConsStockFlow entity = new ConsStockFlow(); + entity.setCreateUserId(createUserId); + entity.setCreateUserName(createUserName); + entity.setShopId(shopId); + entity.setConId(param.getConId()); + entity.setConName(consInfo.getConName()); + entity.setPurchasePrice(consInfo.getPrice()); + BigDecimal balance = NumberUtil.sub(consInfo.getStockNumber(), param.getNumber()); + if (NumberUtil.isLess(balance, BigDecimal.ZERO)) { + throw new CzgException(StrUtil.format("耗材{}报损数量不能大于当前库存{}", entity.getConName(), consInfo.getStockNumber())); + } + entity.setBeforeNumber(consInfo.getStockNumber()); + entity.setInOutNumber(NumberUtil.sub(BigDecimal.ZERO, param.getNumber())); + entity.setAfterNumber(balance); + entity.setInOutType(InOutTypeEnum.OUT.value()); + entity.setInOutItem(InOutItemEnum.DAMAGE_OUT.value()); + entity.setSubTotal(NumberUtil.mul(param.getNumber(), consInfo.getPrice())); + entity.setImgUrls(JSON.toJSONString(param.getImgUrls())); + saveFlow(entity); + consInfo.setStockNumber(entity.getAfterNumber()); + consInfoMapper.update(consInfo); + } + + @Override + public Page findConsStockFlowPage(ConsStockFlowParam param) { + Long shopId = StpKit.USER.getShopId(0L); + PageHelper.startPage(PageUtil.buildPageHelp()); + return PageUtil.convert(new PageInfo<>(mapper.findConsStockFlowPage(shopId, param.getInOutType(), param.getInOutItem(), param.getConId()))); + } + + @Override + public void saveFlow(ConsStockFlow entity) { + super.save(entity); + Long shopId = entity.getShopId(); + BigDecimal afterNumber = entity.getAfterNumber(); + ConsInfo consInfo = consInfoMapper.selectOneById(entity.getConId()); + String shopName = ""; + try { + shopName = StpKit.USER.getShopName(); + } catch (Exception e) { + log.error("获取店铺名称失败"); + } + if (StrUtil.isEmpty(shopName)) { + shopName = productMapper.getShopName(shopId); + } + BigDecimal conWarning = consInfo.getConWarning(); + // 库存小于警告值,发送消息提醒 + if (NumberUtil.isLess(afterNumber, conWarning)) { + List openIdList = consInfoMapper.findOpenIdList(shopId, "con"); + if (CollUtil.isEmpty(openIdList)) { + return; + } + String conName = StrUtil.format("{}数量<预警值{}", consInfo.getConName(), conWarning); + String finalShopName = shopName; + ThreadUtil.execAsync(() -> { + openIdList.parallelStream().forEach(openId -> { + wxAccountUtil.sendStockMsg(finalShopName, conName, afterNumber, openId); + }); + }); + } + } +} diff --git a/cash-service/product-service/src/main/java/com/czg/service/product/service/impl/MkOcrCountStickServiceImpl.java b/cash-service/product-service/src/main/java/com/czg/service/product/service/impl/MkOcrCountStickServiceImpl.java new file mode 100644 index 000000000..545432ac0 --- /dev/null +++ b/cash-service/product-service/src/main/java/com/czg/service/product/service/impl/MkOcrCountStickServiceImpl.java @@ -0,0 +1,72 @@ +package com.czg.service.product.service.impl; + +import cn.hutool.core.codec.Base64; +import cn.hutool.core.util.StrUtil; +import com.czg.constants.SystemConstants; +import com.czg.product.entity.MkOcrCountStick; +import com.czg.product.service.MkOcrCountStickService; +import com.czg.service.RedisService; +import com.czg.service.product.mapper.MkOcrCountStickMapper; +import com.czg.service.product.util.AISmartCountUtils; +import com.czg.service.product.util.MuSmartCountUtils; +import com.mybatisflex.spring.service.impl.ServiceImpl; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 数签子的外链渠道 服务层实现。 + * + * @author ww + * @since 2025-12-24 + */ +@Slf4j +@Service +public class MkOcrCountStickServiceImpl extends ServiceImpl implements MkOcrCountStickService { + + @Resource + private RedisService redisService; + + @Override + public int getCountStick(byte[] fileByte, String fileName) { + List list = list(query().eq(MkOcrCountStick::getStatus, SystemConstants.OneZero.ONE).orderBy(MkOcrCountStick::getSort, true)); + for (MkOcrCountStick mkOcrCountStick : list) { + if ("znds".equals(mkOcrCountStick.getMark())) { + try { + String tickCount = AISmartCountUtils.getTickCount(mkOcrCountStick.getToken(), fileByte, fileName); + if (StrUtil.isNotBlank(tickCount)) { + return Integer.parseInt(tickCount); + } + } catch (Exception e) { + mkOcrCountStick.setStatus(SystemConstants.OneZero.ZERO); + log.error("智能点数失败", e); + updateById(mkOcrCountStick); + } + } else if ("ygmh".equals(mkOcrCountStick.getMark())) { + try { + String token = getToken(mkOcrCountStick.getAccount(), mkOcrCountStick.getPwd()); + return MuSmartCountUtils.detectBambooStick(Base64.encode(fileByte), token); + } catch (Exception e) { + mkOcrCountStick.setStatus(SystemConstants.OneZero.ZERO); + log.error("智能点数失败", e); + updateById(mkOcrCountStick); + } + } + } + return 0; + } + + // 一个木涵 获取token + private String getToken(String username, String password) { + Object token = redisService.get("ocr:ygmh:token"); + if (token != null) { + return token.toString(); + } + String muToken = MuSmartCountUtils.login(username, password); + //时间为3个月减去60秒 + redisService.set("ocr:ygmh:token", muToken, 7775940); + return muToken; + } +} diff --git a/cash-service/product-service/src/main/java/com/czg/service/product/service/impl/MkOcrServiceImpl.java b/cash-service/product-service/src/main/java/com/czg/service/product/service/impl/MkOcrServiceImpl.java index c8a13e56d..1cf752e47 100644 --- a/cash-service/product-service/src/main/java/com/czg/service/product/service/impl/MkOcrServiceImpl.java +++ b/cash-service/product-service/src/main/java/com/czg/service/product/service/impl/MkOcrServiceImpl.java @@ -1,18 +1,148 @@ package com.czg.service.product.service.impl; +import cn.hutool.core.thread.ThreadUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.czg.product.dto.SaleOrderDTO; +import com.czg.product.entity.ConsInfo; +import com.czg.product.param.ConsInOutStockBodyParam; +import com.czg.product.param.ConsInOutStockHeadParam; +import com.czg.sa.StpKit; +import com.czg.service.product.mapper.ConsInfoMapper; import com.czg.service.product.mapper.MkOcrMapper; +import com.czg.utils.AliOcrUtil; +import com.mybatisflex.core.query.QueryWrapper; import com.mybatisflex.spring.service.impl.ServiceImpl; import com.czg.market.entity.MkOcr; import com.czg.product.entity.MkOcrService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + /** * ocr识别结果 服务层实现。 * * @author zs * @since 2025-11-26 */ +@Slf4j @Service -public class MkOcrServiceImpl extends ServiceImpl implements MkOcrService{ +public class MkOcrServiceImpl extends ServiceImpl implements MkOcrService { + + @Resource + private ConsInfoMapper consInfoMapper; + + String consOcrDetail = "你是一名票据OCR结构化专家,请从我提供的票据图片中智能提取信息并只输出JSON,不得添加解释、不补充不存在内容、不得返回空字符串、字段缺失填null、字段不可省略、数字一律用字符串," + + "使用以下固定JSON结构:{\"documentType\":\"\",\"orderNumber\":\"\",\"date\":\"\",\"customerName\":\"\",\"operator\":\"\"," + + "\"items\":[{\"conName\":\"\",\"spec\":\"\",\"unitName\":\"\",\"inOutNumber\":\"\",\"purchasePrice\":\"\",\"subTotal\":\"\"}]," + + "\"totalAmount\":\"\",\"remark\":\"\"}。字段映射规则:documentType对应单据类型/销售单/采购单/出货单;orderNumber对应单号/编号/No;" + + "date对应日期/开单日期;customerName对应客户名称/收货单位/供应商;operator对应业务员/经办人/制单人/操作员;items.conName对应品名/名称;" + + "items.spec对应规格/型号;items.unitName对应单位;items.inOutNumber对应数量;items.purchasePrice对应单价;items.subTotal对应金额/小计;" + + "totalAmount对应总金额/合计金额;remark对应备注。严禁生成图片中不存在的字段内容,看不清或未出现的字段必须为null,不允许推测或补全,不得生成多余字段;" + + "items只能根据识别到的行生成,不得虚构。必须能识别旋转、倾斜、模糊、撕裂、光照差异、列顺序混乱、无表格线等情况并尽量恢复信息。" + + "最终输出必须是纯JSON,不得包含任何非JSON字符。"; + + @Override + public ConsInOutStockHeadParam ocrDetail(Long id) { + MkOcr mkOcr = getOne(new QueryWrapper().eq(MkOcr::getShopId, StpKit.USER.getShopId()).eq(MkOcr::getId, id)); + if (StrUtil.isNotBlank(mkOcr.getResp())) { + return JSONObject.parseObject(mkOcr.getResp(), ConsInOutStockHeadParam.class); + } + return null; + } + + @Override + public Integer ocr(String originalFilename, InputStream inputStream, String type) { + Long shopId = StpKit.USER.getShopId(); + byte[] readAllBytes = null; + try { + readAllBytes = inputStream.readAllBytes(); + } catch (IOException e) { + throw new RuntimeException(e); + } + String md5 = DigestUtil.md5Hex(readAllBytes); + MkOcr ocr = getOne(new QueryWrapper().eq(MkOcr::getShopId, shopId).eq(MkOcr::getMd5, md5)); + if (ocr != null) { + return ocr.getId(); + } + MkOcr mkOcr = new MkOcr(); + mkOcr.setShopId(shopId); + mkOcr.setMd5(md5); + save(mkOcr); + byte[] finalReadAllBytes1 = readAllBytes; + ThreadUtil.execAsync(() -> { + try { + if ("cons".equals(type)) { + mkOcr.setResp(ocrCons(finalReadAllBytes1, originalFilename, shopId)); + } else { + + } + mkOcr.setStatus("SUCCESS"); + } catch (Exception e) { + mkOcr.setErr(e.getMessage()); + mkOcr.setStatus("FAILED"); + log.error("ocr失败:", e); + } finally { + updateById(mkOcr); + } + + }); + + return mkOcr.getId(); + } + + + /** + * 识别 入库单 + */ + private String ocrCons(byte[] finalReadAllBytes1, String originalFilename, Long shopId) { + String infoStr = AliOcrUtil.appCall(finalReadAllBytes1, originalFilename, consOcrDetail); + SaleOrderDTO saleOrderDTO = JSONObject.parseObject(infoStr, SaleOrderDTO.class); + + ArrayList bodyList = new ArrayList<>(); + Set nameList = saleOrderDTO.getItems().stream().map(SaleOrderDTO.Item::getConName).collect(Collectors.toSet()); + Map consInfoMap = new HashMap<>(); + if (!nameList.isEmpty()) { + consInfoMap = consInfoMapper.selectListByQuery(new QueryWrapper().in(ConsInfo::getConName, nameList).eq(ConsInfo::getShopId, shopId)) + .stream().collect(Collectors.toMap(ConsInfo::getConName, consInfo -> consInfo)); + } + ArrayList unInCons = new ArrayList<>(); + for (SaleOrderDTO.Item item : saleOrderDTO.getItems()) { + ConsInOutStockBodyParam bodyParam = new ConsInOutStockBodyParam() + .setConName(item.getConName()) + .setPurchasePrice(new BigDecimal(item.getPurchasePrice())) + .setUnitName(item.getUnitName()) + .setSubTotal(new BigDecimal(item.getSubTotal())) + .setInOutNumber(new BigDecimal(item.getInOutNumber())); + ConsInfo consInfo = consInfoMap.get(item.getConName()); + if (consInfo != null) { + unInCons.add(item); + bodyParam.setConId(consInfo.getId().toString()); + } + bodyList.add(bodyParam); + } + + ConsInOutStockHeadParam headParam = new ConsInOutStockHeadParam(); + headParam.setBatchNo(saleOrderDTO.getOrderNumber()) + .setInOutDate(LocalDate.parse(saleOrderDTO.getDate())) + .setAmountPayable(new BigDecimal(saleOrderDTO.getTotalAmount())) + .setBodyList(bodyList) + .setUnInCons(unInCons) + .setOcrSaleOrder(saleOrderDTO); + return JSON.toJSONString(headParam); + } + } diff --git a/cash-service/product-service/src/main/java/com/czg/service/product/util/AISmartCountUtils.java b/cash-service/product-service/src/main/java/com/czg/service/product/util/AISmartCountUtils.java new file mode 100644 index 000000000..de690405c --- /dev/null +++ b/cash-service/product-service/src/main/java/com/czg/service/product/util/AISmartCountUtils.java @@ -0,0 +1,132 @@ +package com.czg.service.product.util; + +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.hutool.core.util.StrUtil; +import com.czg.exception.CzgException; +import lombok.extern.slf4j.Slf4j; + +import java.io.File; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.net.URLEncoder; + +/** + * 智能点数上传工具类 + * @author ww + */ +@Slf4j +public class AISmartCountUtils { + // 基础配置 + private static final String BASE_URL = "https://api.aismartcount.com/api/v1"; + private static final int TIMEOUT = 30000; + + /** + * 示例:完整调用流程 + */ + public static String getTickCount(String token, byte[] file, String fileName) throws Exception { + String uploadResponse = uploadImage(token, file, fileName); + return countImageDots(token, uploadResponse); + + } + + /** + * 上传图片接口 + * 完全使用你提供的header(String, String)方法设置请求头 + * + * @param imageData 图片数据(InputStream / byte[] / File) + * @return 上传响应结果 + * @throws Exception 上传异常 + */ + private static String uploadImage(String token, Object imageData, String fileName) { + + // 1. 构建请求URL + String uploadUrl = BASE_URL + "/upload/image"; + + // 2. 构建POST请求并设置基础参数 + HttpRequest postRequest = HttpUtil.createPost(uploadUrl) + .header("authorization", token) + .timeout(TIMEOUT); + + // 3. 根据数据类型设置文件上传参数 + if (imageData instanceof File files) { + postRequest.form("file", files); + } else if (imageData instanceof InputStream input) { + postRequest.form("file", input, fileName); + } else if (imageData instanceof byte[] bytes) { + postRequest.form("file", bytes, fileName); + } else { + throw new IllegalArgumentException("不支持的图片数据类型:" + (imageData == null ? "null" : imageData.getClass().getName())); + } + String body = postRequest.execute().body(); + // 解析上传响应并校验状态码 + JSONObject uploadResult = JSONUtil.parseObj(body); + + // 1. 校验响应码(核心) + int code = uploadResult.getInt("code", -1); + if (code != 200) { + log.error("智能点数 图片上传失败 {}", uploadResult); + throw new CzgException("图片上传失败"); + } + JSONObject dataObj = uploadResult.getJSONObject("data"); + if (dataObj == null) { + log.error("智能点数 图片上传响应中未找到data字段 {}", uploadResult); + throw new CzgException("图片上传失败"); + } + String imgUrl = dataObj.getStr("url"); + if (StrUtil.isBlank(imgUrl)) { + log.error("智能点数 图片上传成功,但未返回图片URL {}", uploadResult); + throw new CzgException("图片上传成功,但未返回图片URL"); + } + return imgUrl; + } + + /** + * 统计图片点数接口 + * 手动拼接URL参数,使用header方法设置授权头 + * + * @param imgUrl 图片URL + * @return 统计响应结果 + * @throws Exception 请求异常 + */ + private static String countImageDots(String token, String imgUrl) throws Exception { + String countUrl = BASE_URL + "/count/count-image-dots" + + "?img_url=" + encodeParam(imgUrl) + + "&type=stick&secure=true&upload_source=pc"; + + // 2. 构建请求(仅使用你提供的方法) + String countResponse = HttpUtil.createPost(countUrl) + .header("authorization", token) + .timeout(TIMEOUT) + .execute() + .body(); + JSONObject countResult = JSONUtil.parseObj(countResponse); + int countCode = countResult.getInt("code", -1); + if (countCode != 200) { + log.error("智能点数 点数统计失败,未知错误 {}", countResult); + throw new CzgException("点数统计失败,未知错误"); + } + // 3. 执行请求并返回结果 + return countResult.getStr("total_count"); + } + + + /** + * 通用URL参数编码 + * + * @param param 要编码的参数 + * @return 编码后的字符串 + */ + private static String encodeParam(String param) { + if (StrUtil.isBlank(param)) { + return ""; + } + try { + return URLEncoder.encode(param, StandardCharsets.UTF_8); + } catch (Exception e) { + throw new RuntimeException("URL参数编码失败: " + param, e); + } + } +} \ No newline at end of file diff --git a/cash-service/product-service/src/main/java/com/czg/service/product/util/MuSmartCountUtils.java b/cash-service/product-service/src/main/java/com/czg/service/product/util/MuSmartCountUtils.java new file mode 100644 index 000000000..3902fc930 --- /dev/null +++ b/cash-service/product-service/src/main/java/com/czg/service/product/util/MuSmartCountUtils.java @@ -0,0 +1,122 @@ +package com.czg.service.product.util; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.czg.exception.CzgException; +import lombok.extern.slf4j.Slf4j; + + +/** + * 一个木涵 + * + * @author: ww + */ +@Slf4j +public class MuSmartCountUtils { + // 基础配置 + private static final String LOGIN_URL = "https://uapi.woobx.cn/user/login"; + private static final String DETECT_URL = "https://uapi.woobx.cn/app/object-det/bamboo-stick"; + // 请求超时时间(毫秒) + private static final int TIMEOUT = 10000; + + /** + * 登录接口 - 获取认证Token(线程安全) + * + * @return 认证Token + */ + public static String login(String username, String password) { + // 构建登录请求 + HttpResponse response = HttpRequest.post(LOGIN_URL) + .form("username", username) + .form("password", password) + .timeout(TIMEOUT) + .execute(); + // 校验响应状态 + if (!response.isOk()) { + log.error("登录失败,响应码:{},数据:{}", response.getStatus(), response.body()); + throw new CzgException("登录失败"); + } + + // 解析响应结果 + String responseStr = response.body(); + JSONObject result = JSON.parseObject(responseStr); + if (200 != result.getIntValue("code")) { + log.error("登录失败,响应码:{},数据:{}", response.getStatus(), result); + throw new CzgException("登录失败"); + } + + // 提取并缓存Token + String token = result.getJSONObject("data").getString("authToken"); + if (StrUtil.isBlank(token)) { + log.error("登录失败,响应码:{},数据:{}", response.getStatus(), result); + throw new CzgException("登录失败"); + } + return token; + } + + /** + * 竹签识别核心方法(自动处理Token过期重试) + * + * @param imageBase64 图片Base64编码字符串 + * @return 识别结果JSON对象 + * @throws Exception 识别异常 + */ + public static int detectBambooStick(String imageBase64, String token) throws Exception { + // 参数校验 + if (StrUtil.isBlank(imageBase64)) { + throw new CzgException("图片Base64编码不能为空"); + } + // 第一次调用识别接口 + JSONObject detectResult = doDetectRequest(imageBase64, token); + return MuSmartCountUtils.countBambooStick(detectResult); + } + + /** + * 执行竹签识别请求(核心请求逻辑) + * + * @param imageBase64 图片Base64编码 + * @param token 认证Token + * @return 识别结果 + */ + private static JSONObject doDetectRequest(String imageBase64, String token){ + // 构建识别请求 + HttpResponse response = HttpRequest.post(DETECT_URL) + .header("authorization", "Bearer " + token) + .form("image", imageBase64) + .form("base64", "true") + .timeout(TIMEOUT) + .execute(); + + // 校验响应状态 + if (!response.isOk()) { + throw new RuntimeException("识别请求失败,响应码:" + response.getStatus()); + } + + // 解析响应结果 + String responseStr = response.body(); + return JSON.parseObject(responseStr); + } + + /** + * 解析识别结果,统计竹签数量(自定义分数阈值) + * + * @param detectResult 识别结果JSON + * @return 竹签数量 + */ + private static int countBambooStick(JSONObject detectResult) { + // 参数校验 + if (detectResult == null || 200 != detectResult.getIntValue("code")) { + return 0; + } + + JSONArray dataArray = detectResult.getJSONArray("data"); + if (dataArray == null || dataArray.isEmpty()) { + return 0; + } + return dataArray.size(); + } +} \ No newline at end of file diff --git a/cash-service/product-service/src/main/resources/mapper/MkOcrCountStickMapper.xml b/cash-service/product-service/src/main/resources/mapper/MkOcrCountStickMapper.xml new file mode 100644 index 000000000..70582dcb2 --- /dev/null +++ b/cash-service/product-service/src/main/resources/mapper/MkOcrCountStickMapper.xml @@ -0,0 +1,7 @@ + + + + +