feat:硬件核销相关

This commit is contained in:
dzc committed 2026-01-30 15:24:09 +08:00
1 parent aea52e482e
commit 5cf2b74158
16 files changed
+562 -28

No files matched your search

+6
View File
@@ -2,3 +2,9 @@
```
flutter pub run build_runner build --delete-conflicting-outputs~
```
## flutter打包
```
flutter build apk --build-name 1.1.0 --build-number 110
--build-name为版本号(展示)
--build-number为版本号(内部比较)
```
+2 -1
View File
@@ -1,6 +1,7 @@
library;
export 'product_api.dart';
export './product_api.dart';
export './hard_api.dart';
export './user_api.dart';
export './wallet_api.dart';
export './app_api.dart';
+10
View File
@@ -75,4 +75,14 @@ abstract class WalletApi {
?.map<ServiceWallet>((e) => ServiceWallet.fromJson(e))
.toList();
}
/// 获取通用活动详情
static Future<ServiceComAct?> getComAct(dynamic pa) async {
final res = await HttpManage().dio.get(
"/wallet/act/com/ad/get-act",
queryParameters: pa,
);
if (res.data == null) return null;
return ServiceComAct.fromJson(res.data);
}
}
@@ -134,3 +134,39 @@ class ServiceWallet {
Map<String, dynamic> toJson() => _$ServiceWalletToJson(this);
}
@JsonSerializable()
class ServiceComAct {
/// 活动类型
@JsonKey(
fromJson: JsonTool.stringToWalletActType,
toJson: JsonTool.walletActTypeToString,
)
WalletActType? actType;
/// 活动id
String? actItemId;
/// 用户id
String? userId;
/// 用户名
String? username;
/// 用户头像
String? userAvatar;
/// 详情
Map<String, dynamic>? detail;
ServiceComAct({
required this.actType,
required this.actItemId,
required this.detail,
});
factory ServiceComAct.fromJson(Map<String, dynamic> json) =>
_$ServiceComActFromJson(json);
Map<String, dynamic> toJson() => _$ServiceComActToJson(this);
}
@@ -80,3 +80,23 @@ Map<String, dynamic> _$ServiceWalletToJson(ServiceWallet instance) =>
'cardNumber': instance.cardNumber,
'isFreeze': JsonTool.booleanToString(instance.isFreeze),
};
ServiceComAct _$ServiceComActFromJson(Map<String, dynamic> json) =>
ServiceComAct(
actType: JsonTool.stringToWalletActType(json['actType'] as String?),
actItemId: json['actItemId'] as String?,
detail: json['detail'] as Map<String, dynamic>?,
)
..userId = json['userId'] as String?
..username = json['username'] as String?
..userAvatar = json['userAvatar'] as String?;
Map<String, dynamic> _$ServiceComActToJson(ServiceComAct instance) =>
<String, dynamic>{
'actType': JsonTool.walletActTypeToString(instance.actType),
'actItemId': instance.actItemId,
'userId': instance.userId,
'username': instance.username,
'userAvatar': instance.userAvatar,
'detail': instance.detail,
};
@@ -50,6 +50,7 @@ enum OrderPayType implements BaseEnum {
ZYY("12", "中医院"),
RMYY("13", "人民医院"),
CSXFK("13", "超市消费卡"),
ACT_CANCEL("50", "活动核销"),
;
@override
+2
View File
@@ -9,10 +9,12 @@ export './api/wallet_api_models.dart';
export './base/base_models.dart';
export './page/app_version_enum_models.dart';
export './page/order_page_enum_models.dart';
export './page/order_page_models.dart';
export './page/panel_page_models.dart';
export './page/setting_page_enum_models.dart';
export './page/wallet_page_enum_models.dart';
export './mq/mq_enum_models.dart';
export './mq/mq_models.dart';
@@ -0,0 +1,28 @@
import 'package:pj_supermarket_cloud/common/models/base/base_models.dart';
enum AppPlatformEnum implements BaseEnum {
android("1", "安卓"),
ios("2", "苹果"),
;
@override
final String code;
@override
final String title;
const AppPlatformEnum(this.code, this.title);
}
enum AppVersionStatusEnum implements BaseEnum {
published("1", "已发布"),
unpublished("2", "未发布"),
offShelf("3", "已下架"),
;
@override
final String code;
@override
final String title;
const AppVersionStatusEnum(this.code, this.title);
}
@@ -0,0 +1,14 @@
import 'package:pj_supermarket_cloud/common/index.dart';
enum WalletActType implements BaseEnum {
cancelAct("1", "核销活动"),
couponAct("2", "优惠券活动"),
;
@override
final String code;
@override
final String title;
const WalletActType(this.code, this.title);
}
@@ -93,4 +93,5 @@ const _$OrderPayTypeEnumMap = {
OrderPayType.ZYY: 'ZYY',
OrderPayType.RMYY: 'RMYY',
OrderPayType.CSXFK: 'CSXFK',
OrderPayType.ACT_CANCEL: 'ACT_CANCEL',
};
+9
View File
@@ -47,4 +47,13 @@ abstract class JsonTool {
if (value == null) return null;
return value.toString();
}
static WalletActType? stringToWalletActType(String? value) {
return WalletActType.values.fromCode(value);
}
static String? walletActTypeToString(WalletActType? value) {
if (value == null) return null;
return value.toString();
}
}
+3
View File
@@ -25,4 +25,7 @@ class Constants {
/// 数据库名字
static const databaseName = 'cash_register.db';
/// 设备唯一编码
static const deviceUniqueCode = 'EjFJx0WQ';
}
@@ -1,7 +1,17 @@
import 'dart:convert';
import 'package:common_utils/common_utils.dart';
import 'package:ducafe_ui_core/ducafe_ui_core.dart';
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:external_path/external_path.dart';
import 'package:dio/dio.dart';
import 'package:dio/io.dart';
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:forui/forui.dart';
import 'package:get/get.dart';
import 'package:pj_supermarket_cloud/common/index.dart';
@@ -14,7 +24,18 @@ class VersionManageController extends GetxController {
RxInt totalPage = RxInt(0);
RxInt pageSize = RxInt(15);
RxInt pageSize = RxInt(3);
RxList<ServiceAppVersion> versionList = RxList();
@override
void onInit() async {
super.onInit();
await getVersionList();
}
@override
void onClose() {}
/// 搜索
Future<void> doPage({int? page}) async {
@@ -30,7 +51,18 @@ class VersionManageController extends GetxController {
maskType: EasyLoadingMaskType.black,
);
}
try {} finally {
try {
final params = {
"appUniqueCode": Constants.deviceUniqueCode,
};
final data = await AppApi.openCardList(params);
logger.d("|$_tag|获取版本列表成功:${json.encode(data)}");
if (data == null || ObjectUtil.isEmptyList(data.records)) {
return;
}
versionList.value = data.records!;
totalPage.value = data.total;
} finally {
if (isLoading) {
Future.delayed(const Duration(seconds: 1), () {
EasyLoading.dismiss();
@@ -38,6 +70,121 @@ class VersionManageController extends GetxController {
}
}
}
/// 远程下载apk
Future<void> doDownload(String url) async {
if (url.isEmpty) return;
String downUrl = url;
if (!url.startsWith("http") && !url.startsWith("https")) {
downUrl = "${Constants.ossUrl}/api/admin/sys-file/oss/file?fileName=$url";
}
logger.d("|$_tag|远程下载apk,url:$downUrl");
try {
EasyLoading.show(
status: '下载中...',
maskType: EasyLoadingMaskType.black,
);
if (Platform.isAndroid) {
var hasStorage = await Permission.storage.request().isGranted;
if (!hasStorage) {
EasyLoading.showError("存储权限未授权,无法下载");
return;
}
if (await Permission.manageExternalStorage.isDenied) {
final g = await Permission.manageExternalStorage.request();
hasStorage = hasStorage || g.isGranted;
}
const channel =
MethodChannel('com.example.pj_supermarket_cloud/system_download');
final user = await AppSettingTool.getUserSetting();
final headers = <String, String>{};
if (ObjectUtil.isNotEmpty(user.accessToken)) {
headers['Authorization'] = "Bearer ${user.accessToken}";
}
headers['TENANT-ID'] = "${user.tenantId ?? 1}";
headers['CLIENT-TOC'] = "Y";
headers['Accept'] = "*/*";
headers['User-Agent'] = "Mozilla/5.0 (Android) DownloadManager";
final ext = AppFileTool.getFileExtension(downUrl);
final useExt = ObjectUtil.isEmpty(ext) ? ".apk" : ext;
final fileName = "pj_${DateTime.now().millisecondsSinceEpoch}$useExt";
final mimeType = useExt == ".apk"
? "application/vnd.android.package-archive"
: "application/octet-stream";
final res = await channel.invokeMethod<int>('download', {
'url': downUrl,
'fileName': fileName,
'mimeType': mimeType,
'headers': headers,
});
logger.i("|$_tag|系统下载任务创建:${res ?? -1}");
try {
final info = await channel.invokeMethod<Map<dynamic, dynamic>>(
'query',
{'id': res},
);
logger.i("|$_tag|系统下载状态:${info?['status']},原因:${info?['reason']}");
final st = info?['status'] as int?;
if (st == 1 || st == 4) {
final downloadsDir =
await ExternalPath.getExternalStoragePublicDirectory(
"Downloads");
final savePath = "$downloadsDir/$fileName";
final dio = Dio(BaseOptions(
connectTimeout: const Duration(seconds: 15),
receiveTimeout: const Duration(minutes: 5),
));
dio.httpClientAdapter = IOHttpClientAdapter(
createHttpClient: () {
final client = HttpClient(
context: SecurityContext(withTrustedRoots: false),
);
client.badCertificateCallback = (cert, host, port) => true;
return client;
},
);
await dio.download(
downUrl,
savePath,
options: Options(headers: headers),
);
logger.i("|$_tag|直连下载成功:$savePath");
}
} catch (e) {
final downloadsDir =
await ExternalPath.getExternalStoragePublicDirectory("Downloads");
final savePath = "$downloadsDir/$fileName";
final dio = Dio(BaseOptions(
connectTimeout: const Duration(seconds: 15),
receiveTimeout: const Duration(minutes: 5),
));
dio.httpClientAdapter = IOHttpClientAdapter(
createHttpClient: () {
final client = HttpClient(
context: SecurityContext(withTrustedRoots: false),
);
client.badCertificateCallback = (cert, host, port) => true;
return client;
},
);
await dio.download(
downUrl,
savePath,
options: Options(headers: headers),
);
logger.i("|$_tag|系统查询失败降级直连成功:$savePath;error:$e");
}
EasyLoading.showSuccess("已添加到系统下载");
} else {
EasyLoading.showInfo("当前平台不支持系统下载");
}
} catch (e) {
logger.e("|$_tag|远程下载apk,下载失败:$e");
EasyLoading.showError("下载失败");
} finally {
EasyLoading.dismiss();
}
}
}
Widget settingVersionSetting(BuildContext context) {
@@ -61,15 +208,238 @@ Widget settingVersionSetting(BuildContext context) {
// 版本更新搜索区域
Widget buildSettingVersionSettingSearch(BuildContext context) {
return Container();
final versionC = Get.find<VersionManageController>();
final theme = FTheme.of(context);
return <Widget>[
const Spacer(),
FButton(
prefix: FIcon(
FAssets.icons.search,
size: theme.typography.base.fontSize,
),
label: Text(
'搜索',
style: theme.typography.base.copyWith(
color: theme.colorScheme.secondary,
),
),
onPress: () => versionC.doPage(),
),
].toRow();
}
// 版本更新列表区域
Widget buildSettingVersionSettingList(BuildContext context) {
return Container();
final versionC = Get.find<VersionManageController>();
return Obx(() {
if (versionC.versionList.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.info_outline, size: 48, color: Colors.grey),
const SizedBox(height: 10),
Text("暂无版本信息", style: TextStyle(color: Colors.grey[600])),
],
),
);
}
return GridView.builder(
padding: const EdgeInsets.all(10),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 1,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
childAspectRatio: 6,
),
itemCount: versionC.versionList.length,
itemBuilder: (context, index) {
final item = versionC.versionList[index];
return buildSettingVersionSettingCardCopy(context, item);
},
);
});
}
Widget buildSettingVersionSettingCardCopy(
BuildContext context,
ServiceAppVersion item,
) {
final versionC = Get.find<VersionManageController>();
final theme = FTheme.of(context);
return <Widget>[
<Widget>[
Text(
"${AppPlatformEnum.values.fromCode(item.platform ?? '')?.title ?? '未知平台'} - ${item.versionName ?? '未知版本'}",
style: theme.typography.lg.copyWith(fontWeight: FontWeight.bold),
overflow: TextOverflow.ellipsis,
),
SizedBox(width: 20.w),
FBadge(
label: Text(
AppVersionStatusEnum.values.fromCode(item.status ?? '')?.title ??
'未知状态',
style: theme.typography.sm.copyWith(height: 1, color: Colors.white),
).padding(),
),
].toRow(),
SizedBox(height: 10.w),
Text(
"更新内容",
style: theme.typography.base.copyWith(fontWeight: FontWeight.bold),
),
SizedBox(height: 10.w),
Text(
item.updateContent ?? '暂无更新内容',
style: theme.typography.base.copyWith(color: Colors.grey),
),
SizedBox(height: 10.w),
<Widget>[
FIcon(
FAssets.icons.sendHorizontal,
size: theme.typography.base.fontSize,
),
SizedBox(width: 10.w),
Text(
item.downloadUrl ?? '未知下载地址',
style: theme.typography.sm.copyWith(color: Colors.grey),
),
const Spacer(),
FIcon(
FAssets.icons.arrowDownToLine,
size: theme.typography.base.fontSize,
),
]
.toRow()
.padding(horizontal: 20.w, vertical: 15.w)
.decorated(
color: theme.colorScheme.secondary,
border: Border.all(
color: theme.colorScheme.primary,
width: 1,
),
borderRadius: theme.style.borderRadius,
)
.onTap(() => versionC.doDownload(item.downloadUrl ?? '')),
]
.toColumn(crossAxisAlignment: CrossAxisAlignment.start)
.padding(horizontal: 20.w, vertical: 15.w)
.decorated(
color: theme.colorScheme.muted,
border: Border.all(
color: theme.colorScheme.primary,
width: 1,
),
borderRadius: theme.style.borderRadius,
);
}
// 版本管理卡片
Widget buildSettingVersionSettingCard(BuildContext context) {
return Container();
Widget buildSettingVersionSettingCard(
BuildContext context,
ServiceAppVersion item,
) {
final theme = FTheme.of(context);
return Container(
padding: const EdgeInsets.all(15),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey.shade300),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
"${item.platform ?? '未知平台'} - ${item.versionName ?? '未知版本'}",
style:
theme.typography.lg.copyWith(fontWeight: FontWeight.bold),
overflow: TextOverflow.ellipsis,
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: item.status == '已发布'
? Colors.green.withOpacity(0.1)
: Colors.grey.withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
),
child: Text(
item.status ?? '未知状态',
style: theme.typography.sm.copyWith(
color: item.status == '已发布' ? Colors.green : Colors.grey,
),
),
),
],
),
const SizedBox(height: 8),
Row(
children: [
Text("Build: ${item.versionCode ?? '-'}",
style: theme.typography.base.copyWith(color: Colors.grey)),
const SizedBox(width: 10),
if (item.isForce == true) _buildTag(context, "强制更新", Colors.red),
if (item.isSilent == true)
_buildTag(context, "静默更新", Colors.orange),
],
),
const Divider(height: 20),
Text("更新内容:",
style: theme.typography.base.copyWith(fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
Text(
item.updateContent ?? '暂无更新内容',
style: theme.typography.base.copyWith(color: Colors.grey[800]),
),
if (item.downloadUrl != null && item.downloadUrl!.isNotEmpty) ...[
const SizedBox(height: 10),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(4),
),
child: Row(
children: [
const Icon(Icons.link, size: 16, color: Colors.blue),
const SizedBox(width: 5),
Expanded(
child: Text(
item.downloadUrl!,
style: theme.typography.sm.copyWith(color: Colors.blue),
overflow: TextOverflow.ellipsis,
),
),
],
),
),
],
],
),
);
}
Widget _buildTag(BuildContext context, String text, Color color) {
return Container(
margin: const EdgeInsets.only(left: 8),
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
border: Border.all(color: color),
borderRadius: BorderRadius.circular(4),
),
child: Text(text, style: TextStyle(color: color, fontSize: 10)),
);
}
+51 -18
View File
@@ -49,6 +49,7 @@ class TakeFoodController extends GetxController {
doCancelPay() {
orderId = null;
globalQrCode = null;
orderPayType = OrderPayType.RESERVE_QR_CODE;
payUser.value = ServiceUser();
payUser.refresh();
carList.value = [];
@@ -100,13 +101,36 @@ class TakeFoodController extends GetxController {
BarcodeScanManage.instance.qrcodeScanStream.listen((event) async {
final serviceCode = await BarcodeScanManage.instance.getCodeInfo(event);
if (serviceCode == null) return;
if (serviceCode.payType != OrderPayType.RESERVE_QR_CODE) {
await TtsManage.speak("请出示线上订餐二维码");
return;
if (serviceCode.payType == OrderPayType.RESERVE_QR_CODE) {
orderPayType = OrderPayType.RESERVE_QR_CODE;
globalQrCode = serviceCode;
orderId = serviceCode.id;
await getOrderDetail();
} else if (serviceCode.payType == OrderPayType.ACT_CANCEL) {
orderPayType = OrderPayType.ACT_CANCEL;
// 查询核销活动
final act = await WalletApi.getComAct({
"actItemId": serviceCode.id,
"userId": serviceCode.userId,
});
if (act == null) {
await TtsManage.speak("核销活动不存在");
return;
}
// 更新用户
payUser.value?.username = act.username;
payUser.value?.userId = act.userId;
payUser.value?.userAvatar = act.userAvatar;
payUser.refresh();
// 生成简单购物车
carList.value = [
ServiceCar(productName: "生日活动核销", productPrice: 0, productCount: 1)
];
senSubScreen();
} else {
await TtsManage.speak("无效的二维码");
}
globalQrCode = serviceCode;
orderId = serviceCode.id;
await getOrderDetail();
return;
});
} catch (e) {
doCancelPay();
@@ -123,26 +147,35 @@ class TakeFoodController extends GetxController {
final appSetting = await AppSettingTool.getSysSetting();
final serviceId = appSetting.serviceId;
if (ObjectUtil.isEmpty(serviceId)) {
Toast.error(msg: "订单不存在");
Toast.error(msg: "请前往设置绑定设备");
return;
}
final pa = {
"orderId": orderId,
"hardwareId": serviceId,
};
EasyLoading.show(
status: '核销中...',
maskType: EasyLoadingMaskType.black,
);
disposeOrderQrCodeCancel(serviceId!);
}
Future<void> disposeOrderQrCodeCancel(String serviceId) async {
try {
EasyLoading.show(
status: '核销中...',
maskType: EasyLoadingMaskType.black,
);
final flag = await HardApi.cancelQrCode(pa);
if (flag == false) throw Error();
// 线上订餐二维码支付
if (orderPayType == OrderPayType.RESERVE_QR_CODE) {
final pa = {
"orderId": orderId,
"hardwareId": serviceId,
};
final flag = await HardApi.cancelQrCode(pa);
if (flag == false) throw Error();
}
// 活动核销
if (orderPayType == OrderPayType.ACT_CANCEL) {}
EasyLoading.dismiss();
Toast.success(msg: "核销成功");
TtsManage.speak("核销成功");
doCancelPay();
} catch (e, stack) {
logger.e("获取订单详情错误error:|$e|stack:|$stack|");
logger.e("核销订单orderType:|${orderPayType.title}|error:|$e|stack:|$stack|");
EasyLoading.dismiss();
}
}
+1 -1
View File
@@ -50,7 +50,7 @@ class TakeFoodPage extends GetView<TakeFoodController> {
dataRowHeight: hdHeight,
columns: [
DataColumn2(
label: Text("菜品名称", textAlign: TextAlign.center),
label: Text("物品名称", textAlign: TextAlign.center),
headingRowAlignment: MainAxisAlignment.center,
),
DataColumn2(
+2 -2
View File
@@ -1,7 +1,7 @@
name: pj_supermarket_cloud
description: "A new Flutter project."
description: "智慧后勤-测试"
publish_to: 'none'
version: 1.0.0+1
version: 1.1.1+1
environment:
sdk: ^3.6.0