北京游玩总攻略

很小的时候就希望能去北京看看,看看古时候皇帝生活的地方,看看为了抵御外敌而修建的万里长城,看看皇帝的陵墓。那就做一份北京游玩攻略吧。以下从游住吃三个方面来介绍。推荐自由行

门票预约

!!!!!作为自由行,一定要提前预约门票,万一没约到门票,将会打乱所有计划。

北京各景区放票时间:

  • 毛记:提前6天12:00
Read More

flutter_sound

Flutter中音频的处理库,可以用来播放、录制。播放支持主流的格式,支持网络地址。

播放音频

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

import 'package:flutter_sound/flutter_sound.dart';
import 'package:xtechpark/ws_utils/ws_log/ws_logger.dart';

class WSAudioPlayer {
factory WSAudioPlayer() => _getInstance();

static WSAudioPlayer get instance => _getInstance();
static WSAudioPlayer? _instance;

WSAudioPlayer._internal();

static WSAudioPlayer _getInstance() {
_instance ??= WSAudioPlayer._internal();
return _instance!;
}

FlutterSoundPlayer? _player;

Future init() async {
_player = FlutterSoundPlayer();

await _player?.openPlayer().then((value) {
WSLogger.debug('WSAudioPlayer init ');
});
await _player?.setSubscriptionDuration(const Duration(milliseconds: 100));
_player?.onProgress?.listen((event) {
WSLogger.debug('WSAudioPlayer onProgress ${event.duration}');
});
}

Future play(String url, [Function()? onFinished]) async {
if (_player == null) {
await init();
}
if (_player?.playerState == PlayerState.isPlaying) {
await stop();
}
return _player?.startPlayer(fromURI: url, codec: Codec.mp3, whenFinished: onFinished);
}

Future stop() async {
await _player?.stopPlayer();
}
}


录制音频

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
import 'dart:io';

import 'package:audio_session/audio_session.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_sound/flutter_sound.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:xtechpark/ws_utils/ws_file_util.dart';
import 'package:xtechpark/ws_utils/ws_log/ws_logger.dart';
import 'package:xtechpark/ws_utils/ws_toast_util.dart';
import 'package:path_provider/path_provider.dart';
import 'package:flutter_sound_platform_interface/flutter_sound_recorder_platform_interface.dart';

