已完成用户APP首期功能开发
交付用户端首期页面、配套接口、后台资源及测试文档。用户APP构建、静态分析和三个管理后台构建通过;完整测试仍有2项失败,后端模型注释检查未通过,详见交付记录。
This commit is contained in:
53
apps/user_app/lib/domain/models/cart_item.dart
Normal file
53
apps/user_app/lib/domain/models/cart_item.dart
Normal file
@@ -0,0 +1,53 @@
|
||||
// 功能描述:购物车条目、并发版本和结算快照;版本:1.0.0。
|
||||
import 'primary_models.dart';
|
||||
|
||||
/// 服务端购物车快照;revision 用于阻止过期操作覆盖其他设备的修改。
|
||||
class CartItem {
|
||||
const CartItem({
|
||||
required this.product,
|
||||
required this.quantity,
|
||||
required this.selected,
|
||||
required this.available,
|
||||
required this.revision,
|
||||
this.specification = '',
|
||||
});
|
||||
final ProductSummary product;
|
||||
final int quantity;
|
||||
final bool selected, available;
|
||||
final String revision;
|
||||
final String specification;
|
||||
bool get purchasable => available && quantity > 0 && quantity <= product.stock;
|
||||
int get amount => product.price * quantity;
|
||||
|
||||
factory CartItem.fromJson(Map<String, Object?> json, String Function(String) resolve) {
|
||||
final price = json['price_amount'], stock = json['stock_quantity'], quantity = json['quantity'];
|
||||
if (price is! int ||
|
||||
price < 0 ||
|
||||
stock is! int ||
|
||||
quantity is! int ||
|
||||
quantity < 0 ||
|
||||
quantity > 999 ||
|
||||
json['product_identity'] is! String ||
|
||||
(json['product_identity'] as String).isEmpty ||
|
||||
json['revision'] is! String ||
|
||||
json['selected'] is! bool ||
|
||||
json['available'] is! bool) {
|
||||
throw const FormatException('购物车数据异常');
|
||||
}
|
||||
return CartItem(
|
||||
product: ProductSummary(
|
||||
identity: json['product_identity'] as String,
|
||||
name: json['name'] as String? ?? '',
|
||||
price: price,
|
||||
stock: stock,
|
||||
imageUrl: resolve(json['image_url'] as String? ?? ''),
|
||||
category: json['category_name'] as String? ?? '',
|
||||
),
|
||||
quantity: quantity,
|
||||
selected: json['selected'] as bool,
|
||||
available: json['available'] as bool,
|
||||
revision: json['revision'] as String,
|
||||
specification: json['specification'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -20,31 +20,54 @@ class UserProfile {
|
||||
required this.name,
|
||||
required this.phone,
|
||||
required this.avatar,
|
||||
this.realName = '',
|
||||
});
|
||||
|
||||
final String identity;
|
||||
final String name;
|
||||
final String phone;
|
||||
final String avatar;
|
||||
final String realName;
|
||||
|
||||
factory UserProfile.fromJson(Map<String, Object?> json) => UserProfile(
|
||||
identity: json['identity'] as String? ?? '',
|
||||
name: json['name'] as String? ?? '',
|
||||
phone: json['phone'] as String? ?? '',
|
||||
avatar: json['avatar'] as String? ?? '',
|
||||
realName: json['real_name'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
class WalletSummary {
|
||||
const WalletSummary({required this.balance, required this.withdrawalBalance});
|
||||
const WalletSummary({
|
||||
required this.balance,
|
||||
required this.withdrawalBalance,
|
||||
this.paymentPasswordSet,
|
||||
});
|
||||
|
||||
final int balance;
|
||||
final int withdrawalBalance;
|
||||
// 缺少状态时保持未知,不能误当作未设置支付密码。
|
||||
final bool? paymentPasswordSet;
|
||||
|
||||
factory WalletSummary.fromJson(Map<String, Object?> json) => WalletSummary(
|
||||
balance: (json['balance'] as num?)?.toInt() ?? 0,
|
||||
withdrawalBalance: (json['withdrawal_balance'] as num?)?.toInt() ?? 0,
|
||||
paymentPasswordSet: json['payment_password_set'] as bool?,
|
||||
);
|
||||
}
|
||||
|
||||
/// 手机验证码申请结果,明确区分请求建立与短信已发送。
|
||||
class PaymentCodeRequest {
|
||||
const PaymentCodeRequest({
|
||||
required this.identity,
|
||||
required this.delivered,
|
||||
required this.maskedPhone,
|
||||
required this.retryAfter,
|
||||
});
|
||||
final String identity, maskedPhone;
|
||||
final bool delivered;
|
||||
final int retryAfter;
|
||||
}
|
||||
|
||||
String moneyText(int cents) => '¥${(cents / 100).toStringAsFixed(2)}';
|
||||
|
||||
66
apps/user_app/lib/domain/models/delivery_detail.dart
Normal file
66
apps/user_app/lib/domain/models/delivery_detail.dart
Normal file
@@ -0,0 +1,66 @@
|
||||
// 功能描述:用户可见的配送人员、资质、配送点及订单交付快照;版本:1.0.0。
|
||||
class DeliveryProduct {
|
||||
const DeliveryProduct({required this.name, required this.quantity});
|
||||
final String name;
|
||||
final int quantity;
|
||||
}
|
||||
|
||||
/// 只接受本人配送接口的显式事实;不可用能力保留布尔状态,不由客户端猜测。
|
||||
class DeliveryDetail {
|
||||
DeliveryDetail.fromJson(Map<String, Object?> data)
|
||||
: identity = _required(data, 'identity'),
|
||||
orderNo = _required(data, 'order_no'),
|
||||
statusName = _required(data, 'status_name'),
|
||||
statusMessage = _required(data, 'status_message'),
|
||||
address = _required(data, 'address'),
|
||||
contactName = _required(data, 'contact_name'),
|
||||
contactPhoneMasked = _required(data, 'contact_phone_masked'),
|
||||
stationName = _text(data, 'station_name'),
|
||||
deliveryName = _text(data, 'delivery_name'),
|
||||
deliveryAddress = _text(data, 'delivery_address'),
|
||||
staffName = _text(data, 'staff_name'),
|
||||
staffAvatar = _text(data, 'staff_avatar'),
|
||||
credentialType = _text(data, 'credential_type'),
|
||||
appointmentAt = DateTime.parse(_required(data, 'appointment_at')).toLocal(),
|
||||
trackUpdatedAt = _date(data['track_updated_at']),
|
||||
staffAssigned = _flag(data, 'staff_assigned'),
|
||||
credentialVerified = _flag(data, 'credential_verified'),
|
||||
trackAvailable = _flag(data, 'track_available'),
|
||||
controlledCallAvailable = _flag(data, 'controlled_call_available'),
|
||||
messageAvailable = _flag(data, 'message_available'),
|
||||
vehicleConfigured = _flag(data, 'vehicle_configured'),
|
||||
products = List.unmodifiable([
|
||||
for (final row in data['products'] as List)
|
||||
DeliveryProduct(
|
||||
name: _required(Map<String, Object?>.from(row as Map), 'name'),
|
||||
quantity: (row['quantity'] as int?) ?? 0,
|
||||
),
|
||||
]) {
|
||||
if (products.any((item) => item.quantity < 1)) throw const FormatException('配送商品数量异常');
|
||||
}
|
||||
|
||||
final String identity, orderNo, statusName, statusMessage, address, contactName;
|
||||
final String contactPhoneMasked, stationName, deliveryName, deliveryAddress, staffName;
|
||||
final String staffAvatar;
|
||||
final String credentialType;
|
||||
final DateTime appointmentAt;
|
||||
final DateTime? trackUpdatedAt;
|
||||
final bool staffAssigned, credentialVerified, trackAvailable;
|
||||
final bool controlledCallAvailable, messageAvailable, vehicleConfigured;
|
||||
final List<DeliveryProduct> products;
|
||||
|
||||
static String _text(Map<String, Object?> data, String key) => data[key] as String? ?? '';
|
||||
static String _required(Map<String, Object?> data, String key) {
|
||||
final value = _text(data, key);
|
||||
if (value.trim().isEmpty) throw FormatException('配送资料不完整:$key');
|
||||
return value;
|
||||
}
|
||||
|
||||
static bool _flag(Map<String, Object?> data, String key) {
|
||||
if (data[key] is! bool) throw FormatException('配送状态异常:$key');
|
||||
return data[key] as bool;
|
||||
}
|
||||
|
||||
static DateTime? _date(Object? value) =>
|
||||
value == null ? null : DateTime.tryParse('$value')?.toLocal();
|
||||
}
|
||||
88
apps/user_app/lib/domain/models/delivery_track.dart
Normal file
88
apps/user_app/lib/domain/models/delivery_track.dart
Normal file
@@ -0,0 +1,88 @@
|
||||
// 功能描述:用户可见的隐私化配送轨迹、履约节点与人员摘要;版本:1.0.0。
|
||||
class DeliveryTrackPoint {
|
||||
DeliveryTrackPoint.fromJson(Map<String, Object?> data)
|
||||
: longitude = _number(data, 'longitude'),
|
||||
latitude = _number(data, 'latitude'),
|
||||
occurredAt = DateTime.parse(_required(data, 'occurred_at')).toLocal();
|
||||
|
||||
final double longitude, latitude;
|
||||
final DateTime occurredAt;
|
||||
|
||||
static double _number(Map<String, Object?> data, String key) {
|
||||
final value = data[key];
|
||||
if (value is! num) throw FormatException('配送轨迹坐标异常:$key');
|
||||
return value.toDouble();
|
||||
}
|
||||
}
|
||||
|
||||
class DeliveryTrackEvent {
|
||||
DeliveryTrackEvent.fromJson(Map<String, Object?> data)
|
||||
: statusCode = data['status_code'] as int? ?? 0,
|
||||
title = _required(data, 'title'),
|
||||
detail = _required(data, 'detail'),
|
||||
occurredAt = DateTime.parse(_required(data, 'occurred_at')).toLocal();
|
||||
|
||||
final int statusCode;
|
||||
final String title, detail;
|
||||
final DateTime occurredAt;
|
||||
}
|
||||
|
||||
/// 只接受服务端明确返回的本人订单事实;客户端不推算路线、位置或送达时间。
|
||||
class DeliveryTrack {
|
||||
DeliveryTrack.fromJson(Map<String, Object?> data)
|
||||
: identity = _required(data, 'identity'),
|
||||
statusCode = data['status_code'] as int? ?? 0,
|
||||
statusName = _required(data, 'status_name'),
|
||||
statusMessage = _required(data, 'status_message'),
|
||||
appointmentAt = DateTime.parse(_required(data, 'appointment_at')).toLocal(),
|
||||
updatedAt = _date(data['updated_at']),
|
||||
stationName = _text(data, 'station_name'),
|
||||
staffName = _text(data, 'staff_name'),
|
||||
staffAvatar = _text(data, 'staff_avatar'),
|
||||
staffPhoneMasked = _text(data, 'staff_phone_masked'),
|
||||
controlledCallAvailable = _flag(data, 'controlled_call_available'),
|
||||
routeAvailable = _flag(data, 'route_available'),
|
||||
destinationAvailable = _flag(data, 'destination_available'),
|
||||
destinationLongitude = _optionalNumber(data['destination_longitude']),
|
||||
destinationLatitude = _optionalNumber(data['destination_latitude']),
|
||||
points = List.unmodifiable([
|
||||
for (final row in data['points'] as List)
|
||||
DeliveryTrackPoint.fromJson(Map<String, Object?>.from(row as Map)),
|
||||
]),
|
||||
timeline = List.unmodifiable([
|
||||
for (final row in data['timeline'] as List)
|
||||
DeliveryTrackEvent.fromJson(Map<String, Object?>.from(row as Map)),
|
||||
]);
|
||||
|
||||
final String identity, statusName, statusMessage, stationName, staffName;
|
||||
final String staffAvatar, staffPhoneMasked;
|
||||
final int statusCode;
|
||||
final DateTime appointmentAt;
|
||||
final DateTime? updatedAt;
|
||||
final bool controlledCallAvailable, routeAvailable, destinationAvailable;
|
||||
final double? destinationLongitude, destinationLatitude;
|
||||
final List<DeliveryTrackPoint> points;
|
||||
final List<DeliveryTrackEvent> timeline;
|
||||
|
||||
static String _text(Map<String, Object?> data, String key) => data[key] as String? ?? '';
|
||||
static String _required(Map<String, Object?> data, String key) {
|
||||
final value = _text(data, key);
|
||||
if (value.trim().isEmpty) throw FormatException('配送轨迹资料不完整:$key');
|
||||
return value;
|
||||
}
|
||||
|
||||
static bool _flag(Map<String, Object?> data, String key) {
|
||||
if (data[key] is! bool) throw FormatException('配送轨迹状态异常:$key');
|
||||
return data[key] as bool;
|
||||
}
|
||||
|
||||
static double? _optionalNumber(Object? value) => value is num ? value.toDouble() : null;
|
||||
static DateTime? _date(Object? value) =>
|
||||
value == null ? null : DateTime.tryParse('$value')?.toLocal();
|
||||
}
|
||||
|
||||
String _required(Map<String, Object?> data, String key) {
|
||||
final value = data[key] as String? ?? '';
|
||||
if (value.trim().isEmpty) throw FormatException('配送轨迹资料不完整:$key');
|
||||
return value;
|
||||
}
|
||||
111
apps/user_app/lib/domain/models/deposit.dart
Normal file
111
apps/user_app/lib/domain/models/deposit.dart
Normal file
@@ -0,0 +1,111 @@
|
||||
// 功能描述:用户押金汇总与单瓶押金事实;版本:1.0.0。
|
||||
|
||||
/// 单只气瓶的押金状态,金额单位为分。
|
||||
class DepositRecord {
|
||||
const DepositRecord({
|
||||
required this.identity,
|
||||
required this.depositNo,
|
||||
required this.status,
|
||||
required this.statusName,
|
||||
required this.amount,
|
||||
required this.productName,
|
||||
required this.productCode,
|
||||
required this.paidAt,
|
||||
required this.refundedAt,
|
||||
required this.allowedActions,
|
||||
this.returnRequestIdentity = '',
|
||||
this.returnStatusName = '',
|
||||
});
|
||||
|
||||
final String identity, depositNo, statusName, productName, productCode;
|
||||
final String returnRequestIdentity, returnStatusName;
|
||||
final int status, amount;
|
||||
final DateTime paidAt;
|
||||
final DateTime? refundedAt;
|
||||
final List<String> allowedActions;
|
||||
|
||||
factory DepositRecord.fromJson(Map<String, Object?> data) {
|
||||
final paidAt = DateTime.tryParse(data['paid_at']?.toString() ?? '');
|
||||
if (data['identity'] is! String ||
|
||||
data['deposit_no'] is! String ||
|
||||
data['deposit_status'] is! int ||
|
||||
data['amount'] is! int ||
|
||||
(data['amount'] as int) < 0 ||
|
||||
paidAt == null) {
|
||||
throw const FormatException('押金记录数据异常');
|
||||
}
|
||||
return DepositRecord(
|
||||
identity: data['identity'] as String,
|
||||
depositNo: data['deposit_no'] as String,
|
||||
status: data['deposit_status'] as int,
|
||||
statusName: data['status_name'] as String? ?? '处理中',
|
||||
amount: data['amount'] as int,
|
||||
productName: data['product_name'] as String? ?? '',
|
||||
productCode: data['product_code'] as String? ?? '',
|
||||
paidAt: paidAt.toLocal(),
|
||||
refundedAt: data['refunded_at'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['refunded_at'].toString())?.toLocal(),
|
||||
allowedActions: (data['allowed_actions'] as List? ?? []).whereType<String>().toList(),
|
||||
returnRequestIdentity: data['return_request_identity'] as String? ?? '',
|
||||
returnStatusName: data['return_status_name'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 押金页一次读取的服务端快照。
|
||||
class DepositSummary {
|
||||
const DepositSummary({
|
||||
required this.refundableAmount,
|
||||
required this.usingCount,
|
||||
required this.items,
|
||||
required this.ruleText,
|
||||
});
|
||||
final int refundableAmount, usingCount;
|
||||
final List<DepositRecord> items;
|
||||
final String ruleText;
|
||||
|
||||
factory DepositSummary.fromJson(Map<String, Object?> data) {
|
||||
if (data['refundable_amount'] is! int ||
|
||||
data['using_count'] is! int ||
|
||||
data['items'] is! List) {
|
||||
throw const FormatException('押金汇总数据异常');
|
||||
}
|
||||
return DepositSummary(
|
||||
refundableAmount: data['refundable_amount'] as int,
|
||||
usingCount: data['using_count'] as int,
|
||||
items: (data['items'] as List)
|
||||
.whereType<Map<Object?, Object?>>()
|
||||
.map((item) => DepositRecord.fromJson(Map<String, Object?>.from(item)))
|
||||
.toList(),
|
||||
ruleText: data['rule_text'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// DepositReturnRequest 表示退瓶申请的可见状态,不暴露内部关联主键。
|
||||
class DepositReturnRequest {
|
||||
const DepositReturnRequest({
|
||||
required this.identity,
|
||||
required this.status,
|
||||
required this.statusName,
|
||||
required this.estimatedAmount,
|
||||
required this.refundAmount,
|
||||
});
|
||||
|
||||
final String identity, statusName;
|
||||
final int status, estimatedAmount, refundAmount;
|
||||
|
||||
factory DepositReturnRequest.fromJson(Map<String, Object?> data) {
|
||||
if (data['identity'] is! String || data['return_status'] is! int) {
|
||||
throw const FormatException('退瓶申请数据不完整');
|
||||
}
|
||||
return DepositReturnRequest(
|
||||
identity: data['identity'] as String,
|
||||
status: data['return_status'] as int,
|
||||
statusName: data['status_name'] as String? ?? '处理中',
|
||||
estimatedAmount: data['estimated_amount'] as int? ?? 0,
|
||||
refundAmount: data['refund_amount'] as int? ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
34
apps/user_app/lib/domain/models/device_group.dart
Normal file
34
apps/user_app/lib/domain/models/device_group.dart
Normal file
@@ -0,0 +1,34 @@
|
||||
// 功能:解析本人设备分组及其公开设备标识;版本:1.0.0。
|
||||
import 'client_models.dart';
|
||||
|
||||
/// 设备分组只表达用户自定义归类,不代表设备在线或可控制。
|
||||
class DeviceGroup {
|
||||
const DeviceGroup({
|
||||
required this.identity,
|
||||
required this.name,
|
||||
required this.sortNo,
|
||||
required this.deviceIdentities,
|
||||
});
|
||||
|
||||
final String identity;
|
||||
final String name;
|
||||
final int sortNo;
|
||||
final Set<String> deviceIdentities;
|
||||
|
||||
factory DeviceGroup.fromJson(Map<String, Object?> json) => DeviceGroup(
|
||||
identity: json['identity'] as String? ?? '',
|
||||
name: json['name'] as String? ?? '',
|
||||
sortNo: (json['sort_no'] as num?)?.toInt() ?? 0,
|
||||
deviceIdentities: {
|
||||
for (final value in json['device_identities'] as List? ?? const [])
|
||||
if (value is String && value.isNotEmpty) value,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 同一次页面刷新读取的设备与分组快照,避免两次渲染状态互相错配。
|
||||
class DeviceCatalog {
|
||||
const DeviceCatalog({required this.devices, required this.groups});
|
||||
final List<ClientRecord> devices;
|
||||
final List<DeviceGroup> groups;
|
||||
}
|
||||
137
apps/user_app/lib/domain/models/family_sharing.dart
Normal file
137
apps/user_app/lib/domain/models/family_sharing.dart
Normal file
@@ -0,0 +1,137 @@
|
||||
// 功能:家庭成员、设备及共享权限领域模型;版本:1.0.0。
|
||||
|
||||
class FamilyDevicePermission {
|
||||
const FamilyDevicePermission({
|
||||
required this.deviceIdentity,
|
||||
required this.deviceName,
|
||||
required this.deviceKind,
|
||||
required this.canView,
|
||||
required this.canAlert,
|
||||
required this.canControl,
|
||||
});
|
||||
final String deviceIdentity, deviceName, deviceKind;
|
||||
final bool canView, canAlert, canControl;
|
||||
|
||||
factory FamilyDevicePermission.fromJson(Map<String, Object?> json) => FamilyDevicePermission(
|
||||
deviceIdentity: json['device_identity'] as String? ?? '',
|
||||
deviceName: json['device_name'] as String? ?? '',
|
||||
deviceKind: json['device_kind'] as String? ?? 'unknown',
|
||||
canView: json['can_view'] == true,
|
||||
canAlert: json['can_alert'] == true,
|
||||
canControl: json['can_control'] == true,
|
||||
);
|
||||
|
||||
Map<String, Object?> toJson() => {
|
||||
'device_identity': deviceIdentity,
|
||||
'can_view': canView,
|
||||
'can_alert': canAlert,
|
||||
'can_control': canControl,
|
||||
};
|
||||
}
|
||||
|
||||
class FamilyMember {
|
||||
const FamilyMember({
|
||||
required this.identity,
|
||||
required this.name,
|
||||
required this.phoneMasked,
|
||||
required this.relationship,
|
||||
required this.inviteStatus,
|
||||
required this.isOwner,
|
||||
required this.permissions,
|
||||
this.expiresAt,
|
||||
});
|
||||
final String identity, name, phoneMasked, relationship;
|
||||
final int inviteStatus;
|
||||
final bool isOwner;
|
||||
final List<FamilyDevicePermission> permissions;
|
||||
final DateTime? expiresAt;
|
||||
|
||||
bool get pending => inviteStatus == 10;
|
||||
factory FamilyMember.fromJson(Map<String, Object?> json) => FamilyMember(
|
||||
identity: json['identity'] as String? ?? '',
|
||||
name: json['name'] as String? ?? '',
|
||||
phoneMasked: json['phone_masked'] as String? ?? '',
|
||||
relationship: json['relationship'] as String? ?? '',
|
||||
inviteStatus: (json['invite_status'] as num?)?.toInt() ?? 10,
|
||||
isOwner: json['is_owner'] == true,
|
||||
permissions: (json['device_permissions'] as List? ?? const [])
|
||||
.whereType<Map<Object?, Object?>>()
|
||||
.map((e) => FamilyDevicePermission.fromJson(Map<String, Object?>.from(e)))
|
||||
.toList(),
|
||||
expiresAt: DateTime.tryParse(json['expires_at'] as String? ?? ''),
|
||||
);
|
||||
}
|
||||
|
||||
class FamilyDevice {
|
||||
const FamilyDevice({
|
||||
required this.identity,
|
||||
required this.name,
|
||||
required this.kind,
|
||||
required this.shareCount,
|
||||
required this.mappingConfigured,
|
||||
});
|
||||
final String identity, name, kind;
|
||||
final int shareCount;
|
||||
final bool mappingConfigured;
|
||||
factory FamilyDevice.fromJson(Map<String, Object?> json) => FamilyDevice(
|
||||
identity: json['identity'] as String? ?? '',
|
||||
name: json['name'] as String? ?? '',
|
||||
kind: json['kind'] as String? ?? 'unknown',
|
||||
shareCount: (json['share_count'] as num?)?.toInt() ?? 0,
|
||||
mappingConfigured: json['mapping_configured'] == true,
|
||||
);
|
||||
}
|
||||
|
||||
class FamilyDashboardData {
|
||||
const FamilyDashboardData({
|
||||
required this.householdName,
|
||||
required this.ownerName,
|
||||
required this.memberCount,
|
||||
required this.deviceCount,
|
||||
required this.members,
|
||||
required this.devices,
|
||||
this.incomingInvitations = const [],
|
||||
});
|
||||
final String householdName, ownerName;
|
||||
final int memberCount, deviceCount;
|
||||
final List<FamilyMember> members;
|
||||
final List<FamilyDevice> devices;
|
||||
final List<FamilyInvitation> incomingInvitations;
|
||||
factory FamilyDashboardData.fromJson(Map<String, Object?> json) => FamilyDashboardData(
|
||||
householdName: json['household_name'] as String? ?? '我的家庭',
|
||||
ownerName: json['owner_name'] as String? ?? '',
|
||||
memberCount: (json['member_count'] as num?)?.toInt() ?? 0,
|
||||
deviceCount: (json['device_count'] as num?)?.toInt() ?? 0,
|
||||
members: (json['members'] as List? ?? const [])
|
||||
.whereType<Map<Object?, Object?>>()
|
||||
.map((e) => FamilyMember.fromJson(Map<String, Object?>.from(e)))
|
||||
.toList(),
|
||||
devices: (json['devices'] as List? ?? const [])
|
||||
.whereType<Map<Object?, Object?>>()
|
||||
.map((e) => FamilyDevice.fromJson(Map<String, Object?>.from(e)))
|
||||
.toList(),
|
||||
incomingInvitations: (json['incoming_invitations'] as List? ?? const [])
|
||||
.whereType<Map<Object?, Object?>>()
|
||||
.map((e) => FamilyInvitation.fromJson(Map<String, Object?>.from(e)))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
class FamilyInvitation {
|
||||
const FamilyInvitation({
|
||||
required this.identity,
|
||||
required this.ownerName,
|
||||
required this.name,
|
||||
required this.relationship,
|
||||
required this.expiresAt,
|
||||
});
|
||||
final String identity, ownerName, name, relationship;
|
||||
final DateTime? expiresAt;
|
||||
factory FamilyInvitation.fromJson(Map<String, Object?> json) => FamilyInvitation(
|
||||
identity: json['identity'] as String? ?? '',
|
||||
ownerName: json['owner_name'] as String? ?? '',
|
||||
name: json['name'] as String? ?? '',
|
||||
relationship: json['relationship'] as String? ?? '',
|
||||
expiresAt: DateTime.tryParse(json['expires_at'] as String? ?? ''),
|
||||
);
|
||||
}
|
||||
94
apps/user_app/lib/domain/models/gas_order_checkout.dart
Normal file
94
apps/user_app/lib/domain/models/gas_order_checkout.dart
Normal file
@@ -0,0 +1,94 @@
|
||||
// 功能描述:气瓶下单报价、规格和服务端预约时段强类型模型;版本:1.0.0。
|
||||
import 'shipping_address.dart';
|
||||
|
||||
class GasOrderProductOption {
|
||||
const GasOrderProductOption({
|
||||
required this.name,
|
||||
required this.description,
|
||||
required this.unitPrice,
|
||||
required this.depositAmount,
|
||||
required this.itemIdentities,
|
||||
this.orderable = true,
|
||||
this.unavailableReason = '',
|
||||
});
|
||||
final String name, description;
|
||||
final int unitPrice, depositAmount;
|
||||
final List<String> itemIdentities;
|
||||
final bool orderable;
|
||||
final String unavailableReason;
|
||||
|
||||
factory GasOrderProductOption.fromJson(Map<String, Object?> json) => GasOrderProductOption(
|
||||
name: json['name'] as String? ?? '',
|
||||
description: json['description'] as String? ?? '',
|
||||
unitPrice: (json['unit_price'] as num?)?.toInt() ?? 0,
|
||||
depositAmount: (json['deposit_amount'] as num?)?.toInt() ?? 0,
|
||||
orderable: json['orderable'] != false,
|
||||
unavailableReason: json['unavailable_reason'] as String? ?? '',
|
||||
itemIdentities: List<String>.unmodifiable(
|
||||
(json['item_identities'] as List? ?? const []).whereType<String>(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class GasOrderAppointmentSlot {
|
||||
const GasOrderAppointmentSlot({required this.startAt, required this.label});
|
||||
final DateTime startAt;
|
||||
final String label;
|
||||
|
||||
factory GasOrderAppointmentSlot.fromJson(Map<String, Object?> json) {
|
||||
final start = DateTime.tryParse(json['start_at'] as String? ?? '');
|
||||
if (start == null) throw const FormatException('预约时段无效');
|
||||
return GasOrderAppointmentSlot(startAt: start, label: json['label'] as String? ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
class GasOrderCheckoutData {
|
||||
const GasOrderCheckoutData({
|
||||
required this.stationName,
|
||||
required this.stationStatus,
|
||||
required this.deliveryScope,
|
||||
required this.deliveryFee,
|
||||
required this.products,
|
||||
required this.addresses,
|
||||
required this.slots,
|
||||
});
|
||||
final String stationName, stationStatus, deliveryScope;
|
||||
final int deliveryFee;
|
||||
final List<GasOrderProductOption> products;
|
||||
final List<ShippingAddress> addresses;
|
||||
final List<GasOrderAppointmentSlot> slots;
|
||||
|
||||
factory GasOrderCheckoutData.fromJson(Map<String, Object?> json) => GasOrderCheckoutData(
|
||||
stationName: json['station_name'] as String? ?? '',
|
||||
stationStatus: json['station_status'] as String? ?? '',
|
||||
deliveryScope: json['delivery_scope'] as String? ?? '',
|
||||
deliveryFee: (json['delivery_fee'] as num?)?.toInt() ?? 0,
|
||||
products: List.unmodifiable([
|
||||
for (final item in json['products'] as List? ?? const [])
|
||||
GasOrderProductOption.fromJson(Map<String, Object?>.from(item as Map)),
|
||||
]),
|
||||
addresses: List.unmodifiable([
|
||||
for (final item in json['addresses'] as List? ?? const [])
|
||||
ShippingAddress.fromJson(Map<String, Object?>.from(item as Map)),
|
||||
]),
|
||||
slots: List.unmodifiable([
|
||||
for (final item in json['appointment_slots'] as List? ?? const [])
|
||||
GasOrderAppointmentSlot.fromJson(Map<String, Object?>.from(item as Map)),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
class GasOrderCreateResult {
|
||||
const GasOrderCreateResult({required this.identity, required this.payableAmount});
|
||||
final String identity;
|
||||
final int payableAmount;
|
||||
|
||||
factory GasOrderCreateResult.fromJson(Map<String, Object?> json) {
|
||||
final identity = json['identity'] as String? ?? '';
|
||||
if (identity.isEmpty) throw const FormatException('订单标识缺失');
|
||||
return GasOrderCreateResult(
|
||||
identity: identity,
|
||||
payableAmount: (json['payable_amount'] as num?)?.toInt() ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
135
apps/user_app/lib/domain/models/gas_order_detail.dart
Normal file
135
apps/user_app/lib/domain/models/gas_order_detail.dart
Normal file
@@ -0,0 +1,135 @@
|
||||
// 功能描述:供气订单履约、支付记录及合同正文适配;版本:1.0.0。
|
||||
import 'shop_order_detail.dart';
|
||||
|
||||
/// 复用成交明细与整数分金额,供气配送费、人员和状态历史单独保存。
|
||||
class GasOrderDetail extends ShopOrderDetail {
|
||||
GasOrderDetail.fromJson(super.data, super.resolve)
|
||||
: deliveryFee = _amount(data['delivery_fee']),
|
||||
depositAmount = _amount(data['deposit_amount'] ?? 0),
|
||||
stationName = data['station_name'] as String? ?? '',
|
||||
staffName = data['delivery_staff_name'] as String? ?? '',
|
||||
contractIdentity = data['contract_identity'] as String? ?? '',
|
||||
contractNo = data['contract_no'] as String? ?? '',
|
||||
timeline = List.unmodifiable(
|
||||
(data['timeline'] as List).map(
|
||||
(row) => GasOrderEvent.fromJson(Map<String, Object?>.from(row as Map)),
|
||||
),
|
||||
),
|
||||
payments = List.unmodifiable(
|
||||
(data['payments'] as List).map(
|
||||
(row) => GasOrderPayment.fromJson(Map<String, Object?>.from(row as Map)),
|
||||
),
|
||||
),
|
||||
super.fromJson();
|
||||
final int deliveryFee, depositAmount;
|
||||
final String stationName, staffName, contractIdentity, contractNo;
|
||||
final List<GasOrderEvent> timeline;
|
||||
final List<GasOrderPayment> payments;
|
||||
static int _amount(Object? value) {
|
||||
if (value is! int || value < 0) throw const FormatException('供气金额异常');
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
class GasOrderEvent {
|
||||
GasOrderEvent.fromJson(Map<String, Object?> data)
|
||||
: name = data['status_name'] as String,
|
||||
time = DateTime.parse(data['occurred_at'] as String);
|
||||
final String name;
|
||||
final DateTime time;
|
||||
}
|
||||
|
||||
class GasOrderPayment {
|
||||
GasOrderPayment.fromJson(Map<String, Object?> data)
|
||||
: amount = GasOrderDetail._amount(data['amount']),
|
||||
channel = data['channel'] as String,
|
||||
time = data['paid_at'] == null ? null : DateTime.parse(data['paid_at'] as String);
|
||||
final int amount;
|
||||
final String channel;
|
||||
final DateTime? time;
|
||||
String get channelName => switch (channel) {
|
||||
'alipay' => '支付宝',
|
||||
'wechat' => '微信',
|
||||
'wallet' => '余额',
|
||||
_ => '其他支付渠道',
|
||||
};
|
||||
}
|
||||
|
||||
/// 合同仅用于阅读,未实现的签署/下载能力不能伪装为成功。
|
||||
class GasContractDetail {
|
||||
GasContractDetail.fromJson(Map<String, Object?> data)
|
||||
: identity = data['identity'] as String,
|
||||
number = data['contract_no'] as String,
|
||||
title = data['title'] as String,
|
||||
terms = data['terms'] as String,
|
||||
status = data['contract_status'] as int,
|
||||
signedAt = _date(data['signed_at']),
|
||||
effectiveAt = _date(data['effective_at']),
|
||||
expiredAt = _date(data['expired_at']),
|
||||
hasAttachment = data['has_attachment'] as bool;
|
||||
final String identity, number, title, terms;
|
||||
final int status;
|
||||
final DateTime? signedAt, effectiveAt, expiredAt;
|
||||
final bool hasAttachment;
|
||||
static DateTime? _date(Object? value) {
|
||||
final date = DateTime.tryParse(value?.toString() ?? '');
|
||||
return date == null || date.year < 1900 ? null : date;
|
||||
}
|
||||
}
|
||||
|
||||
/// 合同列表保留服务端业务状态,不将草稿推断为待用户签署。
|
||||
class GasContractSummary extends GasContractDetail {
|
||||
GasContractSummary.fromJson(super.data)
|
||||
: stationName = data['station_name'] as String? ?? '',
|
||||
super.fromJson();
|
||||
final String stationName;
|
||||
String get statusName => switch (status) {
|
||||
0 => '草稿',
|
||||
11 => '生效中',
|
||||
12 => '已到期',
|
||||
13 => '已终止',
|
||||
_ => '状态待更新',
|
||||
};
|
||||
}
|
||||
|
||||
/// 不可变的合同状态与有效期快照,区别于电子签名/签署证据。
|
||||
class GasContractEvent {
|
||||
GasContractEvent.fromJson(Map<String, Object?> data)
|
||||
: identity = data['identity'] as String,
|
||||
action = data['action'] as String,
|
||||
status = data['contract_status'] as int,
|
||||
occurredAt = DateTime.parse(data['occurred_at'] as String),
|
||||
effectiveAt = GasContractDetail._date(data['effective_at']),
|
||||
expiredAt = GasContractDetail._date(data['expired_at']);
|
||||
final String identity, action;
|
||||
final int status;
|
||||
final DateTime occurredAt;
|
||||
final DateTime? effectiveAt, expiredAt;
|
||||
String get actionName => switch (action) {
|
||||
'activate' => '合同生效',
|
||||
'renew' => '合同续期',
|
||||
'terminate' => '合同终止',
|
||||
'create' => '合同建立',
|
||||
_ => '合同变更',
|
||||
};
|
||||
String get statusName => switch (status) {
|
||||
0 => '草稿',
|
||||
11 => '生效中',
|
||||
12 => '已到期',
|
||||
13 => '已终止',
|
||||
_ => '状态待更新',
|
||||
};
|
||||
}
|
||||
|
||||
/// 合同变更申请是客服受理记录,不表示合同条款已经发生变化。
|
||||
class ContractChangeRequest {
|
||||
ContractChangeRequest.fromJson(Map<String, Object?> data)
|
||||
: identity = data['identity'] as String,
|
||||
number = data['ticket_no'] as String,
|
||||
statusName = data['status_name'] as String,
|
||||
description = data['description'] as String,
|
||||
result = data['result'] as String? ?? '',
|
||||
actions = List<String>.from(data['allowed_actions'] as List);
|
||||
final String identity, number, statusName, description, result;
|
||||
final List<String> actions;
|
||||
}
|
||||
54
apps/user_app/lib/domain/models/message_center.dart
Normal file
54
apps/user_app/lib/domain/models/message_center.dart
Normal file
@@ -0,0 +1,54 @@
|
||||
// 功能描述:解析消息中心真实业务消息及分类计数;版本:1.0.0。
|
||||
class UserMessage {
|
||||
const UserMessage({
|
||||
required this.key,
|
||||
required this.category,
|
||||
required this.title,
|
||||
required this.summary,
|
||||
required this.statusText,
|
||||
required this.target,
|
||||
required this.targetIdentity,
|
||||
required this.occurredAt,
|
||||
required this.read,
|
||||
});
|
||||
|
||||
factory UserMessage.fromJson(Map<String, dynamic> json) => UserMessage(
|
||||
key: json['key']?.toString() ?? '',
|
||||
category: json['category']?.toString() ?? '',
|
||||
title: json['title']?.toString() ?? '',
|
||||
summary: json['summary']?.toString() ?? '',
|
||||
statusText: json['status_text']?.toString() ?? '',
|
||||
target: json['target']?.toString() ?? '',
|
||||
targetIdentity: json['target_identity']?.toString() ?? '',
|
||||
occurredAt:
|
||||
DateTime.tryParse(json['occurred_at']?.toString() ?? '') ??
|
||||
DateTime.fromMillisecondsSinceEpoch(0),
|
||||
read: json['read'] == true,
|
||||
);
|
||||
|
||||
final String key, category, title, summary, statusText, target, targetIdentity;
|
||||
final DateTime occurredAt;
|
||||
final bool read;
|
||||
}
|
||||
|
||||
class MessageCenterData {
|
||||
const MessageCenterData({required this.items, required this.counts});
|
||||
factory MessageCenterData.fromJson(Map<String, dynamic> json) {
|
||||
final rawItems = json['items'] is List ? json['items'] as List<dynamic> : const <dynamic>[];
|
||||
final rawCounts = json['counts'] is Map
|
||||
? Map<String, dynamic>.from(json['counts'] as Map<dynamic, dynamic>)
|
||||
: const <String, dynamic>{};
|
||||
return MessageCenterData(
|
||||
items: List.unmodifiable(
|
||||
rawItems.whereType<Map<dynamic, dynamic>>().map(
|
||||
(item) => UserMessage.fromJson(Map<String, dynamic>.from(item)),
|
||||
),
|
||||
),
|
||||
counts: Map.unmodifiable(
|
||||
rawCounts.map((key, value) => MapEntry(key, value is num ? value.toInt() : 0)),
|
||||
),
|
||||
);
|
||||
}
|
||||
final List<UserMessage> items;
|
||||
final Map<String, int> counts;
|
||||
}
|
||||
93
apps/user_app/lib/domain/models/order_summary.dart
Normal file
93
apps/user_app/lib/domain/models/order_summary.dart
Normal file
@@ -0,0 +1,93 @@
|
||||
// 功能描述:订单中心强类型摘要,状态、金额和动作由服务端提供。
|
||||
// 版本:1.0.0
|
||||
import 'dart:convert';
|
||||
import 'client_models.dart';
|
||||
|
||||
/// 订单项退款输入,只保留公开标识和服务端数量。
|
||||
class OrderItemSummary {
|
||||
const OrderItemSummary(
|
||||
this.identity,
|
||||
this.quantity, {
|
||||
this.name = '',
|
||||
this.specification = '',
|
||||
this.imageUrl = '',
|
||||
});
|
||||
final String identity, name, specification, imageUrl;
|
||||
final int quantity;
|
||||
|
||||
/// 商城使用成交快照、气瓶订单使用类型快照,不以当前商品覆盖历史事实。
|
||||
factory OrderItemSummary.fromJson(
|
||||
Map<Object?, Object?> item, {
|
||||
String Function(String value)? resolveImage,
|
||||
}) {
|
||||
Object? snapshot = item['product_snapshot'];
|
||||
if (snapshot is String) {
|
||||
try {
|
||||
snapshot = jsonDecode(snapshot);
|
||||
} on FormatException {
|
||||
snapshot = null;
|
||||
}
|
||||
}
|
||||
final name = snapshot is Map ? snapshot['name'] : item['product_type_name'];
|
||||
final image = snapshot is Map ? snapshot['image_url'] : null;
|
||||
Object? params = item['product_params'];
|
||||
if (params is String) {
|
||||
try {
|
||||
params = jsonDecode(params);
|
||||
} on FormatException {
|
||||
params = null;
|
||||
}
|
||||
}
|
||||
return OrderItemSummary(
|
||||
item['identity'] as String? ?? '',
|
||||
(item['quantity'] as num?)?.toInt() ?? 1,
|
||||
name: name is String ? name : '',
|
||||
specification: params is Map && params['weight'] is String ? params['weight'] as String : '',
|
||||
imageUrl: image is String && image.isNotEmpty ? (resolveImage?.call(image) ?? image) : '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 兼容旧订单记录的领域适配模型;页面不再直接读取原始 Map。
|
||||
class OrderSummary {
|
||||
const OrderSummary({
|
||||
required this.record,
|
||||
required this.amount,
|
||||
required this.statusName,
|
||||
required this.actions,
|
||||
required this.items,
|
||||
required this.orderNo,
|
||||
required this.stationName,
|
||||
this.createdAt,
|
||||
});
|
||||
final ClientRecord record;
|
||||
final int? amount;
|
||||
final String statusName;
|
||||
final Set<String> actions;
|
||||
final List<OrderItemSummary> items;
|
||||
final String orderNo, stationName;
|
||||
final DateTime? createdAt;
|
||||
|
||||
/// 解析服务端状态和权限;旧响应缺少动作时不自行推导权限。
|
||||
factory OrderSummary.fromRecord(
|
||||
ClientRecord record, {
|
||||
String Function(String value)? resolveImage,
|
||||
}) {
|
||||
final raw = record.raw;
|
||||
final rawOrderNo = raw['order_no']?.toString() ?? record.title;
|
||||
return OrderSummary(
|
||||
record: record,
|
||||
amount: (raw['payable_amount'] as num?)?.toInt(),
|
||||
statusName: raw['status_name'] as String? ?? '状态待更新',
|
||||
orderNo: rawOrderNo.startsWith('订单号:') ? rawOrderNo.substring(4) : rawOrderNo,
|
||||
stationName: raw['station_name'] as String? ?? '',
|
||||
createdAt: DateTime.tryParse(raw['created_at']?.toString() ?? ''),
|
||||
actions: Set.unmodifiable((raw['allowed_actions'] as List? ?? []).whereType<String>()),
|
||||
items: List.unmodifiable(
|
||||
(raw['items'] as List? ?? []).whereType<Map<Object?, Object?>>().map(
|
||||
(item) => OrderItemSummary.fromJson(item, resolveImage: resolveImage),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
37
apps/user_app/lib/domain/models/primary_models.dart
Normal file
37
apps/user_app/lib/domain/models/primary_models.dart
Normal file
@@ -0,0 +1,37 @@
|
||||
// 功能描述:一级页面使用的不可变领域数据,金额统一为整数分。
|
||||
// 版本:1.0.0
|
||||
|
||||
/// 已发布内容;正文和版本由内容后台维护。
|
||||
class PublishedContent {
|
||||
const PublishedContent({
|
||||
required this.identity,
|
||||
required this.title,
|
||||
required this.body,
|
||||
required this.type,
|
||||
required this.version,
|
||||
this.publishedAt,
|
||||
});
|
||||
final String identity, title, body, type;
|
||||
final int version;
|
||||
final DateTime? publishedAt;
|
||||
}
|
||||
|
||||
/// 商品公开摘要,只使用公开标识和服务端价格库存。
|
||||
class ProductSummary {
|
||||
const ProductSummary({
|
||||
required this.identity,
|
||||
required this.name,
|
||||
required this.price,
|
||||
required this.stock,
|
||||
this.category = '',
|
||||
this.imageUrl = '',
|
||||
});
|
||||
final String identity, name, category, imageUrl;
|
||||
final int price, stock;
|
||||
}
|
||||
|
||||
/// 有效服务归属;配送点为空表示气站直接服务。
|
||||
class ServiceRelation {
|
||||
const ServiceRelation({required this.gasName, required this.deliveryName});
|
||||
final String gasName, deliveryName;
|
||||
}
|
||||
51
apps/user_app/lib/domain/models/product_detail.dart
Normal file
51
apps/user_app/lib/domain/models/product_detail.dart
Normal file
@@ -0,0 +1,51 @@
|
||||
// 功能描述:商品详情公开字段及严格金额校验,不将缺失参数或图片伪装成真实数据。
|
||||
// 版本:1.0.0。
|
||||
import '../../data/services/api_client.dart';
|
||||
|
||||
class ProductDetail {
|
||||
const ProductDetail({
|
||||
required this.identity,
|
||||
required this.name,
|
||||
required this.price,
|
||||
required this.stock,
|
||||
this.category = '',
|
||||
this.images = const [],
|
||||
this.attributes = const [],
|
||||
});
|
||||
final String identity, name, category;
|
||||
final int price, stock;
|
||||
final List<String> images;
|
||||
final List<({String name, String value})> attributes;
|
||||
factory ProductDetail.fromJson(Map<String, Object?> data, String Function(String) resolveImage) {
|
||||
final price = data['price_amount'],
|
||||
stock = data['stock_quantity'],
|
||||
identity = data['identity'],
|
||||
name = data['name'];
|
||||
if (price is! int ||
|
||||
price < 0 ||
|
||||
stock is! int ||
|
||||
stock < 0 ||
|
||||
identity is! String ||
|
||||
identity.isEmpty ||
|
||||
name is! String) {
|
||||
throw const ApiException(1714, '商品数据异常,请重试');
|
||||
}
|
||||
return ProductDetail(
|
||||
identity: identity,
|
||||
name: name,
|
||||
price: price,
|
||||
stock: stock,
|
||||
category: data['category_name'] as String? ?? '',
|
||||
images: [
|
||||
for (final image in data['images'] as List? ?? [])
|
||||
if (image is Map && image['image_url'] is String)
|
||||
resolveImage(image['image_url'] as String),
|
||||
].where((url) => url.isNotEmpty).toSet().toList(),
|
||||
attributes: [
|
||||
for (final value in data['attributes'] as List? ?? [])
|
||||
if (value is Map && value['name'] is String && value['value'] is String)
|
||||
(name: value['name'] as String, value: value['value'] as String),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
62
apps/user_app/lib/domain/models/product_favorite.dart
Normal file
62
apps/user_app/lib/domain/models/product_favorite.dart
Normal file
@@ -0,0 +1,62 @@
|
||||
// 功能描述:商品收藏状态、并发版本和列表公开数据;版本:1.0.0。
|
||||
import 'primary_models.dart';
|
||||
|
||||
/// 每次操作必须携带读取时的状态版本,不能把切换命令作为可重试写入。
|
||||
class ProductFavoriteState {
|
||||
const ProductFavoriteState({
|
||||
required this.productIdentity,
|
||||
required this.active,
|
||||
required this.revision,
|
||||
});
|
||||
final String productIdentity, revision;
|
||||
final bool active;
|
||||
factory ProductFavoriteState.fromJson(Map<String, Object?> data) {
|
||||
if (data['product_identity'] is! String ||
|
||||
(data['product_identity'] as String).isEmpty ||
|
||||
data['favorite'] is! bool ||
|
||||
data['revision'] is! String) {
|
||||
throw const FormatException('收藏状态数据异常');
|
||||
}
|
||||
return ProductFavoriteState(
|
||||
productIdentity: data['product_identity'] as String,
|
||||
active: data['favorite'] as bool,
|
||||
revision: data['revision'] as String,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 下架收藏仍保留展示信息,available仅用于禁用购买,结算仍由服务端验证。
|
||||
class ProductFavorite {
|
||||
const ProductFavorite({
|
||||
required this.state,
|
||||
required this.product,
|
||||
required this.available,
|
||||
this.specifications = const [],
|
||||
});
|
||||
final ProductFavoriteState state;
|
||||
final ProductSummary product;
|
||||
final bool available;
|
||||
final List<String> specifications;
|
||||
factory ProductFavorite.fromJson(Map<String, Object?> data, String Function(String) resolve) {
|
||||
final state = ProductFavoriteState.fromJson(data);
|
||||
if (data['price_amount'] is! int ||
|
||||
(data['price_amount'] as int) < 0 ||
|
||||
data['stock_quantity'] is! int ||
|
||||
data['available'] is! bool) {
|
||||
throw const FormatException('收藏商品数据异常');
|
||||
}
|
||||
return ProductFavorite(
|
||||
state: state,
|
||||
product: ProductSummary(
|
||||
identity: state.productIdentity,
|
||||
name: data['name'] as String? ?? '',
|
||||
price: data['price_amount'] as int,
|
||||
stock: data['stock_quantity'] as int,
|
||||
category: data['category_name'] as String? ?? '',
|
||||
imageUrl: resolve(data['image_url'] as String? ?? ''),
|
||||
),
|
||||
available: data['available'] as bool,
|
||||
specifications: (data['specifications'] as List? ?? []).whereType<String>().toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
49
apps/user_app/lib/domain/models/product_recommendations.dart
Normal file
49
apps/user_app/lib/domain/models/product_recommendations.dart
Normal file
@@ -0,0 +1,49 @@
|
||||
// 功能描述:推荐商品分页与真实金额校验;版本:1.0.0。
|
||||
import 'primary_models.dart';
|
||||
|
||||
class ProductRecommendations {
|
||||
const ProductRecommendations({required this.items, required this.page, required this.hasMore});
|
||||
final List<ProductSummary> items;
|
||||
final int page;
|
||||
final bool hasMore;
|
||||
factory ProductRecommendations.fromJson(
|
||||
Map<String, Object?> data,
|
||||
String Function(String) resolve,
|
||||
) {
|
||||
if (data['items'] is! List ||
|
||||
data['page'] is! int ||
|
||||
(data['page'] as int) < 1 ||
|
||||
data['has_more'] is! bool) {
|
||||
throw const FormatException('推荐分页数据异常');
|
||||
}
|
||||
final items = <ProductSummary>[];
|
||||
for (final row in data['items'] as List) {
|
||||
if (row is! Map ||
|
||||
row['identity'] is! String ||
|
||||
(row['identity'] as String).isEmpty ||
|
||||
row['name'] is! String ||
|
||||
row['price_amount'] is! int ||
|
||||
(row['price_amount'] as int) < 0 ||
|
||||
row['stock_quantity'] is! int ||
|
||||
(row['stock_quantity'] as int) < 0) {
|
||||
throw const FormatException('推荐商品数据异常');
|
||||
}
|
||||
items.add(
|
||||
ProductSummary(
|
||||
identity: row['identity'] as String,
|
||||
name: row['name'] as String,
|
||||
price: row['price_amount'] as int,
|
||||
stock: row['stock_quantity'] as int,
|
||||
category: row['category_name'] as String? ?? '',
|
||||
imageUrl: resolve(row['image_url'] as String? ?? ''),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (items.isEmpty && data['has_more'] == true) throw const FormatException('推荐分页为空');
|
||||
return ProductRecommendations(
|
||||
items: items,
|
||||
page: data['page'] as int,
|
||||
hasMore: data['has_more'] as bool,
|
||||
);
|
||||
}
|
||||
}
|
||||
132
apps/user_app/lib/domain/models/recharge.dart
Normal file
132
apps/user_app/lib/domain/models/recharge.dart
Normal file
@@ -0,0 +1,132 @@
|
||||
// 功能描述:充值配置、创建结果和入账事实的严格类型;版本:1.0.0。
|
||||
import 'client_models.dart';
|
||||
|
||||
int rechargeCents(Object? value) {
|
||||
if (value is! int || value < 0) throw const FormatException('充值金额格式异常');
|
||||
return value;
|
||||
}
|
||||
|
||||
/// 十进制文本直接转换为分,禁止浮点舍入、指数或负数。
|
||||
int? parseRechargeAmount(String value) {
|
||||
final text = value.trim();
|
||||
if (!RegExp(r'^\d{1,9}(\.\d{1,2})?$').hasMatch(text)) return null;
|
||||
final parts = text.split('.');
|
||||
return int.parse(parts[0]) * 100 + (parts.length == 1 ? 0 : int.parse(parts[1].padRight(2, '0')));
|
||||
}
|
||||
|
||||
class RechargeOptions {
|
||||
const RechargeOptions({
|
||||
required this.min,
|
||||
required this.max,
|
||||
required this.channels,
|
||||
this.agreement,
|
||||
});
|
||||
final int min, max;
|
||||
final List<RechargeChannel> channels;
|
||||
final RechargeAgreement? agreement;
|
||||
factory RechargeOptions.fromJson(Map<String, Object?> json) {
|
||||
final min = rechargeCents(json['min_amount']), max = rechargeCents(json['max_amount']);
|
||||
if (min < 1 || max < min || json['channels'] is! List) throw const FormatException('充值配置异常');
|
||||
return RechargeOptions(
|
||||
min: min,
|
||||
max: max,
|
||||
channels: [
|
||||
for (final item in json['channels'] as List)
|
||||
RechargeChannel.fromJson(Map<String, Object?>.from(item as Map)),
|
||||
],
|
||||
agreement: json['agreement'] == null
|
||||
? null
|
||||
: RechargeAgreement.fromJson(Map<String, Object?>.from(json['agreement'] as Map)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RechargeChannel {
|
||||
const RechargeChannel(this.name, {required this.app, required this.wap});
|
||||
final String name;
|
||||
final bool app, wap;
|
||||
String get title => name == 'wechat' ? '微信支付' : '支付宝';
|
||||
factory RechargeChannel.fromJson(Map<String, Object?> json) {
|
||||
if (!['wechat', 'alipay'].contains(json['channel']) ||
|
||||
json['app'] is! bool ||
|
||||
json['wap'] is! bool) {
|
||||
throw const FormatException('支付方式异常');
|
||||
}
|
||||
return RechargeChannel(
|
||||
json['channel'] as String,
|
||||
app: json['app'] as bool,
|
||||
wap: json['wap'] as bool,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RechargeAgreement {
|
||||
const RechargeAgreement(this.identity, this.version, this.title, this.body);
|
||||
final String identity, title, body;
|
||||
final int version;
|
||||
factory RechargeAgreement.fromJson(Map<String, Object?> json) {
|
||||
if (json['identity'] is! String ||
|
||||
json['body'] is! String ||
|
||||
json['version'] is! int ||
|
||||
(json['identity'] as String).trim().isEmpty ||
|
||||
(json['body'] as String).trim().isEmpty ||
|
||||
(json['version'] as int) < 1) {
|
||||
throw const FormatException('充值协议不完整');
|
||||
}
|
||||
return RechargeAgreement(
|
||||
json['identity'] as String,
|
||||
json['version'] as int,
|
||||
json['title'] as String? ?? '充值协议',
|
||||
json['body'] as String,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 只有充值状态23表示钱包已经入账;渠道关闭或SDK返回都不能替代这一事实。
|
||||
class RechargeRecord {
|
||||
const RechargeRecord({
|
||||
required this.identity,
|
||||
required this.number,
|
||||
required this.amount,
|
||||
required this.status,
|
||||
required this.channel,
|
||||
required this.createdAt,
|
||||
this.paymentStatus,
|
||||
});
|
||||
final String identity, number, channel;
|
||||
final int amount, status;
|
||||
final int? paymentStatus;
|
||||
final DateTime createdAt;
|
||||
bool get credited => status == 23;
|
||||
String get statusText => credited
|
||||
? '已到账'
|
||||
: paymentStatus == 30
|
||||
? '支付已关闭,未入账'
|
||||
: status == 10
|
||||
? '待确认到账'
|
||||
: '状态待确认';
|
||||
String get amountText => moneyText(amount);
|
||||
factory RechargeRecord.fromJson(Map<String, Object?> json) {
|
||||
final date = DateTime.tryParse(json['created_at'] as String? ?? '');
|
||||
if ((json['identity'] as String? ?? '').isEmpty ||
|
||||
json['recharge_status'] is! int ||
|
||||
date == null) {
|
||||
throw const FormatException('充值记录不完整');
|
||||
}
|
||||
return RechargeRecord(
|
||||
identity: json['identity'] as String,
|
||||
number: json['recharge_no'] as String? ?? '',
|
||||
amount: rechargeCents(json['amount']),
|
||||
status: json['recharge_status'] as int,
|
||||
channel: json['channel'] as String? ?? '',
|
||||
createdAt: date.toLocal(),
|
||||
paymentStatus: json['payment_status'] as int?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RechargeRecordPage {
|
||||
const RechargeRecordPage(this.items, this.nextCursor);
|
||||
final List<RechargeRecord> items;
|
||||
final String nextCursor;
|
||||
}
|
||||
49
apps/user_app/lib/domain/models/service_ticket.dart
Normal file
49
apps/user_app/lib/domain/models/service_ticket.dart
Normal file
@@ -0,0 +1,49 @@
|
||||
// 功能描述:工单展示适配,状态动作只取服务端许可,不猜测派单和处理时间。
|
||||
// 版本:1.0.0。
|
||||
import 'client_models.dart';
|
||||
|
||||
class ServiceTicket {
|
||||
const ServiceTicket(this.record);
|
||||
final ClientRecord record;
|
||||
String get identity => record.identity;
|
||||
String get number => text('ticket_no', record.title);
|
||||
String get state => text('status_name', '状态待核实');
|
||||
String get description => text('description', '暂无描述');
|
||||
String get result => text('result');
|
||||
String get address => text('address', '未填写地址');
|
||||
List<Map<String, Object?>> get photos => (record.raw['photos'] as List? ?? [])
|
||||
.whereType<Map<Object?, Object?>>()
|
||||
.map((value) => Map<String, Object?>.from(value))
|
||||
.where((value) => value['identity'] is String)
|
||||
.toList();
|
||||
String get fault =>
|
||||
const {'valve': '阀门故障', 'alarm': '报警器故障', 'leak': '燃气泄漏', 'other': '其他问题'}[text(
|
||||
'fault_type',
|
||||
)] ??
|
||||
category;
|
||||
String get category =>
|
||||
const {
|
||||
'repair': '维修',
|
||||
'installation': '安装',
|
||||
'inspection': '安检',
|
||||
'reinspection': '复检',
|
||||
'customer_service': '客户服务',
|
||||
}[text('category')] ??
|
||||
'其他服务';
|
||||
List<String> get actions =>
|
||||
(record.raw['allowed_actions'] as List?)?.whereType<String>().toList() ?? [];
|
||||
|
||||
/// 返回可展示文字,空值采用调用方指定的占位。
|
||||
String text(String key, [String fallback = '']) {
|
||||
final value = record.raw[key];
|
||||
return value is String && value.trim().isNotEmpty ? value : fallback;
|
||||
}
|
||||
|
||||
/// 使用接口的真实时间转换到本地,缺失时不生成推测进度。
|
||||
String time(String key) {
|
||||
final value = DateTime.tryParse(text(key))?.toLocal();
|
||||
if (value == null) return '未记录';
|
||||
String pad(int n) => n.toString().padLeft(2, '0');
|
||||
return '${value.year}-${pad(value.month)}-${pad(value.day)} ${pad(value.hour)}:${pad(value.minute)}';
|
||||
}
|
||||
}
|
||||
40
apps/user_app/lib/domain/models/shipping_address.dart
Normal file
40
apps/user_app/lib/domain/models/shipping_address.dart
Normal file
@@ -0,0 +1,40 @@
|
||||
// 功能描述:用户收货地址模型,列表脱敏展示、编辑使用本人完整数据。
|
||||
// 版本:1.0.0。
|
||||
class ShippingAddress {
|
||||
const ShippingAddress({
|
||||
this.identity = '',
|
||||
required this.address,
|
||||
required this.contactName,
|
||||
required this.contactPhone,
|
||||
this.longitude = '',
|
||||
this.latitude = '',
|
||||
this.isDefault = false,
|
||||
});
|
||||
final String identity, address, contactName, contactPhone, longitude, latitude;
|
||||
final bool isDefault;
|
||||
|
||||
/// 从兼容地址响应读取字段;历史空联系人在编辑时要求补齐。
|
||||
factory ShippingAddress.fromJson(Map<String, Object?> json) => ShippingAddress(
|
||||
identity: json['identity'] as String? ?? '',
|
||||
address: json['address'] as String? ?? '',
|
||||
contactName: json['contact_name'] as String? ?? '',
|
||||
contactPhone: json['contact_phone'] as String? ?? '',
|
||||
longitude: json['longitude'] as String? ?? '',
|
||||
latitude: json['latitude'] as String? ?? '',
|
||||
isDefault: json['is_default'] == true,
|
||||
);
|
||||
|
||||
String get maskedPhone => contactPhone.length == 11
|
||||
? '${contactPhone.substring(0, 3)}****${contactPhone.substring(7)}'
|
||||
: contactPhone;
|
||||
|
||||
/// 保存地址的完整字段;不接收内部账户编号。
|
||||
Map<String, Object?> toJson() => {
|
||||
'address': address,
|
||||
'contact_name': contactName,
|
||||
'contact_phone': contactPhone,
|
||||
'longitude': longitude,
|
||||
'latitude': latitude,
|
||||
'is_default': isDefault,
|
||||
};
|
||||
}
|
||||
91
apps/user_app/lib/domain/models/shop_order_detail.dart
Normal file
91
apps/user_app/lib/domain/models/shop_order_detail.dart
Normal file
@@ -0,0 +1,91 @@
|
||||
// 功能描述:商城订单成交快照、费用和履约时间;版本:1.0.0。
|
||||
import 'client_models.dart';
|
||||
import 'order_summary.dart';
|
||||
|
||||
/// 历史商品名称与单价不随商品目录改动;图片仅接受服务端快照。
|
||||
class ShopOrderLine {
|
||||
const ShopOrderLine({
|
||||
required this.identity,
|
||||
required this.productIdentity,
|
||||
required this.name,
|
||||
required this.quantity,
|
||||
required this.unitAmount,
|
||||
required this.imageUrl,
|
||||
});
|
||||
final String identity, productIdentity, name, imageUrl;
|
||||
final int quantity, unitAmount;
|
||||
int get amount => quantity * unitAmount;
|
||||
}
|
||||
|
||||
/// 金额一律为整数分;缺失时间保留未知,不依据当前时间补写历史。
|
||||
class ShopOrderDetail {
|
||||
ShopOrderDetail.fromJson(Map<String, Object?> data, String Function(String) resolve)
|
||||
: identity = _text(data, 'identity'),
|
||||
orderNo = _text(data, 'order_no'),
|
||||
statusName = _text(data, 'status_name'),
|
||||
address = _text(data, 'address'),
|
||||
contactName = _text(data, 'contact_name'),
|
||||
contactPhone = _text(data, 'contact_phone'),
|
||||
remark = _text(data, 'remark'),
|
||||
logisticsNo = _text(data, 'logistics_no'),
|
||||
logisticsCompany = _text(data, 'logistics_company'),
|
||||
productAmount = _amount(data, 'product_amount'),
|
||||
discountAmount = _amount(data, 'discount_amount'),
|
||||
payableAmount = _amount(data, 'payable_amount'),
|
||||
createdAt = _date(data['created_at']),
|
||||
paidAt = _date(data['paid_at']),
|
||||
shippedAt = _date(data['shipped_at']),
|
||||
receivedAt = _date(data['received_at']),
|
||||
actions = Set.unmodifiable((data['allowed_actions'] as List).whereType<String>()),
|
||||
items = List.unmodifiable(
|
||||
(data['items'] as List).map((raw) {
|
||||
final row = Map<String, Object?>.from(raw as Map);
|
||||
final qty = _amount(row, 'quantity');
|
||||
if (qty < 1) throw const FormatException('订单商品数量异常');
|
||||
return ShopOrderLine(
|
||||
identity: _text(row, 'identity'),
|
||||
productIdentity: _text(row, 'product_identity'),
|
||||
name: _text(row, 'name'),
|
||||
quantity: qty,
|
||||
unitAmount: _amount(row, 'sale_amount'),
|
||||
imageUrl: resolve(_text(row, 'image_url')),
|
||||
);
|
||||
}),
|
||||
) {
|
||||
if (identity.isEmpty || orderNo.isEmpty || statusName.isEmpty) {
|
||||
throw const FormatException('订单资料不完整');
|
||||
}
|
||||
}
|
||||
final String identity,
|
||||
orderNo,
|
||||
statusName,
|
||||
address,
|
||||
contactName,
|
||||
contactPhone,
|
||||
remark,
|
||||
logisticsNo,
|
||||
logisticsCompany;
|
||||
final int productAmount, discountAmount, payableAmount;
|
||||
final DateTime? createdAt, paidAt, shippedAt, receivedAt;
|
||||
final Set<String> actions;
|
||||
final List<ShopOrderLine> items;
|
||||
OrderSummary get summary => OrderSummary(
|
||||
record: ClientRecord(identity: identity, title: orderNo, subtitle: '', raw: const {}),
|
||||
amount: payableAmount,
|
||||
statusName: statusName,
|
||||
orderNo: orderNo,
|
||||
stationName: '',
|
||||
actions: actions,
|
||||
createdAt: createdAt,
|
||||
items: [for (final i in items) OrderItemSummary(i.identity, i.quantity, name: i.name)],
|
||||
);
|
||||
static String _text(Map<String, Object?> data, String key) => data[key] as String? ?? '';
|
||||
static int _amount(Map<String, Object?> data, String key) {
|
||||
final value = data[key];
|
||||
if (value is! int || value < 0) throw FormatException('订单金额或数量异常:$key');
|
||||
return value;
|
||||
}
|
||||
|
||||
static DateTime? _date(Object? value) =>
|
||||
value == null ? null : DateTime.tryParse(value.toString());
|
||||
}
|
||||
78
apps/user_app/lib/domain/models/usage_statistics.dart
Normal file
78
apps/user_app/lib/domain/models/usage_statistics.dart
Normal file
@@ -0,0 +1,78 @@
|
||||
// 功能描述:定义用气统计的服务端事实模型;版本:1.0.0。
|
||||
|
||||
class UsagePoint {
|
||||
const UsagePoint({required this.date, required this.usage});
|
||||
final DateTime date;
|
||||
final double usage;
|
||||
|
||||
factory UsagePoint.fromJson(Map<String, Object?> json) => UsagePoint(
|
||||
date: DateTime.parse(json['date'] as String),
|
||||
usage: (json['usage'] as num).toDouble(),
|
||||
);
|
||||
}
|
||||
|
||||
class UsageComposition {
|
||||
const UsageComposition({required this.breakfast, required this.lunch, required this.dinner});
|
||||
final double breakfast, lunch, dinner;
|
||||
|
||||
factory UsageComposition.fromJson(Map<String, Object?> json) => UsageComposition(
|
||||
breakfast: (json['breakfast'] as num? ?? 0).toDouble(),
|
||||
lunch: (json['lunch'] as num? ?? 0).toDouble(),
|
||||
dinner: (json['dinner'] as num? ?? 0).toDouble(),
|
||||
);
|
||||
}
|
||||
|
||||
class UsageStatistics {
|
||||
const UsageStatistics({
|
||||
required this.available,
|
||||
required this.deviceIdentity,
|
||||
required this.deviceName,
|
||||
required this.deviceCode,
|
||||
required this.period,
|
||||
required this.periodStart,
|
||||
required this.periodEnd,
|
||||
required this.unit,
|
||||
required this.points,
|
||||
required this.totalUsage,
|
||||
required this.averageUsage,
|
||||
required this.composition,
|
||||
required this.source,
|
||||
required this.calcVersion,
|
||||
required this.updatedAt,
|
||||
required this.unavailableReason,
|
||||
});
|
||||
|
||||
final bool available;
|
||||
final String deviceIdentity, deviceName, deviceCode, period, unit;
|
||||
final DateTime periodStart, periodEnd;
|
||||
final List<UsagePoint> points;
|
||||
final double totalUsage, averageUsage;
|
||||
final UsageComposition composition;
|
||||
final String source, calcVersion, unavailableReason;
|
||||
final DateTime? updatedAt;
|
||||
|
||||
factory UsageStatistics.fromJson(Map<String, Object?> json) {
|
||||
final device = json['device'] as Map<String, Object?>? ?? const {};
|
||||
final composition = json['composition'] as Map<String, Object?>? ?? const {};
|
||||
return UsageStatistics(
|
||||
available: json['available'] == true,
|
||||
deviceIdentity: device['identity'] as String? ?? '',
|
||||
deviceName: device['name'] as String? ?? '',
|
||||
deviceCode: device['code'] as String? ?? '',
|
||||
period: json['period'] as String? ?? 'month',
|
||||
periodStart: DateTime.parse(json['period_start'] as String),
|
||||
periodEnd: DateTime.parse(json['period_end'] as String),
|
||||
unit: json['unit'] as String? ?? 'kg',
|
||||
points: (json['points'] as List<Object?>? ?? const [])
|
||||
.map((item) => UsagePoint.fromJson(item as Map<String, Object?>))
|
||||
.toList(),
|
||||
totalUsage: (json['total_usage'] as num? ?? 0).toDouble(),
|
||||
averageUsage: (json['average_usage'] as num? ?? 0).toDouble(),
|
||||
composition: UsageComposition.fromJson(composition),
|
||||
source: json['source'] as String? ?? '',
|
||||
calcVersion: json['calc_version'] as String? ?? '',
|
||||
updatedAt: DateTime.tryParse(json['updated_at']?.toString() ?? '')?.toLocal(),
|
||||
unavailableReason: json['unavailable_reason'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
93
apps/user_app/lib/domain/models/wallet_account.dart
Normal file
93
apps/user_app/lib/domain/models/wallet_account.dart
Normal file
@@ -0,0 +1,93 @@
|
||||
// 功能描述:银行卡与提现申请的严格领域模型;版本:1.0.0。
|
||||
|
||||
/// 本人已绑定银行卡只保留服务端返回的脱敏字段。
|
||||
class WalletBank {
|
||||
const WalletBank({
|
||||
required this.identity,
|
||||
required this.maskedNumber,
|
||||
required this.bankName,
|
||||
required this.owner,
|
||||
required this.type,
|
||||
required this.isDefault,
|
||||
});
|
||||
|
||||
final String identity, maskedNumber, bankName, owner, type;
|
||||
final bool isDefault;
|
||||
|
||||
factory WalletBank.fromJson(Map<String, Object?> json) {
|
||||
final identity = json['identity'], number = json['card_no_masked'];
|
||||
if (identity is! String || identity.isEmpty || number is! String || number.isEmpty) {
|
||||
throw const FormatException('银行卡数据不完整');
|
||||
}
|
||||
return WalletBank(
|
||||
identity: identity,
|
||||
maskedNumber: number,
|
||||
bankName: json['bank_name'] as String? ?? '银行卡',
|
||||
owner: json['card_owner'] as String? ?? '',
|
||||
type: json['bank_type'] as String? ?? '',
|
||||
isDefault: json['is_default'] == true,
|
||||
);
|
||||
}
|
||||
|
||||
String get typeName => switch (type) {
|
||||
'debit' || 'saving' => '储蓄卡',
|
||||
'credit' => '信用卡',
|
||||
_ => type,
|
||||
};
|
||||
}
|
||||
|
||||
/// 提现状态必须来自服务端,申请成功只表示进入审核流程。
|
||||
class WalletWithdrawal {
|
||||
const WalletWithdrawal({
|
||||
required this.identity,
|
||||
required this.number,
|
||||
required this.amount,
|
||||
required this.fee,
|
||||
required this.status,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
final String identity, number;
|
||||
final int amount, fee, status;
|
||||
final DateTime createdAt;
|
||||
|
||||
factory WalletWithdrawal.fromJson(Map<String, Object?> json) {
|
||||
final identity = json['identity'], amount = json['amount'], fee = json['fee'];
|
||||
final createdAt = DateTime.tryParse(json['created_at'] as String? ?? '');
|
||||
if (identity is! String ||
|
||||
identity.isEmpty ||
|
||||
amount is! int ||
|
||||
amount <= 0 ||
|
||||
fee is! int ||
|
||||
fee < 0 ||
|
||||
json['apply_status'] is! int ||
|
||||
createdAt == null) {
|
||||
throw const FormatException('提现记录数据不完整');
|
||||
}
|
||||
return WalletWithdrawal(
|
||||
identity: identity,
|
||||
number: json['cash_no'] as String? ?? '',
|
||||
amount: amount,
|
||||
fee: fee,
|
||||
status: json['apply_status'] as int,
|
||||
createdAt: createdAt.toLocal(),
|
||||
);
|
||||
}
|
||||
|
||||
String get statusName => switch (status) {
|
||||
10 => '待审核',
|
||||
18 => '处理中',
|
||||
23 => '已到账',
|
||||
30 => '已拒绝',
|
||||
31 => '已退回',
|
||||
_ => '状态待确认',
|
||||
};
|
||||
}
|
||||
|
||||
/// 金额文本直接转整数分,禁止浮点舍入和指数格式。
|
||||
int? parseWithdrawalAmount(String value) {
|
||||
final text = value.trim();
|
||||
if (!RegExp(r'^\d{1,9}(\.\d{1,2})?$').hasMatch(text)) return null;
|
||||
final parts = text.split('.');
|
||||
return int.parse(parts[0]) * 100 + (parts.length == 1 ? 0 : int.parse(parts[1].padRight(2, '0')));
|
||||
}
|
||||
67
apps/user_app/lib/domain/models/wallet_bill.dart
Normal file
67
apps/user_app/lib/domain/models/wallet_bill.dart
Normal file
@@ -0,0 +1,67 @@
|
||||
// 功能描述:钱包账单的严格金额与收支语义模型;版本:1.0.0。
|
||||
import 'client_models.dart';
|
||||
|
||||
/// 账单是已记账事实,不把未完成订单或提现申请伪装成成功流水。
|
||||
class WalletBill {
|
||||
const WalletBill({
|
||||
required this.identity,
|
||||
required this.number,
|
||||
required this.direction,
|
||||
required this.tradeType,
|
||||
required this.amount,
|
||||
required this.fee,
|
||||
required this.balanceAfter,
|
||||
required this.createdAt,
|
||||
required this.channel,
|
||||
});
|
||||
final String identity, number, direction, tradeType, channel;
|
||||
final int amount, fee, balanceAfter;
|
||||
final DateTime createdAt;
|
||||
bool get income => direction == 'income';
|
||||
String get signedAmount => '${income ? '+' : '-'}${moneyText(amount)}';
|
||||
String get title => switch (tradeType) {
|
||||
'recharge' => '余额充值',
|
||||
'refund' => '退款入账',
|
||||
'ec_order' => '商城订单',
|
||||
'gas_order' => '气瓶订单',
|
||||
'withdrawal_reserve' => '提现资金冻结',
|
||||
'withdrawal_release' => '提现资金退回',
|
||||
'withdrawal_complete' => '提现完成',
|
||||
_ => '其他账单',
|
||||
};
|
||||
factory WalletBill.fromJson(Map<String, Object?> json) {
|
||||
int amount(String key, {bool positive = false}) {
|
||||
final value = json[key];
|
||||
if (value is! int || value < (positive ? 1 : 0)) {
|
||||
throw const FormatException('账单金额格式异常');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
final direction = json['direction'];
|
||||
final date = DateTime.tryParse(json['created_at'] as String? ?? '');
|
||||
if (!['income', 'expense'].contains(direction) ||
|
||||
date == null ||
|
||||
(json['identity'] as String? ?? '').isEmpty) {
|
||||
throw const FormatException('账单信息不完整');
|
||||
}
|
||||
return WalletBill(
|
||||
identity: json['identity'] as String,
|
||||
number: json['record_no'] as String? ?? '',
|
||||
direction: direction as String,
|
||||
tradeType: json['trade_type'] as String? ?? '',
|
||||
amount: amount('amount', positive: true),
|
||||
fee: amount('fee'),
|
||||
balanceAfter: amount('balance_after'),
|
||||
createdAt: date.toLocal(),
|
||||
channel: json['pay_channel'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 分页游标由服务端提供,不依据客户端条数猜测是否还有数据。
|
||||
class WalletBillPage {
|
||||
const WalletBillPage(this.items, this.nextCursor);
|
||||
final List<WalletBill> items;
|
||||
final String nextCursor;
|
||||
}
|
||||
Reference in New Issue
Block a user