1. 结算页面架构设计
电商结算页面是用户购物旅程的最后一环,也是转化率的关键节点。一个优秀的结算实现需要兼顾数据完整性、计算准确性和操作流畅性。在Flutter for OpenHarmony项目中,我们采用分层架构设计:
数据层:包含Address、Coupon和Order三个核心模型类,负责数据的存储和基础逻辑。这一层的特点是纯Dart实现,不包含任何UI代码,确保业务逻辑的可测试性。
业务逻辑层:由CheckoutFlow类主导,处理地址选择、优惠券应用、积分抵扣等业务规则。这一层需要特别注意金额计算的精确性,所有涉及货币的操作都应使用定点数计算而非浮点数。
表现层:由多个Widget组成,包括地址选择列表、优惠券卡片、价格明细等。这一层的关键是状态管理,我们采用经典的setState方式实现局部刷新,避免不必要的重建。
提示:在真实项目中,建议将价格计算逻辑放在服务端进行二次验证,防止客户端被篡改导致资损。
2. 核心数据模型实现
2.1 地址管理模型
Address类不只是简单的数据容器,它包含了完整的地址业务逻辑:
dart复制class Address {
// ...字段定义见原始代码...
// 增强版地址验证
bool get isValid {
return name.isNotEmpty &&
phone.length == 11 &&
province.isNotEmpty &&
city.isNotEmpty &&
district.isNotEmpty &&
detail.isNotEmpty;
}
// 地址对比方法
bool isSameAs(Address other) {
return province == other.province &&
city == other.city &&
district == other.district &&
detail == other.detail;
}
// 转换为Map用于持久化
Map<String, dynamic> toMap() {
return {
'id': id,
'name': name,
'phone': phone,
'province': province,
'city': city,
'district': district,
'detail': detail,
'isDefault': isDefault,
'tag': tag,
};
}
// 从Map恢复对象
factory Address.fromMap(Map<String, dynamic> map) {
return Address(
id: map['id'],
name: map['name'],
phone: map['phone'],
province: map['province'],
city: map['city'],
district: map['district'],
detail: map['detail'],
isDefault: map['isDefault'] ?? false,
tag: map['tag'],
);
}
}
关键增强点:
- 增加了isValid验证方法,确保地址数据完整
- 添加isSameAs方法用于地址去重比较
- 实现toMap/fromMap支持本地持久化
- 保持不可变性(immutable)避免意外修改
2.2 优惠券模型进阶实现
Coupon类需要处理更复杂的业务规则:
dart复制class Coupon {
// ...基础字段见原始代码...
// 检查优惠券是否适用于当前订单
bool isApplicable(double orderAmount) {
if (!isValid) return false;
return orderAmount >= minOrderAmount;
}
// 计算优惠金额
double calculateDiscount(double orderAmount) {
if (!isApplicable(orderAmount)) return 0;
switch (type) {
case CouponType.percentage:
return orderAmount * value / 100;
case CouponType.fixed:
return value;
case CouponType.freeShipping:
return 10; // 假设固定运费10元
}
}
// 有效期剩余天数
int get remainingDays {
final now = DateTime.now();
return expiresAt.difference(now).inDays;
}
}
业务规则增强:
- 增加适用性检查,确保订单满足最低金额要求
- 独立计算折扣金额的方法,便于单元测试
- 添加剩余天数显示,提升用户体验
- 严格处理边界情况(如过期券、已使用券)
3. 结算流程深度实现
3.1 价格计算引擎
结算核心是准确计算最终价格,我们需要处理多种优惠叠加的场景:
dart复制class CheckoutCalculator {
final double subtotal;
final Address? address;
final Coupon? coupon;
final int usedPoints;
// 运费计算策略
static const double shippingFee = 10.0;
static const double freeShippingThreshold = 99.0;
CheckoutCalculator({
required this.subtotal,
this.address,
this.coupon,
this.usedPoints = 0,
});
// 计算运费
double get shippingCost {
if (coupon?.type == CouponType.freeShipping) return 0;
if (subtotal >= freeShippingThreshold) return 0;
return shippingFee;
}
// 计算优惠券折扣
double get couponDiscount {
if (coupon == null) return 0;
return coupon!.calculateDiscount(subtotal);
}
// 计算积分抵扣(1积分=0.01元)
double get pointsDiscount {
return usedPoints * 0.01;
}
// 计算最终总价
double calculateTotal() {
double total = subtotal;
// 应用优惠券
total -= couponDiscount;
// 应用积分
total -= pointsDiscount;
// 添加运费
total += shippingCost;
// 确保不为负
return total.clamp(0, double.infinity);
}
}
计算策略说明:
- 运费策略:满99包邮,否则固定10元运费
- 优惠券优先应用,积分抵扣在后
- 严格使用clamp防止负价格
- 每个计算步骤独立,便于测试和维护
3.2 状态管理方案
结算页面需要管理多个交互状态:
dart复制class CheckoutState extends ChangeNotifier {
Address? _selectedAddress;
Coupon? _selectedCoupon;
int _usedPoints = 0;
final List<Address> addresses;
final List<Coupon> coupons;
CheckoutState({
required this.addresses,
required this.coupons,
});
// 地址选择
Address? get selectedAddress => _selectedAddress;
set selectedAddress(Address? value) {
_selectedAddress = value;
notifyListeners();
}
// 优惠券选择
Coupon? get selectedCoupon => _selectedCoupon;
set selectedCoupon(Coupon? value) {
_selectedCoupon = value;
notifyListeners();
}
// 积分使用
int get usedPoints => _usedPoints;
set usedPoints(int value) {
_usedPoints = value.clamp(0, maxUsablePoints);
notifyListeners();
}
// 计算最大可用积分(不超过订单金额)
int get maxUsablePoints {
final calculator = CheckoutCalculator(
subtotal: cartSubtotal,
coupon: _selectedCoupon,
);
final maxDiscount = calculator.calculateTotal();
return (maxDiscount * 100).toInt();
}
}
状态管理特点:
- 使用ChangeNotifier实现细粒度更新
- 自动计算最大可用积分,防止超额使用
- 所有setter都触发通知,简化UI更新
- 业务规则集中在状态类中
4. UI实现与交互优化
4.1 地址选择组件增强
地址列表需要支持添加、编辑和默认设置:
dart复制class AddressSelection extends StatelessWidget {
final List<Address> addresses;
final Address? selectedAddress;
final ValueChanged<Address> onSelected;
final VoidCallback onAddNew;
const AddressSelection({
super.key,
required this.addresses,
this.selectedAddress,
required this.onSelected,
required this.onAddNew,
});
@override
Widget build(BuildContext context) {
return Column(
children: [
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: addresses.length,
itemBuilder: (context, index) {
final address = addresses[index];
return AddressTile(
address: address,
isSelected: selectedAddress?.id == address.id,
onSelected: () => onSelected(address),
);
},
),
TextButton(
onPressed: onAddNew,
child: const Text('+ 添加新地址'),
),
],
);
}
}
class AddressTile extends StatelessWidget {
final Address address;
final bool isSelected;
final VoidCallback onSelected;
const AddressTile({
super.key,
required this.address,
required this.isSelected,
required this.onSelected,
});
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.symmetric(vertical: 4),
color: isSelected ? Colors.blue[50] : null,
child: InkWell(
onTap: onSelected,
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
address.tag ?? '收货地址',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
if (address.isDefault)
Padding(
padding: const EdgeInsets.only(left: 8),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 4,
vertical: 2,
),
decoration: BoxDecoration(
color: Colors.orange,
borderRadius: BorderRadius.circular(4),
),
child: const Text(
'默认',
style: TextStyle(
color: Colors.white,
fontSize: 12,
),
),
),
),
],
),
const SizedBox(height: 8),
Text('${address.name} ${address.phone}'),
Text(address.fullAddress),
if (isSelected)
const Padding(
padding: EdgeInsets.only(top: 8),
child: Icon(Icons.check, color: Colors.green),
),
],
),
),
),
);
}
}
UI优化点:
- 分离AddressTile为独立组件
- 添加视觉反馈(选中状态、默认标记)
- 支持添加新地址操作
- 优化列表性能(shrinkWrap+NeverScrollableScrollPhysics)
4.2 优惠券选择交互优化
优惠券列表需要展示更多有效信息:
dart复制class CouponSelection extends StatelessWidget {
final List<Coupon> coupons;
final Coupon? selectedCoupon;
final ValueChanged<Coupon?> onSelected;
const CouponSelection({
super.key,
required this.coupons,
this.selectedCoupon,
required this.onSelected,
});
@override
Widget build(BuildContext context) {
return Column(
children: [
...coupons.map((coupon) => CouponTile(
coupon: coupon,
isSelected: selectedCoupon?.id == coupon.id,
onSelected: (isSelected) {
onSelected(isSelected ? coupon : null);
},
)),
if (coupons.isEmpty)
const Padding(
padding: EdgeInsets.all(16),
child: Text('暂无可用优惠券'),
),
],
);
}
}
class CouponTile extends StatelessWidget {
final Coupon coupon;
final bool isSelected;
final ValueChanged<bool> onSelected;
const CouponTile({
super.key,
required this.coupon,
required this.isSelected,
required this.onSelected,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Card(
color: isSelected ? theme.primaryColor.withOpacity(0.1) : null,
child: InkWell(
onTap: () => onSelected(!isSelected),
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: theme.primaryColor,
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Text(
coupon.displayValue,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
coupon.description ?? '优惠券',
style: const TextStyle(fontWeight: FontWeight.bold),
),
Text('有效期至 ${DateFormat('yyyy-MM-dd').format(coupon.expiresAt)}'),
Text('满${coupon.minOrderAmount.toInt()}元可用'),
],
),
),
Checkbox(
value: isSelected,
onChanged: (value) => onSelected(value ?? false),
),
],
),
),
),
);
}
}
交互改进:
- 更丰富的优惠券视觉呈现
- 显示详细的有效期信息
- 空状态处理
- 支持点击卡片或复选框选择
5. 订单创建与支付集成
5.1 订单创建流程
dart复制class OrderService {
final HttpClient httpClient;
OrderService(this.httpClient);
Future<Order> createOrder({
required List<CartItem> items,
required Address address,
Coupon? coupon,
int usedPoints = 0,
}) async {
final response = await httpClient.post(
'/api/orders',
body: {
'items': items.map((item) => item.toMap()).toList(),
'address': address.toMap(),
'couponId': coupon?.id,
'usedPoints': usedPoints,
},
);
if (response.statusCode != 200) {
throw Exception('订单创建失败');
}
return Order.fromMap(response.data);
}
}
class CheckoutPage extends StatefulWidget {
final List<CartItem> cartItems;
const CheckoutPage({super.key, required this.cartItems});
@override
State<CheckoutPage> createState() => _CheckoutPageState();
}
class _CheckoutPageState extends State<CheckoutPage> {
final _orderService = OrderService(HttpClient());
final _state = CheckoutState(
addresses: [],
coupons: [],
);
Future<void> _loadData() async {
final addresses = await AddressService().getUserAddresses();
final coupons = await CouponService().getValidCoupons();
setState(() {
_state.addresses = addresses;
_state.coupons = coupons;
_state.selectedAddress = addresses.firstWhere(
(a) => a.isDefault,
orElse: () => addresses.first,
);
});
}
Future<void> _submitOrder() async {
try {
final order = await _orderService.createOrder(
items: widget.cartItems,
address: _state.selectedAddress!,
coupon: _state.selectedCoupon,
usedPoints: _state.usedPoints,
);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PaymentPage(order: order),
),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('订单创建失败: $e')),
);
}
}
@override
void initState() {
super.initState();
_loadData();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('结算')),
body: SingleChildScrollView(
child: Column(
children: [
AddressSelection(
addresses: _state.addresses,
selectedAddress: _state.selectedAddress,
onSelected: (address) {
_state.selectedAddress = address;
},
onAddNew: () => _navigateToAddAddress(),
),
CouponSelection(
coupons: _state.coupons,
selectedCoupon: _state.selectedCoupon,
onSelected: (coupon) {
_state.selectedCoupon = coupon;
},
),
PointsInput(
usedPoints: _state.usedPoints,
maxPoints: _state.maxUsablePoints,
onChanged: (points) {
_state.usedPoints = points;
},
),
PriceSummary(
subtotal: _calculateSubtotal(),
coupon: _state.selectedCoupon,
usedPoints: _state.usedPoints,
),
const SizedBox(height: 20),
Padding(
padding: const EdgeInsets.all(16),
child: ElevatedButton(
onPressed: _canSubmit ? _submitOrder : null,
child: const Text('提交订单'),
),
),
],
),
),
);
}
}
完整流程:
- 初始化加载地址和优惠券数据
- 实时计算价格汇总
- 处理订单提交和异常情况
- 跳转到支付页面
5.2 支付集成方案
dart复制enum PaymentMethod {
wechatPay,
alipay,
unionPay,
}
class PaymentPage extends StatefulWidget {
final Order order;
const PaymentPage({super.key, required this.order});
@override
State<PaymentPage> createState() => _PaymentPageState();
}
class _PaymentPageState extends State<PaymentPage> {
PaymentMethod _selectedMethod = PaymentMethod.wechatPay;
bool _isPaying = false;
Future<void> _handlePayment() async {
setState(() => _isPaying = true);
try {
final result = await PaymentService().pay(
orderId: widget.order.id,
amount: widget.order.totalUsd,
method: _selectedMethod,
);
if (result.success) {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => OrderDetailPage(order: widget.order),
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('支付失败,请重试')),
);
}
} finally {
setState(() => _isPaying = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('选择支付方式')),
body: Column(
children: [
ListTile(
title: const Text('微信支付'),
leading: Radio<PaymentMethod>(
value: PaymentMethod.wechatPay,
groupValue: _selectedMethod,
onChanged: (value) {
setState(() => _selectedMethod = value!);
},
),
),
ListTile(
title: const Text('支付宝'),
leading: Radio<PaymentMethod>(
value: PaymentMethod.alipay,
groupValue: _selectedMethod,
onChanged: (value) {
setState(() => _selectedMethod = value!);
},
),
),
ListTile(
title: const Text('银联支付'),
leading: Radio<PaymentMethod>(
value: PaymentMethod.unionPay,
groupValue: _selectedMethod,
onChanged: (value) {
setState(() => _selectedMethod = value!);
},
),
),
const Spacer(),
Padding(
padding: const EdgeInsets.all(16),
child: ElevatedButton(
onPressed: _isPaying ? null : _handlePayment,
child: _isPaying
? const CircularProgressIndicator()
: Text('立即支付 ¥${widget.order.totalUsd.toStringAsFixed(2)}'),
),
),
],
),
);
}
}
支付关键点:
- 支持多种支付方式选择
- 处理支付中的加载状态
- 支付成功跳转订单详情
- 错误处理和用户反馈
6. 性能优化与调试技巧
6.1 结算页性能优化
- 列表性能优化:
dart复制ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ItemWidget(item: items[index]);
},
)
- 使用builder模式避免一次性构建所有item
- 为列表项添加const构造函数
- 使用AutomaticKeepAlive保持滚动位置
- 计算缓存:
dart复制class _CheckoutPageState extends State<CheckoutPage> {
late final _calculator = CheckoutCalculator(
subtotal: _calculateSubtotal(),
);
double _calculateSubtotal() {
return widget.cartItems.fold(
0,
(sum, item) => sum + item.price * item.quantity,
);
}
}
- 避免重复计算商品小计
- 使用final变量缓存不变的计算结果
- 复杂计算放在initState而非build中
- 选择性重建:
dart复制class PriceSummary extends StatelessWidget {
final double subtotal;
final Coupon? coupon;
final int usedPoints;
const PriceSummary({
super.key,
required this.subtotal,
this.coupon,
this.usedPoints = 0,
});
@override
Widget build(BuildContext context) {
final calculator = CheckoutCalculator(
subtotal: subtotal,
coupon: coupon,
usedPoints: usedPoints,
);
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
// ...价格明细...
],
),
),
);
}
}
- 将价格摘要拆分为独立Widget
- 只有相关参数变化时才会重建
- 减少不必要的UI更新
6.2 常见问题排查
- 价格计算异常:
- 现象:最终价格出现负数或明显错误
- 检查点:
- 确保所有计算使用decimal而非double
- 验证优惠券适用条件(minOrderAmount)
- 检查积分抵扣上限计算
- 地址选择无效:
- 现象:无法选择地址或选择不生效
- 检查点:
- 确认setState被正确调用
- 检查Radio的groupValue匹配逻辑
- 验证地址数据是否完整(isValid)
- 优惠券状态异常:
- 现象:已过期券仍可使用
- 检查点:
- 验证Coupon的isValid逻辑
- 确保从服务端获取的是有效券列表
- 检查本地时间与服务端时间同步
- 订单提交失败:
- 现象:无法创建订单或报错
- 检查点:
- 验证网络请求参数格式
- 检查地址ID是否存在
- 确认优惠券未被其他订单使用
调试技巧:在开发阶段,可以在CheckoutCalculator中添加日志输出,记录每个计算步骤的中间值,便于追踪异常。
7. 测试策略与质量保障
7.1 单元测试重点
- 价格计算测试:
dart复制void main() {
test('百分比折扣计算正确', () {
final coupon = Coupon(
id: '1',
code: 'TEST20',
type: CouponType.percentage,
value: 20,
minOrderAmount: 100,
expiresAt: DateTime.now().add(Duration(days: 7)),
);
final calculator = CheckoutCalculator(
subtotal: 200,
coupon: coupon,
);
expect(calculator.couponDiscount, 40);
expect(calculator.calculateTotal(), 160);
});
}
- 地址验证测试:
dart复制test('不完整地址验证失败', () {
final address = Address(
id: '1',
name: '测试',
phone: '13800138000',
province: '北京',
city: '',
district: '朝阳区',
detail: '某街道1号',
);
expect(address.isValid, false);
});
- 优惠券状态测试:
dart复制test('过期优惠券无效', () {
final coupon = Coupon(
id: '1',
code: 'EXPIRED',
type: CouponType.fixed,
value: 10,
minOrderAmount: 50,
expiresAt: DateTime.now().subtract(Duration(days: 1)),
);
expect(coupon.isValid, false);
});
7.2 集成测试方案
dart复制void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('完整结算流程测试', (tester) async {
// 初始化测试数据
final addresses = [
Address(id: '1', name: '测试', phone: '13800138000',
province: '北京', city: '北京', district: '朝阳区',
detail: '测试地址1', isDefault: true),
];
final coupons = [
Coupon(id: '1', code: 'TEST10', type: CouponType.fixed,
value: 10, minOrderAmount: 100,
expiresAt: DateTime.now().add(Duration(days: 7))),
];
// 加载结算页面
await tester.pumpWidget(MaterialApp(
home: CheckoutPage(
cartItems: [
CartItem(product: Product(id: '1', name: '测试商品', price: 150), quantity: 1),
],
),
));
// 选择地址
await tester.tap(find.text('测试地址1'));
await tester.pump();
// 选择优惠券
await tester.tap(find.text('TEST10'));
await tester.pump();
// 验证价格
expect(find.text('¥140.00'), findsOneWidget);
// 提交订单
await tester.tap(find.text('提交订单'));
await tester.pumpAndSettle();
// 验证跳转到支付页
expect(find.text('选择支付方式'), findsOneWidget);
});
}
7.3 质量检查清单
在发布前需要验证:
- [ ] 所有价格计算使用decimal类型
- [ ] 优惠券适用条件检查完备
- [ ] 地址验证覆盖边界情况
- [ ] 订单提交参数完整
- [ ] 支付失败有重试机制
- [ ] 关键操作有加载状态
- [ ] 网络异常有友好提示
- [ ] 所有用户输入有验证
8. 进阶扩展方向
8.1 多步骤结算流程
dart复制enum CheckoutStep {
address,
shipping,
payment,
review,
}
class SteppedCheckout extends StatefulWidget {
const SteppedCheckout({super.key});
@override
State<SteppedCheckout> createState() => _SteppedCheckoutState();
}
class _SteppedCheckoutState extends State<SteppedCheckout> {
CheckoutStep _currentStep = CheckoutStep.address;
void _nextStep() {
setState(() {
_currentStep = CheckoutStep.values[
(_currentStep.index + 1) % CheckoutStep.values.length
];
});
}
void _prevStep() {
setState(() {
_currentStep = CheckoutStep.values[
(_currentStep.index - 1) % CheckoutStep.values.length
];
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('结算')),
body: Column(
children: [
Stepper(
currentStep: _currentStep.index,
steps: [
Step(
title: const Text('收货地址'),
content: AddressSelection(...),
isActive: _currentStep == CheckoutStep.address,
),
Step(
title: const Text('配送方式'),
content: ShippingSelection(...),
isActive: _currentStep == CheckoutStep.shipping,
),
Step(
title: const Text('支付信息'),
content: PaymentSelection(...),
isActive: _currentStep == CheckoutStep.payment,
),
Step(
title: const Text('订单确认'),
content: OrderReview(...),
isActive: _currentStep == CheckoutStep.review,
),
],
),
ButtonBar(
children: [
if (_currentStep != CheckoutStep.address)
TextButton(
onPressed: _prevStep,
child: const Text('上一步'),
),
ElevatedButton(
onPressed: _nextStep,
child: Text(
_currentStep == CheckoutStep.review
? '提交订单'
: '下一步',
),
),
],
),
],
),
);
}
}
8.2 国际化支持
dart复制class CheckoutLocalizations {
static const Map<String, Map<String, String>> _translations = {
'en': {
'checkoutTitle': 'Checkout',
'addressTitle': 'Shipping Address',
// ...更多翻译...
},
'zh': {
'checkoutTitle': '结算',
'addressTitle': '收货地址',
// ...更多翻译...
},
};
static String get checkoutTitle {
return _translations[locale.languageCode]?['checkoutTitle'] ?? 'Checkout';
}
// ...更多本地化方法...
}
// 在Widget中使用
Text(CheckoutLocalizations.checkoutTitle)
8.3 暗黑模式适配
dart复制Card(
elevation: 2,
color: Theme.of(context).cardColor,
child: ...
)
// 在价格显示中使用主题色
Text(
'¥${price.toStringAsFixed(2)}',
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
),
)
8.4 动画增强体验
dart复制class AnimatedCheckmark extends ImplicitlyAnimatedWidget {
final bool checked;
const AnimatedCheckmark({
super.key,
required this.checked,
super.duration = const Duration(milliseconds: 300),
});
@override
ImplicitlyAnimatedWidgetState<AnimatedCheckmark> createState() => _AnimatedCheckmarkState();
}
class _AnimatedCheckmarkState extends AnimatedWidgetBaseState<AnimatedCheckmark> {
late Animation<double> _sizeAnimation;
late Animation<Color?> _colorAnimation;
@override
void initState() {
super.initState();
_sizeAnimation = Tween<double>(begin: 0, end: 24).animate(curve: Curves.elasticOut);
_colorAnimation = ColorTween(
begin: Colors.transparent,
end: Colors.green,
).animate(curve: Curves.easeIn);
}
@override
Widget build(BuildContext context) {
return Icon(
Icons.check_circle,
size: widget.checked ? _sizeAnimation.value : 0,
color: _colorAnimation.value,
);
}
}
9. 项目实战经验分享
在多个电商项目实战中,我总结了以下宝贵经验:
- 金额计算的坑:
- 一定要使用decimal.js等库处理金额,避免浮点数精度问题
- 服务端必须重新验证所有计算,防止客户端被篡改
- 记录计算日志便于对账和问题追踪
- 优惠叠加策略:
- 明确优惠优先级(如折扣券优先于满减)
- 设置优惠上限防止资损
- 提供"最优优惠"自动选择功能
- 性能优化点:
- 结算页要特别关注首屏加载速度
- 预加载用户地址和优惠券数据
- 对价格计算进行缓存和防抖处理
- 异常处理经验:
- 处理库存不足时的友好提示
- 优惠券失效时的自动更新机制
- 网络中断后的本地数据保存和恢复
- AB测试发现:
- 单页结算转化率高于多步骤
- 显示倒计时(如"优惠即将失效")可提升10%转化
- 默认地址选择准确率影响下单速度
个人心得:结算页的每个细节都直接影响转化率,需要不断通过数据分析优化。例如,我们曾通过优化运费显示方式(从"运费:¥10"改为"满99免运费,当前还差¥30")提升了15%的客单价。