class WSAudioRecorder {
factory WSAudioRecorder() => _getInstance();

static WSAudioRecorder get instance => _getInstance();
static WSAudioRecorder? _instance;

WSAudioRecorder._internal();

static WSAudioRecorder _getInstance() {
_instance ??= WSAudioRecorder._internal();
return _instance!;
}

FlutterSoundRecorder? _recorder;
final Codec _codec = Codec.aacADTS;
String _mPath = 'temp.aac';

/// 录音时长
Duration? duration;

/// 初始化
Future init() async {
_recorder = FlutterSoundRecorder();
await openTheRecorder();
}

/// 释放
dispose() {
_recorder?.closeRecorder();
_recorder = null;
}

/// 初始化配置
Future<void> openTheRecorder() async {
var status = await Permission.microphone.request();
if (status != PermissionStatus.granted) {
WSToastUtil.show('Microphone permission not granted');
return;
}
await _recorder?.openRecorder();
final session = await AudioSession.instance;
await session.configure(AudioSessionConfiguration(
avAudioSessionCategory: AVAudioSessionCategory.playAndRecord,
avAudioSessionCategoryOptions:
AVAudioSessionCategoryOptions.allowBluetooth | AVAudioSessionCategoryOptions.defaultToSpeaker,
avAudioSessionMode: AVAudioSessionMode.spokenAudio,
avAudioSessionRouteSharingPolicy: AVAudioSessionRouteSharingPolicy.defaultPolicy,
avAudioSessionSetActiveOptions: AVAudioSessionSetActiveOptions.none,
androidAudioAttributes: const AndroidAudioAttributes(
contentType: AndroidAudioContentType.speech,
flags: AndroidAudioFlags.none,
usage: AndroidAudioUsage.voiceCommunication,
),
androidAudioFocusGainType: AndroidAudioFocusGainType.gain,
androidWillPauseWhenDucked: true,
));

_recorder?.dispositionStream()?.listen((event) {
WSLogger.debug('debug dispositionStream:$event');
});

_recorder?.setSubscriptionDuration(const Duration(milliseconds: 100));
_recorder?.onProgress?.listen((e) {
WSLogger.debug("debug onProgress:${e.decibels} / ${e.duration}");
duration = e.duration;
});
// _mRecorderIsInited = true;
}

void startRecord() async {
if(_recorder == null) {
await init();
}
if(_recorder?.recorderState == RecorderState.isRecording) {
await _recorder?.stopRecorder();
return;
}
WSLogger.debug("debug startRecord");
var status = await Permission.microphone.request();
if (status != PermissionStatus.granted) {
WSToastUtil.show("Microphone permission not granted");
} else {
Directory tempDir = await getTemporaryDirectory();
_mPath = "${tempDir.path}/${DateTime.now().millisecondsSinceEpoch}.aac";
_recorder?.startRecorder(
toFile: _mPath,
codec: _codec,
audioSource: AudioSource.microphone,
);

WSLogger.debug("debug recording");
}
}

/// 停止录音
void stopRecord(Function(String path, int duration) finished) async {
String? path = await _recorder?.stopRecorder();
if (path == null) {
WSToastUtil.show('record failed');
WSLogger.error('record failed ${_recorder?.recorderState}');
return;
}
if(!await WSFileUtil.isFileExists(path)) {
WSToastUtil.show('record failed');
return;
}
if(duration != null && duration!.inSeconds < 1) {
WSToastUtil.show('recording can\'t be less than 1 second');
return;
}
WSLogger.debug("Stop recording: path = $path,duration = ${duration?.inSeconds}");
if (TargetPlatform.android == defaultTargetPlatform) {
path = "file://$path";
}
finished(path, duration?.inSeconds ?? 0);
}
}

Read More

in_app_purchase苹果内购

使用in_app_purchase这个库

流程

1. 修改XCode配置文件,支持内购

2. 项目中添加in_app_purchase配置

1
2
3
4
5
6
7

dependencies:
flutter:
sdk: flutter
in_app_purchase:
in_app_purchase_storekit:

3. 编写内购代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158

