已完成用户APP首期功能开发

交付用户端首期页面、配套接口、后台资源及测试文档。用户APP构建、静态分析和三个管理后台构建通过;完整测试仍有2项失败,后端模型注释检查未通过,详见交付记录。
This commit is contained in:
czl231
2026-09-13 00:57:32 +08:00
parent a0a4bb1218
commit 3ef33b531d
793 changed files with 40959 additions and 1204 deletions

View File

@@ -2,6 +2,17 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:user_app/domain/models/client_models.dart';
void main() {
test('用户资料保留服务端实名认证事实', () {
final profile = UserProfile.fromJson(const {
'identity': 'user',
'name': '张先生',
'phone': '13800005678',
'avatar': '',
'real_name': '张实名',
});
expect(profile.realName, '张实名');
});
group('moneyText', () {
test('formats minor currency units', () {
expect(moneyText(12345), '¥123.45');

View File

@@ -0,0 +1,39 @@
// 功能描述充值待确认请求的存储隔离和损坏保护版本1.0.0。
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:user_app/data/services/recharge_draft_store.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
const draft = PendingRecharge(
request: 'request-1',
amount: 29,
channel: 'alipay',
payType: 'wap',
);
test('重开存储恢复同一请求,环境和账号分别隔离', () async {
FlutterSecureStorage.setMockInitialValues({});
final store = SecureRechargeDraftStore();
await store.write('api-a#alice', draft);
expect((await SecureRechargeDraftStore().read('api-a#alice'))?.request, 'request-1');
expect((await store.read('api-a#alice'))?.amount, 29);
expect(await store.read('api-a#bob'), isNull);
expect(await store.read('api-b#alice'), isNull);
await store.delete('api-a#bob');
expect(await store.read('api-a#alice'), isNotNull);
await store.delete('api-a#alice');
expect(await store.read('api-a#alice'), isNull);
});
test('损坏请求不能静默当作未创建禁止Mock渠道和无效金额', () {
for (final patch in <Map<String, Object?>>[
{'schema': 2},
{'request': ''},
{'amount': 0},
{'amount': 1.5},
{'channel': 'mock'},
{'channel': 'wechat', 'pay_type': 'wap'},
]) {
expect(() => PendingRecharge.fromJson({...draft.toJson(), ...patch}), throwsFormatException);
}
});
}

View File

@@ -0,0 +1,121 @@
// 功能描述充值先存后发和未确认订单保留版本1.0.0。
import 'package:flutter_test/flutter_test.dart';
import 'package:user_app/data/services/recharge_draft_store.dart';
import 'package:user_app/data/services/recharge_flow.dart';
import 'package:user_app/data/services/api_client.dart';
import 'package:user_app/domain/models/recharge.dart';
import '../support/a1_fixture.dart';
class _Store implements RechargeDraftStore {
PendingRecharge? value;
bool fail = false;
@override
Future<PendingRecharge?> read(String owner) async => value;
@override
Future<void> write(String owner, PendingRecharge draft) async {
if (fail) throw StateError('存储失败');
value = draft;
}
@override
Future<void> delete(String owner) async {
value = null;
}
}
class _Repo extends A1FixtureRepository {
_Repo(this.store);
final _Store store;
int calls = 0, status = 10;
int? queryError;
String owner = 'api#alice';
bool changeOwnerOnQuery = false;
@override
Future<String> rechargeDraftOwner() async => owner;
@override
Future<Map<String, Object?>> createRecharge({
required String request,
required int amount,
required String channel,
required String payType,
}) async {
expect(store.value?.request, request);
calls++;
throw StateError('响应丢失');
}
@override
Future<RechargeRecord> rechargeResult(String request) async {
if (changeOwnerOnQuery) owner = 'api#bob';
if (queryError != null) throw ApiException(queryError!, '查询失败');
return RechargeRecord(
identity: 'order',
number: request,
amount: 100,
status: status,
channel: 'alipay',
createdAt: DateTime(2026),
);
}
}
void main() {
test('明确不存在可原号重发,网络错误和账号切换不能发单', () async {
const draft = PendingRecharge(
request: 'request-1',
amount: 100,
channel: 'alipay',
payType: 'wap',
);
for (final code in [1112, 500]) {
final store = _Store()..value = draft;
final repo = _Repo(store)..queryError = code;
final flow = RechargeFlow(repo, store);
await expectLater(
flow.create(draft),
code == 1112
? throwsStateError
: throwsA(isA<ApiException>().having((e) => e.code, '错误码', 500)),
);
expect(repo.calls, code == 1112 ? 1 : 0);
expect(store.value?.request, draft.request);
}
final store = _Store()..value = draft;
final repo = _Repo(store)..changeOwnerOnQuery = true;
await expectLater(RechargeFlow(repo, store).create(draft), throwsStateError);
expect(repo.calls, 0);
expect(store.value, isNotNull);
});
test('存储失败不发单,响应丢失保留原请求,到账后才清除', () async {
final store = _Store()..fail = true;
final repo = _Repo(store);
final flow = RechargeFlow(repo, store);
const draft = PendingRecharge(
request: 'request-1',
amount: 100,
channel: 'alipay',
payType: 'wap',
);
await expectLater(flow.create(draft), throwsStateError);
expect(repo.calls, 0);
store.fail = false;
await expectLater(flow.create(draft), throwsStateError);
expect(repo.calls, 1);
expect(store.value?.request, 'request-1');
await expectLater(
flow.create(
const PendingRecharge(request: 'request-2', amount: 100, channel: 'alipay', payType: 'wap'),
),
throwsStateError,
);
expect(repo.calls, 1);
expect((await flow.recover())?.credited, isFalse);
expect(store.value, isNotNull);
repo.status = 23;
expect(await flow.create(draft), {'recharge_status': 23});
expect(repo.calls, 1);
expect(store.value, isNotNull);
expect((await flow.recover())?.credited, isTrue);
expect(store.value, isNull);
});
}

View File

@@ -0,0 +1,49 @@
// 功能描述充值金额精度、协议完整性和到账事实校验版本1.0.0。
import 'package:flutter_test/flutter_test.dart';
import 'package:user_app/domain/models/recharge.dart';
void main() {
test('金额按十进制精确转分,拒绝超精度和指数输入', () {
expect(parseRechargeAmount('0.29'), 29);
expect(parseRechargeAmount(' 100.1 '), 10010);
expect(parseRechargeAmount('999999999.99'), 99999999999);
for (final value in ['-1', '1e2', '1.001', '.5', '1.', 'NaN', '']) {
expect(parseRechargeAmount(value), isNull, reason: value);
}
});
test('只有钱包充值完成状态才表示到账', () {
RechargeRecord record(int status, int? paymentStatus) => RechargeRecord.fromJson({
'identity': 'recharge-1',
'amount': 10000,
'recharge_status': status,
'payment_status': paymentStatus,
'created_at': '2026-09-11T00:00:00Z',
});
expect(record(10, 23).credited, isFalse);
expect(record(10, 30).statusText, '支付已关闭,未入账');
expect(record(23, null).credited, isTrue);
expect(record(23, 30).statusText, '已到账');
});
test('缺少协议允许读取配置,残缺协议必须拒绝', () {
final json = <String, Object?>{'min_amount': 1, 'max_amount': 500000, 'channels': <Object>[]};
expect(RechargeOptions.fromJson(json).agreement, isNull);
for (final identity in ['', ' ']) {
expect(
() => RechargeOptions.fromJson({
...json,
'agreement': {
'identity': identity,
'version': 1,
'body': '协议正文',
},
}),
throwsFormatException,
);
}
expect(() => RechargeOptions.fromJson({...json, 'min_amount': 0}), throwsFormatException);
expect(() => RechargeOptions.fromJson({...json, 'max_amount': 0}), throwsFormatException);
expect(() => rechargeCents(1.5), throwsFormatException);
});
}