Author SHA1 Message Date
mazengfei a769eae2a0 fix(global): 修正生产环境接口地址
- 更新生产环境基础URL地址为新的测试平台地址
- 注释掉旧的生产环境地址备份记录
- 保持开发和测试环境地址不变
- 确保接口调用指向正确的服务器环境
2026-08-10 14:01:38 +08:00
mazengfei 0241caa5fc fix(main): 修复计算吃饭数量返回值小于1的问题
- 修改返回值逻辑,当计算结果小于1时返回1
- 防止吃饭数量为0或负值导致的异常情况
- 优化代码可读性,提升鲁棒性
2026-08-05 17:57:38 +08:00
mazengfei 512e0b5b4f feat(weightBilling): 优化按重量计费逻辑并完善界面展示
- 规格栏显示改为直接展示规格重量,去除单价/100克显示
- 新增最小计费重量常量 MIN_BILLABLE_WEIGHT,净重不足时阻止下单
- 下单时检测净重,低于最小计费重量时弹出提示并拦截
- 计算价格时按净重计算,单价基于规格价与规格重量的比值
- 本地明细中 eatNum 仅用于展示份数,不参与金额计算
- SettlementOrder 用实际食用重量 eatWeight 赋值 num 字段,配合重量栏显示逻辑调整
2026-08-05 10:23:48 +08:00
mazengfei a8ade16058 fix(MainScreenPresentation): 修正取餐重量计算逻辑
- 去除净重计算,改以实际重量做后续份数、价格、总重量计算
- 修正计价和取餐份数计算中使用的重量参数
- 调整本地数据订单中数量赋值,使用实际重量替代净重
- 在营养计算和柱状图绘制中扣减餐具重量,确保显示净重
- 保持取餐模式余量计量逻辑兼容,优化逻辑判断条件
2026-07-28 09:18:36 +08:00
mazengfei ffd00266b8 fix(recognition): 防止识别时重复触发导致多次并发处理
- 新增 isRecognizing 标记防止识别过程重复进入
- 在识别开始时设置标记,结束时释放标记
- 多处识别终止和异常处理处均释放识别锁
- 在重量回调中增加去重逻辑,避免同一重量重复触发回调
- 更新 lastCallbackWeight,确保仅在重量变化时触发回调
2026-07-24 16:54:19 +08:00
mazengfei 43f5434cbd feat(business): 增加餐具重量配置及扣减功能
- 在 BusinessFragment 中添加实时秤重读取及餐具重量持久化存储
- 在界面显示餐具重量标签,配置时显示当前保存值
- 定义全局常量 KEY_DISH_WEIGHT 用于存储餐具重量
- 在 MainActivity 内部方法添加获取配置餐具重量接口
- 修改订餐逻辑,取餐提交时自动从就餐重量中扣减餐具重量
- 在主界面相关订单及计价逻辑中扣减餐具重量影响份数和价格
- 调整重量监听及订单添加部分逻辑以应用净重计算
- 更新定时任务延迟时间,提高效率和响应速度
2026-07-24 15:56:08 +08:00
mazengfei 76f60f8ee7 fix(build): 修复ObjectBox库的release依赖配置
- 取消注释并激活app模块中ObjectBox的releaseImplementation依赖
- 取消注释并激活lib_face模块中ObjectBox的releaseImplementation依赖

fix(ui): 修复异常时Toast显示的线程调度问题

- 在CollectFragment中使用lifecycleScope切换到主线程显示Toast

refactor(main): 优化图像处理流程中的协程使用

- MainActivity中将uri2File调用放入IO线程的lifecycleScope中
- 使用withContext切换至IO线程异步获取Bitmap后再进行处理