class ShopPayUtil {
static ShopPayUtil? _instance;

static ShopPayUtil getInstance() {
_instance ??= ShopPayUtil._internal();
return _instance!;
}

ShopPayUtil._internal();

factory ShopPayUtil() => getInstance();

///购买商品
void buy(GoodsModel item) async {
try {
EasyLoading.show();
// 根据商品信息创建订单
var res = await rechargeCreate(item.code!);
if(res != null) {
// 调用苹果内购
await applePay(res);
}
} catch (e, s) {
Logger.error('$e $s');
} finally {
EasyLoading.dismiss();
}
}

/// 创建充值订单
Future<dynamic> rechargeCreate(String goodsCode) async {
// todo 调用服务器接口
}

/// 验单
static Future paymentIpa(String orderNo, String payload, String transactionId) async {
// todo 检查订单准确性
}

/// 消耗虚拟币
Future reviewModeConsume(String outlay) async {
// todo 调用后台接口
}

static StreamSubscription<List<PurchaseDetails>>? _subscription;

/// 苹果内购 (购买)
static Future<void> applePay(Map dataMap) async {
final Stream<List<PurchaseDetails>> purchaseUpdated = InAppPurchase.instance.purchaseStream;
_subscription?.cancel();
// 添加监听器
_subscription = purchaseUpdated.listen((List<PurchaseDetails> purchaseDetailsList) {
_listenToPurchaseUpdated(purchaseDetailsList, dataMap);
}, onDone: () {
_subscription?.cancel();
Logger.debug('=applePay=====onDone');
}, onError: (Object error) {
Logger.error(error);
ToastUtil.show(error.toString());
});


final bool isAvailable = await InAppPurchase.instance.isAvailable();
// 判断是否可用
if (!isAvailable) {
ToastUtil.show('The store cannot be reached, check your network connection please.');
return;
}

if (Platform.isIOS) {
final InAppPurchaseStoreKitPlatformAddition iosPlatformAddition =
InAppPurchase.instance.getPlatformAddition<InAppPurchaseStoreKitPlatformAddition>();
await iosPlatformAddition.setDelegate(PaymentQueueDelegate());
}

Set<String> ids = {dataMap['goodsCode']};
// 查询商品的信息(需要先在苹果后台配置)
final ProductDetailsResponse productDetailResponse = await InAppPurchase.instance.queryProductDetails(ids);

if (productDetailResponse.error != null) {
WSToastUtil.show('Failed to obtain product information.');
return;
}

if (productDetailResponse.productDetails.isEmpty) {
ToastUtil.show('No product');
return;
}
List<ProductDetails> _products = productDetailResponse.productDetails;
// 查询成功
ProductDetails productDetails = _products[0];
PurchaseParam purchaseParam = PurchaseParam(
productDetails: productDetails,
applicationUserName: '${dataMap['orderNo']}',
);
//向苹果服务器发起支付请求
var res = await InAppPurchase.instance.buyConsumable(purchaseParam: purchaseParam);
Logger.debug('buyConsumable result: $res');
}

static Future<void> _listenToPurchaseUpdated(List<PurchaseDetails> purchaseDetailsList, Map dataMap) async {
Logger.debug('===>>>_listenToPurchaseUpdated callback');
for (final PurchaseDetails purchaseDetails in purchaseDetailsList) {
Logger.debug(
'===>>>_listenToPurchaseUpdated status=${purchaseDetails.status} productID=${purchaseDetails.productID} '
'transactionDate=${purchaseDetails.transactionDate}'
'purchaseID=${purchaseDetails.purchaseID} transactionDate=${purchaseDetails.transactionDate}'
'verificationData.localVerificationData=${purchaseDetails.verificationData.localVerificationData}'
'verificationData.serverVerificationData=${purchaseDetails.verificationData.serverVerificationData}'
'verificationData.source=${purchaseDetails.verificationData.source}');
if (purchaseDetails.status == PurchaseStatus.pending) {
/// 等待购买中
} else if (purchaseDetails.status == PurchaseStatus.canceled) {
/// 取消订单
InAppPurchase.instance.completePurchase(purchaseDetails);
} else {
if (purchaseDetails.status == PurchaseStatus.error) {
/// 购买出错
WSToastUtil.show('Purchase error');
InAppPurchase.instance.completePurchase(purchaseDetails);
} else if (purchaseDetails.status == PurchaseStatus.purchased ||
purchaseDetails.status == PurchaseStatus.restored) {
if (purchaseDetails.pendingCompletePurchase) {
await InAppPurchase.instance.completePurchase(purchaseDetails);
}
if (purchaseDetails.productID == dataMap['goodsCode']) {
// 支付成功,发放商品
await deliverProduct(purchaseDetails, dataMap);
}
}
}
}
}

/// 调用后台接口,发放商品
static Future<void> deliverProduct(PurchaseDetails purchaseDetails, Map dataMap) async {
String code = dataMap['goodsCode'];
String payload = purchaseDetails.verificationData.serverVerificationData;
String transactionId = purchaseDetails.purchaseID!;
var res = await paymentIpa(dataMap['orderNo'], payload, transactionId);
// todo 调用后台接口
}
}

class PaymentQueueDelegate implements SKPaymentQueueDelegateWrapper {
@override
bool shouldContinueTransaction(SKPaymentTransactionWrapper transaction, SKStorefrontWrapper storefront) {
return true;
}

@override
bool shouldShowPriceConsent() {
return false;
}
}


Read More