ocr 数签子

登录更新了用户名称的问题
更新用户信息的问题
This commit is contained in:
2025-12-25 11:18:14 +08:00
parent 22a5ae8f68
commit bae1762dba
18 changed files with 866 additions and 388 deletions

View File

@@ -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<MkShopConsumeDiscountRecord> get(@RequestParam Long shopId) {
// return CzgResult.success(shopConsumeDiscountRecordService.get(shopId));
return null;
}
/**
* 配置信息获取
* 权限标识: activate:list

View File

@@ -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<ConsInOutStockHeadParam> ocrResult(@RequestParam Long id) {
return CzgResult.success(consStockFlowService.ocrDetail(id));
return CzgResult.success(ocrService.ocrDetail(id));
}

View File

@@ -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<Integer> uploadAndCount(MultipartFile file) throws IOException {
AssertUtil.isNull(file, "上传文件不能为空");
// 9. 返回成功结果
return CzgResult.success(stickCountService.getCountStick(file.getBytes(), file.getOriginalFilename()));
}
}

View File

@@ -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;
}

View File

@@ -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<MkOcr> {
ConsInOutStockHeadParam ocrDetail(Long id);
Integer ocr(String originalFilename, InputStream inputStream, String type);
}

View File

@@ -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<ConsStockFlow> {
* 手动入库
*
* @param param 手动出库入参
* @return
*/
ConsInOutStockHeadParam inStock(ConsInOutStockHeadParam param);
@@ -78,8 +76,4 @@ public interface ConsStockFlowService extends IService<ConsStockFlow> {
* @param entity 库存变动记录实体
*/
void saveFlow(ConsStockFlow entity);
Integer ocr(String originalFilename, InputStream inputStream);
ConsInOutStockHeadParam ocrDetail(Long id);
}

View File

@@ -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<MkOcrCountStick> {
int getCountStick(byte[] stream, String fileName);
}

View File

@@ -34,7 +34,6 @@ public class AliOcrUtil {
* <p>使用凭据初始化账号Client</p>
*
* @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对应单号/编号/Nodate对应日期/开单日期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())

View File

@@ -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<ShopConfigMapper, ShopCon
});
if (isEnable == 0 && !onyUpValid) {
updateChain().or(or -> {
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<ShopConfigMapper, ShopCon
}
}
extistList.forEach(item -> redisService.del("shopInfo::" + item));
redisService.del("shopInfo::" + mainShopId);
}

View File

@@ -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);
}

View File

@@ -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<UserInfoMapper, UserInfo> 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<UserInfoMapper, UserInfo> 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;
}

View File

@@ -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<MkOcrCountStick> {
}

View File

@@ -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<ConsStockFlowMapper, ConsStockFlow> implements ConsStockFlowService {
/**
* 耗材库存变动记录
*
* @author Tankaikai tankaikai@aliyun.com
* @since 1.0 2025-02-21
*/
@AllArgsConstructor
@Service
@Slf4j
public class ConsStockFlowServiceImpl extends ServiceImpl<ConsStockFlowMapper, ConsStockFlow> 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<ConsStockFlow> entityList = BeanUtil.copyToList(param.getBodyList(), ConsStockFlow.class);
List<ConsStockFlow> insertList = new ArrayList<>();
List<ConsInfo> 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<ConsStockFlow> entityList = BeanUtil.copyToList(param.getBodyList(), ConsStockFlow.class);
List<ConsStockFlow> insertList = new ArrayList<>();
List<ConsInfo> 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<ConsStockFlow> entityList = BeanUtil.copyToList(param.getBodyList(), ConsStockFlow.class);
List<ConsStockFlow> insertList = new ArrayList<>();
List<ConsInfo> 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<ConsStockFlow> entityList = BeanUtil.copyToList(param.getBodyList(), ConsStockFlow.class);
List<ConsStockFlow> insertList = new ArrayList<>();
List<ConsInfo> 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<ConsCheckStockRecordVo> 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<ConsCheckStockRecordVo> 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<ConsStockFlowDTO> 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<String> 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<ConsInOutStockBodyParam> bodyList = new ArrayList<>();
Set<String> nameList = saleOrderDTO.getItems().stream().map(SaleOrderDTO.Item::getConName).collect(Collectors.toSet());
Map<String, ConsInfo> 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<SaleOrderDTO.Item> 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<ConsCheckStockRecordVo> 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<ConsCheckStockRecordVo> 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<ConsStockFlowDTO> 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<String> 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);
});
});
}
}
}

View File

@@ -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<MkOcrCountStickMapper, MkOcrCountStick> implements MkOcrCountStickService {
@Resource
private RedisService redisService;
@Override
public int getCountStick(byte[] fileByte, String fileName) {
List<MkOcrCountStick> 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;
}
}

View File

@@ -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<MkOcrMapper, MkOcr> implements MkOcrService{
public class MkOcrServiceImpl extends ServiceImpl<MkOcrMapper, MkOcr> 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<ConsInOutStockBodyParam> bodyList = new ArrayList<>();
Set<String> nameList = saleOrderDTO.getItems().stream().map(SaleOrderDTO.Item::getConName).collect(Collectors.toSet());
Map<String, ConsInfo> 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<SaleOrderDTO.Item> 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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.czg.service.product.mapper.MkOcrCountStickMapper">
</mapper>