chore(objbox): 移除未使用的ObjectBoxLiveData导入声明
2026-07-17 09:53:43 +08:00
mazengfei 28a475ca43 feat(face): 优化人脸数据同步及类型映射
- 新增接口注释,完善全量及增量人脸数据获取说明
- FaceVO中faceUpdateTimestamp由Long改为String以防JS大数精度丢失
- 增加personType字段标识人员类型
- MainActivity保存人脸数据时映射完整字段,使用personType优先判断用户类型
- NetViewModelV2中获取人脸数据时改用完整构造函数创建FaceEntity
- 累积更新lastFaceTimestamp为当前页最大时间戳,避免时间戳遗漏
- 优化时间戳更新逻辑,避免直接使用列表最后一条数据
2026-07-15 10:56:40 +08:00
mazengfei 16dba32eb7 feat(face): 升级虹软SDK到5.0及完善人脸识别逻辑
- FaceHelper中集成独立口罩检测引擎maskEngine并调用口罩检测接口
- FaceEngine初始化中增加口罩检测引擎初始化及状态管理
- FaceServer注册人脸时打印特征信息日志,便于调试
- CompareResult新增similarPass字段,标记识别是否通过
- FaceApi新增针对人脸数据的增删查接口,方便管理
- FaceDatabase数据库版本升级至2,支持人脸特征库重建迁移
- FaceDao新增多用户查询、人脸计数及临时用户数据删除接口
- FaceEntity新增userId、userFaceId、member标记及更新时间字段及相关方法
- 优化人脸识别失败重试机制,连续失败后回调识别失败结果
- 优化识别回调逻辑,成功与失败结果均及时通知观察者
- 调整配置获取逻辑,禁用人脸活体检测时关闭相关初始化
- 精简并修正部分冗余代码与日志输出,提升代码清晰度与可维护性
2026-07-15 09:17:23 +08:00
mazengfei 5a7b090c02 fix(ml): 优化PyTorch模型推理流程与资源管理
- 修改API接口路径从serve切换到common,统一资源调用
- 增加独立推理线程,隔离ArcSoft线程污染,保障FPU状态稳定
- 推理模块预热,降低PyTorch线程初启功率峰值,避免电流保护触发
- 推理前后记录系统内存信息,辅助OOM重启排查
- 主屏相机与副屏人脸识别推理前暂停,规避硬件并发带宽冲突重启
- 控制相机HAL清理时长,确保DMC带宽资源释放后再启动推理
- 异步推理调用增加超时机制,避免长时间卡死
- 推理结束后恢复相机,确保正常工作流程
2026-07-08 16:23:37 +08:00
lvmeng b6ed47ff87 fix(camera): 解决相机拍照功能的竞态条件和重试机制问题
- 修复了相机回调中的竞态条件,使用isShowCamera标识查找空闲槽位
- 将图片处理逻辑移至IO线程,避免主线程阻塞
- 添加了照片拍摄失败回调处理函数
- 简化了相机拍照调用方式,直接传递回调函数
- 在FoodCollectionAdapter中使用Glide加载图片uri
- 添加了相机绑定失败时的自动重试机制,包含延迟重试逻辑
- 增加了详细的错误日志记录和异常处理
2026-06-30 15:54:45 +08:00
lvmeng 088aabc535 refactor(discount): 简化会员折扣空值处理逻辑
- 使用Elvis操作符替代条件表达式判断空值
- 统一两个位置的折扣计算逻辑实现方式
- 提高代码可读性和简洁性
2026-06-30 09:51:35 +08:00
mazengfeiandClaude Opus 4.6 061680b32b fix(objectbox): 修复32位系统向量查询崩溃,统一使用findIdsWithScores避免加载大对象
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-04 17:26:36 +08:00
mazengfei 5c3e51626c fix(network): 修正人脸识别接口路径并移除硬编码的SDK密钥
- 修复 ApiClient 中的人脸识别增量同步接口路径
- 将 InitActivity 中的 ArcSoft SDK 密钥配置改为注释状态
- 移除临时测试用的硬编码 SDK 配置逻辑
- 调整代码执行顺序以确保正确的初始化流程
2026-06-04 11:19:50 +08:00
lvmeng 936bf206dc ```
refactor(env): 调整环境配置和切换逻辑

- 将本地环境配置从注释状态启用,并更新对应的IP地址和端口
- 修改环境切换对话框中的文本标签,重新排列环境选项顺序
- 更新全局数据类中的环境URL常量定义,调整测试和UAT环境地址
- 移除未使用的SPUtil导入,改用SpTool工具类
- 修改应用启动时的默认环境判断逻辑,将特定设备指向本地环境
- 调整环境切换对话框中的RadioButton选中状态和URL映射关系
```
2026-06-03 16:34:16 +08:00
lvmeng 8e4dcb9f61 fix(arcface): 禁用活体检测功能
- 将enableLive变量强制设置为false以禁用活体检测
- 移除注释掉的调试代码行
2026-06-03 16:32:39 +08:00
lvmeng 8ba4ba6db0 feat(order): 开餐下单新增cookOrderId支持用于溯源追踪
- 在FoodInfo数据模型中新增cookOrderId字段
- 在NewFoodInfo数据模型中新增cookOrderId字段并实现映射
- 在PlaceOrderRequest请求模型中新增cookOrderId参数
- 在订单提交流程中传递cookOrderId参数
- 实现菜品列表到就餐环节的ID回传机制
2026-06-02 09:07:21 +08:00
lvmeng 37f86e202f refactor(camera): 优化相机拍照功能实现
- 移除 PhotoCaptureHelper 中的测试模式和无用的 onSuccess 回调参数
- 将 CollectFragment 中的 UI 操作迁移到正确的协程调度器 (Main 和 IO)
- 使用 lifecycleScope 替代 runOnUiThread 确保生命周期安全
- 重构 ImageUtil.uriToBitmap 方法,添加图片尺寸压缩逻辑以减少内存占用
- 从 InitActivity 中移除已注释的人脸识别相关初始化代码
- 修复 cameraCallback 中的拼写错误 (rerurn@ -> return@)
2026-05-29 13:52:07 +08:00
lvmeng 737baf8486 feat(api): 更新API端点并集成人脸数据库导入功能
- 将所有API端点中的/booth路径替换为/serve
- 新增searchFood API用于菜品搜索功能
- 新增getCollectVectorPage API用于向量数据分页获取
- 添加FaceDbImporter工具类实现从assets导入人脸数据
- 在初始化活动中集成人脸数据导入功能
- 更新订单创建逻辑适配新的API参数结构
- 重构菜品搜索和订单处理相关API调用
- 添加V2数据模型到V1模型的转换扩展函数
- 更新绑定订单接口以使用新的API端点
- 添加测试模式支持以跳过实际拍照操作
2026-05-28 19:57:40 +08:00
lvmeng bf6b2547f3 refactor(network): 重构网络请求配置和API接口实现
- 将BASE_URL改为使用GlobalData.PROD_BASE_URL常量
- 更新ApiClient中的URL匹配规则以支持新的营养模块接口
- 将ApiServiceV2中的接口路径从/neglect/booth改为/nutrition/neglect/booth
- 使用@Url注解动态传入完整URL地址
- 在CollectedFoodActivity中切换到NetViewModelV2和新API模型
- 更新采集功能的数据模型和接口调用方式
- 调整人脸数据获取逻辑以支持增量和全量同步
- 优化文件上传和删除操作的参数传递方式
2026-05-28 15:14:44 +08:00
lvmeng 6abd2f9b0b feat(network): 添加新系统API V2支持及相关数据模型
- 引入 ApiServiceV2 接口定义新系统API端点
- 添加 RemoteRepositoryV2 实现新API的数据访问层
- 创建 NetViewModelV2 提供新API的业务逻辑处理
- 定义 v2 包下的数据模型类包括请求响应对象
- 在 ApiClient 中集成新旧两套API服务实例
- 配置API日志拦截器识别新API调用并打上ApiV2标签
- 实现新系统菜品、人脸、订单、会员等完整功能接口
2026-05-28 09:33:05 +08:00
lvmeng c19d84a7a6 refactor(init): 优化结算模式配置逻辑
- 移除未使用的变量声明
- 将结算模式设置改为不可变值
- 简化支付类型判断逻辑
- 保持结算模式存储功能不变
2026-05-26 11:49:23 +08:00
lvmeng 8f1f15dd74 feat(payment): 添加会员折扣功能并重构支付逻辑
- 新增 getMemberDiscount 接口用于查询会员折扣
- 在人脸识别支付中集成会员折扣计算逻辑
- 将支付金额计算从 vipPrice 切换到 specPrice 并应用会员折扣
- 在初始化活动中添加设备配置获取功能
- 使用 ConstraintLayout 重构主餐品列表布局
- 添加 Timber 日志框架替换原有 Log 工具
- 重构 MainScreenPresentation 中的食物价格计算逻辑
- 移除重复的设备配置获取代码,统一在 InitActivity 中处理
2026-04-27 17:52:45 +08:00
lvmeng 416a8366e1 feat(MainScreenPresentation): 添加布局隐藏功能以支持结算模式
- 导入 invisible 扩展函数
- 在特定结算模式下隐藏 eat detail 布局
- 实现相机恢复后的条件性视图控制
2026-04-24 15:41:21 +08:00
lvmeng 03c5aee785 refactor(ui): 重构主界面食物列表布局和支付模式切换功能
- 将原有的RecyclerView替换为FrameLayout容器,实现动态布局加载
- 新增layout_main_food_list.xml和layout_main_food_list2.xml两个布局文件
- 实现联合支付和独立支付两种模式下的不同UI展示
- 集成食物详情显示功能,通过include方式引入layout_eat_detail.xml
- 更新数据绑定逻辑,支持两种支付模式下的食物列表显示
- 迁移SharedPreferences操作至SpTool工具类统一管理
- 优化食物识别结果显示逻辑和搜索按钮状态控制
- 重构RecyclerView初始化和适配器设置代码结构
2026-04-24 15:37:57 +08:00
mazengfei e3b01ff789 修改uat环境域名端口 2026-04-15 16:52:27 +08:00
mazengfeiandClaude Sonnet 4.6 b6c9fe5aed feat(log): 优化日志写入机制,改用内部存储与单线程执行器
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 17:23:50 +08:00
lvmeng 45562a9fc1 fix(order): 解决订单生成后餐品未拿走的检测逻辑
- 添加abs函数导入用于计算重量绝对值
- 在订单生成成功后增加重量检测逻辑
- 当称重绝对值大于10克时认为餐品未被拿走
- 添加详细的调试日志记录重量信息
- 集成FileUtil工具类用于保存操作日志
- 在人脸数据获取流程中添加日志记录时间戳变化
2026-04-14 15:19:09 +08:00
lvmeng 7f455f2d8f perf(FoodModule): 提高食物查询性能并优化排序逻辑
- 将默认查询数量从15增加到50
- 移除不必要的分数阈值过滤条件
- 移除复杂的按名称分组和计数逻辑
- 简化排序算法,直接按分数排序
- 添加调试日志输出结果数据
2026-04-13 17:20:51 +08:00
mazengfei dde0bbc4d5 优化余量计量模式下拿完所有数据时总重量不对的问题 2026-04-10 17:48:40 +08:00
lvmeng d378cdcf97 fix(main): 修复菜品切换和重量计算相关问题
- 移除未使用的LinearLayoutManager和FoodOrderAdapter导入
- 添加isSwitchFood标志用于菜品切换时重置数据
- 优化resetSelectedFood方法,根据用户状态决定是否重新打开识别功能
- 将currentWeight修改为public访问权限
- 在重量变化时添加判断避免重复更新
- 注释掉不需要的重量更新调用
- 在菜品识别完成时添加重量更新
- 在清空选中项目时重置isSwitchFood标志
- 优化updateWeight方法中的逻辑判断
- 将倒计时重置任务提取为单独方法
- 添加switchFood逻辑控制,避免重复人脸识别
- 修复余量计量模式下的最后餐品处理逻辑
2026-04-10 16:03:27 +08:00
lvmeng e5b682f021 refactor(face): 优化人脸识别状态管理与菜品选择逻辑
- 移除 MainFoodListAdapter 中未使用的适配器和视图参数
- 提取菜品重选逻辑到独立的 resetSelectedFood 方法中
- 删除临时的独立支付模式测试代码
- 在余量计量场景中添加人脸状态检查避免重复数据读取
- 修复倒计时任务状态判断逻辑并清理任务引用
- 添加空值检查防止 CompareResult 为空时的异常
- 实现 resetFaceState 方法清空人脸识别状态
- 在订单提交前重置人脸状态确保下次识别正常进行
2026-04-09 16:02:24 +08:00
lvmeng 0498e124b3 fix(weight): 修复称重逻辑和支付模式相关问题
- 将 WEIGHT_CHANGE_VALUE 从 25 修改为 20
- 添加 currentWeight 变量用于准确记录当前称重值
- 临时修改 settlementMode 为独立支付模式用于测试
- 更新日志输出以更好地跟踪称重变化
- 修复即放即取和余量计量模式下的称重处理逻辑
- 添加食物识别状态控制变量 foodRecognizeState
- 在不同模式下正确设置 eatWeight 和 foodWeight 计算方式
- 优化余量计量模式下的状态重置逻辑
- 添加倒计时功能用于余量计量模式状态重置
- 修复营养计算相关的日志输出格式
2026-04-09 11:59:25 +08:00
lvmeng 30b2b178a9 refactor(SensorScaleUtils): 优化重量读取逻辑
- 移除状态字符串转换的日志代码
- 简化稳定状态判断逻辑,移除重复的lastWeight比较
- 将重量单位转换提取为独立变量,提高代码可读性
- 保持原有的回调机制和重量单位转换功能
2026-04-08 10:59:07 +08:00
lvmeng 2ba55ef027 feat(common): 添加View点击区域扩展功能并优化主界面重量检测逻辑
- 在CommonExt.kt中新增expandClickArea扩展函数,支持扩大View的点击响应区域
- 新增TouchDelegate导入用于实现点击区域扩展功能
- 调整MainActivity.kt中的重量检测逻辑顺序,先处理联合支付模式下的特殊情况
- 添加对当前用户ID的检查,优化即放即取模式下的重量数据处理流程
- 重构重量重置识别条件的位置,确保正确的执行顺序
2026-04-08 10:52:09 +08:00
lvmengandClaude Sonnet 4.6 a45690d3be refactor(utils): 重构 FoodVectorTool 并优化 zero() 提示控制
- 重构 FoodVectorTool:移除单例 pageNum 状态污染问题,分页逻辑下沉为私有 fetchPage,saveFoodVector 改为私有,PAGE_SIZE 移入内部定义,对外统一暴露 loadAndSaveFoodVector 接口
- 更新 InitActivity、MainActivity 调用方,移除重复的 onPageFinish 回调
- SensorScaleUtils.zero() 新增 isShowToastRemind 可选参数,支持按需显示标定成功提示

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 09:25:03 +08:00
lvmeng ab0d9b35f5 feat(data): 更新食物营养数据模型并优化向量数据处理
- 在DataBean中将肉蛋相关字段重命名为肉蛋豆,并添加总重量字段
- 新增FoodVectorTool工具类用于处理食物向量数据的分页获取和存储
- 重构InitActivity中的食物向量数据获取逻辑,使用新的工具类替代原有实现
- 在MainActivity中集成食物向量数据同步功能,在清除本地数据后重新获取
- 更新MainScreenPresentation中营养信息显示逻辑,适配新的数据模型结构
- 修改ApiClient中的日志标记逻辑,为不同接口添加特定的标签
- 调整食物订单列表的数据更新方式,优化适配器通知策略
2026-04-03 18:04:59 +08:00
mazengfeiandClaude Sonnet 4.6 0127697560 fix(main): 优化重量识别逻辑与日志;防抖时间调整为2s;从版本控制中移除.idea和objectbox-models
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 15:09:14 +08:00
mazengfei dbe3753821 人脸数据重置后加刷新逻辑;人脸识别距离调整; 2026-04-03 10:40:57 +08:00
lvmeng b16147b1b2 feat(face): 添加人脸识别距离过滤功能
- 在FaceHelper中添加人脸宽度小于200像素时的过滤逻辑
- 当检测到人脸过小时自动清除左侧人脸数据
- 防止远距离小人脸影响识别准确性
- 保留原有的活体检测流程不变
2026-04-02 19:13:15 +08:00
lvmeng d6528d33fc refactor(MainActivity): 将人脸数据清除操作移至后台线程执行
- 使用 lifecycleScope.launch 在协程中执行人脸数据清除
- 通过 withContext(Dispatchers.IO) 确保清除操作在 IO 线程运行
- 避免主线程阻塞提升界面响应性能
- 保持原有的用户提示和缓存更新逻辑不变
2026-04-02 17:45:41 +08:00
lvmeng 5479091959 refactor(main): 将Thread替换为lifecycleScope并优化协程处理
- 替换CollectFragment中的Thread为activity.lifecycleScope.launch
- 在MainActivity中添加FaceServer相关依赖和协程调度器
- 修改环境切换对话框以支持人脸数据重置功能
- 将照片拍摄和食物向量保存操作从Thread迁移到lifecycleScope
- 优化人脸识别数据更新逻辑,移除不必要的线程创建
- 修改onTakePhotoSuccess方法为suspend函数并调整UI更新方式
- 更新主线程操作使用withContext(Dispatchers.Main)替代runOnUiThread
- 在MainScreenPresentation中添加相机暂停功能
- 修复SensorScaleUtils中标定调用问题
- 扩展UserViewModel的人脸数据获取接口,支持回调处理
2026-04-02 17:35:37 +08:00
mazengfei 12da153df3 优化 2026-04-01 17:11:00 +08:00
mazengfei 3ef93c5f5e 配置生产域名 2026-03-31 19:08:39 +08:00
lvmeng 91800a6bb1 feat(app): 添加环境切换功能并优化基础URL管理
- 在主界面标题时间区域添加点击事件,支持弹出环境切换对话框
- 新增EnvSwitchDialog类实现TEST/UAT/PROD三套环境切换功能
- 新增dialog_env_switch.xml布局文件定义环境切换弹窗UI
- 重构GlobalData类,统一管理基础URL常量并添加持久化支持
- 修改MyApp初始化逻辑,优先读取用户手动设置的环境配置
- 添加SpTool.baseUrl属性用于环境配置的本地存储
- 修复MainActivity布局文件中的TextView垂直内边距问题
- 调整食物识别防抖时间从500ms到3000ms,优化计算营养逻辑执行时机
2026-03-31 09:48:32 +08:00
mazengfei f40c2046f1 优化 2026-03-30 18:31:24 +08:00
mazengfei ee9f27bb9c 优化 2026-03-30 18:03:01 +08:00
lvmeng 5c230e38c3 refactor(activity): 重构 BaseActivity 权限管理和页面跳转功能
- 在 BaseActivity 中新增权限请求和页面跳转的通用方法
- 移除 InitActivity 中原有的相机权限请求相关代码
- 使用 BaseActivity 的通用权限请求方法替代原有实现
- 添加网络连接检测工具类 NetworkUtils
- 在 WiFi 设置页面返回后重新检查网络连接状态
- 优化按钮可见性控制逻辑
2026-03-30 14:01:13 +08:00
lvmeng 10d1f966da 配置接口新增结算模式字段;优化页面刷新; 2026-03-27 17:21:45 +08:00
lvmeng 57f8920442 优化订单显示档口机标识;优化当前菜品信息的问题; 2026-03-26 18:57:20 +08:00
lvmeng fb4278d2b7 联合支付余量计量模式订单显示当前取餐信息 2026-03-25 15:42:22 +08:00
lvmeng fcea0d785d 联合支付功能测试 2026-03-24 18:34:38 +08:00
lvmeng c4a6101a7b 联合支付功能调试 2026-03-23 18:13:55 +08:00
lvmeng bf8444e5d5 联合支付功能调试 2026-03-20 18:06:22 +08:00
lvmeng 549e8d303f 联合支付功能 2026-03-19 17:44:13 +08:00
lvmeng b51be58c64 联合支付模式使用人脸识别生成就餐记录 2026-03-19 14:19:20 +08:00
109 changed files with 4375 additions and 1210 deletions
+3 -6
View File
@@ -1,15 +1,12 @@
*.iml *.iml
.gradle .gradle
/local.properties /local.properties
/.idea/caches /.idea/
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store .DS_Store
/build /build
/captures /captures
.externalNativeBuild .externalNativeBuild
.cxx .cxx
local.properties local.properties
app/objectbox-models/*.json
app/objectbox-models/*.json.bak
-3
View File
@@ -1,3 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
Generated
-1
View File
@@ -1 +0,0 @@
DualScreen
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AndroidProjectSystem">
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
</component>
</project>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="17" />
</component>
</project>
-18
View File
@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2026-03-10T03:02:12.156794100Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="Default" identifier="serial=192.168.1.211:5555;connection=7435869e" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState>
</selectionStates>
</component>
</project>
-13
View File
@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DeviceTable">
<option name="columnSorters">
<list>
<ColumnSorterState>
<option name="column" value="Name" />
<option name="order" value="ASCENDING" />
</ColumnSorterState>
</list>
</option>
</component>
</project>
-20
View File
@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="azul-17" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/lib_face" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>
-6
View File
@@ -1,6 +0,0 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="UsePropertyAccessSyntax" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
</profile>
</component>
-10
View File
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MaterialThemeProjectNewConfig">
<option name="metadata">
<MTProjectMetadataState>
<option name="userId" value="29face46:19ce5e8d9d0:-7fe0" />
</MTProjectMetadataState>
</option>
</component>
</project>
-10
View File
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectMigrations">
<option name="MigrateToGradleLocalJavaHome">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</component>
</project>
-9
View File
@@ -1,9 +0,0 @@
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="zulu-17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>
-17
View File
@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
</set>
</option>
</component>
</project>
Generated
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
+12 -8
View File
@@ -51,13 +51,15 @@ android {
kotlinOptions { kotlinOptions {
jvmTarget = "11" jvmTarget = "11"
} }
viewBinding {
enable = true buildFeatures {
viewBinding = true
buildConfig = true
} }
} }
dependencies { dependencies {
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar", "*.jar")))) //implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar", "*.jar"))))
implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat) implementation(libs.androidx.appcompat)
implementation(libs.material) implementation(libs.material)
@@ -108,17 +110,19 @@ dependencies {
implementation(libs.pytorch.android) implementation(libs.pytorch.android)
implementation(libs.pytorch.android.torchvision) implementation(libs.pytorch.android.torchvision)
implementation("io.github.scwang90:refresh-layout-kernel:3.0.0-alpha") implementation(libs.baseRecyclerViewAdapterHelper4)
implementation("io.github.scwang90:refresh-header-classics:3.0.0-alpha")
implementation(libs.refresh.layout.kernel)
implementation(libs.refresh.header.classics)
val objectboxVersion = "5.0.1" val objectboxVersion = "5.0.1"
debugImplementation("io.objectbox:objectbox-android-objectbrowser:$objectboxVersion") debugImplementation(libs.objectbox.android.objectbrowser)
// releaseImplementation("io.objectbox:objectbox-android:$objectboxVersion") releaseImplementation("io.objectbox:objectbox-android:$objectboxVersion")
// implementation("io.objectbox:objectbox-fulltext:5.0.1") // implementation("io.objectbox:objectbox-fulltext:5.0.1")
implementation(libs.androidx.recyclerview) implementation(libs.androidx.recyclerview)
implementation("org.greenrobot:eventbus:3.2.0") implementation(libs.eventbus)
} }
apply(plugin = "io.objectbox") apply(plugin = "io.objectbox")
Binary file not shown.
-77
View File
@@ -1,77 +0,0 @@
{
"_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.",
"_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.",
"_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.",
"entities": [
{
"id": "1:594331511073099531",
"lastPropertyId": "11:7949771231314213889",
"name": "Food",
"properties": [
{
"id": "1:2317727855243736226",
"name": "id",
"type": 6,
"flags": 1
},
{
"id": "4:1528837175750321569",
"name": "foodVector",
"indexId": "1:6010838397086628487",
"type": 28,
"flags": 8
},
{
"id": "5:5561277398349179032",
"name": "collectId",
"type": 9
},
{
"id": "6:8313847979709019172",
"name": "foodId",
"type": 9
},
{
"id": "7:1375536603452792462",
"name": "foodName",
"type": 9
},
{
"id": "8:7942065542192169896",
"name": "version",
"type": 9
},
{
"id": "9:4394472487596452023",
"name": "otherField",
"type": 9
},
{
"id": "10:5704890122356491756",
"name": "createTime",
"type": 9
},
{
"id": "11:7949771231314213889",
"name": "isDel",
"type": 1
}
],
"relations": []
}
],
"lastEntityId": "1:594331511073099531",
"lastIndexId": "1:6010838397086628487",
"lastRelationId": "0:0",
"lastSequenceId": "0:0",
"modelVersion": 5,
"modelVersionParserMinimum": 5,
"retiredEntityUids": [],
"retiredIndexUids": [],
"retiredPropertyUids": [
340951913928211738,
5484846939472684412
],
"retiredRelationUids": [],
"version": 1
}
-77
View File
@@ -1,77 +0,0 @@
{
"_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.",
"_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.",
"_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.",
"entities": [
{
"id": "1:594331511073099531",
"lastPropertyId": "11:7949771231314213889",
"name": "Food",
"properties": [
{
"id": "1:2317727855243736226",
"name": "id",
"type": 6,
"flags": 1
},
{
"id": "4:1528837175750321569",
"name": "foodVector",
"indexId": "1:6010838397086628487",
"type": 28,
"flags": 8
},
{
"id": "5:5561277398349179032",
"name": "collectId",
"type": 9
},
{
"id": "6:8313847979709019172",
"name": "foodId",
"type": 9
},
{
"id": "7:1375536603452792462",
"name": "foodName",
"type": 9
},
{
"id": "8:7942065542192169896",
"name": "version",
"type": 9
},
{
"id": "9:4394472487596452023",
"name": "otherField",
"type": 9
},
{
"id": "10:5704890122356491756",
"name": "createTime",
"type": 9
},
{
"id": "11:7949771231314213889",
"name": "isDel",
"type": 5
}
],
"relations": []
}
],
"lastEntityId": "1:594331511073099531",
"lastIndexId": "1:6010838397086628487",
"lastRelationId": "0:0",
"lastSequenceId": "0:0",
"modelVersion": 5,
"modelVersionParserMinimum": 5,
"retiredEntityUids": [],
"retiredIndexUids": [],
"retiredPropertyUids": [
340951913928211738,
5484846939472684412
],
"retiredRelationUids": [],
"version": 1
}
+2 -1
View File
@@ -61,7 +61,8 @@
<!-- </activity>--> <!-- </activity>-->
<!-- <activity android:name="com.sw.dualscreen.activity.CollectedDataActivity" />--> <!-- <activity android:name="com.sw.dualscreen.activity.CollectedDataActivity" />-->
<activity android:name="com.sw.dualscreen.activity.SettingActivity" <activity android:name="com.sw.dualscreen.activity.SettingActivity"
android:theme="@style/Theme.AppCompat.Light.NoActionBar" /> android:theme="@style/Theme.AppCompat.Light.NoActionBar"
android:launchMode="singleTask" />
<activity android:name="com.sw.dualscreen.activity.CollectedFoodActivity" <activity android:name="com.sw.dualscreen.activity.CollectedFoodActivity"
android:theme="@style/Theme.AppCompat.Light.NoActionBar" /> android:theme="@style/Theme.AppCompat.Light.NoActionBar" />
<activity android:name="com.sw.dualscreen.activity.PayActivity" <activity android:name="com.sw.dualscreen.activity.PayActivity"
@@ -17,11 +17,16 @@ object GlobalData {
*/ */
var appVersion: String = "1" var appVersion: String = "1"
var appBaseUrl: String = ""
/** /**
* 具体业务baseurl * 具体业务 BaseUrl
*/ */
var appBaseUrl2: String = "http://192.168.1.201:14801" // const val LOCAL_BASE_URL = "http://192.168.1.201:14801"
var appBaseUrl: String = "https://dev.yixiong-tech.com:8081" const val LOCAL_BASE_URL = "http://192.168.10.101:24801"
// const val TEST_BASE_URL = "https://dev.yixiong-tech.com:8083"
const val TEST_BASE_URL = "https://dev.yixiong-tech.com:8081"
const val PROD_BASE_URL = "https://platform-api.uat.shuziweidao.com"
/** /**
* 菜品识别模型版本号,后边以接口返回为准 * 菜品识别模型版本号,后边以接口返回为准
@@ -68,4 +73,8 @@ object GlobalKey {
const val KEY_USER_INFO = "userInfoKey" const val KEY_USER_INFO = "userInfoKey"
const val KEY_PICKUP_MODE = "pickupMode" const val KEY_PICKUP_MODE = "pickupMode"
const val KEY_CHARGE_MODE = "chargeMode" const val KEY_CHARGE_MODE = "chargeMode"
// 餐具重量(克),取餐提交时用于从就餐重量中扣减,默认 0 表示不扣减
const val KEY_DISH_WEIGHT = "dishWeight"
const val KEY_SETTLEMENT_MODE = "settlementMode"
const val KEY_BASE_URL = "baseUrlKey"
} }
+13 -2
View File
@@ -6,6 +6,7 @@ import com.sw.dualscreen.objbox.ObjectBox
import com.sw.dualscreen.sdk.SensorScaleUtils import com.sw.dualscreen.sdk.SensorScaleUtils
import com.sw.dualscreen.utils.ActivityManager import com.sw.dualscreen.utils.ActivityManager
import com.sw.dualscreen.utils.CrashHandler import com.sw.dualscreen.utils.CrashHandler
import com.sw.dualscreen.utils.SpTool
import com.sw.plate.App import com.sw.plate.App
import com.sw.plate.utils.AppUtil import com.sw.plate.utils.AppUtil
import timber.log.Timber import timber.log.Timber
@@ -13,7 +14,7 @@ import timber.log.Timber
class MyApp : App() { class MyApp : App() {
companion object { companion object {
const val DEBUG: Boolean = true const val DEBUG: Boolean = true
var instance: MyApp?=null var instance: MyApp? = null
} }
override fun onCreate() { override fun onCreate() {
@@ -24,13 +25,23 @@ class MyApp : App() {
Timber.d("deviceId = $deviceId") Timber.d("deviceId = $deviceId")
// deviceId = "3ea47dc0-3cf0-3c2f-909c-265a9a65572e" // deviceId = "3ea47dc0-3cf0-3c2f-909c-265a9a65572e"
GlobalData.deviceId = deviceId GlobalData.deviceId = deviceId
GlobalData.appBaseUrl = if ("2987f0c5-5754-33e9-b00a-251db5e2e55f" == deviceId) GlobalData.appBaseUrl2 else GlobalData.appBaseUrl
ObjectBox.init(this) ObjectBox.init(this)
// 初始化崩溃处理器 // 初始化崩溃处理器
CrashHandler.init(this) CrashHandler.init(this)
SensorScaleUtils.startScale() SensorScaleUtils.startScale()
addActivityLifecycleListener() addActivityLifecycleListener()
// 优先读取用户手动切换后持久化的 url
val savedUrl = SpTool.baseUrl
if (!savedUrl.isNullOrEmpty()) {
GlobalData.appBaseUrl = savedUrl
} else {
// 未保存过则使用默认逻辑:特定设备走测试环境,其余走 UAT 测试档口机:2987f0c5-5754-33e9-b00a-251db5e2e55f
GlobalData.appBaseUrl =
if ("2987f0c5-5754-33e9-b00a-251db5e2e55f" == deviceId) GlobalData.TEST_BASE_URL else GlobalData.PROD_BASE_URL
}
} }
private fun addActivityLifecycleListener() { private fun addActivityLifecycleListener() {
@@ -1,6 +1,7 @@
package com.sw.dualscreen.activity package com.sw.dualscreen.activity
import android.content.Context import android.content.Context
import android.content.Intent
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.hardware.display.DisplayManager import android.hardware.display.DisplayManager
import android.os.Bundle import android.os.Bundle
@@ -182,4 +183,52 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
} }
return super.dispatchKeyEvent(event) return super.dispatchKeyEvent(event)
} }
private var permissionCallback: ((isGranted: Boolean) -> Unit)? = null
private var activityCallback: ((intent: Intent?) -> Unit)? = null
fun requestMultiplePermissions(
permissions: Array<String>,
callback: (isGranted: Boolean) -> Unit
) {
this.permissionCallback = callback
requestMultiplePermissionsLauncher.launch(permissions)
}
fun requestPermission(permission: String, callback: (isGranted: Boolean) -> Unit) {
this.permissionCallback = callback
requestPermissionLauncher.launch(permission)
}
val requestMultiplePermissionsLauncher = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { permissions ->
var isGranted = true
permissions.entries.forEach {
if (!it.value) {
isGranted = false
}
}
permissionCallback?.invoke(isGranted)
}
// 权限请求回调
val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
permissionCallback?.invoke(isGranted)
}
fun startActivity(intent: Intent, callback: (Intent?) -> Unit) {
this.activityCallback = callback
startActivityLauncher.launch(intent)
}
// activity页面返回的回调
private val startActivityLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
activityCallback?.invoke(it.data)
}
} }
@@ -5,7 +5,6 @@ import androidx.activity.viewModels
import androidx.core.widget.addTextChangedListener import androidx.core.widget.addTextChangedListener
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.sw.dualscreen.GlobalData
import com.sw.dualscreen.R import com.sw.dualscreen.R
import com.sw.dualscreen.adapter.CollectedFoodNewAdapter import com.sw.dualscreen.adapter.CollectedFoodNewAdapter
import com.sw.dualscreen.databinding.ActivityCollectedFoodBinding import com.sw.dualscreen.databinding.ActivityCollectedFoodBinding
@@ -14,10 +13,10 @@ import com.sw.dualscreen.ext.addOnActionSearchListener
import com.sw.dualscreen.ext.gone import com.sw.dualscreen.ext.gone
import com.sw.dualscreen.ext.hideKeyboard import com.sw.dualscreen.ext.hideKeyboard
import com.sw.dualscreen.ext.visible import com.sw.dualscreen.ext.visible
import com.sw.dualscreen.objbox.CollectedFoodInfo import com.sw.dualscreen.model.response.v2.CollectedFoodV2
import com.sw.dualscreen.objbox.ObjectBox import com.sw.dualscreen.objbox.ObjectBox
import com.sw.dualscreen.viewmodel.BaseViewModel import com.sw.dualscreen.viewmodel.BaseViewModel
import com.sw.dualscreen.viewmodel.UserViewModel import com.sw.dualscreen.viewmodel.NetViewModelV2
import com.sw.plate.utils.ToastUtils import com.sw.plate.utils.ToastUtils
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -27,7 +26,7 @@ class CollectedFoodActivity : BaseActivity<ActivityCollectedFoodBinding>() {
private const val PAGE_SIZE = 100 private const val PAGE_SIZE = 100
} }
private val viewModel by viewModels<UserViewModel>() private val viewModel by viewModels<NetViewModelV2>()
override fun getViewModel(): BaseViewModel { override fun getViewModel(): BaseViewModel {
return viewModel return viewModel
} }
@@ -36,7 +35,7 @@ class CollectedFoodActivity : BaseActivity<ActivityCollectedFoodBinding>() {
return ActivityCollectedFoodBinding.inflate(layoutInflater) return ActivityCollectedFoodBinding.inflate(layoutInflater)
} }
private val list: MutableList<CollectedFoodInfo> = mutableListOf() private val list: MutableList<CollectedFoodV2> = mutableListOf()
private val adapter by lazy { private val adapter by lazy {
CollectedFoodNewAdapter(list).apply { CollectedFoodNewAdapter(list).apply {
// isStateViewEnable = true // isStateViewEnable = true
@@ -127,19 +126,26 @@ class CollectedFoodActivity : BaseActivity<ActivityCollectedFoodBinding>() {
// loadEmptyView() // loadEmptyView()
// } // }
viewModel.getCollectedFoodList( viewModel.getCollectPage(
pageNo = pageNo, pageNum = pageNo.toLong(),
pageSize = PAGE_SIZE, pageSize = PAGE_SIZE.toLong(),
foodName = searchName ?: "" foodName = searchName,
) { items -> onSuccess = { items ->
runOnUiThread { runOnUiThread {
loadFoodList(items) loadFoodList(items)
} }
},
onFailure = {
runOnUiThread {
finishRefresh()
ToastUtils.showToast(it)
} }
} }
)
}
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
private fun loadFoodList(items: List<CollectedFoodInfo>) { private fun loadFoodList(items: List<CollectedFoodV2>) {
finishRefresh() finishRefresh()
if (pageNo == 1 && items.isEmpty()) { if (pageNo == 1 && items.isEmpty()) {
list.clear() list.clear()
@@ -191,7 +197,7 @@ class CollectedFoodActivity : BaseActivity<ActivityCollectedFoodBinding>() {
private fun deleteGoods(position: Int) { private fun deleteGoods(position: Int) {
val food = list[position] val food = list[position]
showWaitingDialog("加载中……") showWaitingDialog("加载中……")
viewModel.deleteCollectFood(food.foodId, GlobalData.foodModelVersion) { deleteSuccess -> viewModel.deleteCollect(food.foodId, food.version) { deleteSuccess ->
if (deleteSuccess) { if (deleteSuccess) {
// Thread {}.start() // Thread {}.start()
lifecycleScope.launch { lifecycleScope.launch {
@@ -4,7 +4,6 @@ import android.Manifest
import android.content.Intent import android.content.Intent
import android.os.CountDownTimer import android.os.CountDownTimer
import android.provider.Settings import android.provider.Settings
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@@ -12,46 +11,27 @@ import com.sw.dualscreen.GlobalData
import com.sw.dualscreen.databinding.ActivityInitBinding import com.sw.dualscreen.databinding.ActivityInitBinding
import com.sw.dualscreen.dialog.RemindDialog import com.sw.dualscreen.dialog.RemindDialog
import com.sw.dualscreen.ext.clickWithDebounce import com.sw.dualscreen.ext.clickWithDebounce
import com.sw.dualscreen.ext.invisible
import com.sw.dualscreen.ext.visible import com.sw.dualscreen.ext.visible
import com.sw.dualscreen.model.response.FoodVector
import com.sw.dualscreen.objbox.Food
import com.sw.dualscreen.objbox.FoodModule import com.sw.dualscreen.objbox.FoodModule
import com.sw.dualscreen.objbox.ObjectBox import com.sw.dualscreen.utils.FoodVectorTool
import com.sw.dualscreen.utils.L import com.sw.dualscreen.utils.L
import com.sw.dualscreen.utils.NetworkUtils
import com.sw.dualscreen.utils.SpTool
import com.sw.dualscreen.viewmodel.BaseViewModel import com.sw.dualscreen.viewmodel.BaseViewModel
import com.sw.dualscreen.viewmodel.UserViewModel import com.sw.dualscreen.viewmodel.NetViewModelV2
import com.sw.plate.utils.AppUtil import com.sw.plate.utils.AppUtil
import com.sw.plate.utils.ToastUtils
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class InitActivity : BaseActivity<ActivityInitBinding>() { class InitActivity : BaseActivity<ActivityInitBinding>() {
companion object { companion object {
private const val PAGE_SIZE = 100 const val PAGE_SIZE = 100
} }
private val userViewModel by viewModels<UserViewModel>() private val userViewModel by viewModels<NetViewModelV2>()
private var pageNum = 1 private var pageNum = 1
// 相机权限请求回调
private val requestCameraPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
if (isGranted) {
// 权限请求成功,打开 MainActivity
goMainActivity()
} else {
// 权限请求失败,显示弹窗
showPermissionDeniedDialog()
}
}
// 设置页面返回的回调
private val startSettingsLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
// 从设置页面返回,重新检查权限
checkCameraPermissionAfterSettings()
}
override fun getViewModel(): BaseViewModel { override fun getViewModel(): BaseViewModel {
return userViewModel return userViewModel
} }
@@ -61,55 +41,22 @@ class InitActivity : BaseActivity<ActivityInitBinding>() {
} }
private fun getCollectedFoodVector(block: () -> Unit) { private fun getCollectedFoodVector(block: () -> Unit) {
userViewModel.getCollectedFoodVector( FoodVectorTool.loadAndSaveFoodVector(
param = mutableMapOf( userViewModel = userViewModel,
"pageNum" to "$pageNum", lifecycleScope = lifecycleScope,
"pageSize" to "$PAGE_SIZE", successBlock = { block() },
"version" to GlobalData.foodModelVersion failureBlock = { getVectorErrorDialog() }
) )
) { items ->
if (items == null) {
getVectorErrorDialog()
return@getCollectedFoodVector
}
val list = items
if (pageNum == 1 && list.isEmpty()) {
block()
return@getCollectedFoodVector
}
if (list.size >= PAGE_SIZE) {
saveFoodVector(list)
pageNum++
getCollectedFoodVector(block)
return@getCollectedFoodVector
}
if (list.isNotEmpty()) {
saveFoodVector(list)
block()
}
}
} }
// private val box by lazy { ObjectBox.boxStore.boxFor(Food::class) } // private val box by lazy { ObjectBox.boxStore.boxFor(Food::class) }
private fun saveFoodVector(list: List<FoodVector>) {
lifecycleScope.launch {
val vectorList = list.map { vt ->
val foodVector = vt.foodVector?.removeSurrounding("[", "]")?.split(",")
?.map { it.toFloatOrNull() ?: 0.0f }?.toFloatArray()
Food(
collectId = vt.id,
foodId = vt.foodId,
foodName = vt.foodName,
version = vt.version,
foodVector = foodVector
)
}
ObjectBox.putAll(vectorList)
}
}
override fun initialize() { override fun initialize() {
super.initialize() super.initialize()
//写死,后续改为从接口获取结算类型 ------------------------------------------
//val settlementMode = 1
//SPUtil.getInstance().put(GlobalKey.KEY_SETTLEMENT_MODE, settlementMode)
registerKeyEvent() registerKeyEvent()
initNetworkTimer() initNetworkTimer()
binding.okButton.clickWithDebounce(500) { binding.okButton.clickWithDebounce(500) {
@@ -118,21 +65,6 @@ class InitActivity : BaseActivity<ActivityInitBinding>() {
} }
showWaitingDialog("网络连接中...") showWaitingDialog("网络连接中...")
checkNetworkTimer?.start() checkNetworkTimer?.start()
// val isSuccess = viewModel.checkEquipmentInfo()
// if (isSuccess) {
// goMainActivity()
// return
// }
// binding.imgQr.setImageBitmap(
// QRCodeUtil.generateQRCode(
// content = GlobalData.deviceId,
// size = 200
// )
// )
// binding.initButton.setOnClickListener {
// viewModel.getDeviceToken()
// }
} }
override fun registerDataChange() { override fun registerDataChange() {
@@ -146,20 +78,21 @@ class InitActivity : BaseActivity<ActivityInitBinding>() {
} }
private fun getCollectedFoodVector() { private fun getCollectedFoodVector() {
binding.okButton.invisible()
showWaitingDialog2("加载中……") showWaitingDialog2("加载中……")
lifecycleScope.launch { lifecycleScope.launch {
FoodModule.init(this@InitActivity) { FoodModule.init(this@InitActivity) {
getCollectedFoodVector { getCollectedFoodVector {
runOnUiThread { runOnUiThread {
hideWaitingDialog() hideWaitingDialog()
}
}
}
binding.root.postDelayed({ binding.root.postDelayed({
checkCameraPermissionAndGo() checkCameraPermissionAndGo()
}, 1000) }, 1000)
} }
} }
}
}
}
private fun goMainActivity() { private fun goMainActivity() {
val intent = Intent(this, MainActivity::class.java) val intent = Intent(this, MainActivity::class.java)
@@ -167,30 +100,30 @@ class InitActivity : BaseActivity<ActivityInitBinding>() {
finish() finish()
} }
// 检查相机权限 private fun hasCameraPermission(): Boolean {
private fun checkCameraPermissionAndGo() { return ContextCompat.checkSelfPermission(
val hasCameraPermission = ContextCompat.checkSelfPermission(
this, this,
Manifest.permission.CAMERA Manifest.permission.CAMERA
) == android.content.pm.PackageManager.PERMISSION_GRANTED ) == android.content.pm.PackageManager.PERMISSION_GRANTED
}
if (hasCameraPermission) { // 检查相机权限
private fun checkCameraPermissionAndGo() {
val isGrantedCameraPermission = hasCameraPermission()
if (isGrantedCameraPermission) {
// 有权限,直接打开 MainActivity // 有权限,直接打开 MainActivity
goMainActivity() goMainActivity()
} else { } else {
// 无权限,请求相机权限 // 无权限,请求相机权限
requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA) requestPermission(Manifest.permission.CAMERA) { isGranted ->
checkCameraPermission(isGranted)
}
} }
} }
// 从设置页面返回后检查权限 // 从设置页面返回后检查权限
private fun checkCameraPermissionAfterSettings() { private fun checkCameraPermission(isGranted: Boolean) {
val hasCameraPermission = ContextCompat.checkSelfPermission( if (isGranted) {
this,
Manifest.permission.CAMERA
) == android.content.pm.PackageManager.PERMISSION_GRANTED
if (hasCameraPermission) {
// 权限已授予,打开 MainActivity // 权限已授予,打开 MainActivity
goMainActivity() goMainActivity()
} else { } else {
@@ -209,7 +142,11 @@ class InitActivity : BaseActivity<ActivityInitBinding>() {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = android.net.Uri.fromParts("package", packageName, null) data = android.net.Uri.fromParts("package", packageName, null)
} }
startSettingsLauncher.launch(intent) startActivity(intent) {
// 从设置页面返回,重新检查权限
val isGrantedCameraPermission = hasCameraPermission()
checkCameraPermission(isGrantedCameraPermission)
}
} }
).show() ).show()
} }
@@ -230,17 +167,57 @@ class InitActivity : BaseActivity<ActivityInitBinding>() {
loadNetworkErrorDialog() loadNetworkErrorDialog()
return return
} }
getDeviceConfig()
getCollectedFoodVector() getCollectedFoodVector()
} }
} }
} }
// var arcsoftAppId = "Hkz1rBk6PZXbS8KwKr67K2eZtsz8bRoMHLg64bUdgCZj"
// var arcsoftSdkKey = "AabXs3sHM8UhhE7oCGf4LVMLrdkGb1nJNksbjjTdVt7k"
// var arcsoftActiveKey = "085F-118G-Q4GH-B2YH"
private fun getDeviceConfig() {
userViewModel.getDeviceConfig(
onSuccess = { deviceConfig ->
runOnUiThread {
if (deviceConfig == null) {
ToastUtils.showToast("获取设备配置数据失败")
return@runOnUiThread
}
val settlementMode = if (deviceConfig.payType == 2) 0 else 1
SpTool.settlementMode = settlementMode
// // TODO: 临时写死用于测试----------------------------------------
// deviceConfig.arcsoftAppId = arcsoftAppId
// deviceConfig.arcsoftSdkKey = arcsoftSdkKey
// deviceConfig.arcsoftActiveKey = arcsoftActiveKey
// // TODO: 临时写死用于测试----------------------------------------
GlobalData.appId = deviceConfig.arcsoftAppId ?: ""
GlobalData.sdkKey = deviceConfig.arcsoftSdkKey ?: ""
GlobalData.activeKey = deviceConfig.arcsoftActiveKey ?: ""
}
},
onFailure = { runOnUiThread { ToastUtils.showToast(it) } }
)
}
private fun loadNetworkErrorDialog() { private fun loadNetworkErrorDialog() {
RemindDialog( RemindDialog(
context = context, context = context,
content = "网络连接异常,请检查WiFi连接状态后重试", content = "网络连接异常,请检查WiFi连接状态后重试",
confirmBlock = { confirmBlock = {
startActivity(Intent("android.settings.WIFI_SETTINGS")) val intent = Intent("android.settings.WIFI_SETTINGS")
startActivity(intent) {
// 从设置页面返回,重新检查网络连接状态
val isConnected = NetworkUtils.isNetworkConnected(this@InitActivity)
if (isConnected.not()) {
loadNetworkErrorDialog()
return@startActivity
}
getCollectedFoodVector()
}
}).show() }).show()
} }
@@ -257,4 +234,27 @@ class InitActivity : BaseActivity<ActivityInitBinding>() {
checkNetworkTimer?.cancel() checkNetworkTimer?.cancel()
super.onDestroy() super.onDestroy()
} }
// private fun loadTestDb() {
// // 从测试库导入人脸数据
// FaceDbImporter.importFromAssets(this, object : FaceDbImporter.ImportCallback {
// public override fun onProgress(current: Int, total: Int) {
// showWaitingDialog("导入人脸数据 " + current + "/" + total)
// }
//
// public override fun onError(message: String?) {
// hideWaitingDialog()
// ToastUtils.showToast("离线数据导入失败:" + message)
// }
//
// public override fun onComplete() {
// hideWaitingDialog()
// ToastUtils.showToast("离线数据导入成功")
// // 后续流程会自动跳转
// }
// })
// }
} }
File diff suppressed because it is too large Load Diff
@@ -20,7 +20,7 @@ import com.sw.dualscreen.utils.Debouncer
import com.sw.dualscreen.utils.SPUtil import com.sw.dualscreen.utils.SPUtil
import com.sw.dualscreen.utils.SoundPoolUtil import com.sw.dualscreen.utils.SoundPoolUtil
import com.sw.dualscreen.viewmodel.BaseViewModel import com.sw.dualscreen.viewmodel.BaseViewModel
import com.sw.dualscreen.viewmodel.UserViewModel import com.sw.dualscreen.viewmodel.NetViewModelV2
import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import timber.log.Timber import timber.log.Timber
@@ -43,7 +43,7 @@ class PayActivity : BaseActivity<ActivityPayBinding>() {
const val PAY_SUCCESS = "pay_success" const val PAY_SUCCESS = "pay_success"
} }
val userViewModel by viewModels<UserViewModel>() val userViewModel by viewModels<NetViewModelV2>()
val recognizeViewModel by viewModels<RecognizeViewModel>() val recognizeViewModel by viewModels<RecognizeViewModel>()
override fun getViewModel(): BaseViewModel { override fun getViewModel(): BaseViewModel {
@@ -166,6 +166,8 @@ class PayActivity : BaseActivity<ActivityPayBinding>() {
super.onDestroy() super.onDestroy()
} }
var memberDiscount = 1.0
var memberInfo: MemberInfo? = null var memberInfo: MemberInfo? = null
fun showPayInfo(type: Int = 1, isVip: Boolean = false, memberInfo: MemberInfo? = null) { fun showPayInfo(type: Int = 1, isVip: Boolean = false, memberInfo: MemberInfo? = null) {
this.memberInfo = memberInfo this.memberInfo = memberInfo
@@ -191,10 +193,20 @@ class PayActivity : BaseActivity<ActivityPayBinding>() {
it.type = type it.type = type
it.foodName = foodInfo?.foodName it.foodName = foodInfo?.foodName
//确认使用vipPrice 还是 specPrice //确认使用vipPrice 还是 specPrice
val onePrice = if (isVip) foodInfo?.vipPrice else foodInfo?.specPrice //val onePrice = if (isVip) foodInfo?.vipPrice else foodInfo?.specPrice
it.totalPrice = (onePrice ?: 0.0) * eatNum if (isVip) {
userViewModel.getMemberDiscount(memberInfo?.faceUserId?:"") { discount ->
memberDiscount = discount ?: 1.0
var onePrice = foodInfo?.specPrice ?: 0.0
onePrice = if (isVip) onePrice * memberDiscount else onePrice
it.totalPrice = onePrice * eatNum
presentation.initView() presentation.initView()
} }
} else {
it.totalPrice = (foodInfo?.specPrice ?: 0.0) * eatNum
presentation.initView()
}
}
} }
fun updateQrCodeImage(qrCodeUrl: String?) { fun updateQrCodeImage(qrCodeUrl: String?) {
@@ -287,13 +299,20 @@ class PayActivity : BaseActivity<ActivityPayBinding>() {
} }
fun bindOrder(userId: String, block: (Boolean) -> Unit) { fun bindOrder(userId: String, block: (Boolean) -> Unit) {
//0-即放即取,1-余量计量
val pickupMode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0) val pickupMode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0)
val mode = if (pickupMode == 1) 1 else 2 val mode = if (pickupMode == 1) 1 else 2
userViewModel.bindOrder(userId, foodOrderId, mode = mode) { bindResult -> val uid = userId.toLongOrNull() ?: run {
runOnUiThread { runOnUiThread { block(false) }
block(bindResult) return
}
} }
userViewModel.bindUserOrder(
userId = uid,
orderNo = foodOrderId,
mode = mode,
onSuccess = { runOnUiThread { block(true) } },
onFailure = { runOnUiThread { block(false) } }
)
} }
fun qrCodePay(scanInfo: String) { fun qrCodePay(scanInfo: String) {
@@ -11,12 +11,12 @@ import com.sw.dualscreen.model.response.FoodInfo
import com.sw.dualscreen.model.response.ClickBackEvent import com.sw.dualscreen.model.response.ClickBackEvent
import com.sw.dualscreen.model.response.UpdateRefreshEvent import com.sw.dualscreen.model.response.UpdateRefreshEvent
import com.sw.dualscreen.viewmodel.BaseViewModel import com.sw.dualscreen.viewmodel.BaseViewModel
import com.sw.dualscreen.viewmodel.UserViewModel import com.sw.dualscreen.viewmodel.NetViewModelV2
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
class SettingActivity : BaseActivity<ActivitySettingBinding>() { class SettingActivity : BaseActivity<ActivitySettingBinding>() {
val viewModel by viewModels<UserViewModel>() val viewModel by viewModels<NetViewModelV2>()
override fun getViewModel(): BaseViewModel { override fun getViewModel(): BaseViewModel {
return viewModel return viewModel
} }
@@ -6,32 +6,43 @@ import com.sw.dualscreen.R
import com.sw.dualscreen.activity.SettingActivity import com.sw.dualscreen.activity.SettingActivity
import com.sw.dualscreen.databinding.FragmentBusinessBinding import com.sw.dualscreen.databinding.FragmentBusinessBinding
import com.sw.dualscreen.model.response.ChargeModeEvent import com.sw.dualscreen.model.response.ChargeModeEvent
import com.sw.dualscreen.model.response.ResetRecognizeEvent
import com.sw.dualscreen.sdk.SensorScaleUtils import com.sw.dualscreen.sdk.SensorScaleUtils
import com.sw.dualscreen.utils.SPUtil import com.sw.dualscreen.utils.SPUtil
import com.sw.dualscreen.utils.SpTool
import com.sw.plate.utils.ToastUtils import com.sw.plate.utils.ToastUtils
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import kotlin.math.roundToInt import kotlin.math.roundToInt
class BusinessFragment: BaseFragment<FragmentBusinessBinding>() { class BusinessFragment: BaseFragment<FragmentBusinessBinding>() {
// 当前秤的实时读数(克),作为待保存的餐具重量
private var dishWeight: Int = 0
override fun inflateViewBinding(): FragmentBusinessBinding { override fun inflateViewBinding(): FragmentBusinessBinding {
return FragmentBusinessBinding.inflate(LayoutInflater.from(context)) return FragmentBusinessBinding.inflate(LayoutInflater.from(context))
} }
override fun initialize() { override fun initialize() {
// 初始化为已保存的餐具重量,未配置过则为 0
dishWeight = SPUtil.getInstance().get(GlobalKey.KEY_DISH_WEIGHT, 0) ?: 0
// 在标题后显示当前已保存的餐具重量
refreshDishWeightLabel()
binding.rgSettlement.let {
//val settlementMode = if (binding.rgSettlement.checkedRadioButtonId == R.id.rbJointPayment) 0 else 1
//val mode = SPUtil.getInstance().get(GlobalKey.KEY_SETTLEMENT_MODE, 1)
it.check(if (SpTool.settlementMode == 0) R.id.rbJointPayment else R.id.rbIndependentPayment)
binding.rbJointPayment.isEnabled = false
binding.rbIndependentPayment.isEnabled = false
}
binding.rgCharge.let { binding.rgCharge.let {
// it.setOnCheckedChangeListener { group, checkedId -> //0-计费,1-不计费
// val mode = if (checkedId == R.id.rbChargeYes) 0 else 1
// SPUtil.getInstance().put(GlobalKey.KEY_CHARGE_MODE, mode)
// }
val mode = SPUtil.getInstance().get(GlobalKey.KEY_CHARGE_MODE, 0) val mode = SPUtil.getInstance().get(GlobalKey.KEY_CHARGE_MODE, 0)
it.check(if (mode == 0) R.id.rbChargeYes else R.id.rbChargeNot) it.check(if (mode == 0) R.id.rbChargeYes else R.id.rbChargeNot)
} }
binding.rgTakeFood.let { binding.rgTakeFood.let {
// it.setOnCheckedChangeListener { group, checkedId -> //0-即放即取,1-余量计量
// val mode = if (checkedId == R.id.rbPickAndPlace) 0 else 1
// SPUtil.getInstance().put(GlobalKey.KEY_PICKUP_MODE, mode)
// }
val mode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0) val mode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0)
it.check(if (mode == 0) R.id.rbPickAndPlace else R.id.rbSurplusCalculate) it.check(if (mode == 0) R.id.rbPickAndPlace else R.id.rbSurplusCalculate)
} }
@@ -39,6 +50,10 @@ class BusinessFragment: BaseFragment<FragmentBusinessBinding>() {
SensorScaleUtils.tare() SensorScaleUtils.tare()
} }
binding.btnConfirm.setOnClickListener { v -> binding.btnConfirm.setOnClickListener { v ->
// if (binding.rgSettlement.checkedRadioButtonId == -1) {
// ToastUtils.showToast("请设置结算模式")
// return@setOnClickListener
// }
if (binding.rgCharge.checkedRadioButtonId == -1) { if (binding.rgCharge.checkedRadioButtonId == -1) {
ToastUtils.showToast("请设置计费模式") ToastUtils.showToast("请设置计费模式")
return@setOnClickListener return@setOnClickListener
@@ -50,6 +65,7 @@ class BusinessFragment: BaseFragment<FragmentBusinessBinding>() {
val chargeMode = if (binding.rgCharge.checkedRadioButtonId == R.id.rbChargeYes) 0 else 1 val chargeMode = if (binding.rgCharge.checkedRadioButtonId == R.id.rbChargeYes) 0 else 1
//原来的支付模式 //原来的支付模式
//0-计费,1-不计费
val oldChargeMode = SPUtil.getInstance().get(GlobalKey.KEY_CHARGE_MODE, 0) val oldChargeMode = SPUtil.getInstance().get(GlobalKey.KEY_CHARGE_MODE, 0)
if (chargeMode != oldChargeMode) { if (chargeMode != oldChargeMode) {
//支付模式有变化通知首页页面刷新 //支付模式有变化通知首页页面刷新
@@ -59,7 +75,11 @@ class BusinessFragment: BaseFragment<FragmentBusinessBinding>() {
val pickupMode = if (binding.rgTakeFood.checkedRadioButtonId == R.id.rbPickAndPlace) 0 else 1 val pickupMode = if (binding.rgTakeFood.checkedRadioButtonId == R.id.rbPickAndPlace) 0 else 1
SPUtil.getInstance().put(GlobalKey.KEY_PICKUP_MODE, pickupMode) SPUtil.getInstance().put(GlobalKey.KEY_PICKUP_MODE, pickupMode)
// 持久化餐具重量(克)
SPUtil.getInstance().put(GlobalKey.KEY_DISH_WEIGHT, dishWeight)
// 刷新标签文字,显示当前已保存的餐具重量
refreshDishWeightLabel()
EventBus.getDefault().post(ResetRecognizeEvent())
//ToastUtils.showToast("设置已保存") //ToastUtils.showToast("设置已保存")
(activity as SettingActivity).let { (activity as SettingActivity).let {
it.showWaitingDialog("设置保存中") it.showWaitingDialog("设置保存中")
@@ -70,8 +90,18 @@ class BusinessFragment: BaseFragment<FragmentBusinessBinding>() {
} }
} }
SensorScaleUtils.addWeightListener { value -> SensorScaleUtils.addWeightListener { value ->
val realWeight = (value * 1000).roundToInt() //val realWeight = (value * 1000).roundToInt()
binding.tvFoodWeight.text = "$realWeight" // 追踪秤的实时读数(克),作为待保存的餐具重量
dishWeight = value
binding.tvFoodWeight.text = "$value"
} }
} }
/**
* 刷新"餐具重量(克)"标签,在末尾显示当前已保存的配置值。
* 未配置(0)时只显示原标题。
*/
private fun refreshDishWeightLabel() {
binding.tvDishWeightLabel.text = if (dishWeight > 0) "餐具重量(克)${dishWeight}g" else "餐具重量(克)"
}
} }
@@ -34,10 +34,9 @@ import com.sw.dualscreen.utils.Debouncer
import com.sw.dualscreen.utils.ImageUploader import com.sw.dualscreen.utils.ImageUploader
import com.sw.dualscreen.utils.ImageUtil import com.sw.dualscreen.utils.ImageUtil
import com.sw.plate.utils.ToastUtils import com.sw.plate.utils.ToastUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import timber.log.Timber import timber.log.Timber
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
@@ -101,13 +100,14 @@ class CollectFragment : BaseFragment<FragmentCollectBinding>() {
private val cameraCallback: (Uri) -> Unit = { uri -> private val cameraCallback: (Uri) -> Unit = { uri ->
try { try {
val index = foodCollectionList.indexOfFirst { it.imageFile == null } activity?.lifecycleScope?.launch(Dispatchers.Main) {
// 主线程原子操作:查找空闲槽位并立即标记,避免竞态
val index = foodCollectionList.indexOfFirst { it.isShowCamera }
if (index == -1) { if (index == -1) {
ToastUtils.showToast("每次只允许保存${MAX_COUNT}条数据") ToastUtils.showToast("每次只允许保存${MAX_COUNT}条数据")
settingActivity?.hideWaitingDialog() settingActivity?.hideWaitingDialog()
rerurn@ cameraCallback return@launch
} }
activity?.runOnUiThread {
foodCollectionList[index].let { foodCollectionList[index].let {
it.imageVector = null it.imageVector = null
it.bitmap = null it.bitmap = null
@@ -116,13 +116,13 @@ class CollectFragment : BaseFragment<FragmentCollectBinding>() {
it.imageUri = uri it.imageUri = uri
} }
collectionAdapter.notifyItemChanged(index) collectionAdapter.notifyItemChanged(index)
} // IO线程处理bitmap
// settingActivity?.hideWaitingDialog() launch(Dispatchers.IO) {
Thread {
ImageUtil.uriToBitmap(requireActivity(), uri)?.let { bitmap -> ImageUtil.uriToBitmap(requireActivity(), uri)?.let { bitmap ->
getImageVector(index, bitmap) getImageVector(index, bitmap)
} }
}.start() }
}
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
settingActivity?.hideWaitingDialog() settingActivity?.hideWaitingDialog()
@@ -131,6 +131,10 @@ class CollectFragment : BaseFragment<FragmentCollectBinding>() {
} }
} }
private val photoFailCallback: (String) -> Unit = { errMsg ->
settingActivity?.hideWaitingDialog()
}
private fun getImageVector(index: Int, bitmap: Bitmap) { private fun getImageVector(index: Int, bitmap: Bitmap) {
// val bitmap = BitmapCropper.cropCenter( // val bitmap = BitmapCropper.cropCenter(
// original = srcBmp, // original = srcBmp,
@@ -141,7 +145,9 @@ class CollectFragment : BaseFragment<FragmentCollectBinding>() {
FoodModule.bitmap2FloatArray(bitmap, false) FoodModule.bitmap2FloatArray(bitmap, false)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
activity?.lifecycleScope?.launch(Dispatchers.Main) {
ToastUtils.showToast("操作失败") ToastUtils.showToast("操作失败")
}
settingActivity?.log("操作失败:${e.message}") settingActivity?.log("操作失败:${e.message}")
settingActivity?.hideWaitingDialog() settingActivity?.hideWaitingDialog()
return return
@@ -151,19 +157,16 @@ class CollectFragment : BaseFragment<FragmentCollectBinding>() {
) )
settingActivity?.log("${this.javaClass.simpleName}-cameraCallback-裁剪bitmap保存文件路径:${file?.absolutePath}") settingActivity?.log("${this.javaClass.simpleName}-cameraCallback-裁剪bitmap保存文件路径:${file?.absolutePath}")
activity?.runOnUiThread {
foodCollectionList[index].let { foodCollectionList[index].let {
it.imageVector = imageVector it.imageVector = imageVector
it.bitmap = null it.bitmap = null
it.isShowCamera = false it.isShowCamera = false
it.imageFile = file it.imageFile = file
} }
collectionAdapter.notifyItemChanged(index) settingActivity?.hideWaitingDialog()
}
if (bitmap.isRecycled.not()) { if (bitmap.isRecycled.not()) {
bitmap.recycle() bitmap.recycle()
} }
settingActivity?.hideWaitingDialog()
} }
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
@@ -173,19 +176,9 @@ class CollectFragment : BaseFragment<FragmentCollectBinding>() {
ToastUtils.showToast("每次只允许保存${MAX_COUNT}条数据") ToastUtils.showToast("每次只允许保存${MAX_COUNT}条数据")
return return
} }
settingActivity?.showWaitingDialog("采集中……") settingActivity?.showWaitingDialog("采集中……")
cameraUtils.takePhoto(cameraCallback) { errMsg -> cameraUtils.takePhoto(succCallback = cameraCallback, failCallback = photoFailCallback)
settingActivity?.hideWaitingDialog()
// ToastUtils.showToast("拍照异常,请重新操作")
// if (cameraErrorCount >= 10) {
// ToastUtils.showToast("拍照异常,请重新操作")
// settingActivity?.log("拍照异常,请重新操作:$errMsg")
// cameraErrorCount = 0
// return@takePhoto
// }
// cameraErrorCount++
// takePhoto()
}
} }
private var cameraErrorCount = 0 private var cameraErrorCount = 0
@@ -263,14 +256,15 @@ class CollectFragment : BaseFragment<FragmentCollectBinding>() {
lifecycleScope.launch { lifecycleScope.launch {
val totalFileCount = foodCollectionList.count { it.imageFile != null } val totalFileCount = foodCollectionList.count { it.imageFile != null }
settingActivity?.showWaitingDialog2("图片上传中0/$totalFileCount") settingActivity?.showWaitingDialog2("图片上传中0/$totalFileCount")
val params = HashMap<String, RequestBody>() val foodId = checkedItem!!.foodId.toLongOrNull() ?: run {
//params["placeId"] = restId.toRequestBody() ToastUtils.showToast("foodId 格式错误")
params["foodId"] = checkedItem!!.foodId.toRequestBody() return@launch
params["foodName"] = checkedItem!!.foodName!!.toRequestBody() }
params["version"] = GlobalData.foodModelVersion.toRequestBody() val foodName = checkedItem!!.foodName ?: ""
val version = GlobalData.foodModelVersion
ImageUploader(totalList = foodCollectionList, uploadImage = { batch -> ImageUploader(totalList = foodCollectionList, uploadImage = { batch ->
val files = batch.map { it.imageFile } val files = batch.mapNotNull { it.imageFile }
val foodVectorList = batch.filter { it.imageVector != null }.map { val foodVectorList = batch.filter { it.imageVector != null }.map {
it.imageVector!!.joinToString( it.imageVector!!.joinToString(
separator = ",", prefix = "[", postfix = "]" separator = ",", prefix = "[", postfix = "]"
@@ -279,8 +273,13 @@ class CollectFragment : BaseFragment<FragmentCollectBinding>() {
val foodVectorJson = val foodVectorJson =
foodVectorList.joinToString(separator = ",", prefix = "[", postfix = "]") foodVectorList.joinToString(separator = ",", prefix = "[", postfix = "]")
Timber.tag(TAG).d("json=$foodVectorJson") Timber.tag(TAG).d("json=$foodVectorJson")
params["foodVector"] = foodVectorJson.toRequestBody() settingActivity?.viewModel?.uploadCollect(
settingActivity?.viewModel?.uploadCollectFoodPics(files, params) foodId = foodId,
foodName = foodName,
version = version,
foodVector = foodVectorJson,
fileList = files
)
}, onProgress = { count, batch, idList -> }, onProgress = { count, batch, idList ->
//batch.forEach { //batch.forEach {
// it.uploadSuccess = true // it.uploadSuccess = true
@@ -23,7 +23,9 @@ class FacePayFragment : BaseFragment<FragmentFacePayBinding>() {
// } // }
payActivity.foodInfo?.let { payActivity.foodInfo?.let {
foodName = it.foodName foodName = it.foodName
payAmount = (it.vipPrice ?: 0.0) * payActivity.eatNum // payAmount = (it.vipPrice ?: 0.0) * payActivity.eatNum
val price = it.specPrice ?: 0.0
payAmount = price * payActivity.memberDiscount * payActivity.eatNum
} }
showSubScreen() showSubScreen()
} }
@@ -75,7 +75,9 @@ class PayResultFragment : BaseFragment<FragmentPayResultBinding>() {
} }
if (memberInfo != null && memberInfo!!.member) { if (memberInfo != null && memberInfo!!.member) {
totalPrice = (payActivity.foodInfo?.vipPrice ?: 0.0) * payActivity.eatNum // totalPrice = (payActivity.foodInfo?.vipPrice ?: 0.0) * payActivity.eatNum
val price = payActivity.foodInfo?.specPrice ?: 0.0
totalPrice = price * payActivity.memberDiscount * payActivity.eatNum
binding.layoutVip.visible() binding.layoutVip.visible()
balance = (memberInfo!!.topUpBalance ?: 0.0) + (memberInfo!!.rewardBalance ?: 0.0) balance = (memberInfo!!.topUpBalance ?: 0.0) + (memberInfo!!.rewardBalance ?: 0.0)
realPayPrice = if (balance >= totalPrice) 0.0 else totalPrice - balance realPayPrice = if (balance >= totalPrice) 0.0 else totalPrice - balance
@@ -7,17 +7,17 @@ import androidx.core.graphics.toColorInt
import com.chad.library.adapter4.BaseQuickAdapter import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.sw.dualscreen.R import com.sw.dualscreen.R
import com.sw.dualscreen.databinding.ListItemSearchFoodBinding import com.sw.dualscreen.databinding.ListItemSearchFood3Binding
import com.sw.dualscreen.ext.format2String import com.sw.dualscreen.ext.format2String
import com.sw.dualscreen.model.response.FoodInfo import com.sw.dualscreen.model.response.FoodInfo
class CollectFoodListAdapter(var list: MutableList<FoodInfo>) : class CollectFoodListAdapter(var list: MutableList<FoodInfo>) :
BaseQuickAdapter<FoodInfo, CollectFoodListAdapter.VH>(list) { BaseQuickAdapter<FoodInfo, CollectFoodListAdapter.VH>(list) {
inner class VH(var binding: ListItemSearchFoodBinding) : QuickViewHolder(binding.root) inner class VH(var binding: ListItemSearchFood3Binding) : QuickViewHolder(binding.root)
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH { override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
val inflater = LayoutInflater.from(context) val inflater = LayoutInflater.from(context)
val binding = ListItemSearchFoodBinding.inflate(inflater, parent, false) val binding = ListItemSearchFood3Binding.inflate(inflater, parent, false)
return VH(binding) return VH(binding)
} }
@@ -8,10 +8,10 @@ import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.sw.dualscreen.databinding.ListItemCollectedFoodBinding import com.sw.dualscreen.databinding.ListItemCollectedFoodBinding
import com.sw.dualscreen.ext.gone import com.sw.dualscreen.ext.gone
import com.sw.dualscreen.ext.visible import com.sw.dualscreen.ext.visible
import com.sw.dualscreen.objbox.CollectedFoodInfo import com.sw.dualscreen.model.response.v2.CollectedFoodV2
class CollectedFoodNewAdapter(var list: MutableList<CollectedFoodInfo>) : class CollectedFoodNewAdapter(var list: MutableList<CollectedFoodV2>) :
BaseQuickAdapter<CollectedFoodInfo, CollectedFoodNewAdapter.VH>(list) { BaseQuickAdapter<CollectedFoodV2, CollectedFoodNewAdapter.VH>(list) {
inner class VH(var binding: ListItemCollectedFoodBinding) : QuickViewHolder(binding.root) inner class VH(var binding: ListItemCollectedFoodBinding) : QuickViewHolder(binding.root)
@@ -21,7 +21,7 @@ class CollectedFoodNewAdapter(var list: MutableList<CollectedFoodInfo>) :
return VH(binding) return VH(binding)
} }
override fun onBindViewHolder(holder: VH, position: Int, item: CollectedFoodInfo?) { override fun onBindViewHolder(holder: VH, position: Int, item: CollectedFoodV2?) {
holder.binding.tvFoodName.text = item?.foodName holder.binding.tvFoodName.text = item?.foodName
holder.binding.tvCollectedNum.text = "已采集${item?.foodCount}" holder.binding.tvCollectedNum.text = "已采集${item?.foodCount}"
holder.binding.divider.run { holder.binding.divider.run {
@@ -40,7 +40,7 @@ class FoodCollectionAdapter (var list: MutableList<FoodCollectionBean>) :
//setImageURI(it.imageUri) //setImageURI(it.imageUri)
//setImageBitmap(it.bitmap) //setImageBitmap(it.bitmap)
if (it.imageUri!=null) { if (it.imageUri!=null) {
setImageURI(it.imageUri) load(it.imageUri)
} else { } else {
load(it.imageFile) load(it.imageFile)
} }
@@ -0,0 +1,66 @@
package com.sw.dualscreen.adapter
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Color
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.TextView
import androidx.core.graphics.toColorInt
import androidx.core.view.updateLayoutParams
import androidx.recyclerview.widget.RecyclerView
import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.sw.dualscreen.databinding.ListItemFoodOrderBinding
import com.sw.dualscreen.ext.dp
import com.sw.dualscreen.ext.format2String
import com.sw.dualscreen.ext.gone
import com.sw.dualscreen.ext.visible
import com.sw.dualscreen.model.response.FoodItem
class FoodOrderAdapter(list: MutableList<FoodItem>) :
BaseQuickAdapter<FoodItem, FoodOrderAdapter.VH>(list) {
inner class VH(var binding: ListItemFoodOrderBinding) : QuickViewHolder(binding.root)
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
val inflater = LayoutInflater.from(context)
val binding = ListItemFoodOrderBinding.inflate(inflater, parent, false)
return VH(binding)
}
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: VH, position: Int, item: FoodItem?) {
val binding = holder.binding
setTextColor(binding.tvFoodName, binding.tvPriceNorm, binding.tvWeight, binding.tvAmount)
item?.let {
binding.tvFoodName.text = it.foodName
binding.tvFoodName.setTextColor(if (it.isLocalData) Color.GREEN else "#FF33446A".toColorInt())
if (it.orderFrom == 2) {
binding.tvPriceNorm.text = if (it.specName.isNullOrBlank()) "${it.specWeight}" else "${it.specName}/${it.specWeight}"
} else {
// 营养秤结算:规格栏显示规格重量 specWeight
binding.tvPriceNorm.text = "${it.specWeight}"
}
//if (it.specName.isNullOrBlank().not()) {
// binding.tvPriceFlag.text = "[${it.specName}]"
//}
binding.tvWeight.text = "${it.num}"
binding.tvAmount.text = it.price.format2String(2)
binding.tvLabel.run {
if (it.orderFrom == 2) visible() else gone()
}
}
binding.root.updateLayoutParams<RecyclerView.LayoutParams> {
topMargin = 2.dp
bottomMargin = 2.dp
}
}
fun setTextColor(vararg tvList: TextView) {
tvList.forEach {
it.setTextColor("#FF33446A".toColorInt())
}
}
}
@@ -0,0 +1,69 @@
package com.sw.dualscreen.dialog
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.WindowManager
import com.sw.dualscreen.GlobalData
import com.sw.dualscreen.databinding.DialogEnvSwitchBinding
import com.sw.dualscreen.ext.dp
import com.sw.dualscreen.utils.SpTool
import com.sw.plate.utils.ToastUtils
/**
* 环境切换弹窗
*
* 提供 TEST / UAT / PROD 三套环境的切换功能,
* 初始化时自动根据 [GlobalData.appBaseUrl] 选中当前环境,
* 确认后更新 [GlobalData.appBaseUrl] 并通过 [onEnvChanged] 回调通知外部。
*
* @param context 上下文
* @param onEnvChanged 确认切换后的回调,参数为新的 baseUrl
*/
class EnvSwitchDialog(
context: Context,
private val onEnvChanged: (newBaseUrl: String) -> Unit = {}
) : BaseDialog(
context,
defWidth = 600.dp,
defHeight = WindowManager.LayoutParams.WRAP_CONTENT
) {
private lateinit var binding: DialogEnvSwitchBinding
override fun getRootView(): View {
binding = DialogEnvSwitchBinding.inflate(LayoutInflater.from(context))
return binding.root
}
override fun initView() {
// 根据当前 appBaseUrl 预选对应 RadioButton,不匹配则不选
when (GlobalData.appBaseUrl) {
GlobalData.LOCAL_BASE_URL -> binding.rbTest.isChecked = true
GlobalData.TEST_BASE_URL -> binding.rbUat.isChecked = true
GlobalData.PROD_BASE_URL -> binding.rbProd.isChecked = true
}
// 取消按钮
binding.btnCancel.setOnClickListener { dismiss() }
// 确认按钮
binding.btnConfirm.setOnClickListener {
// 未选中任何选项时提示用户
if (binding.rgEnv.checkedRadioButtonId == -1) {
ToastUtils.showToast("请选择环境")
return@setOnClickListener
}
val newUrl = when (binding.rgEnv.checkedRadioButtonId) {
binding.rbTest.id -> GlobalData.LOCAL_BASE_URL
binding.rbUat.id -> GlobalData.TEST_BASE_URL
binding.rbProd.id -> GlobalData.PROD_BASE_URL
else -> return@setOnClickListener
}
GlobalData.appBaseUrl = newUrl
SpTool.baseUrl = newUrl
onEnvChanged(newUrl)
dismiss()
}
}
}
@@ -7,8 +7,11 @@ import android.graphics.BitmapFactory
import android.graphics.ImageFormat import android.graphics.ImageFormat
import android.graphics.Rect import android.graphics.Rect
import android.graphics.YuvImage import android.graphics.YuvImage
import android.util.DisplayMetrics
import android.util.TypedValue import android.util.TypedValue
import android.view.TouchDelegate
import android.view.View import android.view.View
import android.view.WindowManager
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.EditText import android.widget.EditText
@@ -159,3 +162,35 @@ fun View.invisible() {
fun View.gone() { fun View.gone() {
visibility = View.GONE visibility = View.GONE
} }
val Context.screenSize: IntArray
get() {
val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
val displayMetrics = DisplayMetrics()
windowManager.defaultDisplay.getMetrics(displayMetrics)
return intArrayOf(
displayMetrics.widthPixels,
displayMetrics.heightPixels
)
}
/**
* 扩大 View 的点击区域
* @param horizontal 水平方向扩大距离(dp)→ 左右各扩大一半
* @param vertical 垂直方向扩大距离(dp)→ 上下各扩大一半
*/
fun View.expandClickArea(horizontal: Int, vertical: Int) {
val density = resources.displayMetrics.density
val hPx = (horizontal * density).toInt()
val vPx = (vertical * density).toInt()
post {
val rect = android.graphics.Rect()
getHitRect(rect)
// 扩大区域:负值 = 向外扩大
rect.inset(-hPx, -vPx)
//rect.bottom += downPx // 只往下扩大
(parent as View).touchDelegate = TouchDelegate(rect, this)
}
}
@@ -0,0 +1,11 @@
package com.sw.dualscreen.model.request.v2
/**
* 新系统绑定用户与订单请求参数
* 对应接口:/neglect/booth/order/bind-user
*/
data class BindUserOrderRequest(
val userId: Long,
val orderNo: String,
val mode: Int? = null
)
@@ -0,0 +1,25 @@
package com.sw.dualscreen.model.request.v2
import java.math.BigDecimal
/**
* 新系统开餐下单请求参数
* 对应接口:/neglect/booth/order/place
*/
data class PlaceOrderRequest(
val foodId: Long,
val foodName: String,
val foodWeight: BigDecimal,
val eatWeight: BigDecimal,
val eatNum: Int,
val notPay: Boolean,
val mode: Int,
val specId: Long?,
val userId: Long?,
val paymentFrom: Int? = null,
val foodMaterialId: Long? = null,
val member: Boolean? = null,
val remark: String? = null,
val deviceId: String? = null,
val cookOrderId: String? = null
)
@@ -34,7 +34,11 @@ data class FoodOrder(
var remark: String? = "", var remark: String? = "",
//即放即取-1,称重-2 //即放即取-1,称重-2
var mode: Int var mode: Int,
//设备模式,1 结算终端,2档口机
var paymentFrom: Int,
//是否会员
var member: Boolean
) )
@Parcelize @Parcelize
@@ -96,12 +100,14 @@ data class UserNutrition(
val fruitsVegetablesNearExcess: String? = null, val fruitsVegetablesNearExcess: String? = null,
//果蔬超量 //果蔬超量
val fruitsVegetablesExcess: String? = null, val fruitsVegetablesExcess: String? = null,
//肉蛋推荐值 //肉蛋推荐值
val meatEggsRecommend: String? = null, val meatEggsBeansRecommend: String? = null,
//肉蛋即将超量 //肉蛋即将超量
val meatEggsNearExcess: String? = null, val meatEggsBeansNearExcess: String? = null,
//肉蛋超量 //肉蛋超量
val meatEggsExcess: String? = null, val meatEggsBeansExcess: String? = null,
//总重量
val eatWeightSum:Double? = null,
) )
//{"code":"00000","data":"{\"orderType\":0,\"orderPayFrom\":2,\"payType\":\"1\",\"paySuc\":0,\"totalFee\":0.01,\"orderCode\":\"1998193614899372032\",\"message\":\"支付成功\",\"payCode\":\"1998193614899372032508227\"}","msg":"成功","total":0} //{"code":"00000","data":"{\"orderType\":0,\"orderPayFrom\":2,\"payType\":\"1\",\"paySuc\":0,\"totalFee\":0.01,\"orderCode\":\"1998193614899372032\",\"message\":\"支付成功\",\"payCode\":\"1998193614899372032508227\"}","msg":"成功","total":0}
@@ -123,7 +129,7 @@ data class RespCodeMsg(
data class PostEvent(var name: String = "") data class PostEvent(var name: String = "")
data class UpdateRefreshEvent(var name:String = "") data class UpdateRefreshEvent(var name: String = "")
data class PaySuccessEvent( data class PaySuccessEvent(
var name: String = "" var name: String = ""
) )
@@ -131,6 +137,9 @@ data class PaySuccessEvent(
data class ChargeModeEvent( data class ChargeModeEvent(
var chargeMode: Int = 0 var chargeMode: Int = 0
) )
data class ResetRecognizeEvent(
var state: Int = 0
)
data class ClickBackEvent( data class ClickBackEvent(
var name: String = "" var name: String = ""
@@ -139,7 +148,9 @@ data class ClickBackEvent(
data class DeviceConfig( data class DeviceConfig(
var arcsoftAppId: String? = null, var arcsoftAppId: String? = null,
var arcsoftSdkKey: String? = null, var arcsoftSdkKey: String? = null,
var arcsoftActiveKey: String? = null var arcsoftActiveKey: String? = null,
//结算模式:1-独立支付模式,2-联合结算模式
var payType: Int = 1
) )
data class FoodVector( data class FoodVector(
@@ -159,3 +170,32 @@ data class WeightRecord(
var lastWeight: Int = 0, var lastWeight: Int = 0,
var currentWeight: Int = 0 var currentWeight: Int = 0
) )
data class FoodItem(
var id: String? = null,
var foodId: String? = null,
var foodName: String? = null,
var foodMaterialId: String? = null,
var specId: String? = null,
var num: Int = 0,
var eatNum: Int = 0,
var price: Double = 0.0,
var foodPrice: Double = 0.0,
var discount: Double = 0.0,
var income: Double = 0.0,
var foodDifferenceId: Long = 0,
var createTime: String? = null,
var specName: String? = null,
var specWeight: Int = 0,
var orderFrom: Int = 0,
var isLocalData: Boolean = false
)
data class FoodOrderModel(
val calorie: Double = 0.0,
val incomeSum: Double = 0.0,
val discountSum: Double = 0.0,
val eatWeightSum: Int = 0,
val orderNo: String? = null,
val list: List<FoodItem>? = null
)
@@ -43,6 +43,8 @@ data class FoodInfo(
val recommendCalorie: Double? = null, val recommendCalorie: Double? = null,
//图片 //图片
var foodImg: String? = "", var foodImg: String? = "",
//按单烹制 ID(菜品列表返回,就餐时原样回传,用于溯源码追踪)
var cookOrderId: String? = "",
//----------------------------------- //-----------------------------------
var score: Int = 0, var score: Int = 0,
@@ -0,0 +1,19 @@
package com.sw.dualscreen.model.response.v2
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
/**
* 新系统采集菜品信息
* 对应接口:/neglect/booth/collect/page
*/
@Parcelize
data class CollectedFoodV2(
val foodId: String?,
val foodName: String?,
val version: String?,
val foodVector: String?,
val picUrls: List<String>?,
/** 已采集数量(客户端手动赋值,非接口字段,用于兼容旧版 UI) */
var foodCount: Int = 0
) : Parcelable
@@ -0,0 +1,22 @@
package com.sw.dualscreen.model.response.v2
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
/**
* 新系统人脸数据 VO
* 对应接口:/nutrition/neglect/common/face/page 和 /nutrition/neglect/common/face/increment
*/
@Parcelize
data class FaceVO(
val userFaceId: String?,
val userId: String?,
val faceFeature: String?,
/** 后端返回 String 类型(防止 JS 大数精度丢失),映射时自行转为 Long */
val faceUpdateTimestamp: String?,
val cardNo: String?,
val member: Boolean?,
val faceDeleted: Boolean?,
/** 人员类型:由后端定义,如 "1"-会员、"2"-临时用户、"3"-其他等 */
val personType: String?
) : Parcelable
@@ -0,0 +1,57 @@
package com.sw.dualscreen.model.response.v2
import android.os.Parcelable
import com.sw.dualscreen.model.response.FoodInfo
import kotlinx.parcelize.Parcelize
import java.math.BigDecimal
/**
* 新系统菜品信息
* 对应接口:/neglect/booth/food/by-names
*/
@Parcelize
data class NewFoodInfo(
val foodId: Long,
val foodName: String?,
val calorie: BigDecimal?,
val protein: BigDecimal?,
val fat: BigDecimal?,
val carbohydrate: BigDecimal?,
val price: BigDecimal?,
val specPrice: BigDecimal?,
val specWeight: BigDecimal?,
val stapleFood: BigDecimal?,
val fruitsVegetables: BigDecimal?,
val meatEggs: BigDecimal?,
val foodMaterialId: Long?,
val specId: Long?,
val foodImg: String?,
val foodLabel: String?,
val vipPrice: BigDecimal? = null,
val recommendCalorie: Int = 0,
val tablewareStatus: Boolean = false,
val tablewareWeight: Int = 0,
//按单烹制 ID(菜品列表返回,就餐时原样回传,用于溯源码追踪)
var cookOrderId: String? = "",
) : Parcelable
/** NewFoodInfo → FoodInfo 映射(V2 数据模型兼容旧版 UI) */
fun NewFoodInfo.toFoodInfo() = FoodInfo(
foodId = foodId.toString(),
foodName = foodName,
calorie = calorie?.toDouble(),
protein = protein?.toDouble(),
fat = fat?.toDouble(),
carbohydrate = carbohydrate?.toDouble(),
specPrice = specPrice?.toDouble(),
vipPrice = vipPrice?.toDouble(),
foodMaterialId = foodMaterialId?.toString(),
specId = specId?.toString(),
specWeight = specWeight?.toDouble(),
stapleFood = stapleFood?.toDouble(),
fruitsVegetables = fruitsVegetables?.toDouble(),
meatEggs = meatEggs?.toDouble(),
recommendCalorie = recommendCalorie.toDouble(),
foodImg = foodImg,
cookOrderId = cookOrderId,
)
@@ -0,0 +1,35 @@
package com.sw.dualscreen.model.response.v2
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import com.sw.dualscreen.model.response.MemberInfo
import java.math.BigDecimal
/**
* 新系统会员信息
* 对应接口:/neglect/booth/member/info 和 /neglect/booth/member/info-by-phone
*/
@Parcelize
data class NewMemberInfo(
val id: String?,
val phone: String?,
val name: String?,
val faceUrl: String?,
val topUpBalance: BigDecimal?,
val rewardBalance: BigDecimal? = BigDecimal.ZERO,
val integralBalance: Int?,
val member: Boolean?
) : Parcelable
/** NewMemberInfo → MemberInfo 映射(V2 数据模型兼容旧版 UIfaceUserId 复用 id */
fun NewMemberInfo.toMemberInfo() = MemberInfo(
id = id,
faceUserId = id,
phone = phone,
name = name,
faceUrl = faceUrl,
topUpBalance = topUpBalance?.toDouble(),
rewardBalance = rewardBalance?.toDouble(),
integralBalance = integralBalance,
member = member ?: false
)
@@ -0,0 +1,55 @@
package com.sw.dualscreen.model.response.v2
import android.os.Parcelable
import com.sw.dualscreen.model.response.FoodItem
import com.sw.dualscreen.model.response.FoodOrderModel
import kotlinx.parcelize.Parcelize
import java.math.BigDecimal
/**
* 新系统结算订单数据
* 对应接口:/neglect/booth/order/settlement
*/
@Parcelize
data class SettlementOrder(
val calorie: BigDecimal?,
val incomeSum: BigDecimal?,
val discountSum: BigDecimal?,
val eatWeightSum: Int?,
val orderNo: String?,
val list: List<SettlementFoodItem>?
) : Parcelable
@Parcelize
data class SettlementFoodItem(
val foodId: String?,
val foodName: String?,
val specId: String?,
val specName: String?,
val specWeight: BigDecimal?,
val eatNum: Int?,
val eatWeight: Int?,
val price: BigDecimal?
) : Parcelable
/** SettlementOrder → FoodOrderModel 映射(V2 数据模型兼容旧版 UI) */
fun SettlementOrder.toFoodOrderModel() = FoodOrderModel(
calorie = calorie?.toDouble() ?: 0.0,
incomeSum = incomeSum?.toDouble() ?: 0.0,
discountSum = discountSum?.toDouble() ?: 0.0,
eatWeightSum = eatWeightSum ?: 0,
orderNo = orderNo,
list = list?.map { it.toFoodItem() }
)
private fun SettlementFoodItem.toFoodItem() = FoodItem(
foodId = foodId,
foodName = foodName,
specId = specId,
specName = specName,
specWeight = specWeight?.toInt() ?: 0,
eatNum = eatNum ?: 0,
price = price?.toDouble() ?: 0.0,
// 重量栏读取 num 字段,这里用实际食用重量 eatWeight 赋值
num = eatWeight ?: 0
)
@@ -1,8 +1,15 @@
package com.sw.dualscreen.network package com.sw.dualscreen.network
import com.sw.dualscreen.BuildConfig
import com.sw.dualscreen.GlobalData
import com.sw.dualscreen.MyApp import com.sw.dualscreen.MyApp
import com.sw.dualscreen.network.api.ApiService import com.sw.dualscreen.network.api.ApiService
import com.sw.dualscreen.network.api.ApiServiceV2
import com.sw.dualscreen.network.interceptor.RequestInterceptor import com.sw.dualscreen.network.interceptor.RequestInterceptor
import com.sw.dualscreen.repository.RemoteRepository
import com.sw.dualscreen.repository.v2.RemoteRepositoryV2
import com.sw.dualscreen.utils.FileUtil
import okhttp3.Interceptor
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit import retrofit2.Retrofit
@@ -11,22 +18,36 @@ import timber.log.Timber
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
object ApiClient { object ApiClient {
private const val BASE_URL = "http://device.shuziweidao.com:8889/" // private const val BASE_URL = "http://device.shuziweidao.com:8889/"
private const val BASE_URL = GlobalData.PROD_BASE_URL
private const val TIME_OUT = 30L // 超时时间(秒) private const val TIME_OUT = 30L
private val okHttpClient = OkHttpClient.Builder() private val okHttpClient = OkHttpClient.Builder()
.connectTimeout(TIME_OUT, TimeUnit.SECONDS) .connectTimeout(TIME_OUT, TimeUnit.SECONDS)
.readTimeout(TIME_OUT, TimeUnit.SECONDS) .readTimeout(TIME_OUT, TimeUnit.SECONDS)
.writeTimeout(TIME_OUT, TimeUnit.SECONDS) .writeTimeout(TIME_OUT, TimeUnit.SECONDS)
.addNetworkInterceptor(HttpLoggingInterceptor(logger = { .addNetworkInterceptor(Interceptor { chain ->
Timber.d("okhttp logger ==>${it}") val request = chain.request()
val url = request.url.toString()
val tag = if (
url.contains("terminal/neglect/common/app/faceFeature/increment/list") ||
url.contains("nutrition/neglect/serve/face/increment")
) "faceIncrement"
else if (url.contains("terminal/neglect/pay/app/turnOrderInfo")) "turnOrderInfo"
else if(url.contains("nutrition/neglect")) "V2"
else "ApiClient"
val loggingInterceptor = HttpLoggingInterceptor(logger = {
Timber.tag(tag).d("okhttp logger ==>${it}")
FileUtil.saveLog("okhttp logger ==>${it}")
}).apply { }).apply {
level = if (MyApp.DEBUG) { level = if (BuildConfig.DEBUG) {
HttpLoggingInterceptor.Level.BODY HttpLoggingInterceptor.Level.BODY
} else { } else {
HttpLoggingInterceptor.Level.NONE HttpLoggingInterceptor.Level.NONE
} }
}
loggingInterceptor.intercept(chain)
}) })
.addInterceptor(RequestInterceptor()) .addInterceptor(RequestInterceptor())
.build() .build()
@@ -35,10 +56,21 @@ object ApiClient {
.baseUrl(BASE_URL) .baseUrl(BASE_URL)
.client(okHttpClient) .client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create()) .addConverterFactory(GsonConverterFactory.create())
// .addCallAdapterFactory(CoroutineCallAdapterFactory()) // 协程适配器
.build() .build()
val apiService: ApiService by lazy { val apiService: ApiService by lazy {
retrofit.create(ApiService::class.java) retrofit.create(ApiService::class.java)
} }
val apiServiceV2: ApiServiceV2 by lazy {
retrofit.create(ApiServiceV2::class.java)
}
val repository: RemoteRepository by lazy {
RemoteRepository(apiService)
}
val repositoryV2: RemoteRepositoryV2 by lazy {
RemoteRepositoryV2(apiServiceV2)
}
} }
@@ -8,6 +8,7 @@ import com.sw.dualscreen.model.response.DinnerType
import com.sw.dualscreen.model.response.FaceData import com.sw.dualscreen.model.response.FaceData
import com.sw.dualscreen.model.response.FoodInfo import com.sw.dualscreen.model.response.FoodInfo
import com.sw.dualscreen.model.response.FoodOrder import com.sw.dualscreen.model.response.FoodOrder
import com.sw.dualscreen.model.response.FoodOrderModel
import com.sw.dualscreen.model.response.FoodSearchReq import com.sw.dualscreen.model.response.FoodSearchReq
import com.sw.dualscreen.model.response.FoodVector import com.sw.dualscreen.model.response.FoodVector
import com.sw.dualscreen.model.response.MemberInfo import com.sw.dualscreen.model.response.MemberInfo
@@ -311,4 +312,22 @@ interface ApiService {
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/boothMachine/app/deleteBoothMachineCollection", @Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/boothMachine/app/deleteBoothMachineCollection",
@QueryMap param: MutableMap<String, String?> @QueryMap param: MutableMap<String, String?>
): ApiResponse<Any?> ): ApiResponse<Any?>
/**
* 查询就餐数据
*/
@GET
suspend fun getFoodOrderList(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/settlement/terminal/app/getFoodInfoBySettlement",
@Query("userId") userId: String
): ApiResponse<FoodOrderModel?>
/**
* 查询会员折扣
*/
@GET
suspend fun getMemberDiscount(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/getMemberDiscount",
@Query("userId") userId: String
): ApiResponse<Double?>
} }
@@ -0,0 +1,134 @@
package com.sw.dualscreen.network.api
import com.sw.dualscreen.GlobalData
import com.sw.dualscreen.model.request.v2.BindUserOrderRequest
import com.sw.dualscreen.model.request.v2.PlaceOrderRequest
import com.sw.dualscreen.model.response.ApiResponse
import com.sw.dualscreen.model.response.DeviceConfig
import com.sw.dualscreen.model.response.FoodSearchReq
import com.sw.dualscreen.model.response.UserNutrition
import com.sw.dualscreen.model.response.v2.CollectedFoodV2
import com.sw.dualscreen.model.response.v2.FaceVO
import com.sw.dualscreen.model.response.v2.NewFoodInfo
import com.sw.dualscreen.model.response.v2.NewMemberInfo
import com.sw.dualscreen.model.response.v2.SettlementOrder
import okhttp3.MultipartBody
import okhttp3.RequestBody
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.Part
import retrofit2.http.PartMap
import retrofit2.http.Url
/**
* 新系统 API 接口服务
* 基于 /nutrition/neglect/booth 模块前缀
* 文档版本:2026-05-27
*/
interface ApiServiceV2 {
@GET
suspend fun getDeviceConfig(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/device/config"
): ApiResponse<DeviceConfig?>
/**
* 获取全量人脸数据
*/
@POST
suspend fun getFacePage(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/common/face/page",
@Body request: Map<String, Long>
): ApiResponse<List<FaceVO>?>
/**
* 获取增量人脸数据
*/
@POST
suspend fun getFaceIncrement(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/common/face/increment",
@Body request: Map<String, Long>
): ApiResponse<List<FaceVO>?>
@POST
suspend fun getFoodByNames(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/food/by-names",
@Body request: FoodSearchReq
): ApiResponse<List<NewFoodInfo>?>
@POST
suspend fun searchFood(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/food/search",
@Body request: Map<String, @JvmSuppressWildcards Any>
): ApiResponse<List<NewFoodInfo>?>
@POST
suspend fun getUserCurrentFood(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/user/current-food",
@Body request: Map<String, Long>
): ApiResponse<UserNutrition?>
@POST
suspend fun placeOrder(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/order/place",
@Body request: PlaceOrderRequest
): ApiResponse<String?>
@POST
suspend fun getSettlementOrders(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/order/settlement",
@Body request: Map<String, Long>
): ApiResponse<SettlementOrder?>
@POST
suspend fun getMemberInfo(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/member/info",
@Body request: Map<String, Long>
): ApiResponse<NewMemberInfo?>
@POST
suspend fun getMemberInfoByPhone(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/member/info-by-phone",
@Body request: Map<String, String>
): ApiResponse<NewMemberInfo?>
@POST
suspend fun getMemberDiscount(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/member/discount",
@Body request: Map<String, Long>
): ApiResponse<String?>
@POST
suspend fun bindUserOrder(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/order/bind-user",
@Body request: BindUserOrderRequest
): ApiResponse<Any?>
@POST
suspend fun getCollectPage(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/collect/page",
@Body request: Map<String, @JvmSuppressWildcards Any>
): ApiResponse<List<CollectedFoodV2>?>
@POST
suspend fun getCollectVectorPage(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/collect/vector-page",
@Body request: Map<String, Long>
): ApiResponse<List<CollectedFoodV2>?>
@Multipart
@POST
suspend fun uploadCollect(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/collect/upload",
@PartMap params: HashMap<String, RequestBody>,
@Part foodPics: List<MultipartBody.Part>
): ApiResponse<List<String>?>
@POST
suspend fun deleteCollect(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/collect/delete",
@Body request: Map<String, @JvmSuppressWildcards Any>
): ApiResponse<Any?>
}
@@ -15,6 +15,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.pytorch.IValue import org.pytorch.IValue
import org.pytorch.Module import org.pytorch.Module
import org.pytorch.Tensor
import org.pytorch.torchvision.TensorImageUtils import org.pytorch.torchvision.TensorImageUtils
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
@@ -29,6 +30,9 @@ object FoodModule {
// private const val THRESHOLD = 0.0 // private const val THRESHOLD = 0.0
private var module: Module? = null private var module: Module? = null
// 专用推理线程:与 Kotlin IO 调度器完全隔离,避免 ArcSoft 污染该线程的 FPU 状态(FPSCR)
private val inferenceExecutor = java.util.concurrent.Executors.newSingleThreadExecutor()
// private lateinit var embeddingsList: List<List<Float>> // private lateinit var embeddingsList: List<List<Float>>
// private lateinit var labelsList: IntArray // private lateinit var labelsList: IntArray
// private lateinit var classInfo: FoodClassInfo // private lateinit var classInfo: FoodClassInfo
@@ -46,6 +50,23 @@ object FoodModule {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val modelPath = copyAssetToCache(context, "best_embedding_model_mobile.pt") val modelPath = copyAssetToCache(context, "best_embedding_model_mobile.pt")
module = Module.load(modelPath) module = Module.load(modelPath)
// 预热:在相机启动前先跑一次 forward,触发 PyTorch 线程池创建
// 在预热前将当前线程优先级调低,使 PyTorch 内部新建的工作线程也继承低优先级
// 从而降低后续推理时的瞬间功率,避免触发 PMIC 过流保护
Timber.tag(TAG).d("预热 forward() 开始")
val warmupTensor = Tensor.fromBlob(
FloatArray(1 * 3 * 224 * 224),
longArrayOf(1, 3, 224, 224)
)
inferenceExecutor.submit { module?.forward(IValue.from(warmupTensor)) }.get()
context.let { ctx ->
val am = ctx.getSystemService(android.content.Context.ACTIVITY_SERVICE)
as android.app.ActivityManager
val mi = android.app.ActivityManager.MemoryInfo()
am.getMemoryInfo(mi)
Timber.tag(TAG).d("预热完成后系统可用内存: ${mi.availMem / 1024 / 1024} MB / 总: ${mi.totalMem / 1024 / 1024} MB")
}
Timber.tag(TAG).d("预热 forward() 完成")
//初始化默认重新拉取数据,先清空本地数据 //初始化默认重新拉取数据,先清空本地数据
val list = ObjectBox.getAll() val list = ObjectBox.getAll()
if (list.isNotEmpty()) { if (list.isNotEmpty()) {
@@ -86,8 +107,28 @@ object FoodModule {
if (module == null) { if (module == null) {
return null return null
} }
val outputTensor = module?.forward(IValue.from(inputTensor))?.toTensor() Timber.tag(TAG).d("module:${module}")
return outputTensor?.dataAsFloatArray val iValue = IValue.from(inputTensor)
Timber.tag(TAG).d("inputTensor:${inputTensor}")
// 推理前记录系统可用内存,排查是否因内存不足触发 OOM 重启
MyApp.instance?.let { ctx ->
val am = ctx.getSystemService(android.content.Context.ACTIVITY_SERVICE)
as android.app.ActivityManager
val mi = android.app.ActivityManager.MemoryInfo()
am.getMemoryInfo(mi)
Timber.tag(TAG).d("forward() 前系统可用内存: ${mi.availMem / 1024 / 1024} MB / 总: ${mi.totalMem / 1024 / 1024} MB, lowMemory=${mi.lowMemory}")
}
Timber.tag(TAG).d("forward() 开始: ${System.currentTimeMillis()}")
val outputIValue = inferenceExecutor.submit<IValue?> { module?.forward(iValue) }
.get(10, java.util.concurrent.TimeUnit.SECONDS) ?: return null
Timber.tag(TAG).d("forward() 结束: ${System.currentTimeMillis()}")
Timber.tag(TAG).d("outputIValue:${outputIValue}")
val outputTensor = outputIValue.toTensor()
Timber.tag(TAG).d("outputTensor:${outputTensor}")
return outputTensor.dataAsFloatArray
} catch (e: OutOfMemoryError) { } catch (e: OutOfMemoryError) {
e.printStackTrace() e.printStackTrace()
} finally { } finally {
@@ -133,18 +174,10 @@ object FoodModule {
//查询比较分数 //查询比较分数
// val tempList = query.findWithScores().sortedBy { it.score }.map { "${it.get().name}|${it.get().foodIdx}|${it.score}" } // val tempList = query.findWithScores().sortedBy { it.score }.map { "${it.get().name}|${it.get().foodIdx}|${it.score}" }
// val idScoreList = query.findIdsWithScores() // val idScoreList = query.findIdsWithScores()
val objScoreList = ObjectBox.query(floatArray, queryCount) val tripleList = ObjectBox.query(floatArray, queryCount)
Timber.tag(TAG).d("idScoreList:${GsonUtils.toJson(objScoreList)}") Timber.tag(TAG).d("idScoreList:${GsonUtils.toJson(tripleList)}")
val nameScoreList = mutableListOf<IdNameScore>() val nameScoreList = tripleList.map { (id, name, score) ->
objScoreList.forEach { IdNameScore(id = id, name = name ?: "", score = score)
val food = it.get()
nameScoreList.add(
IdNameScore(
id = food.id,
name = food.foodName ?: "",
score = it.score
)
)
} }
val nameScoreData = GsonUtils.toJson(nameScoreList) val nameScoreData = GsonUtils.toJson(nameScoreList)
Timber.tag(TAG).d("queryFood,耗时:${System.currentTimeMillis() - startTime},数据:$nameScoreData") Timber.tag(TAG).d("queryFood,耗时:${System.currentTimeMillis() - startTime},数据:$nameScoreData")
@@ -153,7 +186,9 @@ object FoodModule {
suspend fun getFoodScoreList(bitmap: Bitmap, queryCount: Int = 15): List<IdNameScore> { suspend fun getFoodScoreList(bitmap: Bitmap, queryCount: Int = 15): List<IdNameScore> {
val startTime = System.currentTimeMillis() val startTime = System.currentTimeMillis()
val floatArray = bitmap2FloatArray(bitmap, false) val floatArray = withContext(Dispatchers.IO) {
bitmap2FloatArray(bitmap, false)
}
Timber.tag(TAG).d("bitmap2FloatArray,耗时:${System.currentTimeMillis() - startTime}") Timber.tag(TAG).d("bitmap2FloatArray,耗时:${System.currentTimeMillis() - startTime}")
val nameScoreList = queryFoodNameScore(floatArray, queryCount) val nameScoreList = queryFoodNameScore(floatArray, queryCount)
@@ -161,28 +196,26 @@ object FoodModule {
return emptyList() return emptyList()
} }
val maxScoreList = nameScoreList val maxScoreList = nameScoreList
.filter { it.score < 1 - THRESHOLD } // .filter { it.score < 1 - THRESHOLD }
.groupBy { it.name } .groupBy { it.name }
.map { (_, value) -> value.minByOrNull { it.score }!! } .map { (_, value) -> value.minByOrNull { it.score }!! }
.toMutableList() .toMutableList()
val map = mutableMapOf<String, Int>() // val map = mutableMapOf<String, Int>()
nameScoreList.forEach { // nameScoreList.forEach {
val key = it.name // val key = it.name
val count = map[key] ?: 0 // val count = map[key] ?: 0
map[key] = count + 1 // map[key] = count + 1
} // }
val orderList = map.entries.sortedByDescending { it.value }.map { it.key }.toMutableList() // val orderList = map.entries.sortedByDescending { it.value }.map { it.key }.toMutableList()
val firstFood = nameScoreList[0].name // val firstFood = nameScoreList[0].name
orderList.remove(firstFood) // orderList.remove(firstFood)
orderList.add(0, firstFood) // orderList.add(0, firstFood)
//
val sortedScoreList = maxScoreList.sortedWith(compareBy { // val sortedScoreList = maxScoreList.sortedWith(compareBy {
orderList.indexOf(it.name) // orderList.indexOf(it.name)
}) // })
if (bitmap.isRecycled.not()) { val sortedScoreList = maxScoreList.sortedBy { it.score }
bitmap.recycle() Timber.tag("FoodModule").d("getFoodScoreList数据:${sortedScoreList.toString()}")
}
//Timber.tag("FoodModule").d("getFoodScoreList数据:${sortedScoreList.toJsonString()}")
return sortedScoreList return sortedScoreList
} }
@@ -19,12 +19,13 @@ package com.sw.dualscreen.objbox
import android.content.Context import android.content.Context
import android.os.Environment import android.os.Environment
import android.util.Log import android.util.Log
import com.sw.dualscreen.BuildConfig
import com.sw.plate.App import com.sw.plate.App
import io.objectbox.Box import io.objectbox.Box
import io.objectbox.BoxStore import io.objectbox.BoxStore
import io.objectbox.BoxStoreBuilder import io.objectbox.BoxStoreBuilder
import io.objectbox.android.Admin import io.objectbox.android.Admin
import io.objectbox.android.ObjectBoxLiveData
import io.objectbox.config.DebugFlags import io.objectbox.config.DebugFlags
import io.objectbox.exception.DbException import io.objectbox.exception.DbException
import io.objectbox.exception.FileCorruptException import io.objectbox.exception.FileCorruptException
@@ -91,14 +92,14 @@ object ObjectBox {
} }
} }
// if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
val syncAvailable = if (Sync.isAvailable()) "available" else "unavailable" val syncAvailable = if (Sync.isAvailable()) "available" else "unavailable"
Timber.tag(TAG) Timber.tag(TAG)
.d("Using ObjectBox ${BoxStore.getVersion()} (${BoxStore.getVersionNative()}, sync $syncAvailable)") .d("Using ObjectBox ${BoxStore.getVersion()} (${BoxStore.getVersionNative()}, sync $syncAvailable)")
// Enable ObjectBox Admin on debug builds. // Enable ObjectBox Admin on debug builds.
// https://docs.objectbox.io/data-browser // https://docs.objectbox.io/data-browser
Admin(boxStore).start(context.applicationContext) Admin(boxStore).start(context.applicationContext)
// } }
} }
@@ -151,19 +152,118 @@ object ObjectBox {
} }
suspend fun query(floatArray: FloatArray, queryCount: Int) = safeDbOp { suspend fun query(floatArray: FloatArray, queryCount: Int) = safeDbOp {
val query: Query<Food>? = getBox<Food>()?.query() val box = getBox<Food>() ?: return@safeDbOp emptyList<Triple<Long, String?, Double>>()
?.equal(Food_.isDel, false)
?.and() val query: Query<Food> = box.query()
?.nearestNeighbors(Food_.foodVector, floatArray, queryCount) .equal(Food_.isDel, false)
?.build() .and()
.nearestNeighbors(Food_.foodVector, floatArray, queryCount)
.build()
try { try {
query?.findWithScores() // 统一使用 findIdsWithScores,避免在 32 位系统上因加载大对象而崩溃
val idScoreList = query.findIdsWithScores()
if (idScoreList.isNotEmpty()) {
// 根据 ID 查询完整对象,仅提取 id 和 name,丢弃 foodVector 以节省内存
val foodMap = box.get(idScoreList.map { it.id }).associateBy { it.id }
idScoreList.map { scoreId ->
val food = foodMap[scoreId.id]
Triple(scoreId.id, food?.foodName, scoreId.score)
}
} else {
emptyList()
}
} catch (e: DbException) {
Timber.tag(TAG).e(e, "向量查询失败")
emptyList()
} finally { } finally {
// 先关闭Query,释放Cursor query.close()
query?.close()
} }
} ?: emptyList() } ?: emptyList()
// suspend fun queryWithScore(floatArray: FloatArray, queryCount: Int) = safeDbOp {
// val box = getBox<Food>() ?: return@safeDbOp emptyList()
//
// val query: Query<Food>? = box.query()
// ?.equal(Food_.isDel, false)
// ?.and()
// ?.nearestNeighbors(Food_.foodVector, floatArray, queryCount)
// ?.build()
//
// try {
// // 检测是否为 32 位系统
// val is32Bit = Build.SUPPORTED_ABIS.any {
// it.contains("armeabi") && !it.contains("arm64")
// }
//
// if (is32Bit) {
// // 32位系统:使用 findWithScoresAndIds 替代 findWithScores
// val scoreIds = query?.findWithScoresAndIds()
// if (scoreIds != null && scoreIds.isNotEmpty()) {
// // 如果需要分数,返回 ScoreId 对象
// // 注意:这需要修改返回类型或使用其他方式传递分数
// scoreIds.toList()
// } else {
// emptyList()
// }
// } else {
// // 64位系统:正常使用 findWithScores
// query?.findWithScores() ?: emptyList()
// }
// } catch (e: DbException) {
// // 如果仍然失败,记录日志并返回空列表
// Timber.tag(TAG).e(e, "Vector query with score failed, returning empty list")
// emptyList()
// } finally {
// query?.close()
// }
// } ?: emptyList()
// suspend fun query(floatArray: FloatArray, queryCount: Int) = safeDbOp {
// val box = getBox<Food>() ?: return@safeDbOp emptyList()
//
// val query: Query<Food>? = box.query()
// ?.equal(Food_.isDel, false)
// ?.and()
// ?.nearestNeighbors(Food_.foodVector, floatArray, queryCount)
// ?.build()
//
// try {
// // 检测是否为 32 位系统
// val is32Bit = Build.SUPPORTED_ABIS.any {
// it.contains("armeabi") && !it.contains("arm64")
// }
//
// if (is32Bit) {
// // 32位系统:先获取带分数的IDs,再批量获取完整对象
// val scoreIds = query?.findWithScoresAndIds()
// if (scoreIds != null && scoreIds.isNotEmpty()) {
// // 提取 IDs
// val ids = scoreIds.map { it.id }.toLongArray()
// // 批量获取完整对象
// val foods = box.get(ids.toList()).associateBy { it.id }
// // 按照分数顺序组装结果(保持排序)
// scoreIds.mapNotNull { scoreId ->
// foods[scoreId.id]
// }
// } else {
// emptyList()
// }
// } else {
// // 64位系统:直接使用 findWithScores
// query?.findWithScores() ?: emptyList()
// }
// } catch (e: DbException) {
// // 如果仍然失败,记录日志并返回空列表
// Timber.tag(TAG).e(e, "Vector query failed, returning empty list")
// emptyList()
// } finally {
// query?.close()
// }
// } ?: emptyList()
// ... existing code ...
suspend fun get(id: Long) = safeDbOp { suspend fun get(id: Long) = safeDbOp {
getBox<Food>()?.get(id) getBox<Food>()?.get(id)
} }
@@ -1,49 +1,51 @@
package com.sw.dualscreen.presentation package com.sw.dualscreen.presentation
import android.Manifest import android.Manifest
import android.annotation.SuppressLint
import android.app.Presentation import android.app.Presentation
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Outline
import android.graphics.Point import android.graphics.Point
import android.hardware.Camera import android.hardware.Camera
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.text.TextUtils import android.util.Log
import android.util.DisplayMetrics
import android.view.Display import android.view.Display
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewOutlineProvider
import android.view.ViewTreeObserver import android.view.ViewTreeObserver
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.isInvisible
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.arcsoft.face.ErrorInfo import com.arcsoft.face.ErrorInfo
import com.sw.dualscreen.BuildConfig
import com.sw.dualscreen.GlobalKey import com.sw.dualscreen.GlobalKey
import com.sw.dualscreen.MyApp
import com.sw.dualscreen.R import com.sw.dualscreen.R
import com.sw.dualscreen.activity.MainActivity import com.sw.dualscreen.activity.MainActivity
import com.sw.dualscreen.adapter.FoodOrderAdapter
import com.sw.dualscreen.databinding.PresentationMainScreenBinding import com.sw.dualscreen.databinding.PresentationMainScreenBinding
import com.sw.dualscreen.ext.dp import com.sw.dualscreen.ext.dp
import com.sw.dualscreen.ext.format2String import com.sw.dualscreen.ext.format2String
import com.sw.dualscreen.ext.gone import com.sw.dualscreen.ext.gone
import com.sw.dualscreen.ext.invisible
import com.sw.dualscreen.ext.load import com.sw.dualscreen.ext.load
import com.sw.dualscreen.ext.maskName import com.sw.dualscreen.ext.maskName
import com.sw.dualscreen.ext.roundedDecimalPlace import com.sw.dualscreen.ext.roundedDecimalPlace
import com.sw.dualscreen.ext.screenSize
import com.sw.dualscreen.ext.visible import com.sw.dualscreen.ext.visible
import com.sw.dualscreen.model.response.DinnerType import com.sw.dualscreen.model.response.DinnerType
import com.sw.dualscreen.model.response.FoodInfo import com.sw.dualscreen.model.response.FoodInfo
import com.sw.dualscreen.model.response.FoodItem
import com.sw.dualscreen.model.response.UserEnergy import com.sw.dualscreen.model.response.UserEnergy
import com.sw.dualscreen.model.response.UserNutrition import com.sw.dualscreen.model.response.UserNutrition
import com.sw.dualscreen.utils.Debouncer import com.sw.dualscreen.utils.Debouncer
import com.sw.dualscreen.utils.SPUtil import com.sw.dualscreen.utils.SPUtil
import com.sw.dualscreen.viewmodel.UserViewModel import com.sw.dualscreen.utils.countDownByFlow
import com.sw.dualscreen.viewmodel.NetViewModelV2
import com.sw.plate.utils.LightManager import com.sw.plate.utils.LightManager
import com.sw.plate.utils.ToastUtils import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.ConfigUtil import com.sw.plate.utils.arcface.ConfigUtil
@@ -59,24 +61,25 @@ import com.sw.plate.utils.arcface.face.model.CompareResult
import com.sw.plate.utils.arcface.face.model.FacePreviewInfo import com.sw.plate.utils.arcface.face.model.FacePreviewInfo
import com.sw.plate.utils.arcface.face.model.RecognizeConfiguration import com.sw.plate.utils.arcface.face.model.RecognizeConfiguration
import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel
import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel.REGISTER_STATUS_READY import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import java.text.DecimalFormat import kotlin.math.abs
import kotlin.math.max import kotlin.math.max
import kotlin.math.roundToInt import kotlin.math.roundToInt
@SuppressLint("NotifyDataSetChanged", "SetTextI18n")
class MainScreenPresentation( class MainScreenPresentation(
val activity: MainActivity, val activity: MainActivity,
display: Display, display: Display,
val userViewModel: UserViewModel, val userViewModel: NetViewModelV2,
val recognizeViewModel: RecognizeViewModel, val recognizeViewModel: RecognizeViewModel,
private val onDismissListener: () -> Unit = {} private val onDismissListener: () -> Unit = {}
) : Presentation(activity, display), ViewTreeObserver.OnGlobalLayoutListener { ) : Presentation(activity, display), ViewTreeObserver.OnGlobalLayoutListener {
companion object { companion object {
const val TAG = "SubScreenPresentation" const val TAG = "MainScreenPresentation"
} }
private lateinit var binding: PresentationMainScreenBinding private lateinit var binding: PresentationMainScreenBinding
@@ -98,14 +101,14 @@ class MainScreenPresentation(
private var irFaceRectTransformer: FaceRectTransformer? = null private var irFaceRectTransformer: FaceRectTransformer? = null
private var detectWeight = 0.0 //识别菜品时的重量 private var detectWeight = 0.0 //识别菜品时的重量
var lastWeight = 0.0 // 上一次的计算热量结果 var lastWeight = 0 // 上一次的计算热量结果
// private var userNutritionData: UserNutritionData? = null // private var userNutritionData: UserNutritionData? = null
private var userNutrition: UserNutrition? = null private var userNutrition: UserNutrition? = null
private var dinnerTypeInfo: DinnerType? = null private var dinnerTypeInfo: DinnerType? = null
private val debouncer = Debouncer(500) private val debouncer = Debouncer(1500)
private var recognitionTime: Long = 0L // 人脸识别时的时间 private var recognitionTime: Long = 0L // 人脸识别时的时间
var recognitionWeight: Double = 0.0 // 人脸识别时的重量 var recognitionWeight: Int = 0 // 人脸识别时的重量
private var lastFaceTrackId: Int = -1 // 上一次的人脸信息 private var lastFaceTrackId: Int = -1 // 上一次的人脸信息
var mealPickupMode: Int = 0 // 取餐模式 0 即放即取 1 余量取餐 var mealPickupMode: Int = 0 // 取餐模式 0 即放即取 1 余量取餐
@@ -114,6 +117,7 @@ class MainScreenPresentation(
var isKeepFaceState = false var isKeepFaceState = false
var currentUserId: String? = null var currentUserId: String? = null
var isMember: Boolean = false
var facePreviewInfoList: MutableList<FacePreviewInfo>? = null var facePreviewInfoList: MutableList<FacePreviewInfo>? = null
@@ -121,11 +125,13 @@ class MainScreenPresentation(
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = PresentationMainScreenBinding.inflate(layoutInflater) binding = PresentationMainScreenBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
//0-即放即取,1-余量计量
mealPickupMode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0) ?: 0 mealPickupMode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0) ?: 0
window?.setBackgroundDrawableResource(android.R.color.white) window?.setBackgroundDrawableResource(android.R.color.white)
initView() initView()
//0-计费,1-不计费
val chargeMode = SPUtil.getInstance().get(GlobalKey.KEY_CHARGE_MODE, 0) val chargeMode = SPUtil.getInstance().get(GlobalKey.KEY_CHARGE_MODE, 0)
if (chargeMode == 1) { if (activity.settlementMode == 0 || chargeMode == 1) {
setupArcCamera() setupArcCamera()
} }
registerDataChange() registerDataChange()
@@ -134,71 +140,83 @@ class MainScreenPresentation(
private var isFirstGetWeight = true private var isFirstGetWeight = true
fun updateWeight(weight: Double) { fun updateWeight(weight: Int) {
lastWeight = weight * 1000 // 将千克转成克
Timber.tag(TAG) Timber.tag(TAG)
.d("updateWeight lastWeight = $weight, currentStep = $currentStep, isGoStep1 = $isGoStep1") .d("updateWeight weight = $weight, currentStep = $currentStep, isGoStep1 = $isGoStep1")
//0-即放即取,1-余量计量
mealPickupMode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0) ?: 0 mealPickupMode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0) ?: 0
//0-计费,1-不计费
val chargeMode = SPUtil.getInstance().get(GlobalKey.KEY_CHARGE_MODE, 0) val chargeMode = SPUtil.getInstance().get(GlobalKey.KEY_CHARGE_MODE, 0)
// if (mealPickupMode == 1 && chargeMode == 0) { if (currentStep == 2 && weight <= MainActivity.WEIGHT_RESET_RECOGNIZE && isGoStep1) {
// //余量计量+计费模式
// if (isFirstGetWeight && lastWeight > 5) {
// recognitionWeight = lastWeight
// isFirstGetWeight = false
// }
// if (lastWeight > 5) {
// //余量计量,有重量的情况下不回到重新识别状态
// return
// }
// isFirstGetWeight = true
// }
if (currentStep == 2 && lastWeight <= 5 && isGoStep1) {
//人脸识别过程中,秤上物品拿走 //人脸识别过程中,秤上物品拿走
isGoStep1 = false isGoStep1 = false
activity.runOnUiThread { activity.runOnUiThread {
Timber.tag(TAG).e("切回step1") Timber.tag(TAG).e("切回step1,------2000000000000")
step1FoodRecognizing() step1FoodRecognizing()
} }
} else if (lastWeight <= 5) { } else if (weight <= MainActivity.WEIGHT_RESET_RECOGNIZE) {
isGoStep1 = false isGoStep1 = false
activity.runOnUiThread { activity.runOnUiThread {
Timber.tag(TAG).e("切回step1") Timber.tag(TAG).e("切回step1,------2000000000001")
if (mealPickupMode == 0) { if (mealPickupMode == 0) {
//即放即取 //即放即取
step1FoodRecognizing() step1FoodRecognizing()
} else { } else {
//秤上菜品不足,请补充 if (currentStep == 1 && currentFood == null) {
return@runOnUiThread
}
//余量计量
initCountDownResetJob(weight)
} }
} }
} }
// if (currentStep != 3) {
// return
// }
if (currentFood == null) { if (currentFood == null) {
Timber.tag(TAG).e("updateWeight currentFood is null") Timber.tag(TAG).e("updateWeight currentFood is null")
return return
} }
//if (userNutritionData == null) {
if (userNutrition == null) { if (userNutrition == null) {
Timber.tag(TAG).e("updateWeight userNutritionData is null") Timber.tag(TAG).e("updateWeight userNutritionData is null")
return return
} }
// if (dinnerTypeInfo == null) { if (chargeMode == 1 || activity.settlementMode == 0) {
// Timber.tag(TAG).e("updateWeight dinnerType is null") //不计费或者联合支付模式更新营养热量数据
// return
// }
if (chargeMode == 1) {
//不计费
if (mealPickupMode == 0) { if (mealPickupMode == 0) {
//即放即取 //即放即取
calculateNutrition(lastWeight) calculateNutrition(weight)
} else { } else {
//余量计量 //余量计量
calculateNutrition(recognitionWeight - lastWeight) val value = recognitionWeight - weight
Timber.tag(TAG).e("updateWeight 余量计量:${recognitionWeight} - ${weight} = ${value}")
calculateNutrition(value)
} }
} }
lastWeight = weight
}
private var countDownResetJob: Job? = null
private fun initCountDownResetJob(weight: Int) {
if (countDownResetJob == null || countDownResetJob?.isActive == false) {
countDownResetJob = countDownByFlow(
total = 30,
scope = activity.lifecycleScope,
onStart = {},
onTick = { seconds ->
Timber.tag(TAG).e("余量计量模型秤重量已清空,等待${seconds}秒后继续重置识别状态")
if (weight > MainActivity.WEIGHT_RESET_RECOGNIZE) {
countDownResetJob?.cancel()
countDownResetJob = null
}
},
onFinish = {
step1FoodRecognizing()
countDownResetJob?.cancel()
countDownResetJob = null
}
)
}
} }
fun setStepChangeCallback(callback: (Int) -> Unit) { fun setStepChangeCallback(callback: (Int) -> Unit) {
@@ -275,16 +293,58 @@ class MainScreenPresentation(
} }
} }
private fun setTopInfoVisible() {
binding.llTopInfo.visible()
binding.flFoodDetail.gone()
if (activity.settlementMode == 0) {
try {
foodOrderList.clear()
foodOrderAdapter.submitList(foodOrderList)
} catch (e: Exception) {
e.printStackTrace()
}
}
binding.llTopInfo.updateLayoutParams {
height = activity.screenSize[1] / 2 + 120.dp
}
}
private var isGoRecognize = true
/** /**
* 显示识别出的菜品信息 * 显示识别出的菜品信息
*/ */
fun step3ShowRecognizeResult(userId: String) { fun step3ShowRecognizeResult(userId: String) {
step3Debouncer.debounce {
Timber.tag(TAG).d("main,step3,耗时:${System.currentTimeMillis() - startTime}") Timber.tag(TAG).d("main,step3,耗时:${System.currentTimeMillis() - startTime}")
LightManager.closeRedLight() LightManager.closeRedLight()
LightManager.openGreenLight() LightManager.openGreenLight()
currentStep = 3 currentStep = 3
stepChangeCallback(currentStep) stepChangeCallback(currentStep)
//根据接口数据更新热量数据--------------------------
setTopInfoVisible()
if (activity.settlementMode == 0) {
//判断是联合支付调用接口查询就餐信息后,再查询热量营养数据
loadOrderList(userId) {
loadUserNutritionData(userId)
}
activity.layoutEatDetailBinding.root.visible()
} else {
//独立支付
loadUserNutritionData(userId)
}
step3Debouncer.reset()
activity.isSwitchFood = false
}
}
private val step3Debouncer = Debouncer(10*1000)
private var memberDiscount:Double = 1.0
/**
* 根据接口数据更新热量、营养数据
*/
private fun loadUserNutritionData(userId: String) {
userViewModel.getUserNutritionData(userId = userId) { nutrition -> userViewModel.getUserNutritionData(userId = userId) { nutrition ->
activity.runOnUiThread { activity.runOnUiThread {
if (nutrition == null) { if (nutrition == null) {
@@ -401,7 +461,7 @@ class MainScreenPresentation(
} }
private var isLoadUnbilledMode = false private var isLoadUnbilledMode = false
private val df by lazy { DecimalFormat("#.##") } // private val df by lazy { DecimalFormat("#.##") }
/** /**
* 餐品识别成功,不计费模式 * 餐品识别成功,不计费模式
@@ -410,19 +470,6 @@ class MainScreenPresentation(
if (currentUserId.isNullOrBlank()) { if (currentUserId.isNullOrBlank()) {
return return
} }
//todo 清除人脸数据
// binding.dualCameraFaceRectView.clearFaceInfo()
// clearLeftFaceData()
// facePreviewInfoList?.clear()
// recognizeViewModel.updateRegisterStatus(REGISTER_STATUS_READY)
// pauseCamera()
// resumeCamera()
// TODO: 如何清除人脸数据重新识别
// stopCamera()
// setupArcCamera()
isLoadUnbilledMode = true isLoadUnbilledMode = true
Timber.tag(TAG).d("loadUnbilledMode") Timber.tag(TAG).d("loadUnbilledMode")
@@ -440,15 +487,16 @@ class MainScreenPresentation(
//updateFoodInfo(foodInfo) //updateFoodInfo(foodInfo)
binding.nutritionInclude.tvUserName.text = userNutrition?.name.maskName() binding.nutritionInclude.tvUserName.text = userNutrition?.name.maskName()
val recommendCalorie = userNutrition?.recommendCalorie ?: 0.0 val recommendCalorie = (userNutrition?.recommendCalorie ?: 0.0).roundedDecimalPlace(1)
binding.nutritionInclude.tvRecommendHeat.text = binding.nutritionInclude.tvRecommendHeat.text = "推荐热量:${recommendCalorie}kcal"
"推荐热量:${df.format(recommendCalorie)}kcal"
// binding.nutritionInclude.tvRecommendHeat.text = "推荐热量:${recommendCalorie.removeTrailingZeros()}kcal" //0-即放即取,1-余量计量
//updateWeight(lastWeight / 1000) mealPickupMode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0) ?: 0
// ------------------------------------------------- calculateNutrition(
// 删除userNutritionData,改为userNutrition if (mealPickupMode == 1) recognitionWeight - lastWeight
calculateNutrition(recognitionWeight - lastWeight) else recognitionWeight
)
binding.root.postDelayed({ binding.root.postDelayed({
isLoadUnbilledMode = false isLoadUnbilledMode = false
}, 10000) }, 10000)
@@ -465,24 +513,24 @@ class MainScreenPresentation(
private var isFirstOpen = true private var isFirstOpen = true
var startTime = 0L var startTime = 0L
var jointPaymentQueryFinish = false
/** /**
* 餐品识别中 * 餐品识别中
*/ */
fun step1FoodRecognizing() { fun step1FoodRecognizing() {
if (currentStep == 1 && currentFood == null) {
activity.clearRecognizeFood()
return
}
Timber.tag(TAG).d("main,step1,耗时:${System.currentTimeMillis() - startTime}") Timber.tag(TAG).d("main,step1,耗时:${System.currentTimeMillis() - startTime}")
startTime = System.currentTimeMillis() startTime = System.currentTimeMillis()
jointPaymentQueryFinish = false
hideStandbyScreen() hideStandbyScreen()
activity.resetTouchTime() activity.resetTouchTime()
if (isFirstOpen.not()) { if (isFirstOpen.not()) {
activity.resumeAnalysis() activity.resumeAnalysis()
} }
//if (abs(lastWeight.toInt()) <= 5) {
// activity.runOnUiThread {
// ToastUtils.showToast("秤上重量几乎为0,请确认")
// }
// return
//}
currentStep = 1
LightManager.closeGreenLight() LightManager.closeGreenLight()
LightManager.closeRedLight() LightManager.closeRedLight()
@@ -490,9 +538,9 @@ class MainScreenPresentation(
currentFood = null currentFood = null
// userNutritionData = null // userNutritionData = null
userNutrition = null userNutrition = null
recognitionWeight = 0.0 recognitionWeight = 0
stepChangeCallback(currentStep) setTopInfoVisible()
binding.calorieInclude.root.gone() binding.calorieInclude.root.gone()
binding.nutritionInclude.root.gone() binding.nutritionInclude.root.gone()
@@ -511,31 +559,23 @@ class MainScreenPresentation(
binding.flRecognizeIr.gone() binding.flRecognizeIr.gone()
pauseCamera() pauseCamera()
activity.clearFoodList() activity.clearRecognizeFood()
}
// fun foodRecSuccess(foodInfo: FoodInfo) { currentStep = 1
// //识别逻辑完成根据是否计费显示不同页面 stepChangeCallback(currentStep)
// val mode = SPUtil.getInstance().get(GlobalKey.KEY_CHARGE_MODE, 0) }
// if (mode == 0) {
// //计费
// loadBilledMode()
// } else {
// //不计费
// //userViewModel.getUserNutritionData(
// // userId = userId,
// // foodId = currentFood!!.foodId!!
// //)
// loadUnbilledMode()
// }
// }
/** /**
* 用户人脸识别中 * 用户人脸识别中
*/ */
fun step2FaceRecognizing(foodInfo: FoodInfo) { fun step2FaceRecognizing(foodInfo: FoodInfo, hideFoodDetail: Boolean = false) {
isGoStep1 = true isGoStep1 = true
Timber.tag(TAG).d("main,step2,耗时:${System.currentTimeMillis() - startTime}") Timber.tag(TAG).d("main,step2,耗时:${System.currentTimeMillis() - startTime}")
if (hideFoodDetail) {
setTopInfoVisible()
}
startTime = System.currentTimeMillis() startTime = System.currentTimeMillis()
currentStep = 2 currentStep = 2
LightManager.closeRedLight() LightManager.closeRedLight()
@@ -559,7 +599,7 @@ class MainScreenPresentation(
dinnerTypeInfo = null dinnerTypeInfo = null
// userNutritionData = null // userNutritionData = null
userNutrition = null userNutrition = null
recognitionWeight = 0.0 recognitionWeight = 0
stepChangeCallback(currentStep) stepChangeCallback(currentStep)
currentFood = foodInfo currentFood = foodInfo
@@ -567,6 +607,10 @@ class MainScreenPresentation(
// userViewModel.getDinnerType() // userViewModel.getDinnerType()
resumeCamera() resumeCamera()
if (activity.settlementMode == 0) {
activity.layoutEatDetailBinding.root.invisible()
}
} }
fun hideRecImage() { fun hideRecImage() {
@@ -574,7 +618,7 @@ class MainScreenPresentation(
} }
// 当前食物信息 // 当前食物信息
private var currentFood: FoodInfo? = null var currentFood: FoodInfo? = null
fun updateFood(foodInfo: FoodInfo?) { fun updateFood(foodInfo: FoodInfo?) {
//if (isLoadUnbilledMode) return //if (isLoadUnbilledMode) return
Timber.tag(TAG).d("main,updateFood,耗时:${System.currentTimeMillis() - startTime}") Timber.tag(TAG).d("main,updateFood,耗时:${System.currentTimeMillis() - startTime}")
@@ -586,6 +630,7 @@ class MainScreenPresentation(
binding.tvFoodName.text = foodInfo.foodName binding.tvFoodName.text = foodInfo.foodName
binding.ivPreviewImage.gone() binding.ivPreviewImage.gone()
binding.ivFrame.gone() binding.ivFrame.gone()
//0-计费,1-不计费
val chargeMode = SPUtil.getInstance().get(GlobalKey.KEY_CHARGE_MODE, 0) val chargeMode = SPUtil.getInstance().get(GlobalKey.KEY_CHARGE_MODE, 0)
if (chargeMode == 0) { if (chargeMode == 0) {
binding.ivRecImage.let { binding.ivRecImage.let {
@@ -597,22 +642,25 @@ class MainScreenPresentation(
} else { } else {
binding.ivRecImage.gone() binding.ivRecImage.gone()
} }
val mode = SPUtil.getInstance().get(GlobalKey.KEY_CHARGE_MODE, 0) //独立支付+计费模式,使用人脸识别
if (mode == 0) { if (activity.settlementMode == 1 && chargeMode == 0) {
//计费,查询营养信息、价格信息------------------------ //计费,查询营养信息、价格信息------------------------
loadBilledMode() loadBilledMode()
} else { } else {
//不计费 //联合支付,进行人脸识别
//开启人脸识别 if (activity.settlementMode == 0) {
if (activity.isSwitchFood.not()) {
step2FaceRecognizing(foodInfo)
}
} else {
//不计费,开启人脸识别
if (currentStep == 1 || binding.tvFaceTip.isVisible.not()) { if (currentStep == 1 || binding.tvFaceTip.isVisible.not()) {
if (activity.isSwitchFood.not()) {
step2FaceRecognizing(foodInfo) step2FaceRecognizing(foodInfo)
} }
} }
//if (currentStep == 1) { }
// step2FaceRecognizing(foodInfo) }
//} else {
// updateFoodInfo(foodInfo)
//}
} }
private fun updateFoodInfo(foodInfo: FoodInfo) { private fun updateFoodInfo(foodInfo: FoodInfo) {
@@ -636,12 +684,89 @@ class MainScreenPresentation(
// } // }
} }
private fun calculateNutrition(weight1: Double) { private var lastAddWeight = 0
private fun addFoodToOrder(weight: Int, foodInfo: FoodInfo) {
Timber.tag(TAG) .d("addFoodToOrder: ${weight == lastAddWeight}")
// if (weight == lastAddWeight) return
lastAddWeight = weight
if (weight <= MainActivity.MIN_BILLABLE_WEIGHT) {
//0-即放即取,1-余量计量
mealPickupMode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0) ?: 0
if (foodOrderList.isNotEmpty() && mealPickupMode == 1) {
val last = foodOrderList.last()
if (last.isLocalData) {
foodOrderList.remove(last)
// foodOrderAdapter.submitList(foodOrderList)
foodOrderAdapter.notifyDataSetChanged()
}
}
return
}
// 按重量计价:每克单价 = 规格价 / 规格重量,再按会员折扣折算
// 单价 = specPrice / specWeight,金额 = 当前秤上净重 × 单价 × 会员折扣
val specWeight = foodInfo.specWeight ?: 0.0
val specPrice = foodInfo.specPrice ?: 0.0
val price = if (specWeight > 0.0) {
weight * (specPrice / specWeight) * memberDiscount
} else {
0.0
}
// eatNum 仅用于本地明细展示(份数),不再参与金额计算
val eatNum = activity.getEatNum(weight.toDouble(), foodInfo.specWeight)
if (foodOrderList.isNotEmpty() && mealPickupMode == 1) {
val last = foodOrderList.last()
if (last.isLocalData) {
last.eatNum = eatNum
last.num = weight
last.price = price
// foodOrderAdapter.submitList(foodOrderList)
foodOrderAdapter.notifyItemChanged(foodOrderList.lastIndex)
return
}
}
foodOrderList.add(
FoodItem(
id = System.currentTimeMillis().toString(),
foodId = foodInfo.foodId,
foodName = foodInfo.foodName,
price = price,
specId = foodInfo.specId,
foodMaterialId = foodInfo.foodMaterialId,
specWeight = foodInfo.specWeight?.toInt() ?: 0,
orderFrom = 2,
num = weight,
eatNum = eatNum,
isLocalData = true
)
)
// foodOrderAdapter.submitList(foodOrderList)
foodOrderAdapter.notifyDataSetChanged()
Timber.tag(TAG) .d(
"addFoodToOrder: size=${foodOrderList.size},新增数据:${foodOrderList.last()}"
)
//foodOrderAdapter.notifyDataSetChanged()
var eatWeightSum = 0
foodOrderList.forEach {
eatWeightSum += it.num
}
binding.detailInclude.tvTotalWeight.text = "总重量:${eatWeightSum}"
Timber.tag(TAG) .d("addFoodToOrder: 更新总重量,新增数据------------------------------")
}
private fun calculateNutrition(value: Int) {
Timber.tag(TAG) .d("calculateNutrition:weight=$value,recognitionWeight=$recognitionWeight,foodInfo=$currentFood")
if (currentFood == null || userNutrition == null) { if (currentFood == null || userNutrition == null) {
return return
} }
var weight = weight1 var weight = value
if (weight < 0) weight = 0.0 if (weight < 0) weight = 0
// 扣减餐具重量,后续营养计算、柱图绘制、取餐明细均以净重为准
weight = (weight - activity.getDishWeight()).coerceAtLeast(0)
if (activity.settlementMode == 0) {
activity.runOnUiThread {
addFoodToOrder(weight, currentFood!!)
}
}
debouncer.debounce { debouncer.debounce {
Timber.tag(TAG) Timber.tag(TAG)
.d("calculateNutrition weight = $weight, recognitionWeight = $recognitionWeight") .d("calculateNutrition weight = $weight, recognitionWeight = $recognitionWeight")
@@ -652,7 +777,7 @@ class MainScreenPresentation(
currentFood!!, currentFood!!,
// userNutritionData!!, // userNutritionData!!,
userNutrition!!, userNutrition!!,
weight, weight.toDouble(),
dinnerType = dinnerType ?: "" dinnerType = dinnerType ?: ""
) )
// Timber.tag(TAG).d("calculateNutrition calcResultInfo = $calcResultInfo") // Timber.tag(TAG).d("calculateNutrition calcResultInfo = $calcResultInfo")
@@ -666,6 +791,15 @@ class MainScreenPresentation(
val fruitsVegetables = energy.fruitsVegetables val fruitsVegetables = energy.fruitsVegetables
val meatEggs = energy.meatEggs val meatEggs = energy.meatEggs
val showTotalCalorie = "总热量:${totalKcal.roundedDecimalPlace(1)}千卡"
binding.detailInclude.tvTotalCalorie.text = showTotalCalorie
activity.layoutEatDetailBinding.tvTotalCalorie.text = showTotalCalorie
val totalWeight = (userNutrition?.eatWeightSum ?: 0.0) + weight
val showTotalWeight = "总重量:${totalWeight.roundedDecimalPlace(1)}"
binding.detailInclude.tvTotalWeight.text = showTotalWeight
activity.layoutEatDetailBinding.tvTotalWeight.text = showTotalWeight
val include = binding.nutritionInclude val include = binding.nutritionInclude
// 设置热量--------------------------------------------------------------------- // 设置热量---------------------------------------------------------------------
@@ -684,7 +818,7 @@ class MainScreenPresentation(
// 避免0作为被除数 // 避免0作为被除数
maxKcal = if (maxKcal <= 0.0) 1.0 else maxKcal maxKcal = if (maxKcal <= 0.0) 1.0 else maxKcal
Timber.tag(TAG).d("maxKcal 1 = $maxKcal") Timber.tag(TAG).d("maxKcal 1 = $maxKcal")
include.totalKcalTv.text = totalKcal.format2String(2) include.totalKcalTv.text = totalKcal.roundedDecimalPlace(1).toString()
var totalKcalHeight = (totalKcal / maxKcal).toFloat() var totalKcalHeight = (totalKcal / maxKcal).toFloat()
if (totalKcalHeight == 0F) { if (totalKcalHeight == 0F) {
include.divKcalView.visible() include.divKcalView.visible()
@@ -702,7 +836,7 @@ class MainScreenPresentation(
) )
val grainIndex = UserNutritionUtils.findCalorieIndex(energy.grain, grainArray) val grainIndex = UserNutritionUtils.findCalorieIndex(energy.grain, grainArray)
include.customFoodColumn.setImageDrawable(false, grainIndex) include.customFoodColumn.setImageDrawable(false, grainIndex)
include.totalFoodTv.text = grain.format2String(2) include.totalFoodTv.text = grain.roundedDecimalPlace(1).toString()
columnMax = if (columnMax <= 0.0) 1.0 else columnMax columnMax = if (columnMax <= 0.0) 1.0 else columnMax
var grainHeight = (grain / columnMax).toFloat() var grainHeight = (grain / columnMax).toFloat()
if (grainHeight == 0F) { if (grainHeight == 0F) {
@@ -722,7 +856,7 @@ class MainScreenPresentation(
val fruitsIndex = val fruitsIndex =
UserNutritionUtils.findCalorieIndex(energy.fruitsVegetables, fruitsArray) UserNutritionUtils.findCalorieIndex(energy.fruitsVegetables, fruitsArray)
include.customVegetableColumn.setImageDrawable(false, fruitsIndex) include.customVegetableColumn.setImageDrawable(false, fruitsIndex)
include.totalVegetableTv.text = fruitsVegetables.format2String(2) include.totalVegetableTv.text = fruitsVegetables.roundedDecimalPlace(1).toString()
var fruitsHeight = (fruitsVegetables / columnMax).toFloat() var fruitsHeight = (fruitsVegetables / columnMax).toFloat()
if (fruitsHeight == 0F) { if (fruitsHeight == 0F) {
include.divVegetableView.visible() include.divVegetableView.visible()
@@ -734,14 +868,14 @@ class MainScreenPresentation(
// 设置肉蛋----------------------------------------------------------- // 设置肉蛋-----------------------------------------------------------
val meatEggsArray = UserNutritionUtils.getCalorieArray( val meatEggsArray = UserNutritionUtils.getCalorieArray(
left = userNutrition?.meatEggsRecommend, left = userNutrition?.meatEggsBeansRecommend,
mid = userNutrition?.meatEggsNearExcess, mid = userNutrition?.meatEggsBeansNearExcess,
right = userNutrition?.meatEggsExcess right = userNutrition?.meatEggsBeansExcess
) )
val meatEggsIndex = val meatEggsIndex =
UserNutritionUtils.findCalorieIndex(energy.meatEggs, meatEggsArray) UserNutritionUtils.findCalorieIndex(energy.meatEggs, meatEggsArray)
include.customMeatColumn.setImageDrawable(false, meatEggsIndex) include.customMeatColumn.setImageDrawable(false, meatEggsIndex)
include.totalMeatTv.text = meatEggs.format2String(2) include.totalMeatTv.text = meatEggs.roundedDecimalPlace(1).toString()
var meatHeight = (meatEggs / columnMax).toFloat() var meatHeight = (meatEggs / columnMax).toFloat()
if (meatHeight == 0F) { if (meatHeight == 0F) {
include.divMeatView.visible() include.divMeatView.visible()
@@ -765,6 +899,7 @@ class MainScreenPresentation(
*/ */
fun updateMealPickupMode(mode: Int) { fun updateMealPickupMode(mode: Int) {
Timber.tag(TAG).d("updateMealPickupMode mode = $mode") Timber.tag(TAG).d("updateMealPickupMode mode = $mode")
Timber.tag(TAG).e("切回step1,------2000000000003")
mealPickupMode = mode mealPickupMode = mode
step1FoodRecognizing() step1FoodRecognizing()
} }
@@ -785,7 +920,16 @@ class MainScreenPresentation(
fun resumeCamera() { fun resumeCamera() {
Timber.tag(TAG).d("resumeCamera isRecognition = $isRecognition") Timber.tag(TAG).d("resumeCamera isRecognition = $isRecognition")
isRecognition = true isRecognition = true
if (rgbCameraHelper != null && rgbCameraHelper!!.isStopped) { if (rgbCameraHelper == null) {
Timber.tag(TAG).d("resumeCamera isRecognition ---1")
setupArcCamera()
binding.root.postDelayed({
if (rgbCameraHelper != null) {
rgbCameraHelper!!.start()
}
}, 2000)
} else if (rgbCameraHelper!!.isStopped) {
Timber.tag(TAG).d("resumeCamera isRecognition ---2")
rgbCameraHelper!!.start() rgbCameraHelper!!.start()
} }
} }
@@ -795,6 +939,23 @@ class MainScreenPresentation(
isRecognition = false isRecognition = false
recognizeViewModel.onPreviewFrame(ByteArray(1382400), true) recognizeViewModel.onPreviewFrame(ByteArray(1382400), true)
//triggerReidentify()
}
/**
* 重置人脸跟踪状态,让已在画面中的人脸重新触发识别
* 适用场景:切换按钮后需要重新识别当前画面中的人脸
*/
fun triggerReidentify() {
// 清除UI上的人脸框
binding.dualCameraFaceRectView.clearFaceInfo()
// 清除本地跟踪数据
clearLeftFaceData()
// 重置上一次的 trackId,避免 drawPreviewInfo 中的去重逻辑拦截
lastFaceTrackId = -1
// 插入空帧,让引擎重置内部跟踪状态
// isRecognition 保持 true,相机继续送帧,下一帧真实人脸会产生新 trackId
recognizeViewModel.onPreviewFrame(ByteArray(1382400), true)
} }
private var recognizeTime = 0L private var recognizeTime = 0L
@@ -842,23 +1003,30 @@ class MainScreenPresentation(
recognizeViewModel.recognizeUserId.observe( recognizeViewModel.recognizeUserId.observe(
activity, activity,
Observer { compareResult: CompareResult -> Observer { result: CompareResult? ->
if (result == null) return@Observer
if (currentStep != 2 || System.currentTimeMillis() - recognizeTime <= 2000) { if (currentStep != 2 || System.currentTimeMillis() - recognizeTime <= 2000) {
//忽略非识别中及秒内的再次识别 //忽略非识别中及5秒内的再次识别
return@Observer return@Observer
} }
// if (isGoRecognize.not()) {
// return@Observer
// }
recognizeTime = System.currentTimeMillis() recognizeTime = System.currentTimeMillis()
activity.runOnUiThread { activity.runOnUiThread {
activity.showWaitingDialog("加载中,请稍后……") activity.showWaitingDialog("加载中,请稍后……")
} }
Timber.tag(TAG) Timber.tag(TAG)
.i("recognizeUserId observe compareResult = ${compareResult.trackId}, userId = ${compareResult.faceEntity.userName}, step = $currentStep") .i("recognizeUserId observe compareResult = ${result.trackId}, userId = ${result.faceEntity.userName}, step = $currentStep")
recognitionTime = System.currentTimeMillis() recognitionTime = System.currentTimeMillis()
//识别成功时更新lastWeight,解决使用过程中给秤上放东西,导致重量错误问题
lastWeight = activity.currentWeight
recognitionWeight = lastWeight recognitionWeight = lastWeight
lastFaceTrackId = compareResult.trackId lastFaceTrackId = result.trackId
val faceEntity = compareResult.faceEntity val faceEntity = result.faceEntity
val userId = faceEntity.userName val userId = faceEntity.userName
currentUserId = userId currentUserId = userId
isMember = faceEntity.userType == "1"
isAgainLoadData = false isAgainLoadData = false
if (userId == null) return@Observer if (userId == null) return@Observer
//ToastUtils.showToast("人脸识别成功,userId = $userId") //ToastUtils.showToast("人脸识别成功,userId = $userId")
@@ -872,7 +1040,12 @@ class MainScreenPresentation(
return@runOnUiThread return@runOnUiThread
} }
} }
// pauseCamera()
//先查询会员折扣
userViewModel.getMemberDiscount(userId) { discount ->
memberDiscount = discount ?: 1.0
step3ShowRecognizeResult(userId) step3ShowRecognizeResult(userId)
}
}) })
recognizeViewModel.drawRectInfoText.observe(activity, Observer { info -> recognizeViewModel.drawRectInfoText.observe(activity, Observer { info ->
@@ -978,6 +1151,7 @@ class MainScreenPresentation(
} }
if (facePreviewInfoList.isNullOrEmpty()) { if (facePreviewInfoList.isNullOrEmpty()) {
currentUserId = null currentUserId = null
isMember = false
} }
//recognizeViewModel.clearLeftFace(facePreviewInfoList) //recognizeViewModel.clearLeftFace(facePreviewInfoList)
clearLeftFaceData() clearLeftFaceData()
@@ -1135,52 +1309,101 @@ class MainScreenPresentation(
val listIsEmpty = facePreviewInfoList.isEmpty() val listIsEmpty = facePreviewInfoList.isEmpty()
val listFirstTrackId = if (listIsEmpty.not()) facePreviewInfoList[0].trackId else null val listFirstTrackId = if (listIsEmpty.not()) facePreviewInfoList[0].trackId else null
Timber.tag(TAG) // Timber.tag(TAG)
.d("listIsEmpty=$listIsEmpty,lastFaceTrackId=$lastFaceTrackId,listFirstTrackId=$listFirstTrackId") // .d("drawPreviewInfo,listIsEmpty=$listIsEmpty,lastFaceTrackId=$lastFaceTrackId,listFirstTrackId=$listFirstTrackId")
if (listIsEmpty || (lastFaceTrackId != listFirstTrackId)) { if (listIsEmpty || (lastFaceTrackId != listFirstTrackId)) {
if (lastFaceTrackId != -1 && currentUserId != null) { if (lastFaceTrackId != -1 && currentUserId != null) {
//0-即放即取,1-余量计量
mealPickupMode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0) ?: 0 mealPickupMode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0) ?: 0
Timber.tag(TAG).i("$lastFaceTrackId 用户离开") Timber.tag(TAG).i("drawPreviewInfo,$lastFaceTrackId 用户离开")
lastFaceTrackId = -1 lastFaceTrackId = -1
Timber.tag(TAG) Timber.tag(TAG)
.d("postUserData userNutritionData是否null ${userNutrition == null}, currentFood是否null: ${currentFood == null}currentUserId是否null${currentUserId == null}") .d("drawPreviewInfo,postUserData userNutritionData是否null ${userNutrition == null}, currentFood是否null: ${currentFood == null}currentUserId是否null${currentUserId == null}")
if (userNutrition == null || currentFood == null || currentUserId == null) { if (userNutrition == null || currentFood == null || currentUserId == null) {
return return
} }
//重置人脸
recognizeViewModel.resetFaceState()
Timber.tag(TAG).d("drawPreviewInfo,开始提交订单")
createOrder(notEnoughOneBlock = { createOrder(notEnoughOneBlock = {
Timber.tag(TAG).d("drawPreviewInfo,notEnoughOneBlock")
if (activity.settlementMode == 0) {
Timber.tag(TAG).d("drawPreviewInfo,notEnoughOneBlock,联合模式")
setTopInfoVisible()
}
// if (mealPickupMode == 0) { // if (mealPickupMode == 0) {
Timber.tag(TAG).d("drawPreviewInfo,notEnoughOneBlock,step2FaceRecognizing")
currentStep = 2 currentStep = 2
step2FaceRecognizing(currentFood!!) step2FaceRecognizing(currentFood!!)
// } // }
}) { }, successBlock = {
Timber.tag(TAG).d("drawPreviewInfo,successBlock,订单已生成")
// TODO: 测试人脸识别问题-------------------- // TODO: 测试人脸识别问题--------------------
//recognizeViewModel.updateRegisterStatus(REGISTER_STATUS_READY) //recognizeViewModel.updateRegisterStatus(REGISTER_STATUS_READY)
if (MyApp.DEBUG) { if (BuildConfig.DEBUG) {
ToastUtils.showToast("订单已生成") ToastUtils.showToast("订单已生成")
} }
currentUserId = null currentUserId = null
isMember = false
//ToastUtils.showToast("isKeepFaceState=$isKeepFaceState,mealPickupMode=$mealPickupMode") //ToastUtils.showToast("isKeepFaceState=$isKeepFaceState,mealPickupMode=$mealPickupMode")
//|| isKeepFaceState //|| isKeepFaceState
// if (activity.settlementMode == 0) {
// activity.isAnalyzing = false
// step1FoodRecognizing()
// } else {
Timber.tag(TAG).d("drawPreviewInfo,successBlock,订单已生成,mealPickupMode=$mealPickupMode")
if (mealPickupMode == 1) { if (mealPickupMode == 1) {
Timber.tag(TAG).d("drawPreviewInfo,successBlock,订单已生成,step2FaceRecognizing")
if (activity.currentWeight > MainActivity.WEIGHT_RESET_RECOGNIZE) {
//余量计量拿走秤上最后一点餐品时,不用再进行人脸识别了
step2FaceRecognizing(currentFood!!) step2FaceRecognizing(currentFood!!)
}
isKeepFaceState = false isKeepFaceState = false
if (activity.settlementMode == 0) {
Timber.tag(TAG).d("drawPreviewInfo,successBlock,订单已生成,step2FaceRecognizing,联合支付")
setTopInfoVisible()
}
} else { } else {
Timber.tag(TAG).d("drawPreviewInfo,successBlock,订单已生成,称重为:${activity.currentWeight}")
//即放即取未拿走餐品,不用重置为默认识别中状态
if (abs(activity.currentWeight) > 10) {
//提交订单后秤重量绝对值大于10克,都认为餐品还没拿走
Timber.tag(TAG).d("drawPreviewInfo,successBlock,订单已生成,未拿走餐品")
return@createOrder
}
Timber.tag(TAG).d("drawPreviewInfo,successBlock,订单已生成,step1FoodRecognizing")
Timber.tag(TAG).e("切回step1,------2000000000002")
activity.isAnalyzing = false activity.isAnalyzing = false
step1FoodRecognizing() step1FoodRecognizing()
} }
} // }
})
} }
} }
} }
fun createOrder(notEnoughOneBlock: () -> Unit = {}, block: () -> Unit) { /**
* 人脸离开后调用创建订单接口
*/
fun createOrder(notEnoughOneBlock: () -> Unit = {}, successBlock: () -> Unit) {
//0-即放即取,1-余量计量
mealPickupMode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0) ?: 0 mealPickupMode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0) ?: 0
val eatWeight = if (mealPickupMode == 0) { // 餐具重量(克),从就餐重量中扣减
val dishWeight = activity.getDishWeight()
//if (settlementMode == 0) {
// //使用联合支付方式
// ToastUtils.showToast("使用联合支付方式")
// return
//}
val rawEatWeight = if (mealPickupMode == 0) {
//即放即取 //即放即取
recognitionWeight recognitionWeight
} else { } else {
recognitionWeight - lastWeight recognitionWeight - lastWeight
} }
// 扣减餐具重量(不足则归零),foodWeight 保持不变
val eatWeight = (rawEatWeight - dishWeight).coerceAtLeast(0)
activity.log("createOrder,rawEatWeight=$rawEatWeight,dishWeight=$dishWeight,eatWeight=$eatWeight")
//val userNutritionParam = UserNutritionParam( //val userNutritionParam = UserNutritionParam(
// userId = userNutritionData?.userId!!, // userId = userNutritionData?.userId!!,
// foodId = currentFood?.foodId!!, // foodId = currentFood?.foodId!!,
@@ -1190,16 +1413,21 @@ class MainScreenPresentation(
// foodWeight = lastWeight // foodWeight = lastWeight
//) //)
//userViewModel.postUserNutritionData(listOf(userNutritionParam)) //userViewModel.postUserNutritionData(listOf(userNutritionParam))
val realWeight = eatWeight.toDouble()
val specWeight = currentFood!!.specWeight
val eatNum = activity.getEatNum(realWeight, specWeight)
activity.log("createOrder,realWeight=$realWeight,specWeight=$specWeight")
activity.createOrder( activity.createOrder(
foodInfo = currentFood!!, foodInfo = currentFood!!,
foodWeight = lastWeight.roundToInt(), foodWeight = lastWeight,
eatWeight = eatWeight.roundToInt(), eatWeight = eatWeight,
eatNum = activity.getEatNum(eatWeight, currentFood!!.specWeight), eatNum = eatNum,
userId = currentUserId, userId = currentUserId,
isMember = isMember,
notEnoughOneBlock = notEnoughOneBlock, notEnoughOneBlock = notEnoughOneBlock,
block = { successBlock = {
activity.runOnUiThread { activity.runOnUiThread {
block() successBlock()
} }
} }
) )
@@ -1244,4 +1472,75 @@ class MainScreenPresentation(
// } // }
} }
private var foodOrderList = mutableListOf<FoodItem>()
private val foodOrderAdapter by lazy {
FoodOrderAdapter(foodOrderList)
}
private fun initRv(rv: RecyclerView) {
rv.run {
if (adapter == null) {
layoutManager =
LinearLayoutManager(activity, LinearLayoutManager.VERTICAL, false)
adapter = foodOrderAdapter
}
}
}
/**
* 加载订单列表
*/
private fun loadOrderList(userId: String, callback: () -> Unit) {
binding.llTopInfo.gone()
binding.flFoodDetail.visible()
binding.detailInclude.let {
it.root.updateLayoutParams {
height = activity.screenSize[1] / 2 + 120.dp
}
initRv(it.rvFoodList)
initRv(activity.layoutEatDetailBinding.rvFoodList)
userViewModel.getFoodOrderList(userId) { model ->
activity.runOnUiThread {
if (model == null) return@runOnUiThread
foodOrderList.clear()
foodOrderList.addAll(model.list ?: emptyList())
//foodOrderAdapter.submitList(foodOrderList)
foodOrderAdapter.notifyDataSetChanged()
//binding.detailInclude.tvTotalWeight.text = "总重量:${model.eatWeightSum}克"
Timber.tag(TAG).d("addFoodToOrder: 更新总重量,订单列表------------------------------")
// binding.detailInclude.tvTotalCalorie.text = "总热量:${model.calorie.roundedDecimalPlace(1)}千卡"
jointPaymentQueryFinish = true
//重新加载营养信息
callback()
}
}
}
}
/**
* 构造符合分辨率的 NV21 格式全黑帧
* @param width 图像宽度(如 1920
* @param height 图像高度(如 1080
* @return 合法的全黑帧字节数组
*/
private fun createBlackNV21Frame(width: Int, height: Int): ByteArray {
val frameSize = width * height
val nv21Data = ByteArray(frameSize * 3 / 2) // NV21 格式长度 = 宽×高×1.5
// Y 分量(亮度)设为 0(全黑),UV 分量设为 128(默认值)
// 1. 填充 Y 分量(前 frameSize 个字节)
// Arrays.fill(nv21Data, 0, frameSize, 0.toByte())
nv21Data.fill(0, 0, frameSize)
// 2. 填充 UV 分量(后 frameSize/2 个字节)
// Arrays.fill(nv21Data, frameSize, nv21Data.size, 128.toByte())
nv21Data.fill(128.toByte(), frameSize, nv21Data.size)
return nv21Data
}
// 调用示例:构造 1920×1080 的全黑帧(长度 1382400
// private val emptyFrame = createBlackNV21Frame(1920, 1080)
private val emptyFrame = createBlackNV21Frame(1280, 720)
} }
@@ -34,7 +34,7 @@ import com.sw.dualscreen.ext.visible
import com.sw.dualscreen.model.response.MemberInfo import com.sw.dualscreen.model.response.MemberInfo
import com.sw.dualscreen.model.response.TextBean import com.sw.dualscreen.model.response.TextBean
import com.sw.dualscreen.utils.SpannedUtils import com.sw.dualscreen.utils.SpannedUtils
import com.sw.dualscreen.viewmodel.UserViewModel import com.sw.dualscreen.viewmodel.NetViewModelV2
import com.sw.plate.utils.ToastUtils import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.ConfigUtil import com.sw.plate.utils.arcface.ConfigUtil
import com.sw.plate.utils.arcface.ErrorCodeUtil import com.sw.plate.utils.arcface.ErrorCodeUtil
@@ -56,7 +56,7 @@ import java.util.concurrent.atomic.AtomicBoolean
class PayPresentation( class PayPresentation(
val activity: PayActivity, val activity: PayActivity,
display: Display, display: Display,
val userViewModel: UserViewModel, val userViewModel: NetViewModelV2,
val recognizeViewModel: RecognizeViewModel, val recognizeViewModel: RecognizeViewModel,
private val onDismissListener: () -> Unit = {} private val onDismissListener: () -> Unit = {}
) : Presentation(activity, display), ViewTreeObserver.OnGlobalLayoutListener { ) : Presentation(activity, display), ViewTreeObserver.OnGlobalLayoutListener {
@@ -9,6 +9,7 @@ import com.sw.dualscreen.model.response.FaceData
import com.sw.dualscreen.model.response.FoodInfo import com.sw.dualscreen.model.response.FoodInfo
import com.sw.dualscreen.model.response.FoodSearchReq import com.sw.dualscreen.model.response.FoodSearchReq
import com.sw.dualscreen.model.response.FoodOrder import com.sw.dualscreen.model.response.FoodOrder
import com.sw.dualscreen.model.response.FoodOrderModel
import com.sw.dualscreen.model.response.FoodVector import com.sw.dualscreen.model.response.FoodVector
import com.sw.dualscreen.model.response.MemberInfo import com.sw.dualscreen.model.response.MemberInfo
import com.sw.dualscreen.model.response.UserFaceModel import com.sw.dualscreen.model.response.UserFaceModel
@@ -352,4 +353,16 @@ class RemoteRepository constructor(
) )
} }
} }
suspend fun getFoodOrderList(userId: String): ApiResponse<FoodOrderModel?> {
return safeApiCall {
apiService.getFoodOrderList(userId = userId)
}
}
suspend fun getMemberDiscount(userId: String): ApiResponse<Double?> {
return safeApiCall {
apiService.getMemberDiscount(userId = userId)
}
}
} }
@@ -0,0 +1,237 @@
package com.sw.dualscreen.repository.v2
import com.sw.dualscreen.model.request.v2.BindUserOrderRequest
import com.sw.dualscreen.model.request.v2.PlaceOrderRequest
import com.sw.dualscreen.model.response.ApiResponse
import com.sw.dualscreen.model.response.DeviceConfig
import com.sw.dualscreen.model.response.FoodSearchReq
import com.sw.dualscreen.model.response.UserNutrition
import com.sw.dualscreen.model.response.v2.CollectedFoodV2
import com.sw.dualscreen.model.response.v2.FaceVO
import com.sw.dualscreen.model.response.v2.NewFoodInfo
import com.sw.dualscreen.model.response.v2.NewMemberInfo
import com.sw.dualscreen.model.response.v2.SettlementOrder
import com.sw.dualscreen.network.api.ApiServiceV2
import com.sw.dualscreen.repository.BaseRepository
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.File
class RemoteRepositoryV2 constructor(
private val apiService: ApiServiceV2
) : BaseRepository() {
suspend fun getDeviceConfig(): ApiResponse<DeviceConfig?> {
return safeApiCall {
apiService.getDeviceConfig()
}
}
suspend fun getFacePage(
pageNum: Long = 1L,
pageSize: Long = 100L
): ApiResponse<List<FaceVO>?> {
return safeApiCall {
apiService.getFacePage(
request = mapOf(
"pageNum" to pageNum,
"pageSize" to pageSize
)
)
}
}
suspend fun getFaceIncrement(
pageNum: Long = 1L,
pageSize: Long = 100L,
timestamp: Long
): ApiResponse<List<FaceVO>?> {
return safeApiCall {
apiService.getFaceIncrement(
request = mapOf(
"pageNum" to pageNum,
"pageSize" to pageSize,
"timestamp" to timestamp
)
)
}
}
suspend fun getFoodByNames(
nameList: List<String>,
deviceType: Int = 2
): ApiResponse<List<NewFoodInfo>?> {
return safeApiCall {
apiService.getFoodByNames(
request = FoodSearchReq(nameList, deviceType)
)
}
}
suspend fun searchFood(
name: String,
pageNum: Long = 1L,
pageSize: Long = 100L
): ApiResponse<List<NewFoodInfo>?> {
return safeApiCall {
apiService.searchFood(
request = mapOf(
"name" to name,
"pageNum" to pageNum,
"pageSize" to pageSize
)
)
}
}
suspend fun getUserCurrentFood(
userId: Long
): ApiResponse<UserNutrition?> {
return safeApiCall {
apiService.getUserCurrentFood(
request = mapOf("id" to userId)
)
}
}
suspend fun placeOrder(
orderRequest: PlaceOrderRequest
): ApiResponse<String?> {
return safeApiCall {
apiService.placeOrder(request = orderRequest)
}
}
suspend fun getSettlementOrders(
userId: Long
): ApiResponse<SettlementOrder?> {
return safeApiCall {
apiService.getSettlementOrders(
request = mapOf("id" to userId)
)
}
}
suspend fun getMemberInfo(
userId: Long
): ApiResponse<NewMemberInfo?> {
return safeApiCall {
apiService.getMemberInfo(
request = mapOf("id" to userId)
)
}
}
suspend fun getMemberInfoByPhone(
phone: String,
password: String = ""
): ApiResponse<NewMemberInfo?> {
return safeApiCall {
apiService.getMemberInfoByPhone(
request = mapOf(
"phone" to phone,
"password" to password
)
)
}
}
suspend fun getMemberDiscount(
userId: Long
): ApiResponse<String?> {
return safeApiCall {
apiService.getMemberDiscount(
request = mapOf("id" to userId)
)
}
}
suspend fun bindUserOrder(
userId: Long,
orderNo: String,
mode: Int? = null
): ApiResponse<Any?> {
return safeApiCall {
apiService.bindUserOrder(
request = BindUserOrderRequest(userId, orderNo, mode)
)
}
}
suspend fun getCollectPage(
pageNum: Long = 1L,
pageSize: Long = 100L,
foodName: String? = null
): ApiResponse<List<CollectedFoodV2>?> {
return safeApiCall {
val request = mutableMapOf<String, Any>(
"pageNum" to pageNum,
"pageSize" to pageSize
)
if (!foodName.isNullOrEmpty()) {
request["foodName"] = foodName
}
apiService.getCollectPage(request = request)
}
}
suspend fun getCollectVectorPage(
pageNum: Long = 1L,
pageSize: Long = 100L
): ApiResponse<List<CollectedFoodV2>?> {
return safeApiCall {
apiService.getCollectVectorPage(
request = mutableMapOf(
"pageNum" to pageNum,
"pageSize" to pageSize
)
)
}
}
suspend fun uploadCollect(
foodId: Long,
foodName: String,
version: String,
foodVector: String,
fileList: List<File>
): ApiResponse<List<String>?> {
val params = hashMapOf<String, RequestBody>()
params["foodId"] = foodId.toString().toRequestBody("text/plain".toMediaTypeOrNull())
params["foodName"] = foodName.toRequestBody("text/plain".toMediaTypeOrNull())
params["version"] = version.toRequestBody("text/plain".toMediaTypeOrNull())
params["foodVector"] = foodVector.toRequestBody("text/plain".toMediaTypeOrNull())
val fileParts = mutableListOf<MultipartBody.Part>()
fileList.forEach { file ->
val requestFile = file.asRequestBody("multipart/form-data".toMediaTypeOrNull())
val filePart = MultipartBody.Part.createFormData(
"foodPics",
file.name,
requestFile
)
fileParts.add(filePart)
}
return safeApiCall {
apiService.uploadCollect(params = params, foodPics = fileParts)
}
}
suspend fun deleteCollect(
foodId: Long,
version: String
): ApiResponse<Any?> {
return safeApiCall {
apiService.deleteCollect(
request = mapOf<String, Any>(
"foodId" to foodId,
"version" to version
)
)
}
}
}
@@ -7,7 +7,7 @@ import timber.log.Timber
import kotlin.math.roundToInt import kotlin.math.roundToInt
typealias Callback = (Double) -> Unit typealias Callback = (Double) -> Unit
typealias WeightCallback = (Double) -> Unit typealias WeightCallback = (Int) -> Unit
private const val TAG = "SensorScaleUtils" private const val TAG = "SensorScaleUtils"
@@ -24,6 +24,7 @@ object SensorScaleUtils {
var isZero = false var isZero = false
private var callback: Callback? = {} private var callback: Callback? = {}
private var lastWeight: Double? = null private var lastWeight: Double? = null
private var lastCallbackWeight: Double? = null
private fun init() { private fun init() {
mSensorScale = SensorScale(object : OnScaleResult { mSensorScale = SensorScale(object : OnScaleResult {
@@ -31,21 +32,28 @@ object SensorScaleUtils {
* 读取重量 * 读取重量
*/ */
override fun readWeight(state: Int, value: Double) { override fun readWeight(state: Int, value: Double) {
val stateStr = when (state) { // val stateStr = when (state) {
SensorScale.STATE_STABLE -> "稳定" // SensorScale.STATE_STABLE -> "稳定"
SensorScale.STATE_UNSTABLE -> "不稳定" // SensorScale.STATE_UNSTABLE -> "不稳定"
SensorScale.STATE_OVER_WEIGHT -> "量程溢出" // SensorScale.STATE_OVER_WEIGHT -> "量程溢出"
else -> "未知" // else -> "未知"
} // }
// Log.d(TAG, "readWeight state = ${stateStr}, weight = $value") // Log.d(TAG, "readWeight state = ${stateStr}, weight = $value")
// 只使用稳定值 // 只使用稳定值
if (state == SensorScale.STATE_STABLE && lastWeight != value) { if (state == SensorScale.STATE_STABLE) {
if (lastWeight != value) {
lastWeight = value lastWeight = value
callback?.invoke(value) callback?.invoke(value)
} }
// 去重:仅在重量变化时回调 weightCallbackList,避免同一重量反复触发
if (lastCallbackWeight != value) {
lastCallbackWeight = value
weightCallbackList.forEach { weightCallbackList.forEach {
it.invoke(value) val currentWeight = value * 1000
it.invoke(currentWeight.roundToInt())
}
}
} }
} }
@@ -87,8 +95,7 @@ object SensorScaleUtils {
// Thread.sleep(1000) // Thread.sleep(1000)
// TODO: 暂时不标定--------- // TODO: 暂时不标定---------
//zero() zero()
}.start() }.start()
} }
} }
@@ -130,15 +137,17 @@ object SensorScaleUtils {
/** /**
* 零位标定 * 零位标定
*/ */
fun zero() { fun zero(isShowToastRemind: Boolean = false) {
Timber.d("zero isOpened = $isOpened, mSensorScale = $mSensorScale") Timber.d("zero isOpened = $isOpened, mSensorScale = $mSensorScale")
if (!isOpened) return if (!isOpened) return
mSensorScale?.zero { mSensorScale?.zero {
isZero = true isZero = true
Timber.d("zero 零位标定操作成功") Timber.d("zero 零位标定操作成功")
if (isShowToastRemind) {
ToastUtils.showToast("零位标定操作成功") ToastUtils.showToast("零位标定操作成功")
} }
} }
}
/** /**
* 去皮置零 * 去皮置零
@@ -34,7 +34,6 @@ class CameraUtils(private var activity: ComponentActivity) {
photoCaptureHelper = PhotoCaptureHelper( photoCaptureHelper = PhotoCaptureHelper(
context = activity, context = activity,
cameraController = cameraController!!, cameraController = cameraController!!,
onSuccess = {},
onError = { msg -> onError = { msg ->
//toast(msg) //toast(msg)
failCallback?.invoke(msg) failCallback?.invoke(msg)
@@ -0,0 +1,168 @@
package com.sw.dualscreen.utils;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Handler;
import android.os.Looper;
import com.sw.plate.utils.arcface.facedb.FaceDatabase;
import com.sw.plate.utils.arcface.facedb.entity.FaceEntity;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
/**
* 从 assets 目录下的测试人脸数据库导入数据到 Room 数据库
*
* <p>以分批方式读取,每批 {@link #BATCH_SIZE} 条,边读边写,
* 避免 4350 条 + 17MB 特征数据一次性加载到内存。</p>
*/
public class FaceDbImporter {
private static final String TAG = "FaceDbImporter";
/** 每批写入条数 */
private static final int BATCH_SIZE = 200;
/** assets 中测试数据库的相对路径 */
private static final String ASSETS_DB_PATH = "db/faceDB.db";
/**
* 导入回调
*/
public interface ImportCallback {
/** 进度更新,已切到主线程 */
void onProgress(int current, int total);
/** 导入失败,已切到主线程 */
void onError(String message);
/** 导入完成,已切到主线程 */
void onComplete();
}
private FaceDbImporter() {
}
/**
* 从 assets 导入测试人脸数据到 Room 数据库
*
* @param context 上下文
* @param callback 回调(所有回调均已在主线程)
*/
public static void importFromAssets(Context context, ImportCallback callback) {
Context appContext = context.getApplicationContext();
new Thread(() -> {
File tempFile = null;
SQLiteDatabase testDb = null;
Cursor cursor = null;
try {
// 1. 从 assets 复制到缓存目录(SQLiteDatabase 需要文件路径)
tempFile = new File(appContext.getCacheDir(), "faceDB_import_temp.db");
copyAssetToFile(appContext, ASSETS_DB_PATH, tempFile);
// 2. 以只读方式打开测试数据库
testDb = SQLiteDatabase.openDatabase(
tempFile.getAbsolutePath(),
null,
SQLiteDatabase.OPEN_READONLY
);
// 3. 查询总记录数
int totalCount = 0;
Cursor countCursor = testDb.rawQuery("SELECT COUNT(*) FROM face", null);
if (countCursor.moveToFirst()) {
totalCount = countCursor.getInt(0);
}
countCursor.close();
if (totalCount == 0) {
postToMain(() -> {
if (callback != null) callback.onComplete();
});
return;
}
// 4. 分批读取 + 批量写入
cursor = testDb.rawQuery(
"SELECT user_name, feature_data, register_time FROM face ORDER BY faceId",
null
);
List<FaceEntity> batch = new ArrayList<>(BATCH_SIZE);
int processedCount = 0;
while (cursor.moveToNext()) {
String userName = cursor.getString(0);
byte[] featureData = cursor.getBlob(1);
long registerTime = cursor.getLong(2);
FaceEntity entity = new FaceEntity(userName, null, featureData);
entity.setUserType("2");
entity.setRegisterTime(registerTime);
batch.add(entity);
if (batch.size() >= BATCH_SIZE) {
FaceDatabase.getInstance(appContext).faceDao().insert(batch);
processedCount += batch.size();
notifyProgress(callback, processedCount, totalCount);
batch.clear();
}
}
// 5. 写入剩余不足一批的数据
if (!batch.isEmpty()) {
FaceDatabase.getInstance(appContext).faceDao().insert(batch);
processedCount += batch.size();
notifyProgress(callback, processedCount, totalCount);
}
L.d(TAG, "导入完成,共 " + processedCount + " 条记录");
postToMain(() -> {
if (callback != null) callback.onComplete();
});
} catch (Exception e) {
L.e(TAG, "导入失败: " + e.getMessage());
postToMain(() -> {
if (callback != null) callback.onError(e.getMessage());
});
} finally {
if (cursor != null) cursor.close();
if (testDb != null && testDb.isOpen()) testDb.close();
if (tempFile != null && tempFile.exists()) tempFile.delete();
}
}).start();
}
/**
* 从 assets 复制文件到指定路径
*/
private static void copyAssetToFile(Context context, String assetPath, File destFile)
throws Exception {
try (InputStream is = context.getAssets().open(assetPath);
FileOutputStream fos = new FileOutputStream(destFile)) {
byte[] buffer = new byte[8192];
int length;
while ((length = is.read(buffer)) > 0) {
fos.write(buffer, 0, length);
}
fos.flush();
}
}
private static void notifyProgress(ImportCallback callback, int current, int total) {
postToMain(() -> {
if (callback != null) {
callback.onProgress(current, total);
}
});
}
private static void postToMain(Runnable runnable) {
new Handler(Looper.getMainLooper()).post(runnable);
}
}
@@ -19,11 +19,18 @@ import java.io.StringReader;
import java.text.DecimalFormat; import java.text.DecimalFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class FileUtil { public class FileUtil {
private static boolean isSaveLog = true; private static boolean isSaveLog = true;
public static final String FILE_STR_PATH = App.getContext().getExternalCacheDir().getAbsolutePath(); // public static final String FILE_STR_PATH = App.getContext().getExternalCacheDir().getAbsolutePath();
// 使用内部存储,不会被系统缓存清理策略影响
public static final String FILE_STR_PATH = new File(App.getContext().getFilesDir(), "logs").getAbsolutePath();
// 单线程执行器,保证日志串行写入,避免多线程并发竞争
// 单线程执行器,保证日志串行写入,避免多线程并发竞争
private static final ExecutorService LOG_EXECUTOR = Executors.newSingleThreadExecutor();
public static final int SIZETYPE_B = 1;//获取文件大小单位为B的double值 public static final int SIZETYPE_B = 1;//获取文件大小单位为B的double值
public static final int SIZETYPE_KB = 2;//获取文件大小单位为KB的double值 public static final int SIZETYPE_KB = 2;//获取文件大小单位为KB的double值
public static final int SIZETYPE_MB = 3;//获取文件大小单位为MB的double值 public static final int SIZETYPE_MB = 3;//获取文件大小单位为MB的double值
@@ -610,24 +617,19 @@ public class FileUtil {
if (!isSaveLog) { if (!isSaveLog) {
return; return;
} }
new Thread() { // 提交到单线程队列,串行执行,避免并发写入竞争
@Override LOG_EXECUTOR.execute(() -> {
public void run() {
super.run();
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
for (String log : logs) { for (String log : logs) {
sb.append("===") sb.append("===")
.append((AppUtil.formatDateGetCurrentTime())) .append((AppUtil.formatDateGetCurrentTime()))
.append("===") .append("===")
.append("\n")
.append(log) .append(log)
.append("\n"); .append("\n");
} }
String fileName = "log" + AppUtil.formatDateGetDay(System.currentTimeMillis()) + ".txt"; String fileName = "log" + AppUtil.formatDateGetDay(System.currentTimeMillis()) + ".txt";
FileUtil.saveStrFile(sb.toString(), fileName, FILE_STR_PATH, true); FileUtil.saveStrFile(sb.toString(), fileName, FILE_STR_PATH, true);
});
}
}.start();
} }
private static final String[][] MIME_MapTable = { private static final String[][] MIME_MapTable = {
@@ -0,0 +1,80 @@
package com.sw.dualscreen.utils
import androidx.lifecycle.LifecycleCoroutineScope
import com.sw.dualscreen.model.response.v2.CollectedFoodV2
import com.sw.dualscreen.objbox.Food
import com.sw.dualscreen.objbox.ObjectBox
import com.sw.dualscreen.viewmodel.NetViewModelV2
import kotlinx.coroutines.launch
object FoodVectorTool {
/** 每页拉取条数 */
private const val PAGE_SIZE = 100
/**
* 统一入口:分页拉取所有食物向量数据并保存到本地 ObjectBox
*/
fun loadAndSaveFoodVector(
userViewModel: NetViewModelV2,
lifecycleScope: LifecycleCoroutineScope,
successBlock: () -> Unit,
failureBlock: (String) -> Unit
) {
fetchPage(
pageNum = 1,
userViewModel = userViewModel,
lifecycleScope = lifecycleScope,
successBlock = successBlock,
failureBlock = failureBlock
)
}
private fun fetchPage(
pageNum: Int,
userViewModel: NetViewModelV2,
lifecycleScope: LifecycleCoroutineScope,
successBlock: () -> Unit,
failureBlock: (String) -> Unit
) {
userViewModel.getCollectVectorPage(
pageNum = pageNum.toLong(),
pageSize = PAGE_SIZE.toLong(),
onSuccess = { items ->
if (pageNum == 1 && items.isEmpty()) {
successBlock()
return@getCollectVectorPage
}
if (items.isNotEmpty()) {
saveFoodVector(lifecycleScope, items)
}
if (items.size >= PAGE_SIZE) {
fetchPage(pageNum + 1, userViewModel, lifecycleScope, successBlock, failureBlock)
} else {
successBlock()
}
},
onFailure = { failureBlock(it) }
)
}
/**
* 将 CollectedFoodV2 列表保存到 ObjectBox 本地数据库
*/
private fun saveFoodVector(lifecycleScope: LifecycleCoroutineScope, list: List<CollectedFoodV2>) {
lifecycleScope.launch {
val vectorList = list.map { item ->
val foodVector = item.foodVector?.removeSurrounding("[", "]")
?.split(",")?.map { it.toFloatOrNull() ?: 0.0f }?.toFloatArray()
Food(
collectId = item.foodId,
foodId = item.foodId,
foodName = item.foodName,
version = item.version,
foodVector = foodVector
)
}
ObjectBox.putAll(vectorList)
}
}
}
@@ -15,17 +15,23 @@ object ImageUtil {
fun uriToBitmap(context: Context, uri: Uri): Bitmap? { fun uriToBitmap(context: Context, uri: Uri): Bitmap? {
return try { return try {
val options = BitmapFactory.Options() val options = BitmapFactory.Options().apply {
//options.inSampleSize = 2; // 这会将图片的尺寸缩小到原来的1/2 inJustDecodeBounds = true
options.inJustDecodeBounds = false }
// options.inPreferredConfig = Bitmap.Config.ARGB_8888 // 先只读尺寸,计算合适的采样率
options.inPreferredConfig = Bitmap.Config.RGB_565
context.contentResolver.openInputStream(uri)?.use { stream -> context.contentResolver.openInputStream(uri)?.use { stream ->
BitmapFactory.decodeStream(stream, null, options) BitmapFactory.decodeStream(stream, null, options)
// BitmapFactory.decodeStream(stream) }
// 目标最长边不超过 800px,减少内存占用
val maxDim = maxOf(options.outWidth, options.outHeight)
val sampleSize = (maxDim / 800).coerceAtLeast(1)
options.apply {
inJustDecodeBounds = false
inSampleSize = sampleSize
inPreferredConfig = Bitmap.Config.RGB_565
} }
context.contentResolver.openInputStream(uri)?.use { stream -> context.contentResolver.openInputStream(uri)?.use { stream ->
BitmapFactory.decodeStream(stream) BitmapFactory.decodeStream(stream, null, options)
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
@@ -0,0 +1,24 @@
package com.sw.dualscreen.utils
import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
/**
* 网络连接检测工具类
* 提供检测设备网络连接状态的功能
*/
object NetworkUtils {
/**
* 检测设备是否连接到网络
* @param context 应用上下文
* @return true 表示已连接网络,false 表示未连接
*/
fun isNetworkConnected(context: Context): Boolean {
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val network = connectivityManager.activeNetwork ?: return false
val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return false
return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
}
}
@@ -2,6 +2,9 @@ package com.sw.dualscreen.utils
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.os.Handler
import android.os.Looper
import androidx.core.content.FileProvider
import androidx.camera.core.ImageCapture import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException import androidx.camera.core.ImageCaptureException
import androidx.camera.view.CameraController import androidx.camera.view.CameraController
@@ -19,9 +22,9 @@ import java.io.File
class PhotoCaptureHelper( class PhotoCaptureHelper(
private val context: Context, private val context: Context,
private val cameraController: CameraController, private val cameraController: CameraController,
private val onSuccess: (Uri) -> Unit = {},
private val onError: (String) -> Unit = {} private val onError: (String) -> Unit = {}
) { ) {
private val callbackList: MutableList<(Uri) -> Unit> = mutableListOf() private val callbackList: MutableList<(Uri) -> Unit> = mutableListOf()
fun addSuccessCallback(callback:(Uri) -> Unit) { fun addSuccessCallback(callback:(Uri) -> Unit) {
if (callbackList.contains(callback).not()) { if (callbackList.contains(callback).not()) {
@@ -41,7 +44,8 @@ class PhotoCaptureHelper(
*/ */
fun takePhoto( fun takePhoto(
fileNamePrefix: String = "IMG_", fileNamePrefix: String = "IMG_",
fileExtension: String = ".jpg" fileExtension: String = ".jpg",
isRetry: Boolean = false
) { ) {
Timber.d("开始拍照采集") Timber.d("开始拍照采集")
@@ -63,20 +67,22 @@ class PhotoCaptureHelper(
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) { override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
val photoUri = outputFileResults.savedUri ?: Uri.fromFile(photoFile) val photoUri = outputFileResults.savedUri ?: Uri.fromFile(photoFile)
Timber.d("照片保存成功: $photoUri") Timber.d("照片保存成功: $photoUri")
onSuccess(photoUri)
callbackList.forEach { callbackList.forEach {
it(photoUri) it(photoUri)
} }
} }
override fun onError(exception: ImageCaptureException) { override fun onError(exception: ImageCaptureException) {
Timber.e(exception)
val errorMsg = "拍照失败: ${exception.message}" val errorMsg = "拍照失败: ${exception.message}"
if (errorMsg.contains("Not bound to a valid Camera")) { if (errorMsg.contains("Not bound to a valid Camera") && !isRetry) {
if (bindCamera != null) { Timber.w(exception, "相机未绑定,尝试重新绑定后重试")
bindCamera?.invoke() bindCamera?.invoke()
//takePhoto() // 延迟重试,等待相机异步绑定完成
} Handler(Looper.getMainLooper()).postDelayed({
takePhoto(fileNamePrefix, fileExtension, isRetry = true)
}, 300)
return
} }
Timber.e(exception, errorMsg) Timber.e(exception, errorMsg)
onError(errorMsg) onError(errorMsg)
@@ -1,5 +1,6 @@
package com.sw.dualscreen.utils package com.sw.dualscreen.utils
import com.sw.dualscreen.GlobalKey
import com.sw.dualscreen.MyApp import com.sw.dualscreen.MyApp
import com.sw.dualscreen.utils.SPUtil.Companion.getInstance import com.sw.dualscreen.utils.SPUtil.Companion.getInstance
@@ -16,15 +17,26 @@ object SpTool {
var lastFaceTimestamp: Long var lastFaceTimestamp: Long
get() = spUtil?.get(LAST_FACE_TIMESTAMP, 0L)?:0L get() = spUtil?.get(LAST_FACE_TIMESTAMP, 0L) ?: 0L
set(timestamp) { set(timestamp) {
spUtil?.put(LAST_FACE_TIMESTAMP, timestamp) spUtil?.put(LAST_FACE_TIMESTAMP, timestamp)
} }
var firstGetFace: Boolean var firstGetFace: Boolean
get() = spUtil?.get(IS_FIRST_GET_FACE, true)?:true get() = spUtil?.get(IS_FIRST_GET_FACE, true) ?: true
set(value) { set(value) {
spUtil?.boolean(IS_FIRST_GET_FACE, value) spUtil?.boolean(IS_FIRST_GET_FACE, value)
} }
var baseUrl: String?
get() = spUtil?.get(GlobalKey.KEY_BASE_URL, "")
set(value) {
spUtil?.put(GlobalKey.KEY_BASE_URL, value)
}
var settlementMode: Int
get() = spUtil?.get(GlobalKey.KEY_SETTLEMENT_MODE, 0) ?: 0
set(value) {
spUtil?.put(GlobalKey.KEY_SETTLEMENT_MODE, value)
}
} }
@@ -15,7 +15,7 @@ import com.sw.dualscreen.databinding.BottomSheetDialogBinding
import com.sw.dualscreen.model.response.FoodInfo import com.sw.dualscreen.model.response.FoodInfo
import com.sw.dualscreen.utils.Debouncer import com.sw.dualscreen.utils.Debouncer
import com.sw.dualscreen.utils.KeyboardUtils import com.sw.dualscreen.utils.KeyboardUtils
import com.sw.dualscreen.viewmodel.UserViewModel import com.sw.dualscreen.viewmodel.NetViewModelV2
import timber.log.Timber import timber.log.Timber
import androidx.core.graphics.drawable.toDrawable import androidx.core.graphics.drawable.toDrawable
@@ -24,7 +24,7 @@ import androidx.core.graphics.drawable.toDrawable
*/ */
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
class CustomBottomSheetDialog( class CustomBottomSheetDialog(
val viewModel: UserViewModel, val viewModel: NetViewModelV2,
val itemClickCallback: (FoodInfo) -> Unit val itemClickCallback: (FoodInfo) -> Unit
) : BottomSheetDialogFragment() { ) : BottomSheetDialogFragment() {
private lateinit var binding: BottomSheetDialogBinding private lateinit var binding: BottomSheetDialogBinding
@@ -87,7 +87,7 @@ class CustomBottomSheetDialog(
companion object { companion object {
fun newInstance( fun newInstance(
viewModel: UserViewModel, viewModel: NetViewModelV2,
itemClickCallback: (FoodInfo) -> Unit itemClickCallback: (FoodInfo) -> Unit
): CustomBottomSheetDialog { ): CustomBottomSheetDialog {
return CustomBottomSheetDialog(viewModel, itemClickCallback) return CustomBottomSheetDialog(viewModel, itemClickCallback)
@@ -0,0 +1,575 @@
package com.sw.dualscreen.viewmodel
import androidx.lifecycle.viewModelScope
import com.arcsoft.face.ErrorInfo
import com.sw.dualscreen.GlobalData
import com.sw.dualscreen.model.request.UserNutritionParam
import com.sw.dualscreen.model.request.v2.BindUserOrderRequest
import com.sw.dualscreen.model.request.v2.PlaceOrderRequest
import com.sw.dualscreen.model.response.ApiResponse
import com.sw.dualscreen.model.response.DinnerType
import com.sw.dualscreen.model.response.FoodInfo
import com.sw.dualscreen.model.response.FoodOrderModel
import com.sw.dualscreen.model.response.FoodSearchReq
import com.sw.dualscreen.model.response.MemberInfo
import com.sw.dualscreen.model.response.UserNutrition
import com.sw.dualscreen.model.response.v2.CollectedFoodV2
import com.sw.dualscreen.model.response.v2.FaceVO
import com.sw.dualscreen.model.response.v2.NewFoodInfo
import com.sw.dualscreen.model.response.v2.NewMemberInfo
import com.sw.dualscreen.model.response.v2.SettlementOrder
import com.sw.dualscreen.model.response.v2.toFoodInfo
import com.sw.dualscreen.model.response.v2.toFoodOrderModel
import com.sw.dualscreen.model.response.v2.toMemberInfo
import com.sw.dualscreen.network.ApiClient
import com.sw.dualscreen.utils.FileUtil
import com.sw.dualscreen.utils.SpTool
import com.sw.plate.App
import com.sw.plate.utils.Base64
import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.FaceApi
import com.sw.plate.utils.arcface.facedb.entity.FaceEntity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.File
class NetViewModelV2 : BaseViewModel() {
companion object {
private const val TAG = "NetViewModelV2"
const val PAGE_SIZE = 100L
}
private val faceApi: FaceApi = FaceApi()
/** 人脸加载完成 */
private val _loadFaceResult = MutableStateFlow<Boolean>(false)
val loadFaceResult: StateFlow<Boolean> = _loadFaceResult
/** 饭点类型 早餐/午餐/晚餐 */
private val _dinnerTypeInfo = MutableStateFlow<DinnerType?>(null)
val dinnerTypeInfo: StateFlow<DinnerType?> = _dinnerTypeInfo
fun getDeviceConfig(
onSuccess: (config: com.sw.dualscreen.model.response.DeviceConfig?) -> Unit,
onFailure: (message: String) -> Unit
) {
launch {
try {
val response = ApiClient.repositoryV2.getDeviceConfig()
if (parseResponse(response)) {
onSuccess(response.data)
} else {
onFailure(response.msg ?: "获取设备配置失败")
}
} catch (e: Exception) {
Timber.e(e, "获取设备配置异常")
onFailure("网络异常: ${e.message}")
}
}
}
/** 获取人脸全量数据(递归分页 + FaceApi 集成) */
fun getFacePage(
pageNum: Long = 1L,
pageSize: Long = PAGE_SIZE,
onSuccess: () -> Unit = {},
onFailure: (String) -> Unit = {}
) {
var currentPageNum = pageNum
Timber.tag(TAG).d("getFacePage index = $currentPageNum")
launch {
_loadFaceResult.value = false
// 重置持久化时间戳和内存累加器
SpTool.lastFaceTimestamp = 0
lastFaceTimestamp = 0L
FileUtil.saveLog("获取人脸全量数据开始,重置时间戳为0")
val response = ApiClient.repositoryV2.getFacePage(currentPageNum, pageSize)
if (!parseResponse(response)) {
onFailure("获取人脸数据失败,${response.msg}(${response.code})")
return@launch
}
withContext(Dispatchers.Default) {
val list = response.data ?: emptyList()
if (pageNum == 1L && list.isEmpty()) {
onFailure("查询人脸数据为空")
return@withContext
}
val faceEntity = list.map { vo ->
FaceEntity(
vo.userId, // userName
Base64.decode(vo.faceFeature), // featureData
vo.personType ?: if (vo.member == true) "1" else "2", // userType
vo.cardNo ?: "", // cardNo
vo.userId ?: "", // userId
vo.userFaceId ?: "", // userFaceId
vo.member ?: false, // member
vo.faceUpdateTimestamp?.toLongOrNull() ?: 0L // faceUpdateTimestamp
)
}
faceApi.updateFaceData(currentPageNum.toInt(), faceEntity)
// 取当前页最大时间戳(后端返回 String 类型,需转为 Long 比较),跨页累加防止中间页有更大值被覆盖
val pageMaxTimestamp = list.maxOf { it.faceUpdateTimestamp?.toLongOrNull() ?: 0L }
lastFaceTimestamp = maxOf(lastFaceTimestamp, pageMaxTimestamp)
if (list.size >= pageSize) {
currentPageNum++
getFacePage(currentPageNum, pageSize, onSuccess, onFailure)
return@withContext
}
SpTool.lastFaceTimestamp = lastFaceTimestamp
FileUtil.saveLog("获取人脸[全量]数据结束,时间戳为:$lastFaceTimestamp")
_loadFaceResult.value = true
onSuccess()
}
}
}
/** 获取人脸增量数据(FaceApi 集成) */
fun getFaceIncrement(
pageNum: Long = 1L,
pageSize: Long = PAGE_SIZE,
timestamp: Long,
onAllQueryFinished: () -> Unit,
onPageQueryFinished: (List<FaceVO>) -> Unit,
onFailure: (String) -> Unit = {}
) {
Timber.tag(TAG).d("getFaceIncrement timestamp=$timestamp")
launch {
val response = ApiClient.repositoryV2.getFaceIncrement(pageNum, pageSize, timestamp)
if (!parseResponse(response)) {
onFailure(response.msg ?: "获取增量人脸数据失败")
return@launch
}
withContext(Dispatchers.Default) {
val list = response.data ?: emptyList()
if (pageNum == 1L && list.isEmpty()) {
return@withContext
}
onPageQueryFinished(list)
// 取列表中最大时间戳(后端返回 String 类型,需转为 Long),而非依赖列表最后一条
lastFaceTimestamp = list.maxOf { it.faceUpdateTimestamp?.toLongOrNull() ?: 0L }
SpTool.lastFaceTimestamp = lastFaceTimestamp
FileUtil.saveLog("获取人脸[增量]数据结束,时间戳为:$lastFaceTimestamp")
onAllQueryFinished()
}
}
}
fun getFoodByNames(
names: String,
onSuccess: (list: List<NewFoodInfo>) -> Unit,
onFailure: (message: String) -> Unit
) {
launch {
try {
val response = ApiClient.repositoryV2.getFoodByNames(names.split(","))
if (parseResponse(response)) {
onSuccess(response.data ?: emptyList())
} else {
onFailure(response.msg ?: "查询菜品失败")
}
} catch (e: Exception) {
Timber.e(e, "查询菜品异常")
onFailure("网络异常: ${e.message}")
}
}
}
fun placeOrder(
orderRequest: PlaceOrderRequest,
onSuccess: (orderNo: String?) -> Unit,
onFailure: (message: String) -> Unit
) {
launchWithLoading {
try {
val response = ApiClient.repositoryV2.placeOrder(orderRequest)
if (parseResponse(response)) {
onSuccess(response.data)
} else {
onFailure(response.msg ?: "下单失败")
}
} catch (e: Exception) {
Timber.e(e, "开餐下单异常")
onFailure("网络异常: ${e.message}")
}
}
}
fun bindUserOrder(
userId: Long,
orderNo: String,
mode: Int? = null,
onSuccess: () -> Unit,
onFailure: (message: String) -> Unit
) {
launch {
try {
val response = ApiClient.repositoryV2.bindUserOrder(userId, orderNo, mode)
if (parseResponse(response)) {
onSuccess()
} else {
onFailure(response.msg ?: "绑定订单失败")
}
} catch (e: Exception) {
Timber.e(e, "绑定订单异常")
onFailure("网络异常: ${e.message}")
}
}
}
fun getCollectPage(
pageNum: Long = 1L,
pageSize: Long = PAGE_SIZE,
foodName: String? = null,
onSuccess: (list: List<CollectedFoodV2>) -> Unit,
onFailure: (message: String) -> Unit
) {
launch {
try {
val response = ApiClient.repositoryV2.getCollectPage(pageNum, pageSize, foodName)
if (parseResponse(response)) {
onSuccess(response.data ?: emptyList())
} else {
onFailure(response.msg ?: "获取采集列表失败")
}
} catch (e: Exception) {
Timber.e(e, "获取采集列表异常")
onFailure("网络异常: ${e.message}")
}
}
}
fun getCollectVectorPage(
pageNum: Long = 1L,
pageSize: Long = PAGE_SIZE,
onSuccess: (list: List<CollectedFoodV2>) -> Unit,
onFailure: (message: String) -> Unit
) {
launch {
try {
val response = ApiClient.repositoryV2.getCollectVectorPage(pageNum, pageSize)
if (parseResponse(response)) {
onSuccess(response.data ?: emptyList())
} else {
onFailure(response.msg ?: "获取采集列表失败")
}
} catch (e: Exception) {
Timber.e(e, "获取采集列表异常")
onFailure("网络异常: ${e.message}")
}
}
}
/** 上传采集图片(suspend,直接返回 picUrls 列表) */
suspend fun uploadCollect(
foodId: Long,
foodName: String,
version: String,
foodVector: String,
fileList: List<File>
): List<String>? {
Timber.tag(TAG).d("uploadCollect foodId=$foodId, foodName=$foodName")
val fileListNotNull = fileList.filter { it.exists() }
if (fileListNotNull.isEmpty()) {
return null
}
val response = ApiClient.repositoryV2.uploadCollect(
foodId, foodName, version, foodVector, fileListNotNull
)
if (!parseResponse(response)) {
return null
}
return response.data
}
/** 删除采集菜品 */
fun deleteCollect(
foodId: String?,
version: String?,
block: (Boolean) -> Unit
) {
Timber.tag(TAG).d("deleteCollect foodId=$foodId, version=$version")
val id = foodId?.toLongOrNull() ?: run {
block(false)
return
}
launchWithLoading {
try {
val response = ApiClient.repositoryV2.deleteCollect(id, version ?: "")
if (parseResponse(response)) {
block(true)
} else {
block(false)
}
} catch (e: Exception) {
Timber.e(e, "删除采集菜品异常")
block(false)
}
}
}
// ========== 以下方法暂用 V1 API 实现(待 V2 接口补充) ==========
/** 激活虹软人脸识别引擎 */
fun activeEngine() {
Timber.tag(TAG).d("activeEngine")
faceApi.activeEngine(
App.getContext(),
GlobalData.appId,
GlobalData.sdkKey,
GlobalData.activeKey,
object : FaceApi.ActiveCallback {
override fun onSuccess(activeCode: Int) {
Timber.tag(TAG).d("activeEngine activeCode = $activeCode")
viewModelScope.launch(Dispatchers.Main) {
when (activeCode) {
ErrorInfo.MOK -> {
ToastUtils.showToast("激活引擎成功")
}
ErrorInfo.MERR_ASF_ALREADY_ACTIVATED -> {
// 引擎已激活,无需再次激活
}
else -> {
ToastUtils.showToast("激活引擎失败($activeCode)")
}
}
}
}
override fun onFail(e: Exception?) {
viewModelScope.launch(Dispatchers.Main) {
ToastUtils.showToast("激活引擎异常,${e?.message}")
}
}
})
}
/** 获取支付二维码 */
fun getQrCodeImg(
orderId: String,
userId: String? = null,
totalFee: String? = null,
block: (String?) -> Unit
) {
Timber.tag(TAG).d("getQrCodeImg orderId = $orderId, userId = $userId")
launch {
val response = repository.getQrCodeImg(
orderNo = orderId, memberId = userId, totalFee = totalFee
)
if (parseResponse(response)) {
block(response.data)
} else {
block(null)
}
}
}
/** 扫码支付 */
fun qrCodePay(
authCode: String,
orderNo: String,
memberId: String?,
block: (Boolean, String?) -> Unit
) {
launch {
val response = repository.qrCodePay(authCode, orderNo, memberId)
if (parseResponse(response)) {
block(true, response.data)
} else {
block(false, null)
}
}
}
/** 现金支付 */
fun cashPay(param: HashMap<String, String>, block: (Boolean) -> Unit) {
Timber.tag(TAG).d("cashPay")
launchWithLoading {
val response = repository.cashPay(param)
if (parseResponse(response)) {
block(response.data ?: false)
} else {
block(false)
}
}
}
/** 会员支付 */
fun memberPay(param: HashMap<String, String?>, block: (Boolean) -> Unit) {
Timber.tag(TAG).d("memberPay")
launchWithLoading {
val response = repository.memberPay(param)
if (parseResponse(response)) {
block(true)
} else {
block(false)
}
}
}
/** 查询订单支付状态 */
suspend fun queryOrderState(orderId: String, block: (Boolean) -> Unit) {
Timber.tag(TAG).d("queryOrderState")
val response = repository.queryOrderState(orderNo = orderId)
if (parseResponse(response)) {
block(response.data == "1")
} else {
block(false)
}
}
/** 获取饭点类型 */
fun getDinnerType(
onSuccess: (DinnerType?) -> Unit = {},
onFailure: (message: String) -> Unit = {}
) {
_dinnerTypeInfo.value = null
launch {
try {
val response = repository.getDinnerType()
if (parseResponse(response)) {
_dinnerTypeInfo.value = response.data
onSuccess(response.data)
} else {
onFailure(response.msg ?: "获取饭点类型失败")
}
} catch (e: Exception) {
Timber.e(e, "获取饭点类型异常")
onFailure("网络异常: ${e.message}")
}
}
}
// ========== V1 兼容包装方法(暂用 V1 API,后续逐步迁移至 V2 ==========
private var lastFaceTimestamp = 0L
/** 搜索食物 */
fun searchByFoodName(foodName: String, action: (List<FoodInfo>) -> Unit = {}) {
Timber.tag(TAG).d("searchFood foodName = $foodName")
launchWithLoading {
try {
val response = ApiClient.repositoryV2.searchFood(name = foodName)
if (parseResponse(response)) {
val list = (response.data ?: emptyList()).map { it.toFoodInfo() }
action(list)
}
} catch (e: Exception) {
Timber.e(e, "搜索菜品异常")
}
}
}
/** 获取用户就餐营养数据 */
fun getUserNutritionData(
userId: String,
block: (UserNutrition?) -> Unit
) {
Timber.tag(TAG).d("getUserNutritionData userId = $userId")
val uid = userId.toLongOrNull() ?: run {
block(null)
return
}
launch {
try {
val response = ApiClient.repositoryV2.getUserCurrentFood(uid)
if (parseResponse(response)) {
block(response.data)
} else {
block(null)
}
} catch (e: Exception) {
Timber.e(e, "获取就餐营养数据异常")
block(null)
}
}
}
/** 获取订单列表 */
fun getFoodOrderList(userId: String, block: (FoodOrderModel?) -> Unit) {
Timber.tag(TAG).d("getFoodOrderList userId = $userId")
val uid = userId.toLongOrNull() ?: run {
block(null)
return
}
launchWithLoading {
try {
val response = ApiClient.repositoryV2.getSettlementOrders(uid)
if (parseResponse(response)) {
block(response.data?.toFoodOrderModel())
} else {
block(null)
}
} catch (e: Exception) {
Timber.e(e, "查询订单列表异常")
block(null)
}
}
}
/** 根据 ID 查询会员信息 */
fun getMemberInfoById(memberId: String, block: (MemberInfo?) -> Unit) {
Timber.tag(TAG).d("getMemberInfo memberId=$memberId")
val uid = memberId.toLongOrNull() ?: run {
block(null)
return
}
launchWithLoading {
try {
val response = ApiClient.repositoryV2.getMemberInfo(uid)
if (parseResponse(response)) {
block(response.data?.toMemberInfo())
} else {
block(null)
}
} catch (e: Exception) {
Timber.e(e, "查询会员信息异常")
block(null)
}
}
}
/** 根据手机号查询会员信息 */
fun getMemberInfoByPhone(phone: String, key: String, block: (MemberInfo?) -> Unit) {
Timber.tag(TAG).d("getMemberInfoByPhone phone=$phone")
launchWithLoading {
try {
val response = ApiClient.repositoryV2.getMemberInfoByPhone(phone, key)
if (parseResponse(response)) {
block(response.data?.toMemberInfo())
} else {
block(null)
}
} catch (e: Exception) {
Timber.e(e, "查询会员异常")
block(null)
}
}
}
/** 获取会员折扣 */
fun getMemberDiscount(userId: String, block: (Double?) -> Unit) {
Timber.tag(TAG).d("getMemberDiscount userId = $userId")
val uid = userId.toLongOrNull() ?: run {
block(null)
return
}
launchWithLoading {
try {
val response = ApiClient.repositoryV2.getMemberDiscount(uid)
if (parseResponse(response)) {
block(response.data?.toDoubleOrNull())
} else {
block(null)
}
} catch (e: Exception) {
Timber.e(e, "查询会员折扣异常")
block(null)
}
}
}
}
@@ -10,6 +10,7 @@ import com.sw.dualscreen.model.response.DeviceConfig
import com.sw.dualscreen.model.response.DinnerType import com.sw.dualscreen.model.response.DinnerType
import com.sw.dualscreen.model.response.FoodInfo import com.sw.dualscreen.model.response.FoodInfo
import com.sw.dualscreen.model.response.FoodOrder import com.sw.dualscreen.model.response.FoodOrder
import com.sw.dualscreen.model.response.FoodOrderModel
import com.sw.dualscreen.model.response.FoodVector import com.sw.dualscreen.model.response.FoodVector
import com.sw.dualscreen.model.response.MemberInfo import com.sw.dualscreen.model.response.MemberInfo
import com.sw.dualscreen.model.response.UserFaceModel import com.sw.dualscreen.model.response.UserFaceModel
@@ -17,6 +18,7 @@ import com.sw.dualscreen.model.response.UserFaceModel2
import com.sw.dualscreen.model.response.UserNutrition import com.sw.dualscreen.model.response.UserNutrition
import com.sw.dualscreen.objbox.CollectedFoodInfo import com.sw.dualscreen.objbox.CollectedFoodInfo
import com.sw.dualscreen.utils.Debouncer import com.sw.dualscreen.utils.Debouncer
import com.sw.dualscreen.utils.FileUtil
import com.sw.dualscreen.utils.SPUtil import com.sw.dualscreen.utils.SPUtil
import com.sw.dualscreen.utils.SpTool import com.sw.dualscreen.utils.SpTool
import com.sw.plate.App import com.sw.plate.App
@@ -90,21 +92,37 @@ class UserViewModel : BaseViewModel() {
/** /**
* 获取人脸数据 * 获取人脸数据
*/ */
fun getUserFaceCache(pageNo: Int = 1, pageSize: Int = PAGE_SIZE) { fun getUserFaceCache(
pageNo: Int = 1,
pageSize: Int = PAGE_SIZE,
onSuccess: () -> Unit = {},
onFailure: (String) -> Unit = {}
) {
// Initialize current page number
var currentPageNo = pageNo var currentPageNo = pageNo
// Debug log to track current page
Timber.tag(TAG).d("getUserFaceCache index = $currentPageNo") Timber.tag(TAG).d("getUserFaceCache index = $currentPageNo")
// Launch a coroutine to perform the face data retrieval
launch { launch {
// Set loading state to false
_loadFaceResult.value = false _loadFaceResult.value = false
//获取全量数据时,时间戳改为0 //获取全量数据时,时间戳改为0
SpTool.lastFaceTimestamp = 0 SpTool.lastFaceTimestamp = 0
FileUtil.saveLog("获取人脸全量数据开始,重置时间戳为0")
val response = repository.getUserFaceCache(currentPageNo) val response = repository.getUserFaceCache(currentPageNo)
if (parseResponse(response)) { val check = parseResponse(response)
if (!check) {
// 获取失败
onFailure("获取人脸数据失败,${response.msg}(${response.code})")
return@launch
}
// 获取成功一次后缓存状态 // 获取成功一次后缓存状态
SPUtil.getInstance().put(GlobalKey.KEY_FIRST_RUN, true) SPUtil.getInstance().put(GlobalKey.KEY_FIRST_RUN, true)
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
val list: List<UserFaceModel> = response.data ?: emptyList() val list: List<UserFaceModel> = response.data ?: emptyList()
if (pageNo == 1 && list.isEmpty()) { if (pageNo == 1 && list.isEmpty()) {
//ToastUtils.showToast("查询人脸数据为空") //ToastUtils.showToast("查询人脸数据为空")
onFailure("查询人脸数据为空")
return@withContext return@withContext
} }
//val item = list.map { it.member } //val item = list.map { it.member }
@@ -128,9 +146,10 @@ class UserViewModel : BaseViewModel() {
lastFaceTimestamp = list.last().faceUpdateTimestamp ?: 0L lastFaceTimestamp = list.last().faceUpdateTimestamp ?: 0L
} }
SpTool.lastFaceTimestamp = lastFaceTimestamp SpTool.lastFaceTimestamp = lastFaceTimestamp
FileUtil.saveLog("获取人脸[全量]数据结束,时间戳为:$lastFaceTimestamp")
_loadFaceResult.value = true _loadFaceResult.value = true
onSuccess()
} }
// val nextPageIndex = response.result?.nextPageIndex ?: -1 // val nextPageIndex = response.result?.nextPageIndex ?: -1
// if (nextPageIndex > 0) { // if (nextPageIndex > 0) {
// getUserFaceCache(nextPageIndex) // getUserFaceCache(nextPageIndex)
@@ -139,7 +158,6 @@ class UserViewModel : BaseViewModel() {
// } // }
} }
} }
}
// fun getUserFaceCache2(index: Int = 0) { // fun getUserFaceCache2(index: Int = 0) {
// Timber.d("getUserFaceCache index = $index") // Timber.d("getUserFaceCache index = $index")
@@ -206,6 +224,7 @@ class UserViewModel : BaseViewModel() {
lastFaceTimestamp = list.last().faceUpdateTimestamp ?: 0L lastFaceTimestamp = list.last().faceUpdateTimestamp ?: 0L
} }
SpTool.lastFaceTimestamp = lastFaceTimestamp SpTool.lastFaceTimestamp = lastFaceTimestamp
FileUtil.saveLog("获取人脸[增量]数据结束,时间戳为:$lastFaceTimestamp")
onAllQueryFinished() onAllQueryFinished()
} }
} }
@@ -620,4 +639,28 @@ class UserViewModel : BaseViewModel() {
} }
} }
} }
fun getFoodOrderList(userId: String, block: (FoodOrderModel?) -> Unit) {
Timber.tag(TAG).d("getFoodOrderList")
launchWithLoading {
val response = repository.getFoodOrderList(userId)
if (parseResponse(response)) {
block(response.data)
} else {
block(null)
}
}
}
fun getMemberDiscount(userId: String, block: (Double?) -> Unit) {
Timber.tag(TAG).d("getMemberDiscount")
launchWithLoading {
val response = repository.getMemberDiscount(userId)
if (parseResponse(response)) {
block(response.data)
} else {
block(null)
}
}
}
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#F2F2F2"/>
<!-- <stroke android:color="#cdcdcd" android:width="1dp"/>-->
<corners android:radius="5dp"/>
</shape>
+18 -45
View File
@@ -22,6 +22,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical"
android:layout_marginStart="28dp" android:layout_marginStart="28dp"
android:paddingVertical="10dp"
android:textColor="#ff0a1428" android:textColor="#ff0a1428"
android:textSize="26sp" android:textSize="26sp"
android:textStyle="bold" android:textStyle="bold"
@@ -48,19 +49,23 @@
android:textColor="#FF0A1428" android:textColor="#FF0A1428"
android:textSize="52sp" android:textSize="52sp"
android:textStyle="bold" /> android:textStyle="bold" />
<!-- <TextView-->
<!-- android:id="@+id/tvShowWeight"--> <TextView
<!-- android:layout_width="wrap_content"--> android:id="@+id/tvShowWeight"
<!-- android:layout_height="80dp"--> android:layout_width="match_parent"
<!-- android:textSize="30sp"--> android:layout_height="80dp"
<!-- android:gravity="center"--> android:textSize="20sp"
<!-- tools:text="100克"/>--> android:gravity="center"
android:maxLines="1"
android:textColor="@color/black"
tools:text="100克"/>
<FrameLayout <FrameLayout
android:id="@+id/foodDisplayLayout" android:id="@+id/foodDisplayLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="560dp" android:layout_height="560dp"
android:layout_marginHorizontal="28dp" android:layout_marginHorizontal="28dp"
android:layout_marginTop="80dp" android:layout_marginTop="0dp"
android:visibility="visible"> android:visibility="visible">
<androidx.camera.view.PreviewView <androidx.camera.view.PreviewView
@@ -84,53 +89,21 @@
android:layout_margin="27dp" android:layout_margin="27dp"
android:background="@drawable/ic_camera_rect" /> android:background="@drawable/ic_camera_rect" />
<!-- preview_border--> <!-- preview_border-->
</FrameLayout> </FrameLayout>
<androidx.recyclerview.widget.RecyclerView <FrameLayout
android:id="@+id/recyclerview" android:id="@+id/flFoodList"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_marginHorizontal="14dp" android:layout_weight="1"/>
android:layout_marginTop="42dp"
android:layout_weight="1"
android:overScrollMode="never"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="2"
tools:itemCount="8"
tools:listitem="@layout/list_item_search_food" />
<TextView
android:id="@+id/tvToSearch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="38dp"
android:layout_marginBottom="0dp"
android:padding="10dp"
android:text="以上都不是,手动搜索"
android:textColor="#FFFF3232"
android:textSize="31sp"
android:textStyle="bold"
android:visibility="gone" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="54dp"
android:gravity="center">
<TextView
android:id="@+id/tvWeightRealInfo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/black"
android:textSize="12sp"
tools:text="以上都不是,手动搜索" />
</LinearLayout>
<FrameLayout <FrameLayout
android:id="@+id/flPay" android:id="@+id/flPay"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@color/white" android:background="@color/white"
android:layout_marginTop="45dp"
android:visibility="gone"> android:visibility="gone">
<androidx.appcompat.widget.AppCompatButton <androidx.appcompat.widget.AppCompatButton
+3 -3
View File
@@ -13,7 +13,7 @@
<RadioGroup <RadioGroup
android:id="@+id/rgSetting" android:id="@+id/rgSetting"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="90dp" android:layout_height="75dp"
android:orientation="horizontal"> android:orientation="horizontal">
<Space <Space
@@ -31,7 +31,7 @@
android:maxLines="1" android:maxLines="1"
android:paddingHorizontal="50dp" android:paddingHorizontal="50dp"
android:paddingTop="8dp" android:paddingTop="8dp"
android:paddingBottom="35dp" android:paddingBottom="15dp"
android:text="经营设置" android:text="经营设置"
android:textColor="@color/setting_radio" android:textColor="@color/setting_radio"
android:textSize="30sp" android:textSize="30sp"
@@ -53,7 +53,7 @@
android:maxLines="1" android:maxLines="1"
android:paddingHorizontal="50dp" android:paddingHorizontal="50dp"
android:paddingTop="8dp" android:paddingTop="8dp"
android:paddingBottom="35dp" android:paddingBottom="15dp"
android:text="餐品采集" android:text="餐品采集"
android:textColor="@color/setting_radio" android:textColor="@color/setting_radio"
android:textSize="30sp" android:textSize="30sp"
@@ -0,0 +1,106 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="600dp"
android:layout_height="wrap_content"
android:background="@drawable/bg_white_radius12"
android:orientation="vertical"
android:paddingHorizontal="40dp"
android:paddingVertical="36dp">
<!-- 标题 -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="环境切换"
android:textColor="#ff141428"
android:textSize="36sp"
android:textStyle="bold" />
<!-- 分割线 -->
<View
android:layout_width="match_parent"
android:layout_height="2dp"
android:layout_marginTop="24dp"
android:background="#FFDCDCF0" />
<!-- 环境选项 -->
<RadioGroup
android:id="@+id/rgEnv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:orientation="vertical">
<RadioButton
android:id="@+id/rbTest"
android:layout_width="match_parent"
android:layout_height="72dp"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:text="本地环境"
android:textColor="#ff141428"
android:textSize="28sp" />
<RadioButton
android:id="@+id/rbUat"
android:layout_width="match_parent"
android:layout_height="72dp"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:text="测试环境"
android:textColor="#ff141428"
android:textSize="28sp" />
<RadioButton
android:id="@+id/rbProd"
android:layout_width="match_parent"
android:layout_height="72dp"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:text="生产环境"
android:textColor="#ff141428"
android:textSize="28sp" />
</RadioGroup>
<!-- 分割线 -->
<View
android:layout_width="match_parent"
android:layout_height="2dp"
android:layout_marginTop="24dp"
android:background="#FFDCDCF0" />
<!-- 底部按钮 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="88dp"
android:layout_marginTop="24dp"
android:gravity="center"
android:orientation="horizontal">
<Button
android:id="@+id/btnCancel"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginEnd="20dp"
android:layout_weight="1"
android:background="@drawable/bg_border_red"
android:text="取消"
android:textColor="#FFFF3232"
android:textSize="28sp" />
<Button
android:id="@+id/btnConfirm"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="20dp"
android:layout_weight="1"
android:background="@drawable/bg_btn_save2"
android:text="确认"
android:textColor="#FFFFFF"
android:textSize="28sp" />
</LinearLayout>
</LinearLayout>
@@ -6,6 +6,7 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> android:orientation="vertical">
<!-- 结算设置模块 -->
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@@ -16,6 +17,85 @@
android:paddingTop="35dp" android:paddingTop="35dp"
android:paddingBottom="42dp"> android:paddingBottom="42dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawablePadding="17dp"
android:text="结算设置"
android:textColor="@color/black"
android:textSize="36sp"
android:textStyle="bold"
app:drawableStartCompat="@drawable/line_shape" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="(不允许修改,由后端配置)"
android:textColor="#FFFF3232"
android:textSize="20sp" />
</LinearLayout>
<RadioGroup
android:id="@+id/rgSettlement"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="35dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<RadioButton
android:id="@+id/rbJointPayment"
android:layout_width="0dp"
android:layout_height="120dp"
android:layout_weight="1"
android:background="@drawable/setting_check_selector"
android:button="@null"
android:gravity="center"
android:maxLines="1"
android:text="联合支付"
android:textColor="@color/setting_radio"
android:textSize="30sp"
android:textStyle="bold" />
<RadioButton
android:id="@+id/rbIndependentPayment"
android:layout_width="0dp"
android:layout_height="120dp"
android:layout_marginStart="32dp"
android:layout_weight="1"
android:background="@drawable/setting_check_selector"
android:button="@null"
android:gravity="center"
android:maxLines="1"
android:text="独立支付"
android:textColor="@color/setting_radio"
android:textSize="30sp"
android:textStyle="bold"
tools:checked="true" />
</RadioGroup>
</LinearLayout>
<!-- 计费设置模块 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/setting_border"
android:orientation="vertical"
android:layout_marginHorizontal="32dp"
android:layout_marginTop="42dp"
android:paddingHorizontal="32dp"
android:paddingTop="35dp"
android:paddingBottom="42dp">
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@@ -134,6 +214,7 @@
android:background="@drawable/setting_border_gray"> android:background="@drawable/setting_border_gray">
<TextView <TextView
android:id="@+id/tvDishWeightLabel"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="120dp" android:layout_height="120dp"
android:layout_marginStart="42dp" android:layout_marginStart="42dp"
+11 -11
View File
@@ -11,14 +11,14 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginHorizontal="32dp" android:layout_marginHorizontal="32dp"
android:background="@drawable/setting_border" android:background="@drawable/setting_border"
android:paddingBottom="16dp"> android:paddingBottom="10dp">
<TextView <TextView
android:id="@+id/tvChargeSetting" android:id="@+id/tvChargeSetting"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="32dp" android:layout_marginStart="32dp"
android:layout_marginTop="30dp" android:layout_marginTop="20dp"
android:drawablePadding="17dp" android:drawablePadding="17dp"
android:text="餐品采集" android:text="餐品采集"
android:textColor="@color/black" android:textColor="@color/black"
@@ -48,7 +48,7 @@
android:id="@+id/flPreview" android:id="@+id/flPreview"
android:layout_width="456dp" android:layout_width="456dp"
android:layout_height="342dp" android:layout_height="342dp"
android:layout_marginTop="30dp" android:layout_marginTop="20dp"
app:layout_constraintStart_toStartOf="@id/tvChargeSetting" app:layout_constraintStart_toStartOf="@id/tvChargeSetting"
app:layout_constraintTop_toBottomOf="@id/tvChargeSetting"> app:layout_constraintTop_toBottomOf="@id/tvChargeSetting">
@@ -111,9 +111,9 @@
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvFoodList" android:id="@+id/rvFoodList"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="382dp" android:layout_height="358dp"
android:layout_marginHorizontal="16dp" android:layout_marginHorizontal="16dp"
android:layout_marginTop="16dp" android:layout_marginVertical="10dp"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager" app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
@@ -129,7 +129,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="100dp" android:layout_height="100dp"
android:layout_marginHorizontal="32dp" android:layout_marginHorizontal="32dp"
android:layout_marginTop="32dp" android:layout_marginTop="20dp"
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="horizontal"> android:orientation="horizontal">
@@ -164,22 +164,22 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_marginHorizontal="16dp" android:layout_marginHorizontal="16dp"
android:layout_marginTop="18dp" android:layout_marginTop="12dp"
android:minHeight="348dp" android:minHeight="348dp"
android:layout_weight="1" android:layout_weight="1"
app:spanCount="2" app:spanCount="2"
tools:itemCount="10" tools:itemCount="11"
android:overScrollMode="never" android:overScrollMode="never"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager" app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
tools:listitem="@layout/list_item_search_food"/> tools:listitem="@layout/list_item_search_food3"/>
<androidx.appcompat.widget.AppCompatButton <androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnSave" android:id="@+id/btnSave"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="100dp" android:layout_height="100dp"
android:layout_marginHorizontal="32dp" android:layout_marginHorizontal="32dp"
android:layout_marginBottom="32dp" android:layout_marginBottom="10dp"
android:layout_marginTop="20dp" android:layout_marginTop="10dp"
android:text="保存" android:text="保存"
android:background="@drawable/bg_btn_save" android:background="@drawable/bg_btn_save"
android:textColor="@color/white" android:textColor="@color/white"
@@ -285,7 +285,7 @@
<TextView <TextView
android:id="@+id/meatTv" android:id="@+id/meatTv"
style="@style/takeFoodNameTv" style="@style/takeFoodNameTv"
android:text="肉" android:text="肉蛋豆"
android:textSize="20sp" android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"
@@ -0,0 +1,94 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:ignore="HardcodedText,UseCompatTextViewDrawableXml">
<FrameLayout
android:id="@+id/llFoodTitle"
android:layout_width="match_parent"
android:layout_height="80dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="取餐明细"
android:textColor="#ff33446a"
android:textSize="30sp"
android:textStyle="bold"
android:drawablePadding="17dp"
android:drawableStart="@drawable/ic_take_food" />
<TextView
android:id="@+id/tvUserName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|end"
android:layout_marginEnd="30dp"
android:singleLine="true"
android:textColor="#FF889AC2"
android:textSize="26sp"
tools:text="张三" />
</FrameLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginBottom="8dp"
android:background="#FFDDE5F0" />
<include layout="@layout/layout_food_list_header" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="8dp"
android:background="#FFDDE5F0" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvFoodList"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:overScrollMode="never"
tools:listitem="@layout/layout_food_list_header" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp">
<TextView
android:id="@+id/tvTotalWeight"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#ff889ac2"
android:textSize="26sp"
tools:text="总重量:680克"
android:drawablePadding="17dp"
android:drawableStart="@drawable/ic_flag_weight" />
<Space
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<TextView
android:id="@+id/tvTotalCalorie"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#ff889ac2"
android:textSize="26sp"
tools:text="总热量:793千卡"
android:drawablePadding="17dp"
android:drawableStart="@drawable/ic_flag_calorie" />
</LinearLayout>
</LinearLayout>
@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="50dp"
android:orientation="horizontal"
android:gravity="center_vertical">
<!-- 菜品名称列,权重2.5 -->
<TextView
android:id="@+id/tvFoodName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="2.1"
android:text="菜品名称"
android:textColor="#FF889AC2"
android:textSize="26sp"
android:paddingHorizontal="5dp"
android:singleLine="true"
android:gravity="center_vertical|start" />
<!-- 规格列,权重1.0 -->
<TextView
android:id="@+id/tvPriceNorm"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.6"
android:text="规格"
android:textColor="#FF889AC2"
android:textSize="26sp"
android:paddingHorizontal="5dp"
android:singleLine="true"
android:gravity="center" />
<!-- 重量列,权重1.0 -->
<TextView
android:id="@+id/tvWeight"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.0"
android:text="重量(克)"
android:textColor="#FF889AC2"
android:textSize="26sp"
android:paddingHorizontal="5dp"
android:singleLine="true"
android:gravity="center" />
<!-- 小计列,权重1.0 -->
<TextView
android:id="@+id/tvAmount"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.0"
android:text="小计(元)"
android:textColor="#FF889AC2"
android:textSize="26sp"
android:paddingHorizontal="5dp"
android:singleLine="true"
android:gravity="center" />
</LinearLayout>
@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<TextView
android:id="@+id/tvToSearch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="38dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:padding="10dp"
tools:text="以上都不是,手动搜索"
android:textColor="#FFFF3232"
android:textSize="31sp"
android:textStyle="bold"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvFoodList"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/tvToSearch"
app:layout_constraintTop_toTopOf="parent"
android:layout_marginHorizontal="14dp"
android:layout_marginTop="42dp"
android:overScrollMode="never"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="2"
tools:itemCount="8"
tools:listitem="@layout/list_item_search_food" />
<include
android:id="@+id/detailInclude"
layout="@layout/layout_eat_detail"
android:layout_width="match_parent"
android:layout_height="0dp"
android:visibility="gone"/>
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvFoodList"
android:layout_width="match_parent"
android:layout_height="264dp"
android:layout_marginHorizontal="14dp"
android:layout_marginTop="42dp"
android:overScrollMode="never"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="2"
tools:itemCount="8"
tools:listitem="@layout/list_item_search_food" />
<TextView
android:id="@+id/tvToSearch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="38dp"
android:layout_gravity="center_horizontal"
android:padding="10dp"
tools:text="以上都不是,手动搜索"
android:textColor="#FFFF3232"
android:textSize="31sp"
android:textStyle="bold"/>
<include
android:id="@+id/detailInclude"
layout="@layout/layout_eat_detail"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginHorizontal="14dp"/>
</LinearLayout>
+1 -1
View File
@@ -2,7 +2,7 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="150dp"> android:layout_height="130dp">
<ImageView <ImageView
android:id="@+id/ivPageBack" android:id="@+id/ivPageBack"
@@ -3,7 +3,8 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="212dp" android:layout_width="212dp"
android:layout_height="159dp" android:layout_height="159dp"
android:layout_margin="16dp" android:layout_marginHorizontal="16dp"
android:layout_marginVertical="10dp"
android:background="@drawable/bg_gray2"> android:background="@drawable/bg_gray2">
<ImageView <ImageView
@@ -0,0 +1,110 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="50dp"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="horizontal"
android:gravity="center_vertical"
xmlns:tools="http://schemas.android.com/tools">
<!-- 菜品名称列,权重2.5 -->
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="2.1"
android:gravity="center_vertical">
<TextView
android:id="@+id/tvFoodName"
android:layout_width="0dp"
android:layout_height="wrap_content"
tools:text="菜品名称"
android:textColor="#FF889AC2"
android:textSize="25sp"
android:paddingHorizontal="5dp"
android:singleLine="true"
android:ellipsize="end"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintWidth_default="wrap"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/tvLabel"
android:gravity="center_vertical|start" />
<TextView
android:id="@+id/tvLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="档口"
android:textColor="@color/black"
android:textSize="13sp"
android:background="@drawable/bg_order_flag"
android:paddingHorizontal="5dp"
android:paddingVertical="1dp"
android:layout_marginHorizontal="2dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/tvFoodName"
android:visibility="gone"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- 规格列,权重1.0 -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.6"
android:gravity="center">
<TextView
android:id="@+id/tvPriceNorm"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="规格"
android:textColor="#FF889AC2"
android:textSize="25sp"
android:paddingHorizontal="5dp"
android:singleLine="true"
android:gravity="center" />
<TextView
android:id="@+id/tvPriceFlag"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="[大份]"
android:textColor="@color/black"
android:textSize="15sp"
android:paddingHorizontal="5dp"
android:singleLine="true"
android:gravity="center"
android:visibility="gone"/>
</LinearLayout>
<!-- 重量列,权重1.0 -->
<TextView
android:id="@+id/tvWeight"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.0"
tools:text="重量(克)"
android:textColor="#FF889AC2"
android:textSize="25sp"
android:paddingHorizontal="5dp"
android:singleLine="true"
android:gravity="center" />
<!-- 小计列,权重1.0 -->
<TextView
android:id="@+id/tvAmount"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.0"
tools:text="小计(元)"
android:textColor="#FF889AC2"
android:textSize="25sp"
android:paddingHorizontal="5dp"
android:singleLine="true"
android:gravity="center" />
</LinearLayout>
@@ -8,8 +8,8 @@
android:ellipsize="end" android:ellipsize="end"
android:gravity="center" android:gravity="center"
android:maxLines="1" android:maxLines="1"
android:layout_marginHorizontal="14dp" android:layout_marginHorizontal="16dp"
android:layout_marginVertical="14dp" android:layout_marginVertical="16dp"
android:paddingHorizontal="15dp" android:paddingHorizontal="15dp"
android:textAlignment="center" android:textAlignment="center"
tools:textColor="@color/black" tools:textColor="@color/black"
@@ -8,8 +8,8 @@
android:ellipsize="end" android:ellipsize="end"
android:gravity="center" android:gravity="center"
android:maxLines="1" android:maxLines="1"
android:layout_marginHorizontal="14dp" android:layout_marginHorizontal="16dp"
android:layout_marginVertical="14dp" android:layout_marginVertical="16dp"
android:paddingHorizontal="15dp" android:paddingHorizontal="15dp"
android:textAlignment="center" android:textAlignment="center"
android:textColor="@color/color_text_dialog" android:textColor="@color/color_text_dialog"
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<CheckedTextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/tvFoodName"
android:layout_width="match_parent"
android:layout_height="90dp"
tools:background="@drawable/bg_text_main"
android:ellipsize="end"
android:gravity="center"
android:maxLines="1"
android:layout_marginHorizontal="16dp"
android:layout_marginVertical="8dp"
android:paddingHorizontal="15dp"
android:textAlignment="center"
tools:textColor="@color/black"
android:textSize="30sp"
android:textStyle="bold"
tools:text="肉沫干拌面" />
@@ -13,11 +13,13 @@
android:padding="28dp"> android:padding="28dp">
<LinearLayout <LinearLayout
android:id="@+id/llTopInfo"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@drawable/setting_border" android:background="@drawable/setting_border"
android:orientation="vertical" android:orientation="vertical"
android:paddingBottom="26dp"> android:paddingBottom="26dp"
android:visibility="visible">
<TextView <TextView
android:id="@+id/tvFoodName" android:id="@+id/tvFoodName"
@@ -30,10 +32,10 @@
android:textSize="40sp" android:textSize="40sp"
android:textStyle="bold" /> android:textStyle="bold" />
<!-- android:layout_width="300dp"--> <!--android:layout_width="300dp"-->
<!-- android:layout_height="450dp"--> <!--android:layout_height="450dp"-->
<!-- android:layout_width="330dp"--> <!--android:layout_width="330dp"-->
<!-- android:layout_height="495dp"--> <!--android:layout_height="495dp"-->
<FrameLayout <FrameLayout
android:id="@+id/flCameraView" android:id="@+id/flCameraView"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -187,6 +189,20 @@
</LinearLayout> </LinearLayout>
<FrameLayout
android:id="@+id/flFoodDetail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/setting_border"
android:paddingHorizontal="15dp"
android:visibility="gone">
<include
android:id="@+id/detailInclude"
layout="@layout/layout_eat_detail"/>
</FrameLayout>
<FrameLayout <FrameLayout
android:id="@+id/flBottomView" android:id="@+id/flBottomView"
android:layout_width="match_parent" android:layout_width="match_parent"
+13 -2
View File
@@ -2,6 +2,7 @@
agp = "8.10.1" agp = "8.10.1"
core = "3.4.1" core = "3.4.1"
androidCore = "3.3.0" androidCore = "3.3.0"
eventbus = "3.2.0"
glide = "4.16.0" glide = "4.16.0"
kotlin = "2.0.21" kotlin = "2.0.21"
coreKtx = "1.10.1" coreKtx = "1.10.1"
@@ -14,6 +15,9 @@ activity = "1.8.0"
constraintlayout = "2.1.4" constraintlayout = "2.1.4"
accompanistPermissions = "0.37.3" accompanistPermissions = "0.37.3"
cameraCore = "1.4.1" cameraCore = "1.4.1"
objectboxVersion = "5.0.1"
refreshLayoutKernel = "3.0.0-alpha"
refreshHeaderClassics = "3.0.0-alpha"
retrofit = "3.0.0" retrofit = "3.0.0"
okhttp = "4.12.0" okhttp = "4.12.0"
timber = "5.0.1" timber = "5.0.1"
@@ -30,11 +34,13 @@ pytorch = "1.13.0"
recyclerview = "1.3.2" recyclerview = "1.3.2"
#baseQuickAdapter = "4.1.2" baseQuickAdapter = "3.0.4"
baseQuickAdapter4 = "4.1.2"
[libraries] [libraries]
android-core = { module = "com.google.zxing:android-core", version.ref = "androidCore" } android-core = { module = "com.google.zxing:android-core", version.ref = "androidCore" }
core = { module = "com.google.zxing:core", version.ref = "core" } core = { module = "com.google.zxing:core", version.ref = "core" }
eventbus = { module = "org.greenrobot:eventbus", version.ref = "eventbus" }
glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" } glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" }
junit = { group = "junit", name = "junit", version.ref = "junit" } junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
@@ -56,7 +62,10 @@ hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", ve
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroid" } hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroid" }
logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" } logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" }
objectbox-android-objectbrowser = { module = "io.objectbox:objectbox-android-objectbrowser", version.ref = "objectboxVersion" }
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
refresh-header-classics = { module = "io.github.scwang90:refresh-header-classics", version.ref = "refreshHeaderClassics" }
refresh-layout-kernel = { module = "io.github.scwang90:refresh-layout-kernel", version.ref = "refreshLayoutKernel" }
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" } converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" }
timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }
@@ -68,10 +77,12 @@ androidx-fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref
androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" } androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" }
#noinspection Aligned16KB
pytorch-android = { group = "org.pytorch", name = "pytorch_android", version.ref = "pytorch" } pytorch-android = { group = "org.pytorch", name = "pytorch_android", version.ref = "pytorch" }
pytorch-android-torchvision = { group = "org.pytorch", name = "pytorch_android_torchvision", version.ref = "pytorch" } pytorch-android-torchvision = { group = "org.pytorch", name = "pytorch_android_torchvision", version.ref = "pytorch" }
#base-quick-adapter-ktx = { module = "io.github.cymchad:BaseRecyclerViewAdapterHelper4", version.ref = "baseQuickAdapter" } #baseRecyclerViewAdapterHelper = { module = "com.github.CymChad:BaseRecyclerViewAdapterHelper", version.ref = "baseQuickAdapter" }
baseRecyclerViewAdapterHelper4 = { module = "io.github.cymchad:BaseRecyclerViewAdapterHelper4", version.ref = "baseQuickAdapter4" }
androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" } androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" }
+1 -1
View File
@@ -1,7 +1,7 @@
#Mon Jul 14 11:28:37 CST 2025 #Mon Jul 14 11:28:37 CST 2025
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.11.1-all.zip distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.11.1-bin.zip
#distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip #distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
#distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.14.3-all.zip #distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.14.3-all.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
+1 -1
View File
@@ -68,5 +68,5 @@ dependencies {
val objectboxVersion = "5.0.1" val objectboxVersion = "5.0.1"
debugImplementation("io.objectbox:objectbox-android-objectbrowser:$objectboxVersion") debugImplementation("io.objectbox:objectbox-android-objectbrowser:$objectboxVersion")
// releaseImplementation("io.objectbox:objectbox-android:$objectboxVersion") releaseImplementation("io.objectbox:objectbox-android:$objectboxVersion")
} }
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -88,14 +88,8 @@ public class ConfigUtil {
* 默认相机分辨率 * 默认相机分辨率
*/ */
// private static final String DEFAULT_PREVIEW_SIZE = "1280x720"; // private static final String DEFAULT_PREVIEW_SIZE = "1280x720";
// private static final String DEFAULT_PREVIEW_SIZE = "1080x720";
// private static final String DEFAULT_PREVIEW_SIZE = "720x1080";
private static final String DEFAULT_PREVIEW_SIZE = "720x1280"; private static final String DEFAULT_PREVIEW_SIZE = "720x1280";
// private static final String DEFAULT_PREVIEW_SIZE = "608x456"; // private static final String DEFAULT_PREVIEW_SIZE = "400x640";
// private static final String DEFAULT_PREVIEW_SIZE = "1024x768";
// private static final String DEFAULT_PREVIEW_SIZE = "800x600";
// private static final String DEFAULT_PREVIEW_SIZE = "640x480";
/** /**
* 获取String类型的preference * 获取String类型的preference
@@ -244,7 +238,7 @@ public class ConfigUtil {
/** /**
* TODO: 该Demo基于单人脸识别实现,若想使用多人脸识别,请将 return true 改成 return getBoolean,并修改相关配置项的preference.xml和业务代码 * TODO: 该Demo基于单人脸识别实现,若想使用多人脸识别,请将 return true 改成 return getBoolean,并修改相关配置项的preference.xml和业务代码
* <p> *
* 获取识别界面是否保留最大人脸 * 获取识别界面是否保留最大人脸
* *
* @param context 上下文 * @param context 上下文
@@ -349,7 +343,7 @@ public class ConfigUtil {
return Float.parseFloat(getString(context, R.string.preference_ir_liveness_threshold, String.valueOf(RECOMMEND_IR_LIVENESS_THRESHOLD))); return Float.parseFloat(getString(context, R.string.preference_ir_liveness_threshold, String.valueOf(RECOMMEND_IR_LIVENESS_THRESHOLD)));
} }
public static float getLivenessFqThreshold(Context context) { public static float getLivenessFqThreshold(Context context){
return Float.parseFloat(getString(context, R.string.preference_liveness_fq_threshold, String.valueOf(RECOMMEND_LIVENESS_FQ_THRESHOLD))); return Float.parseFloat(getString(context, R.string.preference_liveness_fq_threshold, String.valueOf(RECOMMEND_LIVENESS_FQ_THRESHOLD)));
} }
@@ -27,7 +27,6 @@ public class ErrorCodeUtil {
} }
return "unknown error"; return "unknown error";
} }
/** /**
* 将ArcSoftImageUtil错误码转换为对应的错误码常量名,便于理解 * 将ArcSoftImageUtil错误码转换为对应的错误码常量名,便于理解
* TODO:目前每次都遍历,如果使用频繁,建议将Field缓存处理,避免每次都反射 * TODO:目前每次都遍历,如果使用频繁,建议将Field缓存处理,避免每次都反射
@@ -1,6 +1,7 @@
package com.sw.plate.utils.arcface; package com.sw.plate.utils.arcface;
import android.content.Context; import android.content.Context;
import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import com.arcsoft.face.FaceEngine; import com.arcsoft.face.FaceEngine;
@@ -31,26 +32,22 @@ public class FaceApi {
*/ */
public void updateFaceData(int index, List<FaceEntity> list) { public void updateFaceData(int index, List<FaceEntity> list) {
Log.d(TAG, "updateFaceData: index = " + index + ", listSize = " + list.size()); Log.d(TAG, "updateFaceData: index = " + index + ", listSize = " + list.size());
FaceDao faceDao = FaceDatabase.getInstance(App.getContext()).faceDao(); FaceDao faceDao = getFaceDao();
// 档口机业务:首页(index==1)全量同步前清空旧特征库,避免特征重复堆积
if (index == 1) { if (index == 1) {
faceDao.deleteAll(); faceDao.deleteAll();
faceDao.resetId(); faceDao.resetId();
} }
List<Long> insertIdList = faceDao.insert(list); faceDao.insert(list);
Log.d(TAG, "updateFaceData: count = " + insertIdList.size());
List<FaceEntity> queryList = faceDao.getAllFaces(); List<FaceEntity> queryList = faceDao.getAllFaces();
Log.d(TAG, "updateFaceData: queryList.size = " + queryList.size()); Log.d(TAG, "updateFaceData: queryList.size = " + queryList.size());
} }
public void updateFaceData2(int index, List<FaceEntity> list) { public void clearFaceData() {
Log.d(TAG, "updateFaceData: index = " + index + ", listSize = " + list.size()); FaceDao faceDao = getFaceDao();
FaceDao faceDao = FaceDatabase.getInstance(App.getContext()).faceDao();
if (index == 0) {
faceDao.deleteAll(); faceDao.deleteAll();
faceDao.resetId(); faceDao.resetId();
} }
faceDao.insert(list);
}
/** /**
* 激活arcsoft 人脸 * 激活arcsoft 人脸
@@ -92,4 +89,35 @@ public class FaceApi {
// List<FacePreviewInfo> facePreviewInfoList = recognizeViewModel.onPreviewFrame(nv21, true); // List<FacePreviewInfo> facePreviewInfoList = recognizeViewModel.onPreviewFrame(nv21, true);
return null; return null;
} }
public void deleteByUserName(String userName) {
getFaceDao().deleteFaceById(userName);
}
public Long insert(FaceEntity entity) {
if (entity == null) {
return 0L;
}
return getFaceDao().insert(entity);
}
public FaceEntity queryByUserName(String userName) {
if (TextUtils.isEmpty(userName)) {
return null;
}
return getFaceDao().queryByUserName(userName);
}
public int queryFaceCount() {
return getFaceDao().getFaceCount();
}
public List<FaceEntity> queryAllByUserName(String userName) {
return getFaceDao().queryAllByUserName(userName);
}
public FaceDao getFaceDao() {
return FaceDatabase.getInstance(App.getContext()).faceDao();
}
} }
@@ -77,7 +77,7 @@ public class FaceRectTransformer {
rect.bottom *= verticalRatio; rect.bottom *= verticalRatio;
Rect newRect = new Rect(); Rect newRect = new Rect();
// L.e("cameraDisplayOrientation " + cameraDisplayOrientation + " === " + cameraId); L.e("cameraDisplayOrientation " + cameraDisplayOrientation + " === " + cameraId);
switch (cameraDisplayOrientation) { switch (cameraDisplayOrientation) {
case 0: case 0:
if (cameraId == Camera.CameraInfo.CAMERA_FACING_FRONT) { if (cameraId == Camera.CameraInfo.CAMERA_FACING_FRONT) {
@@ -2,7 +2,6 @@ package com.sw.plate.utils.arcface;
import android.content.Context; import android.content.Context;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint; import android.graphics.Paint;
import android.graphics.Path; import android.graphics.Path;
import android.graphics.Rect; import android.graphics.Rect;
@@ -47,6 +46,10 @@ public class FaceRectView extends View {
} }
} }
public int getRectColor() {
return paint.getColor();
}
public void clearFaceInfo() { public void clearFaceInfo() {
drawInfoList.clear(); drawInfoList.clear();
postInvalidate(); postInvalidate();
@@ -216,7 +219,6 @@ public class FaceRectView extends View {
paint.setStyle(Paint.Style.STROKE); paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(faceRectThickness); paint.setStrokeWidth(faceRectThickness);
paint.setColor(drawInfo.getColor()); paint.setColor(drawInfo.getColor());
// paint.setColor(Color.parseColor("#FF0000"));
paint.setAntiAlias(true); paint.setAntiAlias(true);
Path mPath = new Path(); Path mPath = new Path();
@@ -27,7 +27,7 @@ public class FileUtil {
} }
public static boolean saveDataToFile(byte[] data, File file, boolean append) { public static boolean saveDataToFile(byte[] data, File file, boolean append) {
if (data == null) { if (data == null){
return false; return false;
} }
File parentFile = file.getParentFile(); File parentFile = file.getParentFile();

Some files were not shown because too many files have changed in this diff Show More