feat: integrate unified payment and wallet refunds
This commit is contained in:
@@ -79,6 +79,44 @@ class ClientRepository {
|
||||
subtitleKeys: const ['amount', 'direction'],
|
||||
);
|
||||
|
||||
Future<List<ClientRecord>> refunds() => _records(
|
||||
'$root/refunds',
|
||||
titleKeys: const ['refund_no'],
|
||||
subtitleKeys: const ['reason', 'amount'],
|
||||
statusKey: 'refund_status',
|
||||
);
|
||||
|
||||
Future<Map<String, Object?>> payOrder({
|
||||
required String business,
|
||||
required String identity,
|
||||
required String requestNo,
|
||||
required String channel,
|
||||
required String payType,
|
||||
String openid = '',
|
||||
}) async => jsonMap(await _api.post(
|
||||
'$root/$business/orders/$identity/pay',
|
||||
body: {
|
||||
'request_no': requestNo,
|
||||
'channel': channel,
|
||||
'pay_type': payType,
|
||||
if (openid.isNotEmpty) 'openid': openid,
|
||||
},
|
||||
));
|
||||
|
||||
Future<void> createRefund({
|
||||
required String business,
|
||||
required String identity,
|
||||
required String requestNo,
|
||||
required String reason,
|
||||
required List<Map<String, Object?>> items,
|
||||
}) async {
|
||||
await _api.post('$root/$business/orders/$identity/refunds', body: {
|
||||
'request_no': requestNo,
|
||||
'reason': reason,
|
||||
'items': items,
|
||||
});
|
||||
}
|
||||
|
||||
Future<Map<String, Object?>?> serviceRelation() async {
|
||||
final value = await _api.get('$root/service-relation');
|
||||
if (value == null || value == '') return null;
|
||||
|
||||
2
apps/user_app/lib/data/services/payment_jsapi.dart
Normal file
2
apps/user_app/lib/data/services/payment_jsapi.dart
Normal file
@@ -0,0 +1,2 @@
|
||||
export 'payment_jsapi_stub.dart'
|
||||
if (dart.library.js_interop) 'payment_jsapi_web.dart';
|
||||
2
apps/user_app/lib/data/services/payment_jsapi_stub.dart
Normal file
2
apps/user_app/lib/data/services/payment_jsapi_stub.dart
Normal file
@@ -0,0 +1,2 @@
|
||||
Future<void> invokeWechatJsapi(String clientArgs) =>
|
||||
throw UnsupportedError('微信 JSAPI 仅支持 Web');
|
||||
25
apps/user_app/lib/data/services/payment_jsapi_web.dart
Normal file
25
apps/user_app/lib/data/services/payment_jsapi_web.dart
Normal file
@@ -0,0 +1,25 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:js_interop';
|
||||
|
||||
@JS('WeixinJSBridge.invoke')
|
||||
external void _invoke(String name, JSAny? arguments, JSFunction callback);
|
||||
|
||||
Future<void> invokeWechatJsapi(String clientArgs) {
|
||||
final completer = Completer<void>();
|
||||
final arguments = jsonDecode(clientArgs).jsify();
|
||||
_invoke(
|
||||
'getBrandWCPayRequest',
|
||||
arguments,
|
||||
(JSAny? rawResult) {
|
||||
final result = rawResult.dartify();
|
||||
final message = result is Map ? result['err_msg']?.toString() ?? '' : '';
|
||||
if (message == 'get_brand_wcpay_request:ok') {
|
||||
completer.complete();
|
||||
} else {
|
||||
completer.completeError(StateError('微信支付未完成:$message'));
|
||||
}
|
||||
}.toJS,
|
||||
);
|
||||
return completer.future;
|
||||
}
|
||||
56
apps/user_app/lib/data/services/payment_launcher.dart
Normal file
56
apps/user_app/lib/data/services/payment_launcher.dart
Normal file
@@ -0,0 +1,56 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:fluwx/fluwx.dart';
|
||||
import 'package:tobias/tobias.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'payment_jsapi.dart';
|
||||
|
||||
/// 只负责调起支付渠道;支付结果必须重新向服务端查询确认。
|
||||
class PaymentLauncher {
|
||||
PaymentLauncher({Fluwx? wechat, Tobias? alipay})
|
||||
: _wechat = wechat ?? Fluwx(),
|
||||
_alipay = alipay ?? Tobias();
|
||||
|
||||
final Fluwx _wechat;
|
||||
final Tobias _alipay;
|
||||
|
||||
Future<void> launch(Map<String, Object?> payment) async {
|
||||
final channel = payment['channel'];
|
||||
final payType = payment['pay_type'];
|
||||
final argsText = payment['client_args'] as String? ?? '';
|
||||
|
||||
// 支付宝手机网站支付返回可直接打开的收银台 URL。
|
||||
if (payType == 'wap') {
|
||||
final uri = Uri.tryParse(argsText);
|
||||
if (uri == null ||
|
||||
!await launchUrl(uri, mode: LaunchMode.externalApplication)) {
|
||||
throw StateError('无法打开支付页面');
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (kIsWeb && payType == 'jsapi') {
|
||||
await invokeWechatJsapi(argsText);
|
||||
return;
|
||||
}
|
||||
if (channel == 'alipay') {
|
||||
await _alipay.pay(argsText);
|
||||
return;
|
||||
}
|
||||
|
||||
final args = (jsonDecode(argsText) as Map).cast<String, dynamic>();
|
||||
final accepted = await _wechat.pay(
|
||||
which: Payment(
|
||||
appId: payment['app_id'] as String? ?? '',
|
||||
partnerId: args['partnerId'] as String,
|
||||
prepayId: args['prepayId'] as String,
|
||||
packageValue: args['package'] as String,
|
||||
nonceStr: args['nonceStr'] as String,
|
||||
timestamp: int.parse(args['timeStamp'] as String),
|
||||
sign: args['sign'] as String,
|
||||
),
|
||||
);
|
||||
if (!accepted) throw StateError('微信未接受支付请求');
|
||||
}
|
||||
}
|
||||
@@ -1,47 +1,172 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../../../data/repositories/client_repository.dart';
|
||||
import '../../../data/services/payment_launcher.dart';
|
||||
import '../../../domain/models/client_models.dart';
|
||||
import '../shared/record_list_page.dart';
|
||||
import '../shared/record_list_view_model.dart';
|
||||
|
||||
class OrdersPage extends StatelessWidget {
|
||||
const OrdersPage({required this.repository, super.key});
|
||||
OrdersPage({required this.repository, super.key});
|
||||
|
||||
final ClientRepository repository;
|
||||
final PaymentLauncher _launcher = PaymentLauncher();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => DefaultTabController(
|
||||
length: 3,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('我的订单'),
|
||||
bottom: const TabBar(
|
||||
tabs: [
|
||||
Tab(text: '商城'),
|
||||
Tab(text: '供气'),
|
||||
Tab(text: '服务工单'),
|
||||
Future<void> _openActions(
|
||||
BuildContext context,
|
||||
ClientRecord record,
|
||||
String business,
|
||||
) async {
|
||||
final action = await showModalBottomSheet<String>(
|
||||
context: context,
|
||||
builder: (context) => SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (record.status == 16 || record.status == 18) ...[
|
||||
ListTile(
|
||||
title: const Text('支付宝支付'),
|
||||
onTap: () => Navigator.pop(context, 'alipay'),
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('微信支付'),
|
||||
onTap: () => Navigator.pop(context, 'wechat'),
|
||||
),
|
||||
],
|
||||
if (record.status == 18 || record.status == 35)
|
||||
ListTile(
|
||||
title: const Text('申请退款'),
|
||||
onTap: () => Navigator.pop(context, 'refund'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: TabBarView(
|
||||
children: [
|
||||
RecordListPage(
|
||||
title: '商城订单',
|
||||
eyebrow: '交易',
|
||||
viewModel: RecordListViewModel(repository.shopOrders),
|
||||
);
|
||||
if (action == null || !context.mounted) return;
|
||||
try {
|
||||
if (action == 'refund') {
|
||||
final reason = await _reason(context);
|
||||
if (reason == null || !context.mounted) return;
|
||||
final rawItems = record.raw['items'] as List? ?? const [];
|
||||
await repository.createRefund(
|
||||
business: business,
|
||||
identity: record.identity,
|
||||
requestNo: const Uuid().v7(),
|
||||
reason: reason,
|
||||
items: rawItems.map((item) {
|
||||
final value = (item as Map).cast<String, Object?>();
|
||||
return <String, Object?>{
|
||||
'identity': value['identity'],
|
||||
'quantity': value['quantity'] ?? 1,
|
||||
};
|
||||
}).toList(),
|
||||
);
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('退款申请已提交')),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
final payment = await repository.payOrder(
|
||||
business: business,
|
||||
identity: record.identity,
|
||||
requestNo: const Uuid().v7(),
|
||||
channel: action,
|
||||
payType: kIsWeb
|
||||
? (action == 'alipay' ? 'wap' : 'jsapi')
|
||||
: 'app',
|
||||
openid: kIsWeb && action == 'wechat'
|
||||
? Uri.base.queryParameters['openid'] ?? ''
|
||||
: '',
|
||||
);
|
||||
await _launcher.launch(payment);
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('支付结果确认中,请稍后刷新')),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(error.toString())),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> _reason(BuildContext context) async {
|
||||
final controller = TextEditingController();
|
||||
final value = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('申请退款'),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
maxLines: 3,
|
||||
decoration: const InputDecoration(labelText: '退款原因'),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
RecordListPage(
|
||||
title: '供气订单',
|
||||
eyebrow: '履约',
|
||||
viewModel: RecordListViewModel(repository.gasOrders),
|
||||
),
|
||||
RecordListPage(
|
||||
title: '服务工单',
|
||||
eyebrow: '服务',
|
||||
viewModel: RecordListViewModel(repository.tickets),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(context, controller.text.trim()),
|
||||
child: const Text('提交'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
);
|
||||
controller.dispose();
|
||||
return value?.isEmpty == true ? null : value;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => DefaultTabController(
|
||||
length: 4,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('我的订单'),
|
||||
bottom: const TabBar(
|
||||
tabs: [
|
||||
Tab(text: '商城'),
|
||||
Tab(text: '供气'),
|
||||
Tab(text: '退款'),
|
||||
Tab(text: '工单'),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: TabBarView(
|
||||
children: [
|
||||
RecordListPage(
|
||||
title: '商城订单',
|
||||
eyebrow: '交易',
|
||||
viewModel: RecordListViewModel(repository.shopOrders),
|
||||
onRecordTap: (record) =>
|
||||
_openActions(context, record, 'shop'),
|
||||
),
|
||||
RecordListPage(
|
||||
title: '供气订单',
|
||||
eyebrow: '履约',
|
||||
viewModel: RecordListViewModel(repository.gasOrders),
|
||||
onRecordTap: (record) =>
|
||||
_openActions(context, record, 'gas'),
|
||||
),
|
||||
RecordListPage(
|
||||
title: '退款记录',
|
||||
eyebrow: '资金',
|
||||
viewModel: RecordListViewModel(repository.refunds),
|
||||
),
|
||||
RecordListPage(
|
||||
title: '服务工单',
|
||||
eyebrow: '服务',
|
||||
viewModel: RecordListViewModel(repository.tickets),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../core/widgets.dart';
|
||||
import '../../../domain/models/client_models.dart';
|
||||
import 'record_list_view_model.dart';
|
||||
|
||||
class RecordListPage extends StatefulWidget {
|
||||
@@ -10,6 +11,7 @@ class RecordListPage extends StatefulWidget {
|
||||
required this.viewModel,
|
||||
this.description,
|
||||
this.floatingActionButton,
|
||||
this.onRecordTap,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@@ -18,6 +20,7 @@ class RecordListPage extends StatefulWidget {
|
||||
final String? description;
|
||||
final RecordListViewModel viewModel;
|
||||
final Widget? floatingActionButton;
|
||||
final ValueChanged<ClientRecord>? onRecordTap;
|
||||
|
||||
@override
|
||||
State<RecordListPage> createState() => _RecordListPageState();
|
||||
@@ -74,7 +77,10 @@ class _RecordListPageState extends State<RecordListPage> {
|
||||
else
|
||||
SliverList.builder(
|
||||
itemCount: state.records.length,
|
||||
itemBuilder: (context, index) => RecordCard(record: state.records[index]),
|
||||
itemBuilder: (context, index) => RecordCard(
|
||||
record: state.records[index],
|
||||
onTap: widget.onRecordTap == null ? null : () => widget.onRecordTap!(state.records[index]),
|
||||
),
|
||||
),
|
||||
const SliverPadding(padding: EdgeInsets.only(bottom: 24)),
|
||||
],
|
||||
|
||||
@@ -6,7 +6,7 @@ packages:
|
||||
description:
|
||||
name: args
|
||||
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.7.0"
|
||||
async:
|
||||
@@ -14,7 +14,7 @@ packages:
|
||||
description:
|
||||
name: async
|
||||
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.13.1"
|
||||
boolean_selector:
|
||||
@@ -22,7 +22,7 @@ packages:
|
||||
description:
|
||||
name: boolean_selector
|
||||
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
characters:
|
||||
@@ -30,7 +30,7 @@ packages:
|
||||
description:
|
||||
name: characters
|
||||
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
clock:
|
||||
@@ -38,7 +38,7 @@ packages:
|
||||
description:
|
||||
name: clock
|
||||
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
code_assets:
|
||||
@@ -46,7 +46,7 @@ packages:
|
||||
description:
|
||||
name: code_assets
|
||||
sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
collection:
|
||||
@@ -54,7 +54,7 @@ packages:
|
||||
description:
|
||||
name: collection
|
||||
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.19.1"
|
||||
crypto:
|
||||
@@ -62,7 +62,7 @@ packages:
|
||||
description:
|
||||
name: crypto
|
||||
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.7"
|
||||
cupertino_icons:
|
||||
@@ -70,7 +70,7 @@ packages:
|
||||
description:
|
||||
name: cupertino_icons
|
||||
sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.9"
|
||||
fake_async:
|
||||
@@ -78,7 +78,7 @@ packages:
|
||||
description:
|
||||
name: fake_async
|
||||
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.3.3"
|
||||
ffi:
|
||||
@@ -86,7 +86,7 @@ packages:
|
||||
description:
|
||||
name: ffi
|
||||
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
ffi_leak_tracker:
|
||||
@@ -94,7 +94,7 @@ packages:
|
||||
description:
|
||||
name: ffi_leak_tracker
|
||||
sha256: "4093d4ef9ca06ffe2786e73bfb25e22aa92112b9bb4ec941f11e3e6b61489a97"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.1.2"
|
||||
fixnum:
|
||||
@@ -102,7 +102,7 @@ packages:
|
||||
description:
|
||||
name: fixnum
|
||||
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
flutter:
|
||||
@@ -115,7 +115,7 @@ packages:
|
||||
description:
|
||||
name: flutter_lints
|
||||
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.0.0"
|
||||
flutter_secure_storage:
|
||||
@@ -123,7 +123,7 @@ packages:
|
||||
description:
|
||||
name: flutter_secure_storage
|
||||
sha256: "7686b1d6a29985dcbb808c59518226e603e3bfa7c0ddfd1a0d00e4cda77c868e"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "10.3.1"
|
||||
flutter_secure_storage_darwin:
|
||||
@@ -131,7 +131,7 @@ packages:
|
||||
description:
|
||||
name: flutter_secure_storage_darwin
|
||||
sha256: "82329fa5cdf343773b1b6897dea959105a29f092454259edff92f9f6637e8149"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.3.2"
|
||||
flutter_secure_storage_linux:
|
||||
@@ -139,23 +139,23 @@ packages:
|
||||
description:
|
||||
name: flutter_secure_storage_linux
|
||||
sha256: a5f35ddab43cf5c8215d2feb4ce1957851f28c5c37e6f04335066a0602087bf5
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
flutter_secure_storage_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_platform_interface
|
||||
sha256: "06686df417f34fe9f963ed217b7fdce250e2f637ddd6c374d7eb054549770633"
|
||||
url: "https://pub.dev"
|
||||
sha256: "8ceea1223bee3c6ac1a22dabd8feefc550e4729b3675de4b5900f55afcb435d6"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
version: "2.0.1"
|
||||
flutter_secure_storage_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_web
|
||||
sha256: "073a62b3aeb866ab4ce795f960413948e51e5a42a9b0c8333b6daf5bb3208a1c"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
flutter_secure_storage_windows:
|
||||
@@ -163,7 +163,7 @@ packages:
|
||||
description:
|
||||
name: flutter_secure_storage_windows
|
||||
sha256: "471951813a97006d899db4948acc654a4f28c440083ea08178935ce20b173ec1"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.2.2"
|
||||
flutter_test:
|
||||
@@ -176,12 +176,20 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
fluwx:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: fluwx
|
||||
sha256: "4b526d281be8560a490bd1b945373a23ab2a12088c8fc5997ffb31cc6fb41082"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.0.2"
|
||||
go_router:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: go_router
|
||||
sha256: "5922b2861e2235a3504896f0d6fa07d84141b480cf52eecd2f42cd25585a9e8a"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "17.3.0"
|
||||
hooks:
|
||||
@@ -189,7 +197,7 @@ packages:
|
||||
description:
|
||||
name: hooks
|
||||
sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
http:
|
||||
@@ -197,7 +205,7 @@ packages:
|
||||
description:
|
||||
name: http
|
||||
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.6.0"
|
||||
http_parser:
|
||||
@@ -205,39 +213,31 @@ packages:
|
||||
description:
|
||||
name: http_parser
|
||||
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.1.2"
|
||||
jni:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: jni
|
||||
sha256: f038e58b4dc2c9037f50e233175086337e0b305e356d28211bf55f21c504cbd3
|
||||
url: "https://pub.dev"
|
||||
sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
version: "1.0.0"
|
||||
jni_flutter:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: jni_flutter
|
||||
sha256: "7b717011ea40d04fd47c2731d3d1d36eb99eba3435c2753d62489e8c3c9991d5"
|
||||
url: "https://pub.dev"
|
||||
sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
jni_util:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: jni_util
|
||||
sha256: "1ba86da04a5f2bf18fde2edb235587e70c5b0fc5bd4ba955f46b00942c3fc35f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
version: "1.0.1"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "11.0.2"
|
||||
leak_tracker_flutter_testing:
|
||||
@@ -245,7 +245,7 @@ packages:
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.10"
|
||||
leak_tracker_testing:
|
||||
@@ -253,7 +253,7 @@ packages:
|
||||
description:
|
||||
name: leak_tracker_testing
|
||||
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
lints:
|
||||
@@ -261,7 +261,7 @@ packages:
|
||||
description:
|
||||
name: lints
|
||||
sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.1.0"
|
||||
logging:
|
||||
@@ -269,7 +269,7 @@ packages:
|
||||
description:
|
||||
name: logging
|
||||
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
matcher:
|
||||
@@ -277,7 +277,7 @@ packages:
|
||||
description:
|
||||
name: matcher
|
||||
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.12.19"
|
||||
material_color_utilities:
|
||||
@@ -285,7 +285,7 @@ packages:
|
||||
description:
|
||||
name: material_color_utilities
|
||||
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.13.0"
|
||||
meta:
|
||||
@@ -293,31 +293,31 @@ packages:
|
||||
description:
|
||||
name: meta
|
||||
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.18.0"
|
||||
objective_c:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: objective_c
|
||||
sha256: b7fb95a6d9a4f009edd63dc5ac69f07420b23a16161c6dd8660290b59c602e8e
|
||||
url: "https://pub.dev"
|
||||
sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "9.5.0"
|
||||
version: "9.4.1"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_config
|
||||
sha256: ffcf4cf3d6c0b74ac43708d9f56625506e8a68aa935abe9d267a7330f320eb5d
|
||||
url: "https://pub.dev"
|
||||
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
version: "2.2.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path
|
||||
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
path_provider:
|
||||
@@ -325,7 +325,7 @@ packages:
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.6"
|
||||
path_provider_android:
|
||||
@@ -333,7 +333,7 @@ packages:
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.3.1"
|
||||
path_provider_foundation:
|
||||
@@ -341,7 +341,7 @@ packages:
|
||||
description:
|
||||
name: path_provider_foundation
|
||||
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.6.0"
|
||||
path_provider_linux:
|
||||
@@ -349,7 +349,7 @@ packages:
|
||||
description:
|
||||
name: path_provider_linux
|
||||
sha256: "58c2005f147315b11e9b4a7bc889cd5203e250cba8e3f012dae259b4972b5c16"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.2.2"
|
||||
path_provider_platform_interface:
|
||||
@@ -357,7 +357,7 @@ packages:
|
||||
description:
|
||||
name: path_provider_platform_interface
|
||||
sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
path_provider_windows:
|
||||
@@ -365,7 +365,7 @@ packages:
|
||||
description:
|
||||
name: path_provider_windows
|
||||
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
platform:
|
||||
@@ -373,7 +373,7 @@ packages:
|
||||
description:
|
||||
name: platform
|
||||
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.1.6"
|
||||
plugin_platform_interface:
|
||||
@@ -381,7 +381,7 @@ packages:
|
||||
description:
|
||||
name: plugin_platform_interface
|
||||
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.8"
|
||||
pub_semver:
|
||||
@@ -389,7 +389,7 @@ packages:
|
||||
description:
|
||||
name: pub_semver
|
||||
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
record_use:
|
||||
@@ -397,7 +397,7 @@ packages:
|
||||
description:
|
||||
name: record_use
|
||||
sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.6.0"
|
||||
sky_engine:
|
||||
@@ -410,7 +410,7 @@ packages:
|
||||
description:
|
||||
name: source_span
|
||||
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.10.2"
|
||||
stack_trace:
|
||||
@@ -418,7 +418,7 @@ packages:
|
||||
description:
|
||||
name: stack_trace
|
||||
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.12.1"
|
||||
stream_channel:
|
||||
@@ -426,7 +426,7 @@ packages:
|
||||
description:
|
||||
name: stream_channel
|
||||
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
string_scanner:
|
||||
@@ -434,7 +434,7 @@ packages:
|
||||
description:
|
||||
name: string_scanner
|
||||
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
term_glyph:
|
||||
@@ -442,7 +442,7 @@ packages:
|
||||
description:
|
||||
name: term_glyph
|
||||
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.2.2"
|
||||
test_api:
|
||||
@@ -450,23 +450,95 @@ packages:
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.7.11"
|
||||
tobias:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: tobias
|
||||
sha256: "1cfd203bf57f8d4daa3e734d24ef97568eb5265af62d1498c32da4e69905c70f"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "5.3.4"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: typed_data
|
||||
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
url_launcher:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: url_launcher
|
||||
sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.3.2"
|
||||
url_launcher_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: b413d49b73867ac08dd2f9890efd3cc11f2a0e577618d50843440a1fb3776c32
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.3.32"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_ios
|
||||
sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.4.1"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_linux
|
||||
sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.2.2"
|
||||
url_launcher_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_macos
|
||||
sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.2.5"
|
||||
url_launcher_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_platform_interface
|
||||
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
url_launcher_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_web
|
||||
sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.4.3"
|
||||
url_launcher_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_windows
|
||||
sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.1.5"
|
||||
uuid:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: uuid
|
||||
sha256: "9b129329f58692f6e6578329498a8fe9fbe98f090beb764ffbb8ee2eadd01dcd"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.6.0"
|
||||
vector_math:
|
||||
@@ -474,7 +546,7 @@ packages:
|
||||
description:
|
||||
name: vector_math
|
||||
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
vm_service:
|
||||
@@ -482,7 +554,7 @@ packages:
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "15.2.0"
|
||||
web:
|
||||
@@ -490,7 +562,7 @@ packages:
|
||||
description:
|
||||
name: web
|
||||
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
win32:
|
||||
@@ -498,7 +570,7 @@ packages:
|
||||
description:
|
||||
name: win32
|
||||
sha256: ba6f4bba816c8d7e3c1580e170f3786d216951cc6b94babc3b814c08d2cb2738
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.3.0"
|
||||
xdg_directories:
|
||||
@@ -506,7 +578,7 @@ packages:
|
||||
description:
|
||||
name: xdg_directories
|
||||
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
yaml:
|
||||
@@ -514,9 +586,9 @@ packages:
|
||||
description:
|
||||
name: yaml
|
||||
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.12.2 <4.0.0"
|
||||
flutter: ">=3.38.4"
|
||||
flutter: ">=3.44.0"
|
||||
|
||||
@@ -38,6 +38,9 @@ dependencies:
|
||||
http: ^1.6.0
|
||||
flutter_secure_storage: ^10.3.1
|
||||
uuid: ^4.6.0
|
||||
fluwx: ^6.0.2
|
||||
tobias: ^5.3.4
|
||||
url_launcher: ^6.3.2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Platform API
|
||||
|
||||
平台总后台 API 严格沿用 `sample/server` 的 BSM-SDK Core 分层和运行方式。
|
||||
平台总后台 API 采用 BSM-SDK Core 分层和运行方式。
|
||||
|
||||
启动前请设置运行配置;JWT 密钥必须为 16、24 或 32 个字符。
|
||||
|
||||
|
||||
@@ -153,7 +153,7 @@ func writeDeliveryResourceContract(output io.Writer) error {
|
||||
}
|
||||
|
||||
func writeMockData() error {
|
||||
config.New("platform")
|
||||
config.New("heqi")
|
||||
if config.Spec.Databases == nil {
|
||||
return fmt.Errorf("database configuration is required")
|
||||
}
|
||||
@@ -175,7 +175,7 @@ func writeMockData() error {
|
||||
}
|
||||
|
||||
func migrateDatabase() error {
|
||||
config.New("platform")
|
||||
config.New("heqi")
|
||||
if config.Spec.Databases == nil {
|
||||
return fmt.Errorf("database configuration is required")
|
||||
}
|
||||
@@ -196,6 +196,9 @@ func migrateDatabase() error {
|
||||
if err := prepareAdditiveMigrations(migrationDatabase, driver); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := resetLegacyPaymentSchema(migrationDatabase, driver); err != nil {
|
||||
return err
|
||||
}
|
||||
const legacyPhoneIndex = "idx_platform_account_phone"
|
||||
if migrationDatabase.Migrator().HasIndex(&models.PlatformAccount{}, legacyPhoneIndex) {
|
||||
if err := migrationDatabase.Migrator().DropIndex(&models.PlatformAccount{}, legacyPhoneIndex); err != nil {
|
||||
@@ -213,6 +216,30 @@ func migrateDatabase() error {
|
||||
return initdb.New(databaseService)
|
||||
}
|
||||
|
||||
// resetLegacyPaymentSchema 执行经业务明确授权的开发期破坏性支付模型重置,不迁移旧支付或退款历史。
|
||||
func resetLegacyPaymentSchema(databaseService *gorm.DB, driver string) error {
|
||||
statements := []string{}
|
||||
if driver == "postgres" {
|
||||
statements = append(statements,
|
||||
`DROP TABLE IF EXISTS "wallet_refund" CASCADE`,
|
||||
`DROP TABLE IF EXISTS "wallet_payment" CASCADE`,
|
||||
`DROP TABLE IF EXISTS "gasorder_payment" CASCADE`,
|
||||
)
|
||||
} else {
|
||||
statements = append(statements,
|
||||
`DROP TABLE IF EXISTS wallet_refund`,
|
||||
`DROP TABLE IF EXISTS wallet_payment`,
|
||||
`DROP TABLE IF EXISTS gasorder_payment`,
|
||||
)
|
||||
}
|
||||
for _, statement := range statements {
|
||||
if err := databaseService.Exec(statement).Error; err != nil {
|
||||
return fmt.Errorf("reset legacy payment schema: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// prepareAdditiveMigrations 先处理无法由 GORM AutoMigrate 安全完成的新增非空字段。
|
||||
// 旧轨迹没有服务端接收时间时,以定位发生时间(再退化到创建时间)回填,
|
||||
// 避免直接 ADD NOT NULL 因历史行存在而中断整库迁移。
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// 平台 HTTP API 进程入口,沿用 sample/server 的 BSM 启动规范。
|
||||
// 平台 HTTP API 进程入口,沿用仓库统一的 BSM 启动规范。
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Service: platform
|
||||
Service: heqi
|
||||
Port: 12426
|
||||
|
||||
Databases:
|
||||
@@ -22,3 +22,28 @@ Global:
|
||||
DeliveryArrivalRadiusMeters: 200
|
||||
UploadVideoMaxSize: 104857600
|
||||
FieldEncryptionKey: change-me-32-byte-development-key
|
||||
|
||||
Payment:
|
||||
ExpireMinutes: 30
|
||||
RefundWindowDays: 3
|
||||
InternalServiceToken: change-me-payment-worker-token
|
||||
Alipay:
|
||||
Enabled: false
|
||||
Production: false
|
||||
AppID: ""
|
||||
PrivateKeyPath: ""
|
||||
AppPublicCertPath: ""
|
||||
AlipayPublicCertPath: ""
|
||||
AlipayRootCertPath: ""
|
||||
NotifyURL: "https://example.invalid/heqi/payment-return/v1/alipay/notify"
|
||||
ReturnURL: "https://example.invalid/payment/result"
|
||||
Wechat:
|
||||
Enabled: false
|
||||
MerchantID: ""
|
||||
MerchantCertificateSerial: ""
|
||||
MerchantPrivateKeyPath: ""
|
||||
APIv3Key: ""
|
||||
AppAppID: ""
|
||||
OfficialAccountAppID: ""
|
||||
MiniProgramAppID: ""
|
||||
NotifyURL: "https://example.invalid/heqi/payment-return/v1/wechat/notify"
|
||||
@@ -54,8 +54,13 @@ require (
|
||||
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||
github.com/redis/go-redis/v9 v9.19.0 // indirect
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
|
||||
github.com/smartwalle/alipay/v3 v3.2.31 // indirect
|
||||
github.com/smartwalle/ncrypto v1.0.4 // indirect
|
||||
github.com/smartwalle/ngx v1.1.2 // indirect
|
||||
github.com/smartwalle/nsign v1.0.9 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||
github.com/wechatpay-apiv3/wechatpay-go v0.2.21 // indirect
|
||||
go.etcd.io/etcd/api/v3 v3.6.11 // indirect
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.6.11 // indirect
|
||||
go.etcd.io/etcd/client/v3 v3.6.11 // indirect
|
||||
|
||||
@@ -4,6 +4,7 @@ git.apinb.com/bsm-sdk/core v0.2.0 h1:/e9yqpsbKBrRgMiGpS3KX2O4qDLxo5V5GpPSLNGxEKw
|
||||
git.apinb.com/bsm-sdk/core v0.2.0/go.mod h1:E9T6Eboo/0Zb36BjkKbIgvFzq4fQ2Q8P/7y5zmYTI6Y=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
||||
github.com/agiledragon/gomonkey v2.0.2+incompatible/go.mod h1:2NGfXu1a80LLr2cmWXGBDaHEjb1idR6+FVlX5T3D9hw=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
@@ -116,6 +117,14 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||
github.com/smartwalle/alipay/v3 v3.2.31 h1:KVwWJZ5JvNxPridHrcpTwgJeMzi1IqZopid0sDJQewU=
|
||||
github.com/smartwalle/alipay/v3 v3.2.31/go.mod h1:0G9wqvo1719hxo6ZWntujuPNyVQFWikS52LF62rRYDg=
|
||||
github.com/smartwalle/ncrypto v1.0.4 h1:P2rqQxDepJwgeO5ShoC+wGcK2wNJDmcdBOWAksuIgx8=
|
||||
github.com/smartwalle/ncrypto v1.0.4/go.mod h1:Dwlp6sfeNaPMnOxMNayMTacvC5JGEVln3CVdiVDgbBk=
|
||||
github.com/smartwalle/ngx v1.1.2 h1:W+K262lHUvdfJ/2e762dIFwN543kxC/4qLkuCQLRS8o=
|
||||
github.com/smartwalle/ngx v1.1.2/go.mod h1:mx/nz2Pk5j+RBs7t6u6k22MPiBG/8CtOMpCnALIG8Y0=
|
||||
github.com/smartwalle/nsign v1.0.9 h1:8poAgG7zBd8HkZy9RQDwasC6XZvJpDGQWSjzL2FZL6E=
|
||||
github.com/smartwalle/nsign v1.0.9/go.mod h1:eY6I4CJlyNdVMP+t6z1H6Jpd4m5/V+8xi44ufSTxXgc=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
@@ -124,6 +133,7 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
@@ -132,6 +142,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
github.com/wechatpay-apiv3/wechatpay-go v0.2.21 h1:uIyMpzvcaHA33W/QPtHstccw+X52HO1gFdvVL9O6Lfs=
|
||||
github.com/wechatpay-apiv3/wechatpay-go v0.2.21/go.mod h1:A254AUBVB6R+EqQFo3yTgeh7HtyqRRtN2w9hQSOrd4Q=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Package config 沿用 sample/server 的 BSM 配置加载与校验方式。
|
||||
// Package config 使用仓库统一的 BSM 配置加载与校验方式。
|
||||
package config
|
||||
|
||||
import (
|
||||
@@ -31,7 +31,42 @@ type WalletConfig struct {
|
||||
ManualRechargeMaxAmount int64 `yaml:"-"`
|
||||
}
|
||||
|
||||
// SrvConfig 与 sample/server 配置结构保持一致。
|
||||
// AlipayConfig 保存支付宝证书模式商户配置。私钥和证书只允许服务端读取。
|
||||
type AlipayConfig struct {
|
||||
Enabled bool `yaml:"Enabled"`
|
||||
Production bool `yaml:"Production"`
|
||||
AppID string `yaml:"AppID"`
|
||||
PrivateKeyPath string `yaml:"PrivateKeyPath"`
|
||||
AppPublicCertPath string `yaml:"AppPublicCertPath"`
|
||||
AlipayPublicCertPath string `yaml:"AlipayPublicCertPath"`
|
||||
AlipayRootCertPath string `yaml:"AlipayRootCertPath"`
|
||||
NotifyURL string `yaml:"NotifyURL"`
|
||||
ReturnURL string `yaml:"ReturnURL"`
|
||||
}
|
||||
|
||||
// WechatPayConfig 保存微信支付 API v3 平台单商户配置。
|
||||
type WechatPayConfig struct {
|
||||
Enabled bool `yaml:"Enabled"`
|
||||
MerchantID string `yaml:"MerchantID"`
|
||||
MerchantCertificateSerial string `yaml:"MerchantCertificateSerial"`
|
||||
MerchantPrivateKeyPath string `yaml:"MerchantPrivateKeyPath"`
|
||||
APIv3Key string `yaml:"APIv3Key"`
|
||||
AppAppID string `yaml:"AppAppID"`
|
||||
OfficialAccountAppID string `yaml:"OfficialAccountAppID"`
|
||||
MiniProgramAppID string `yaml:"MiniProgramAppID"`
|
||||
NotifyURL string `yaml:"NotifyURL"`
|
||||
}
|
||||
|
||||
// PaymentConfig 保存统一支付、退款和 Worker 内部调用参数。
|
||||
type PaymentConfig struct {
|
||||
ExpireMinutes int `yaml:"ExpireMinutes"`
|
||||
RefundWindowDays int `yaml:"RefundWindowDays"`
|
||||
InternalServiceToken string `yaml:"InternalServiceToken"`
|
||||
Alipay AlipayConfig `yaml:"Alipay"`
|
||||
Wechat WechatPayConfig `yaml:"Wechat"`
|
||||
}
|
||||
|
||||
// SrvConfig 与仓库现有进程的配置结构保持一致。
|
||||
type SrvConfig struct {
|
||||
conf.Base `yaml:",inline"`
|
||||
Databases *conf.DBConf `yaml:"Databases"`
|
||||
@@ -39,6 +74,7 @@ type SrvConfig struct {
|
||||
Apm *conf.ApmConf `yaml:"APM"`
|
||||
Global GlobalConfig `yaml:"Global"`
|
||||
Wallet WalletConfig `yaml:"-"`
|
||||
Payment PaymentConfig `yaml:"Payment"`
|
||||
}
|
||||
|
||||
// New 初始化 BSM 配置并校验服务监听地址。
|
||||
@@ -64,6 +100,9 @@ func New(srvKey string) {
|
||||
panic("Global.FieldEncryptionKey must contain at least 32 characters")
|
||||
}
|
||||
Spec.Wallet.ManualRechargeMaxAmount = Spec.Global.ManualRechargeMaxAmount
|
||||
if Spec.Payment.ExpireMinutes <= 0 || Spec.Payment.RefundWindowDays <= 0 {
|
||||
panic("Payment expiration and refund window must be greater than zero")
|
||||
}
|
||||
registerURL, err := url.ParseRequestURI(Spec.Global.UserRegisterURL)
|
||||
if err != nil || (registerURL.Scheme != "http" && registerURL.Scheme != "https") || registerURL.Host == "" {
|
||||
panic("Global.UserRegisterURL must be a valid HTTP or HTTPS URL")
|
||||
|
||||
@@ -17,7 +17,7 @@ var (
|
||||
MemoryService *cache.Cache
|
||||
)
|
||||
|
||||
// NewImpl 与 sample/server 保持一致,所有进程通过此处创建共享基础设施。
|
||||
// NewImpl 遵循仓库统一约定,所有进程通过此处创建共享基础设施。
|
||||
func NewImpl() {
|
||||
MemoryService = with.Memory(nil)
|
||||
RedisService = with.RedisCache(config.Spec.Cache)
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"git.apinb.com/bsm-sdk/core/infra"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
|
||||
common "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/payment"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -36,6 +37,36 @@ func ServiceRelation(ctx *gin.Context) {
|
||||
infra.Response.Success(ctx, response)
|
||||
}
|
||||
|
||||
// PayGasOrder 为本人未履约供气订单创建统一第三方支付单。
|
||||
func PayGasOrder(ctx *gin.Context) {
|
||||
account, ok := common.UserAccount(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var request struct {
|
||||
RequestNo string `json:"request_no" binding:"required"`
|
||||
Channel string `json:"channel" binding:"required,oneof=alipay wechat"`
|
||||
PayType string `json:"pay_type" binding:"required"`
|
||||
OpenID string `json:"openid"`
|
||||
}
|
||||
if ctx.ShouldBindJSON(&request) != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
var order models.GasorderBasic
|
||||
if err := impl.DBService.Where("identity = ? AND user_account_id = ? AND order_status IN ?", ctx.Param("identity"), account.ID, []int{common.StatusCreated, common.StatusAssigned}).First(&order).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
payOrder, err := payment.Create(ctx, payment.CreateInput{RequestNo: request.RequestNo, BusinessType: "gasorder", BusinessIdentity: order.Identity,
|
||||
UserIdentity: account.Identity, Channel: request.Channel, PayType: request.PayType, Subject: "和气供气订单 " + order.OrderNo, OpenID: request.OpenID, Amount: order.PayableAmount})
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, payment.PublicResponse(payOrder))
|
||||
}
|
||||
|
||||
// ListGasContracts 返回用户自己的供气合同。
|
||||
func ListGasContracts(ctx *gin.Context) {
|
||||
account, ok := common.UserAccount(ctx)
|
||||
@@ -47,7 +78,18 @@ func ListGasContracts(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, common.ResourceResponse(list))
|
||||
response := make([]gin.H, 0, len(list))
|
||||
for _, order := range list {
|
||||
var items []models.GasorderItem
|
||||
if err := impl.DBService.Where("gasorder_basic_id = ?", order.ID).Find(&items).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
value := common.ResourceResponse(order).(map[string]any)
|
||||
value["items"] = common.ResourceResponse(items)
|
||||
response = append(response, value)
|
||||
}
|
||||
infra.Response.Success(ctx, response)
|
||||
}
|
||||
|
||||
// ListGasOrders 返回用户自己的供气订单。
|
||||
|
||||
48
backend/api/internal/logic/client/user/refund.go
Normal file
48
backend/api/internal/logic/client/user/refund.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"git.apinb.com/bsm-sdk/core/errcode"
|
||||
"git.apinb.com/bsm-sdk/core/infra"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/payment"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func CreateRefund(businessType string) gin.HandlerFunc {
|
||||
return func(ctx *gin.Context) {
|
||||
account, ok := common.UserAccount(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var request struct {
|
||||
RequestNo string `json:"request_no" binding:"required"`
|
||||
Reason string `json:"reason" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
Items []payment.RefundItemInput `json:"items" binding:"required,min=1"`
|
||||
}
|
||||
if ctx.ShouldBindJSON(&request) != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
refund, err := payment.CreateRefund(account.ID, account.Identity, businessType, ctx.Param("identity"), payment.RefundInput{RequestNo: request.RequestNo, Reason: request.Reason, Description: request.Description, Items: request.Items})
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, common.ResourceResponse(refund))
|
||||
}
|
||||
}
|
||||
func ListRefunds(ctx *gin.Context) {
|
||||
account, ok := common.UserAccount(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var list []models.PaymentRefund
|
||||
if err := impl.DBService.Where("user_identity = ?", account.Identity).Order("created_at desc").Find(&list).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, common.ResourceResponse(list))
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"git.apinb.com/bsm-sdk/core/infra"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
|
||||
common "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/payment"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
@@ -21,7 +22,18 @@ func PublicProducts(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, common.ResourceResponse(list))
|
||||
response := make([]gin.H, 0, len(list))
|
||||
for _, order := range list {
|
||||
var items []models.EcOrderItem
|
||||
if err := impl.DBService.Where("ec_order_id = ?", order.ID).Find(&items).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
value := common.ResourceResponse(order).(map[string]any)
|
||||
value["items"] = common.ResourceResponse(items)
|
||||
response = append(response, value)
|
||||
}
|
||||
infra.Response.Success(ctx, response)
|
||||
}
|
||||
|
||||
// CreateShopOrder 按服务端价格创建订单并原子扣减库存。
|
||||
@@ -150,13 +162,31 @@ func PayShopOrder(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
var request struct {
|
||||
PaymentPassword string `json:"payment_password" binding:"required"`
|
||||
PaymentPassword string `json:"payment_password"`
|
||||
RequestNo string `json:"request_no" binding:"required"`
|
||||
Channel string `json:"channel" binding:"required,oneof=wallet alipay wechat"`
|
||||
PayType string `json:"pay_type"`
|
||||
OpenID string `json:"openid"`
|
||||
}
|
||||
if ctx.ShouldBindJSON(&request) != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
if request.Channel != "wallet" {
|
||||
var order models.EcOrder
|
||||
if err := impl.DBService.Where("identity = ? AND user_account_id = ? AND order_status = ?", ctx.Param("identity"), account.ID, 16).First(&order).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
payOrder, err := payment.Create(ctx, payment.CreateInput{RequestNo: request.RequestNo, BusinessType: "ec_order", BusinessIdentity: order.Identity,
|
||||
UserIdentity: account.Identity, Channel: request.Channel, PayType: request.PayType, Subject: "和气商城订单 " + order.OrderNo, OpenID: request.OpenID, Amount: order.PayableAmount})
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, payment.PublicResponse(payOrder))
|
||||
return
|
||||
}
|
||||
err := impl.DBService.Transaction(func(tx *gorm.DB) error {
|
||||
var order models.EcOrder
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"git.apinb.com/bsm-sdk/core/infra"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/config"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/payment"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
@@ -126,6 +127,8 @@ func CreateRecharge(client string) gin.HandlerFunc {
|
||||
Amount int64 `json:"amount" binding:"required,gt=0"`
|
||||
Channel string `json:"channel" binding:"required,oneof=mock wechat alipay"`
|
||||
RequestNo string `json:"request_no" binding:"required"`
|
||||
PayType string `json:"pay_type"`
|
||||
OpenID string `json:"openid"`
|
||||
}
|
||||
if ctx.ShouldBindJSON(&request) != nil || request.Amount > config.Spec.Global.ManualRechargeMaxAmount ||
|
||||
(request.Channel == "mock" && !config.Spec.Global.MockPaymentEnabled) {
|
||||
@@ -150,6 +153,16 @@ func CreateRecharge(client string) gin.HandlerFunc {
|
||||
}
|
||||
order = existing
|
||||
}
|
||||
if request.Channel != "mock" {
|
||||
payOrder, payErr := payment.Create(ctx, payment.CreateInput{RequestNo: request.RequestNo, BusinessType: "recharge", BusinessIdentity: order.Identity,
|
||||
UserIdentity: owner.Identity, Channel: request.Channel, PayType: request.PayType, Subject: "和气钱包充值 " + order.RechargeNo, OpenID: request.OpenID, Amount: order.Amount})
|
||||
if payErr != nil {
|
||||
infra.Response.Error(ctx, payErr)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, payment.PublicResponse(payOrder))
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, ResourceResponse(order))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package common
|
||||
package common
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -419,7 +419,7 @@ var relationIdentityModels = map[string]any{
|
||||
"gasorder_track_id": &models.GasorderTrack{},
|
||||
"platform_role_id": &models.PlatformRole{},
|
||||
"wallet_basic_id": &models.WalletBasic{},
|
||||
"wallet_payment_id": &models.WalletPayment{},
|
||||
"payment_order_id": &models.PaymentOrder{},
|
||||
"wallet_bank_id": &models.WalletBank{},
|
||||
"related_record_id": &models.WalletRecord{},
|
||||
}
|
||||
|
||||
@@ -56,9 +56,9 @@ func listWalletChild(ctx *gin.Context, model any, table string) {
|
||||
}
|
||||
|
||||
func ListBank(ctx *gin.Context) { listWalletChild(ctx, &models.WalletBank{}, "wallet_bank") }
|
||||
func ListPayment(ctx *gin.Context) { listWalletChild(ctx, &models.WalletPayment{}, "wallet_payment") }
|
||||
func ListPayment(ctx *gin.Context) { listWalletChild(ctx, &models.PaymentOrder{}, "payment_order") }
|
||||
func ListRecord(ctx *gin.Context) { listWalletChild(ctx, &models.WalletRecord{}, "wallet_record") }
|
||||
func ListRefund(ctx *gin.Context) { listWalletChild(ctx, &models.WalletRefund{}, "wallet_refund") }
|
||||
func ListRefund(ctx *gin.Context) { listWalletChild(ctx, &models.PaymentRefund{}, "payment_refund") }
|
||||
func ListRecharge(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
@@ -87,9 +87,9 @@ func getWalletChild(ctx *gin.Context, model any, table string) {
|
||||
}
|
||||
|
||||
func GetBank(ctx *gin.Context) { getWalletChild(ctx, &models.WalletBank{}, "wallet_bank") }
|
||||
func GetPayment(ctx *gin.Context) { getWalletChild(ctx, &models.WalletPayment{}, "wallet_payment") }
|
||||
func GetPayment(ctx *gin.Context) { getWalletChild(ctx, &models.PaymentOrder{}, "payment_order") }
|
||||
func GetRecord(ctx *gin.Context) { getWalletChild(ctx, &models.WalletRecord{}, "wallet_record") }
|
||||
func GetRefund(ctx *gin.Context) { getWalletChild(ctx, &models.WalletRefund{}, "wallet_refund") }
|
||||
func GetRefund(ctx *gin.Context) { getWalletChild(ctx, &models.PaymentRefund{}, "payment_refund") }
|
||||
func GetRecharge(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
|
||||
@@ -32,9 +32,9 @@ var adminMenus = []Menu{
|
||||
{Identity: "finance", GroupCode: "finance", Name: "财务管理", Icon: "icon-bar-chart", Path: "/finance", SortNo: 70, Status: common.StatusEnable},
|
||||
{Identity: "wallet_basic", ParentIdentity: "finance", GroupCode: "finance", Name: "钱包", Path: "/finance/wallet", SortNo: 1, Status: common.StatusEnable},
|
||||
{Identity: "wallet_bank", ParentIdentity: "finance", GroupCode: "finance", Name: "银行卡", Path: "/finance/banks", SortNo: 2, Status: common.StatusEnable},
|
||||
{Identity: "wallet_payment", ParentIdentity: "finance", GroupCode: "finance", Name: "支付记录", Path: "/finance/payments", SortNo: 3, Status: common.StatusEnable},
|
||||
{Identity: "payment_order", ParentIdentity: "finance", GroupCode: "finance", Name: "支付记录", Path: "/finance/payments", SortNo: 3, Status: common.StatusEnable},
|
||||
{Identity: "wallet_record", ParentIdentity: "finance", GroupCode: "finance", Name: "钱包流水", Path: "/finance/records", SortNo: 4, Status: common.StatusEnable},
|
||||
{Identity: "wallet_refund", ParentIdentity: "finance", GroupCode: "finance", Name: "退款记录", Path: "/finance/refunds", SortNo: 5, Status: common.StatusEnable},
|
||||
{Identity: "payment_refund", ParentIdentity: "finance", GroupCode: "finance", Name: "退款记录", Path: "/finance/refunds", SortNo: 5, Status: common.StatusEnable},
|
||||
{Identity: "wallet_recharge", ParentIdentity: "finance", GroupCode: "finance", Name: "钱包充值", Path: "/finance/recharge", SortNo: 6, Status: common.StatusEnable},
|
||||
{Identity: "wallet_apply_cash", ParentIdentity: "finance", GroupCode: "finance", Name: "提现申请", Path: "/finance/withdrawals", SortNo: 7, Status: common.StatusEnable},
|
||||
{Identity: "fin_settlement", ParentIdentity: "finance", GroupCode: "finance", Name: "结算结果", Path: "/finance/settlements", SortNo: 8, Status: common.StatusEnable},
|
||||
|
||||
@@ -371,8 +371,8 @@ func AdjustOrderAmount(ctx *gin.Context) {
|
||||
}
|
||||
var paymentCount int64
|
||||
if err := tx.Model(&models.GasorderPayment{}).
|
||||
Joins("JOIN wallet_payment ON wallet_payment.id = gasorder_payment.wallet_payment_id").
|
||||
Where("gasorder_payment.gasorder_basic_id = ? AND wallet_payment.payment_status = ?", order.ID, common.StatusPaid).
|
||||
Joins("JOIN payment_order ON payment_order.id = gasorder_payment.payment_order_id").
|
||||
Where("gasorder_payment.gasorder_basic_id = ? AND payment_order.payment_status = ?", order.ID, 23).
|
||||
Count(&paymentCount).Error; err != nil || paymentCount > 0 {
|
||||
return errors.New("paid order cannot be adjusted")
|
||||
}
|
||||
|
||||
@@ -22,9 +22,9 @@ func ExpectedResources() []ResourceContract {
|
||||
{"gasorder", "gasorder_basic", "/gasorder_basic", "list", "append_only"},
|
||||
{"finance", "wallet_basic", "/wallet_basic", "list", "readonly"},
|
||||
{"finance", "wallet_bank", "/wallet_bank", "list", "readonly"},
|
||||
{"finance", "wallet_payment", "/wallet_payment", "list", "readonly"},
|
||||
{"finance", "payment_order", "/payment_order", "list", "readonly"},
|
||||
{"finance", "wallet_record", "/wallet_record", "list", "readonly"},
|
||||
{"finance", "wallet_refund", "/wallet_refund", "list", "readonly"},
|
||||
{"finance", "payment_refund", "/payment_refund", "list", "readonly"},
|
||||
{"finance", "wallet_recharge", "/wallet_recharge", "list", "append_only"},
|
||||
{"finance", "wallet_apply_cash", "/wallet_apply_cash", "list", "append_only"},
|
||||
{"finance", "fin_settlement", "/fin_settlement", "list", "readonly"},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package gas
|
||||
package gas
|
||||
|
||||
import (
|
||||
"strings"
|
||||
@@ -45,14 +45,14 @@ func listWalletChild(ctx *gin.Context, model any, table string) {
|
||||
}
|
||||
|
||||
func ListWalletBank(ctx *gin.Context) { listWalletChild(ctx, &models.WalletBank{}, "wallet_bank") }
|
||||
func ListWalletPayment(ctx *gin.Context) {
|
||||
listWalletChild(ctx, &models.WalletPayment{}, "wallet_payment")
|
||||
func ListPaymentOrder(ctx *gin.Context) {
|
||||
listWalletChild(ctx, &models.PaymentOrder{}, "payment_order")
|
||||
}
|
||||
func ListWalletRecord(ctx *gin.Context) {
|
||||
listWalletChild(ctx, &models.WalletRecord{}, "wallet_record")
|
||||
}
|
||||
func ListWalletRefund(ctx *gin.Context) {
|
||||
listWalletChild(ctx, &models.WalletRefund{}, "wallet_refund")
|
||||
func ListPaymentRefund(ctx *gin.Context) {
|
||||
listWalletChild(ctx, &models.PaymentRefund{}, "payment_refund")
|
||||
}
|
||||
func ListWalletApplyCash(ctx *gin.Context) {
|
||||
listWalletChild(ctx, &models.WalletApplyCash{}, "wallet_apply_cash")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package gas
|
||||
package gas
|
||||
|
||||
import "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
||||
|
||||
@@ -35,9 +35,9 @@ var adminMenus = []Menu{
|
||||
{Identity: "finance", GroupCode: "finance", Name: "财务管理", Icon: "icon-bar-chart", Path: "/finance", SortNo: 70, Status: common.StatusEnable},
|
||||
{Identity: "wallet_basic", ParentIdentity: "finance", GroupCode: "finance", Name: "钱包", Path: "/finance/wallet", SortNo: 1, Status: common.StatusEnable},
|
||||
{Identity: "wallet_bank", ParentIdentity: "finance", GroupCode: "finance", Name: "银行卡", Path: "/finance/banks", SortNo: 2, Status: common.StatusEnable},
|
||||
{Identity: "wallet_payment", ParentIdentity: "finance", GroupCode: "finance", Name: "支付记录", Path: "/finance/payments", SortNo: 3, Status: common.StatusEnable},
|
||||
{Identity: "payment_order", ParentIdentity: "finance", GroupCode: "finance", Name: "支付记录", Path: "/finance/payments", SortNo: 3, Status: common.StatusEnable},
|
||||
{Identity: "wallet_record", ParentIdentity: "finance", GroupCode: "finance", Name: "钱包流水", Path: "/finance/records", SortNo: 4, Status: common.StatusEnable},
|
||||
{Identity: "wallet_refund", ParentIdentity: "finance", GroupCode: "finance", Name: "退款记录", Path: "/finance/refunds", SortNo: 5, Status: common.StatusEnable},
|
||||
{Identity: "payment_refund", ParentIdentity: "finance", GroupCode: "finance", Name: "退款记录", Path: "/finance/refunds", SortNo: 5, Status: common.StatusEnable},
|
||||
{Identity: "wallet_apply_cash", ParentIdentity: "finance", GroupCode: "finance", Name: "提现申请", Path: "/finance/withdrawals", SortNo: 6, Status: common.StatusEnable},
|
||||
{Identity: "fin_settlement", ParentIdentity: "finance", GroupCode: "finance", Name: "财务结算", Path: "/finance/settlements", SortNo: 7, Status: common.StatusEnable},
|
||||
{Identity: "fin_reconciliation", ParentIdentity: "finance", GroupCode: "finance", Name: "财务对账", Path: "/finance/reconciliations", SortNo: 8, Status: common.StatusEnable},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package gas
|
||||
package gas
|
||||
|
||||
type ResourceMode string
|
||||
|
||||
@@ -30,8 +30,8 @@ func ExpectedResources() []ResourceContract {
|
||||
{"contract", "gasorder_contract_revision", ReadOnly}, {"gasorder", "gasorder_basic", AppendOnly},
|
||||
{"contract", "product_info", ReadOnly},
|
||||
{"finance", "wallet_basic", ReadOnly}, {"finance", "wallet_bank", ReadOnly},
|
||||
{"finance", "wallet_payment", ReadOnly}, {"finance", "wallet_record", ReadOnly},
|
||||
{"finance", "wallet_refund", ReadOnly}, {"finance", "wallet_apply_cash", AppendOnly},
|
||||
{"finance", "payment_order", ReadOnly}, {"finance", "wallet_record", ReadOnly},
|
||||
{"finance", "payment_refund", ReadOnly}, {"finance", "wallet_apply_cash", AppendOnly},
|
||||
{"finance", "fin_settlement", ReadOnly}, {"finance", "fin_reconciliation", ReadOnly},
|
||||
{"ticket", "cs_ticket", Writable},
|
||||
}
|
||||
|
||||
129
backend/api/internal/logic/payment/callback.go
Normal file
129
backend/api/internal/logic/payment/callback.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/config"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/core/auth/verifiers"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/core/downloader"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/core/notify"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/services/payments"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
func digest(value string) string {
|
||||
sum := sha256.Sum256([]byte(value))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
// AlipayNotify 验证支付宝证书签名、商户身份、金额和状态后幂等入账。
|
||||
func AlipayNotify(ctx *gin.Context) {
|
||||
client, err := alipayClient()
|
||||
if err != nil {
|
||||
ctx.String(http.StatusServiceUnavailable, "failure")
|
||||
return
|
||||
}
|
||||
if err = ctx.Request.ParseForm(); err != nil || client.VerifySign(ctx, ctx.Request.PostForm) != nil {
|
||||
ctx.String(http.StatusBadRequest, "failure")
|
||||
return
|
||||
}
|
||||
values := ctx.Request.PostForm
|
||||
amount, amountErr := strconv.ParseFloat(values.Get("total_amount"), 64)
|
||||
if amountErr != nil || values.Get("app_id") != config.Spec.Payment.Alipay.AppID || (values.Get("trade_status") != "TRADE_SUCCESS" && values.Get("trade_status") != "TRADE_FINISHED") {
|
||||
ctx.String(http.StatusBadRequest, "failure")
|
||||
return
|
||||
}
|
||||
err = complete(values.Get("out_trade_no"), values.Get("trade_no"), int64(amount*100+0.5), "alipay", digest(values.Encode()))
|
||||
if err != nil {
|
||||
ctx.String(http.StatusConflict, "failure")
|
||||
return
|
||||
}
|
||||
ctx.String(http.StatusOK, "success")
|
||||
}
|
||||
|
||||
// WechatNotify 使用微信平台证书验签并解密 API v3 通知后幂等入账。
|
||||
func WechatNotify(ctx *gin.Context) {
|
||||
if _, err := wechatClient(ctx); err != nil {
|
||||
ctx.JSON(http.StatusServiceUnavailable, gin.H{"code": "FAIL", "message": "channel unavailable"})
|
||||
return
|
||||
}
|
||||
visitor := downloader.MgrInstance().GetCertificateVisitor(config.Spec.Payment.Wechat.MerchantID)
|
||||
handler := notify.NewNotifyHandler(config.Spec.Payment.Wechat.APIv3Key, verifiers.NewSHA256WithRSAVerifier(visitor))
|
||||
transaction := new(payments.Transaction)
|
||||
if _, err := handler.ParseNotifyRequest(ctx, ctx.Request, transaction); err != nil || transaction.OutTradeNo == nil || transaction.Amount == nil || transaction.Amount.Total == nil || transaction.TradeState == nil || *transaction.TradeState != "SUCCESS" {
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{"code": "FAIL", "message": "invalid notification"})
|
||||
return
|
||||
}
|
||||
tradeNo := ""
|
||||
if transaction.TransactionId != nil {
|
||||
tradeNo = *transaction.TransactionId
|
||||
}
|
||||
if err := complete(*transaction.OutTradeNo, tradeNo, *transaction.Amount.Total, "wechat", digest(transaction.String())); err != nil {
|
||||
ctx.JSON(http.StatusConflict, gin.H{"code": "FAIL", "message": "payment conflict"})
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, gin.H{"code": "SUCCESS", "message": "成功"})
|
||||
}
|
||||
|
||||
func complete(paymentNo, tradeNo string, amount int64, channel, callbackDigest string) error {
|
||||
return impl.DBService.Transaction(func(tx *gorm.DB) error {
|
||||
var order models.PaymentOrder
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("payment_no = ?", paymentNo).First(&order).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if order.PaymentStatus == StatusPaid {
|
||||
return nil
|
||||
}
|
||||
if order.Channel != channel || order.Amount != amount {
|
||||
return errors.New("payment identity or amount mismatch")
|
||||
}
|
||||
if time.Now().After(order.ExpiresAt) {
|
||||
return tx.Model(&order).Updates(map[string]any{"payment_status": 50, "channel_trade_no": tradeNo, "callback_digest": callbackDigest, "failure_code": "PAID_AFTER_EXPIRED"}).Error
|
||||
}
|
||||
now := time.Now()
|
||||
if err := tx.Model(&order).Updates(map[string]any{"payment_status": StatusPaid, "channel_trade_no": tradeNo, "callback_digest": callbackDigest, "paid_at": &now}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
switch order.BusinessType {
|
||||
case "ec_order":
|
||||
return tx.Model(&models.EcOrder{}).Where("identity = ? AND order_status = ?", order.BusinessIdentity, 16).Updates(map[string]any{"order_status": 18, "paid_at": &now}).Error
|
||||
case "gasorder":
|
||||
return tx.Model(&models.GasorderBasic{}).Where("identity = ? AND order_status IN ?", order.BusinessIdentity, []int{16, 18}).Update("order_status", 35).Error
|
||||
case "recharge":
|
||||
return completeRecharge(tx, order, now)
|
||||
}
|
||||
return errors.New("unsupported payment business")
|
||||
})
|
||||
}
|
||||
|
||||
func completeRecharge(tx *gorm.DB, payment models.PaymentOrder, now time.Time) error {
|
||||
var recharge models.WalletRechargeOrder
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("identity = ? AND recharge_status = ?", payment.BusinessIdentity, 10).First(&recharge).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
var wallet models.WalletBasic
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&wallet, recharge.WalletBasicID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
wallet.Balance += payment.Amount
|
||||
if err := tx.Model(&wallet).Update("balance", wallet.Balance).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Model(&recharge).Updates(map[string]any{"recharge_status": 23, "completed_at": &now}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
date := now.In(time.Local)
|
||||
return tx.Create(&models.WalletRecord{Entity: models.Entity{Identity: models.NewIdentity(), Status: 1}, WalletBasicID: wallet.ID,
|
||||
RecordNo: "WR" + now.Format("20060102150405.000000"), RequestNo: "recharge:" + recharge.Identity, Direction: "income", TradeType: "recharge",
|
||||
Amount: payment.Amount, BalanceAfter: wallet.Balance, WithdrawalBalanceAfter: wallet.WithdrawalBalance, InTradeNo: recharge.RechargeNo,
|
||||
PayChannel: payment.Channel, OperatorIdentity: payment.UserIdentity, Ymd: int32(date.Year()*10000 + int(date.Month())*100 + date.Day()), Ym: int32(date.Year()*100 + int(date.Month()))}).Error
|
||||
}
|
||||
68
backend/api/internal/logic/payment/close.go
Normal file
68
backend/api/internal/logic/payment/close.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/config"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/smartwalle/alipay/v3"
|
||||
)
|
||||
|
||||
// CloseExpired 批量关闭已过期的渠道支付单;单笔失败留待下一轮安全重试。
|
||||
func CloseExpired(ctx context.Context, limit int) (int, error) {
|
||||
var orders []models.PaymentOrder
|
||||
if err := impl.DBService.Where("payment_status = ? AND expires_at <= ?", StatusPending, time.Now()).Order("expires_at asc").Limit(limit).Find(&orders).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
closed := 0
|
||||
for _, order := range orders {
|
||||
if err := closeChannelOrder(ctx, order); err != nil {
|
||||
continue
|
||||
}
|
||||
now := time.Now()
|
||||
result := impl.DBService.Model(&models.PaymentOrder{}).Where("id = ? AND payment_status = ?", order.ID, StatusPending).Updates(map[string]any{"payment_status": StatusClosed, "closed_at": &now})
|
||||
if result.Error == nil && result.RowsAffected == 1 {
|
||||
closed++
|
||||
}
|
||||
}
|
||||
return closed, nil
|
||||
}
|
||||
|
||||
func closeChannelOrder(ctx context.Context, order models.PaymentOrder) error {
|
||||
if order.Channel == "alipay" {
|
||||
client, err := alipayClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = client.TradeClose(ctx, alipay.TradeClose{OutTradeNo: order.PaymentNo})
|
||||
return err
|
||||
}
|
||||
if order.Channel == "wechat" {
|
||||
client, err := wechatClient(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = client.Post(ctx, fmt.Sprintf("https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/%s/close", order.PaymentNo), map[string]string{"mchid": config.Spec.Payment.Wechat.MerchantID})
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CloseExpiredHandler 只接受 Worker 共享凭证,不暴露为平台用户动作。
|
||||
func CloseExpiredHandler(ctx *gin.Context) {
|
||||
if config.Spec.Payment.InternalServiceToken == "" || ctx.GetHeader("X-Heqi-Worker-Token") != config.Spec.Payment.InternalServiceToken {
|
||||
ctx.AbortWithStatus(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
count, err := CloseExpired(ctx, 100)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "close expired payments failed"})
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, gin.H{"closed": count})
|
||||
}
|
||||
139
backend/api/internal/logic/payment/provider.go
Normal file
139
backend/api/internal/logic/payment/provider.go
Normal file
@@ -0,0 +1,139 @@
|
||||
// Package payment 统一承载支付宝、微信和钱包支付请求及渠道回调。
|
||||
package payment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/config"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"github.com/smartwalle/alipay/v3"
|
||||
wechatcore "github.com/wechatpay-apiv3/wechatpay-go/core"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/core/option"
|
||||
wechatapp "github.com/wechatpay-apiv3/wechatpay-go/services/payments/app"
|
||||
wechatjsapi "github.com/wechatpay-apiv3/wechatpay-go/services/payments/jsapi"
|
||||
wechatnative "github.com/wechatpay-apiv3/wechatpay-go/services/payments/native"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/utils"
|
||||
)
|
||||
|
||||
var ErrChannelUnavailable = errors.New("payment channel is not configured")
|
||||
|
||||
func money(amount int64) string { return fmt.Sprintf("%d.%02d", amount/100, amount%100) }
|
||||
|
||||
func createChannelOrder(ctx context.Context, order models.PaymentOrder, openID string) (string, error) {
|
||||
switch order.Channel {
|
||||
case "alipay":
|
||||
return createAlipayOrder(order)
|
||||
case "wechat":
|
||||
return createWechatOrder(ctx, order, openID)
|
||||
default:
|
||||
return "", errors.New("unsupported payment channel")
|
||||
}
|
||||
}
|
||||
|
||||
func alipayClient() (*alipay.Client, error) {
|
||||
cfg := config.Spec.Payment.Alipay
|
||||
if !cfg.Enabled || cfg.AppID == "" || cfg.PrivateKeyPath == "" {
|
||||
return nil, ErrChannelUnavailable
|
||||
}
|
||||
privateKey, err := os.ReadFile(cfg.PrivateKeyPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client, err := alipay.New(cfg.AppID, string(privateKey), cfg.Production)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = client.LoadAppCertPublicKeyFromFile(cfg.AppPublicCertPath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = client.LoadAliPayRootCertFromFile(cfg.AlipayRootCertPath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = client.LoadAlipayCertPublicKeyFromFile(cfg.AlipayPublicCertPath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func createAlipayOrder(order models.PaymentOrder) (string, error) {
|
||||
client, err := alipayClient()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
trade := alipay.Trade{NotifyURL: config.Spec.Payment.Alipay.NotifyURL, ReturnURL: config.Spec.Payment.Alipay.ReturnURL,
|
||||
Subject: order.Subject, OutTradeNo: order.PaymentNo, TotalAmount: money(order.Amount), TimeoutExpress: fmt.Sprintf("%dm", config.Spec.Payment.ExpireMinutes)}
|
||||
if order.PayType == "app" {
|
||||
trade.ProductCode = "QUICK_MSECURITY_PAY"
|
||||
return client.TradeAppPay(alipay.TradeAppPay{Trade: trade})
|
||||
}
|
||||
if order.PayType == "wap" {
|
||||
trade.ProductCode = "QUICK_WAP_WAY"
|
||||
value, err := client.TradeWapPay(alipay.TradeWapPay{Trade: trade})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return value.String(), nil
|
||||
}
|
||||
return "", errors.New("unsupported alipay product")
|
||||
}
|
||||
|
||||
func wechatClient(ctx context.Context) (*wechatcore.Client, error) {
|
||||
cfg := config.Spec.Payment.Wechat
|
||||
if !cfg.Enabled || cfg.MerchantID == "" || cfg.MerchantPrivateKeyPath == "" || len(cfg.APIv3Key) != 32 {
|
||||
return nil, ErrChannelUnavailable
|
||||
}
|
||||
key, err := utils.LoadPrivateKeyWithPath(cfg.MerchantPrivateKeyPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return wechatcore.NewClient(ctx, option.WithWechatPayAutoAuthCipher(cfg.MerchantID, cfg.MerchantCertificateSerial, key, cfg.APIv3Key))
|
||||
}
|
||||
|
||||
func createWechatOrder(ctx context.Context, order models.PaymentOrder, openID string) (string, error) {
|
||||
cfg := config.Spec.Payment.Wechat
|
||||
client, err := wechatClient(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
marshal := func(value any) (string, error) { raw, err := json.Marshal(value); return string(raw), err }
|
||||
switch order.PayType {
|
||||
case "app":
|
||||
resp, _, err := (&wechatapp.AppApiService{Client: client}).PrepayWithRequestPayment(ctx, wechatapp.PrepayRequest{
|
||||
Appid: wechatcore.String(cfg.AppAppID), Mchid: wechatcore.String(cfg.MerchantID), Description: wechatcore.String(order.Subject),
|
||||
OutTradeNo: wechatcore.String(order.PaymentNo), TimeExpire: &order.ExpiresAt, NotifyUrl: wechatcore.String(cfg.NotifyURL), Amount: &wechatapp.Amount{Total: wechatcore.Int64(order.Amount)}})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return marshal(resp)
|
||||
case "jsapi", "mini":
|
||||
if strings.TrimSpace(openID) == "" {
|
||||
return "", errors.New("openid is required")
|
||||
}
|
||||
appid := cfg.OfficialAccountAppID
|
||||
if order.PayType == "mini" {
|
||||
appid = cfg.MiniProgramAppID
|
||||
}
|
||||
resp, _, err := (&wechatjsapi.JsapiApiService{Client: client}).PrepayWithRequestPayment(ctx, wechatjsapi.PrepayRequest{
|
||||
Appid: wechatcore.String(appid), Mchid: wechatcore.String(cfg.MerchantID), Description: wechatcore.String(order.Subject),
|
||||
OutTradeNo: wechatcore.String(order.PaymentNo), TimeExpire: &order.ExpiresAt, NotifyUrl: wechatcore.String(cfg.NotifyURL),
|
||||
Amount: &wechatjsapi.Amount{Total: wechatcore.Int64(order.Amount)}, Payer: &wechatjsapi.Payer{Openid: wechatcore.String(openID)}})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return marshal(resp)
|
||||
case "native":
|
||||
resp, _, err := (&wechatnative.NativeApiService{Client: client}).Prepay(ctx, wechatnative.PrepayRequest{
|
||||
Appid: wechatcore.String(cfg.AppAppID), Mchid: wechatcore.String(cfg.MerchantID), Description: wechatcore.String(order.Subject),
|
||||
OutTradeNo: wechatcore.String(order.PaymentNo), TimeExpire: &order.ExpiresAt, NotifyUrl: wechatcore.String(cfg.NotifyURL), Amount: &wechatnative.Amount{Total: wechatcore.Int64(order.Amount)}})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return marshal(resp)
|
||||
}
|
||||
return "", errors.New("unsupported wechat product")
|
||||
}
|
||||
166
backend/api/internal/logic/payment/refund.go
Normal file
166
backend/api/internal/logic/payment/refund.go
Normal file
@@ -0,0 +1,166 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/config"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type RefundItemInput struct {
|
||||
Identity string `json:"identity"`
|
||||
Quantity int `json:"quantity"`
|
||||
}
|
||||
type RefundInput struct {
|
||||
RequestNo, Reason, Description string
|
||||
Items []RefundItemInput
|
||||
}
|
||||
|
||||
// CreateRefund 校验本人订单、退款窗口、履约状态、数量和累计金额后创建待审核退款。
|
||||
func CreateRefund(userID uint64, userIdentity, businessType, businessIdentity string, input RefundInput) (models.PaymentRefund, error) {
|
||||
var result models.PaymentRefund
|
||||
err := impl.DBService.Transaction(func(tx *gorm.DB) error {
|
||||
var pay models.PaymentOrder
|
||||
if err := tx.Where("business_type = ? AND business_identity = ? AND user_identity = ? AND payment_status = ?", businessType, businessIdentity, userIdentity, StatusPaid).Order("paid_at desc").First(&pay).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if pay.PaidAt == nil || time.Now().After(pay.PaidAt.AddDate(0, 0, config.Spec.Payment.RefundWindowDays)) {
|
||||
return errors.New("refund window expired")
|
||||
}
|
||||
var wallet models.WalletBasic
|
||||
if err := tx.Where("owner_type = ? AND owner_identity = ? AND owner_id = ?", "user", userIdentity, userID).First(&wallet).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
items, whole, err := refundItems(tx, businessType, businessIdentity, userID, input.Items)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var used int64
|
||||
if err := tx.Model(&models.PaymentRefund{}).Where("payment_order_id = ? AND refund_status IN ?", pay.ID, []int{10, 20}).Select("COALESCE(SUM(amount),0)").Scan(&used).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
var amount int64
|
||||
for _, item := range items {
|
||||
amount += item.Amount
|
||||
}
|
||||
if whole {
|
||||
amount = pay.Amount - used
|
||||
}
|
||||
if amount <= 0 || used+amount > pay.Amount {
|
||||
return errors.New("refund amount exceeds payment")
|
||||
}
|
||||
result = models.PaymentRefund{Entity: models.Entity{Identity: models.NewIdentity(), Status: 1}, RefundStatus: 10, PaymentOrderID: pay.ID, WalletBasicID: wallet.ID,
|
||||
RefundNo: "RF" + time.Now().Format("20060102150405.000000"), RequestNo: input.RequestNo, BusinessType: businessType, BusinessIdentity: businessIdentity,
|
||||
UserIdentity: userIdentity, Amount: amount, Reason: input.Reason, Description: input.Description}
|
||||
if err := tx.Create(&result).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for index := range items {
|
||||
items[index].PaymentRefundID = result.ID
|
||||
items[index].Identity = models.NewIdentity()
|
||||
if err := tx.Create(&items[index]).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return result, err
|
||||
}
|
||||
|
||||
func refundItems(tx *gorm.DB, businessType, identity string, userID uint64, requested []RefundItemInput) ([]models.PaymentRefundItem, bool, error) {
|
||||
if len(requested) == 0 {
|
||||
return nil, false, errors.New("refund items are required")
|
||||
}
|
||||
result := make([]models.PaymentRefundItem, 0, len(requested))
|
||||
whole := true
|
||||
switch businessType {
|
||||
case "ec_order":
|
||||
var order models.EcOrder
|
||||
if err := tx.Where("identity = ? AND user_account_id = ? AND order_status = ? AND logistics_status < ?", identity, userID, 18, 30).First(&order).Error; err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
var all []models.EcOrderItem
|
||||
if err := tx.Where("ec_order_id = ?", order.ID).Find(&all).Error; err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
by := map[string]models.EcOrderItem{}
|
||||
for _, value := range all {
|
||||
by[value.Identity] = value
|
||||
}
|
||||
for _, request := range requested {
|
||||
value, ok := by[request.Identity]
|
||||
if !ok || request.Quantity <= 0 || request.Quantity > value.Quantity {
|
||||
return nil, false, gorm.ErrInvalidData
|
||||
}
|
||||
if request.Quantity != value.Quantity {
|
||||
whole = false
|
||||
}
|
||||
result = append(result, models.PaymentRefundItem{OrderItemIdentity: value.Identity, Quantity: request.Quantity, Amount: value.SaleAmount * int64(request.Quantity)})
|
||||
}
|
||||
if len(requested) != len(all) {
|
||||
whole = false
|
||||
}
|
||||
case "gasorder":
|
||||
var order models.GasorderBasic
|
||||
if err := tx.Where("identity = ? AND user_account_id = ? AND order_status = ?", identity, userID, 35).First(&order).Error; err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
var all []models.GasorderItem
|
||||
if err := tx.Where("gasorder_basic_id = ?", order.ID).Find(&all).Error; err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
by := map[string]models.GasorderItem{}
|
||||
for _, value := range all {
|
||||
by[value.Identity] = value
|
||||
}
|
||||
for _, request := range requested {
|
||||
value, ok := by[request.Identity]
|
||||
if !ok || request.Quantity != 1 {
|
||||
return nil, false, gorm.ErrInvalidData
|
||||
}
|
||||
result = append(result, models.PaymentRefundItem{OrderItemIdentity: value.Identity, Quantity: 1, Amount: value.UnitPrice})
|
||||
}
|
||||
if len(requested) != len(all) {
|
||||
whole = false
|
||||
}
|
||||
default:
|
||||
return nil, false, errors.New("business does not support refund")
|
||||
}
|
||||
return result, whole, nil
|
||||
}
|
||||
|
||||
// ReviewRefund 驳回时记录原因;通过时在同一事务中立即增加钱包与不可变流水。
|
||||
func ReviewRefund(identity, reviewer, remark string, approve bool) error {
|
||||
return impl.DBService.Transaction(func(tx *gorm.DB) error {
|
||||
var refund models.PaymentRefund
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("identity = ? AND refund_status = ?", identity, 10).First(&refund).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
now := time.Now()
|
||||
if !approve {
|
||||
if remark == "" {
|
||||
return errors.New("reject reason required")
|
||||
}
|
||||
return tx.Model(&refund).Updates(map[string]any{"refund_status": 30, "review_remark": remark, "reviewer_identity": reviewer, "reviewed_at": &now}).Error
|
||||
}
|
||||
var wallet models.WalletBasic
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&wallet, refund.WalletBasicID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
wallet.Balance += refund.Amount
|
||||
if err := tx.Model(&wallet).Update("balance", wallet.Balance).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Model(&refund).Updates(map[string]any{"refund_status": 20, "review_remark": remark, "reviewer_identity": reviewer, "reviewed_at": &now, "completed_at": &now}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
date := now.In(time.Local)
|
||||
return tx.Create(&models.WalletRecord{Entity: models.Entity{Identity: models.NewIdentity(), Status: 1}, WalletBasicID: wallet.ID, RecordNo: models.NewIdentity(), RequestNo: "refund:" + refund.Identity,
|
||||
Direction: "income", TradeType: "refund", Amount: refund.Amount, BalanceAfter: wallet.Balance, WithdrawalBalanceAfter: wallet.WithdrawalBalance, InTradeNo: refund.RefundNo, PayChannel: "wallet", OperatorIdentity: reviewer,
|
||||
Ymd: int32(date.Year()*10000 + int(date.Month())*100 + date.Day()), Ym: int32(date.Year()*100 + int(date.Month())), Remark: "退款审核:" + remark}).Error
|
||||
})
|
||||
}
|
||||
61
backend/api/internal/logic/payment/service.go
Normal file
61
backend/api/internal/logic/payment/service.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/config"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
StatusPending = 10
|
||||
StatusPaid = 23
|
||||
StatusClosed = 30
|
||||
)
|
||||
|
||||
type CreateInput struct {
|
||||
RequestNo, BusinessType, BusinessIdentity, UserIdentity string
|
||||
Channel, PayType, Subject, OpenID string
|
||||
Amount int64
|
||||
}
|
||||
|
||||
// Create 创建幂等支付单并向渠道请求客户端调起参数。
|
||||
func Create(ctx context.Context, input CreateInput) (models.PaymentOrder, error) {
|
||||
if input.Amount <= 0 || input.RequestNo == "" || input.BusinessIdentity == "" || input.UserIdentity == "" {
|
||||
return models.PaymentOrder{}, gorm.ErrInvalidData
|
||||
}
|
||||
var existing models.PaymentOrder
|
||||
if err := impl.DBService.Where("business_type = ? AND request_no = ?", input.BusinessType, input.RequestNo).First(&existing).Error; err == nil {
|
||||
if existing.BusinessIdentity != input.BusinessIdentity || existing.Amount != input.Amount || existing.Channel != input.Channel || existing.PayType != input.PayType {
|
||||
return existing, errors.New("idempotency conflict")
|
||||
}
|
||||
return existing, nil
|
||||
}
|
||||
order := models.PaymentOrder{Entity: models.Entity{Identity: models.NewIdentity(), Status: 1}, PaymentStatus: StatusPending,
|
||||
PaymentNo: "PAY" + time.Now().Format("20060102150405.000000"), RequestNo: input.RequestNo, BusinessType: input.BusinessType,
|
||||
BusinessIdentity: input.BusinessIdentity, UserIdentity: input.UserIdentity, MerchantIdentity: "platform", Channel: input.Channel,
|
||||
PayType: input.PayType, Amount: input.Amount, Subject: input.Subject, ExpiresAt: time.Now().Add(time.Duration(config.Spec.Payment.ExpireMinutes) * time.Minute)}
|
||||
args, err := createChannelOrder(ctx, order, input.OpenID)
|
||||
if err != nil {
|
||||
return order, err
|
||||
}
|
||||
order.ClientArgs = args
|
||||
if err = impl.DBService.Create(&order).Error; err != nil {
|
||||
return order, err
|
||||
}
|
||||
return order, nil
|
||||
}
|
||||
|
||||
// PublicResponse 仅返回客户端调起支付所需的非密钥参数。
|
||||
func PublicResponse(order models.PaymentOrder) map[string]any {
|
||||
response := map[string]any{"identity": order.Identity, "payment_no": order.PaymentNo, "payment_status": order.PaymentStatus,
|
||||
"channel": order.Channel, "pay_type": order.PayType, "amount": order.Amount, "client_args": order.ClientArgs, "expires_at": order.ExpiresAt}
|
||||
if order.Channel == "wechat" && order.PayType == "app" {
|
||||
response["app_id"] = config.Spec.Payment.Wechat.AppAppID
|
||||
}
|
||||
return response
|
||||
}
|
||||
@@ -103,9 +103,9 @@ func GetDashboardOverview() (DashboardStatistics, error) {
|
||||
Scan(&result.TodayOrderAmount).Error; err != nil {
|
||||
return DashboardStatistics{}, err
|
||||
}
|
||||
if err := impl.DBService.Model(&models.WalletPayment{}).
|
||||
if err := impl.DBService.Model(&models.PaymentOrder{}).
|
||||
Select("COALESCE(SUM(amount), 0)").
|
||||
Where("status = ? AND payment_status = ?", common.StatusEnable, common.StatusPaid).
|
||||
Where("status = ? AND payment_status = ?", common.StatusEnable, 23).
|
||||
Scan(&result.PaidAmount).Error; err != nil {
|
||||
return DashboardStatistics{}, err
|
||||
}
|
||||
@@ -126,9 +126,9 @@ func GetDashboardOverview() (DashboardStatistics, error) {
|
||||
}
|
||||
result.ProductStatuses = namedStatuses(productStatuses, productStatusNames)
|
||||
|
||||
if err := impl.DBService.Model(&models.WalletPayment{}).
|
||||
if err := impl.DBService.Model(&models.PaymentOrder{}).
|
||||
Select("pay_channel AS name, COALESCE(SUM(amount), 0) AS value").
|
||||
Where("status = ? AND payment_status = ?", common.StatusEnable, common.StatusPaid).
|
||||
Where("status = ? AND payment_status = ?", common.StatusEnable, 23).
|
||||
Group("pay_channel").Order("value DESC").Scan(&result.PaymentChannels).Error; err != nil {
|
||||
return DashboardStatistics{}, err
|
||||
}
|
||||
|
||||
@@ -68,8 +68,9 @@ var PlatformMenus = [][]Menu{
|
||||
{Identity: "finance", GroupCode: "finance", Name: "财务管理", Icon: "icon-bar-chart", Path: "/finance", SortNo: 100, Status: common.StatusEnable},
|
||||
{Identity: "wallet_apply_cash", ParentIdentity: "finance", GroupCode: "finance", Name: "提现记录", Path: "/finance/withdrawals", SortNo: 1, Status: common.StatusEnable},
|
||||
{Identity: "fin_payment", ParentIdentity: "finance", GroupCode: "finance", Name: "支付记录", Path: "/finance/payments", SortNo: 2, Status: common.StatusEnable},
|
||||
{Identity: "fin_settlement", ParentIdentity: "finance", GroupCode: "finance", Name: "财务结算", Path: "/finance/settlements", SortNo: 3, Status: common.StatusEnable},
|
||||
{Identity: "fin_reconciliation", ParentIdentity: "finance", GroupCode: "finance", Name: "财务对账", Path: "/finance/reconciliations", SortNo: 4, Status: common.StatusEnable},
|
||||
{Identity: "payment_refund", ParentIdentity: "finance", GroupCode: "finance", Name: "退款审核", Path: "/finance/refunds", SortNo: 3, Status: common.StatusEnable},
|
||||
{Identity: "fin_settlement", ParentIdentity: "finance", GroupCode: "finance", Name: "财务结算", Path: "/finance/settlements", SortNo: 4, Status: common.StatusEnable},
|
||||
{Identity: "fin_reconciliation", ParentIdentity: "finance", GroupCode: "finance", Name: "财务对账", Path: "/finance/reconciliations", SortNo: 5, Status: common.StatusEnable},
|
||||
},
|
||||
{
|
||||
{Identity: "content", GroupCode: "content", Name: "内容管理", Icon: "icon-file", Path: "/content", SortNo: 110, Status: common.StatusEnable},
|
||||
|
||||
33
backend/api/internal/logic/platform/payment/refund.go
Normal file
33
backend/api/internal/logic/platform/payment/refund.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"git.apinb.com/bsm-sdk/core/errcode"
|
||||
"git.apinb.com/bsm-sdk/core/infra"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
||||
paylogic "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/payment"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func ListRefund(ctx *gin.Context) { common.ListResource(ctx, &models.PaymentRefund{}) }
|
||||
func GetRefund(ctx *gin.Context) { common.GetResource(ctx, &models.PaymentRefund{}) }
|
||||
func review(approve bool) gin.HandlerFunc {
|
||||
return func(ctx *gin.Context) {
|
||||
var request struct {
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
if ctx.ShouldBindJSON(&request) != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
operator, _ := common.PlatformOperator(ctx)
|
||||
if err := paylogic.ReviewRefund(ctx.Param("identity"), operator, request.Remark, approve); err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"reviewed": true})
|
||||
}
|
||||
}
|
||||
|
||||
var ApproveRefund = review(true)
|
||||
var RejectRefund = review(false)
|
||||
@@ -1,4 +1,4 @@
|
||||
package platform
|
||||
package platform
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
@@ -89,7 +89,7 @@ func ExpectedResources() []ResourceContract {
|
||||
resourceContract("finance", "fin_payment", ReadOnly, "list"), resourceContract("finance", "fin_settlement", Writable, "list"), resourceContract("finance", "fin_reconciliation", ReadOnly, "list"),
|
||||
resourceContract("content", "cms_content", Writable, "list"), resourceContract("customer_service", "cs_ticket", Writable, "list"),
|
||||
resourceContract("platform", "platform_account", Writable, "list"), resourceContract("platform", "platform_role", Writable, "list"), resourceContract("platform", "platform_menu", ReadOnly, "tree"),
|
||||
resourceContract("wallet", "wallet_basic", ReadOnly, "list"), resourceContract("wallet", "wallet_bank", ReadOnly, "list"), resourceContract("wallet", "wallet_payment", ReadOnly, "list"), resourceContract("wallet", "wallet_record", ReadOnly, "list"), resourceContract("wallet", "wallet_refund", ReadOnly, "list"), resourceContract("wallet", "wallet_apply_cash", ReadOnly, "list"),
|
||||
resourceContract("wallet", "wallet_basic", ReadOnly, "list"), resourceContract("wallet", "wallet_bank", ReadOnly, "list"), resourceContract("wallet", "payment_order", ReadOnly, "list"), resourceContract("wallet", "wallet_record", ReadOnly, "list"), resourceContract("wallet", "payment_refund", ReadOnly, "list"), resourceContract("wallet", "wallet_apply_cash", ReadOnly, "list"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package wallet
|
||||
package wallet
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@@ -44,12 +44,12 @@ func ListWalletBasic(ctx *gin.Context) {
|
||||
func GetWalletBasic(ctx *gin.Context) { getWalletByIdentity[models.WalletBasic](ctx) }
|
||||
func ListWalletBank(ctx *gin.Context) { listWalletPage[models.WalletBank](ctx, "", nil) }
|
||||
func GetWalletBank(ctx *gin.Context) { getWalletByIdentity[models.WalletBank](ctx) }
|
||||
func ListWalletPayment(ctx *gin.Context) { listWalletPage[models.WalletPayment](ctx, "", nil) }
|
||||
func GetWalletPayment(ctx *gin.Context) { getWalletByIdentity[models.WalletPayment](ctx) }
|
||||
func ListPaymentOrder(ctx *gin.Context) { listWalletPage[models.PaymentOrder](ctx, "", nil) }
|
||||
func GetPaymentOrder(ctx *gin.Context) { getWalletByIdentity[models.PaymentOrder](ctx) }
|
||||
func ListWalletRecord(ctx *gin.Context) { listWalletPage[models.WalletRecord](ctx, "", nil) }
|
||||
func GetWalletRecord(ctx *gin.Context) { getWalletByIdentity[models.WalletRecord](ctx) }
|
||||
func ListWalletRefund(ctx *gin.Context) { listWalletPage[models.WalletRefund](ctx, "", nil) }
|
||||
func GetWalletRefund(ctx *gin.Context) { getWalletByIdentity[models.WalletRefund](ctx) }
|
||||
func ListPaymentRefund(ctx *gin.Context) { listWalletPage[models.PaymentRefund](ctx, "", nil) }
|
||||
func GetPaymentRefund(ctx *gin.Context) { getWalletByIdentity[models.PaymentRefund](ctx) }
|
||||
func ListWalletApplyCash(ctx *gin.Context) { listWalletPage[models.WalletApplyCash](ctx, "", nil) }
|
||||
func GetWalletApplyCash(ctx *gin.Context) { getWalletByIdentity[models.WalletApplyCash](ctx) }
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import "git.apinb.com/bsm-sdk/core/database"
|
||||
type GasorderPayment struct {
|
||||
Entity // 公共实体字段,Status 保存支付尝试状态
|
||||
GasorderBasicID uint64 `gorm:"column:gasorder_basic_id;not null;index;uniqueIndex:idx_gasorder_payment_attempt" json:"gasorder_basic_id"` // 订单自增主键
|
||||
WalletPaymentID uint64 `gorm:"column:wallet_payment_id;not null;uniqueIndex" json:"wallet_payment_id"` // 钱包支付记录自增主键
|
||||
PaymentOrderID uint64 `gorm:"column:payment_order_id;not null;uniqueIndex" json:"payment_order_id"` // 钱包支付记录自增主键
|
||||
AttemptNo int `gorm:"column:attempt_no;not null;uniqueIndex:idx_gasorder_payment_attempt" json:"attempt_no"` // 支付尝试序号
|
||||
Amount int64 `gorm:"column:amount;not null;check:amount > 0" json:"amount"` // 支付金额,单位分
|
||||
}
|
||||
|
||||
34
backend/api/internal/models/payment_order.go
Normal file
34
backend/api/internal/models/payment_order.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.apinb.com/bsm-sdk/core/database"
|
||||
)
|
||||
|
||||
// PaymentOrder 对应 payment_order,是全项目唯一的支付尝试事实。
|
||||
type PaymentOrder struct {
|
||||
Entity // 公共实体字段
|
||||
PaymentStatus int `gorm:"column:payment_status;not null;default:10;index" json:"payment_status"` // 10待支付、20确认中、23成功、30关闭、40失败、50异常
|
||||
PaymentNo string `gorm:"column:payment_no;type:varchar(64);not null;uniqueIndex" json:"payment_no"` // 平台支付单号
|
||||
RequestNo string `gorm:"column:request_no;type:varchar(128);not null;uniqueIndex:idx_payment_request" json:"request_no"` // 客户端幂等请求号
|
||||
BusinessType string `gorm:"column:business_type;type:varchar(32);not null;index;uniqueIndex:idx_payment_request" json:"business_type"` // 业务类型
|
||||
BusinessIdentity string `gorm:"column:business_identity;type:varchar(36);not null;index" json:"business_identity"` // 业务对象标识
|
||||
UserIdentity string `gorm:"column:user_identity;type:varchar(36);not null;index" json:"user_identity"` // 付款用户标识
|
||||
MerchantIdentity string `gorm:"column:merchant_identity;type:varchar(36);not null;default:'platform';index" json:"merchant_identity"` // 商户配置标识
|
||||
Channel string `gorm:"column:channel;type:varchar(32);not null;index" json:"channel"` // 支付渠道
|
||||
PayType string `gorm:"column:pay_type;type:varchar(32);not null" json:"pay_type"` // 渠道支付产品
|
||||
ChannelTradeNo string `gorm:"column:channel_trade_no;type:varchar(128);not null;default:'';uniqueIndex:idx_payment_channel_trade,where:channel_trade_no <> ''" json:"channel_trade_no"` // 渠道交易号
|
||||
Amount int64 `gorm:"column:amount;not null;check:amount > 0" json:"amount"` // 实付金额,单位分
|
||||
Subject string `gorm:"column:subject;type:varchar(256);not null" json:"subject"` // 渠道订单标题
|
||||
ClientArgs string `gorm:"column:client_args;type:text;not null;default:''" json:"-"` // 客户端调起参数
|
||||
CallbackDigest string `gorm:"column:callback_digest;type:varchar(64);not null;default:''" json:"-"` // 回调原文摘要
|
||||
FailureCode string `gorm:"column:failure_code;type:varchar(64);not null;default:''" json:"failure_code"` // 稳定失败码
|
||||
FailureMessage string `gorm:"column:failure_message;type:varchar(512);not null;default:''" json:"failure_message"` // 脱敏失败说明
|
||||
ExpiresAt time.Time `gorm:"column:expires_at;type:timestamptz;not null;index" json:"expires_at"` // 支付过期时间
|
||||
PaidAt *time.Time `gorm:"column:paid_at;type:timestamptz" json:"paid_at"` // 渠道确认支付时间
|
||||
ClosedAt *time.Time `gorm:"column:closed_at;type:timestamptz" json:"closed_at"` // 渠道关单时间
|
||||
}
|
||||
|
||||
func init() { database.AppendMigrate(&PaymentOrder{}) }
|
||||
func (*PaymentOrder) TableName() string { return "payment_order" }
|
||||
45
backend/api/internal/models/payment_refund.go
Normal file
45
backend/api/internal/models/payment_refund.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.apinb.com/bsm-sdk/core/database"
|
||||
)
|
||||
|
||||
// PaymentRefund 对应 payment_refund,保存用户发起、财务审核并退入钱包的退款申请。
|
||||
type PaymentRefund struct {
|
||||
Entity // 公共实体字段
|
||||
RefundStatus int `gorm:"column:refund_status;not null;default:10;index" json:"refund_status"` // 10待审核、20已通过、30已驳回
|
||||
PaymentOrderID uint64 `gorm:"column:payment_order_id;not null;index" json:"payment_order_id"` // 原支付单内部主键
|
||||
WalletBasicID uint64 `gorm:"column:wallet_basic_id;not null;index" json:"wallet_basic_id"` // 入账钱包内部主键
|
||||
RefundNo string `gorm:"column:refund_no;type:varchar(64);not null;uniqueIndex" json:"refund_no"` // 平台退款单号
|
||||
RequestNo string `gorm:"column:request_no;type:varchar(128);not null;uniqueIndex:idx_refund_request" json:"request_no"` // 用户端幂等号
|
||||
BusinessType string `gorm:"column:business_type;type:varchar(32);not null;index" json:"business_type"` // 原业务类型
|
||||
BusinessIdentity string `gorm:"column:business_identity;type:varchar(36);not null;index" json:"business_identity"` // 原业务标识
|
||||
UserIdentity string `gorm:"column:user_identity;type:varchar(36);not null;index;uniqueIndex:idx_refund_request" json:"user_identity"` // 申请用户标识
|
||||
Amount int64 `gorm:"column:amount;not null;check:amount > 0" json:"amount"` // 核定退款金额,单位分
|
||||
Reason string `gorm:"column:reason;type:varchar(256);not null" json:"reason"` // 用户退款原因
|
||||
Description string `gorm:"column:description;type:text;not null;default:''" json:"description"` // 用户补充说明
|
||||
ReviewRemark string `gorm:"column:review_remark;type:text;not null;default:''" json:"review_remark"` // 财务审核说明
|
||||
ReviewerIdentity string `gorm:"column:reviewer_identity;type:varchar(36);not null;default:''" json:"reviewer_identity"` // 审核人标识
|
||||
ReviewedAt *time.Time `gorm:"column:reviewed_at;type:timestamptz" json:"reviewed_at"` // 审核时间
|
||||
CompletedAt *time.Time `gorm:"column:completed_at;type:timestamptz" json:"completed_at"` // 钱包入账完成时间
|
||||
}
|
||||
|
||||
func init() { database.AppendMigrate(&PaymentRefund{}) }
|
||||
func (*PaymentRefund) TableName() string { return "payment_refund" }
|
||||
|
||||
// PaymentRefundItem 对应 payment_refund_item,保存退款订单项、数量和服务端核定金额。
|
||||
type PaymentRefundItem struct {
|
||||
ID uint64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"` // 数据库自增主键
|
||||
Identity string `gorm:"column:identity;type:varchar(36);not null;uniqueIndex" json:"identity"` // 对外退款明细标识
|
||||
PaymentRefundID uint64 `gorm:"column:payment_refund_id;not null;index" json:"payment_refund_id"` // 退款申请内部主键
|
||||
OrderItemIdentity string `gorm:"column:order_item_identity;type:varchar(36);not null;index" json:"order_item_identity"` // 原订单项标识
|
||||
Quantity int `gorm:"column:quantity;not null;check:quantity > 0" json:"quantity"` // 退款数量
|
||||
Amount int64 `gorm:"column:amount;not null;check:amount >= 0" json:"amount"` // 分摊退款金额,单位分
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:timestamptz;not null" json:"created_at"` // 创建时间
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamptz;not null" json:"updated_at"` // 更新时间
|
||||
}
|
||||
|
||||
func init() { database.AppendMigrate(&PaymentRefundItem{}) }
|
||||
func (*PaymentRefundItem) TableName() string { return "payment_refund_item" }
|
||||
@@ -14,8 +14,8 @@ func TestBusinessModelsUseDedicatedStatusFields(t *testing.T) {
|
||||
{GasorderBasic{}, "OrderStatus"},
|
||||
{ProductInfo{}, "ProductStatus"},
|
||||
{EcOrder{}, "OrderStatus"},
|
||||
{WalletPayment{}, "PaymentStatus"},
|
||||
{WalletRefund{}, "RefundStatus"},
|
||||
{PaymentOrder{}, "PaymentStatus"},
|
||||
{PaymentRefund{}, "RefundStatus"},
|
||||
{WalletApplyCash{}, "ApplyStatus"},
|
||||
{FinPayment{}, "PaymentStatus"},
|
||||
{FinReconciliation{}, "ReconciliationStatus"},
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
package models
|
||||
|
||||
import "git.apinb.com/bsm-sdk/core/database"
|
||||
|
||||
// WalletPayment 对应 wallet_payment,保存第三方或余额支付单。
|
||||
type WalletPayment struct {
|
||||
Entity // 公共实体字段
|
||||
PaymentStatus int `gorm:"column:payment_status;not null;default:10;index" json:"payment_status"` // 支付业务状态
|
||||
WalletBasicID uint64 `gorm:"column:wallet_basic_id;not null;index" json:"wallet_basic_id"` // 钱包自增主键
|
||||
PaymentNo string `gorm:"column:payment_no;type:varchar(64);not null;uniqueIndex" json:"payment_no"` // 内部支付单号
|
||||
OrderNo string `gorm:"column:order_no;type:varchar(128);not null;index" json:"order_no"` // 业务订单号
|
||||
TradeNo string `gorm:"column:trade_no;type:varchar(128);not null;default:'';uniqueIndex:idx_wallet_payment_trade,where:trade_no <> ''" json:"trade_no"` // 第三方交易流水号
|
||||
PaymentType string `gorm:"column:payment_type;type:varchar(32);not null" json:"payment_type"` // 支付业务类型
|
||||
PayChannel string `gorm:"column:pay_channel;type:varchar(32);not null;uniqueIndex:idx_wallet_payment_trade,where:trade_no <> ''" json:"pay_channel"` // 支付渠道
|
||||
PayType string `gorm:"column:pay_type;type:varchar(64);not null;default:''" json:"pay_type"` // 渠道支付类型
|
||||
Amount int64 `gorm:"column:amount;not null;check:amount > 0" json:"amount"` // 支付金额,单位分
|
||||
Args string `gorm:"column:args;type:text;not null;default:''" json:"args"` // 支付参数
|
||||
Remark string `gorm:"column:remark;type:text;not null;default:''" json:"remark"` // 备注
|
||||
CallbackMsg string `gorm:"column:callback_msg;type:text;not null;default:''" json:"callback_msg"` // 支付回调信息
|
||||
}
|
||||
|
||||
func init() { database.AppendMigrate(&WalletPayment{}) }
|
||||
func (table *WalletPayment) TableName() string { return "wallet_payment" }
|
||||
@@ -1,27 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.apinb.com/bsm-sdk/core/database"
|
||||
)
|
||||
|
||||
// WalletRefund 对应 wallet_refund,保存支付退款结果。
|
||||
type WalletRefund struct {
|
||||
Entity // 公共实体字段
|
||||
RefundStatus int `gorm:"column:refund_status;not null;default:10;index" json:"refund_status"` // 退款业务状态
|
||||
WalletBasicID uint64 `gorm:"column:wallet_basic_id;not null;index" json:"wallet_basic_id"` // 钱包自增主键
|
||||
WalletPaymentID uint64 `gorm:"column:wallet_payment_id;not null;index" json:"wallet_payment_id"` // 原支付记录自增主键
|
||||
RefundNo string `gorm:"column:refund_no;type:varchar(64);not null;uniqueIndex" json:"refund_no"` // 内部退款单号
|
||||
OrderIdentity string `gorm:"column:order_identity;type:varchar(64);not null;index" json:"order_identity"` // 订单业务标识
|
||||
Amount int64 `gorm:"column:amount;not null;check:amount > 0" json:"amount"` // 退款金额,单位分
|
||||
Fee int64 `gorm:"column:fee;not null;default:0;check:fee >= 0" json:"fee"` // 退款手续费,单位分
|
||||
Reason string `gorm:"column:reason;type:text;not null;default:''" json:"reason"` // 退款原因
|
||||
OrderInfo string `gorm:"column:order_info;type:text;not null;default:''" json:"order_info"` // 订单信息快照
|
||||
Result string `gorm:"column:result;type:text;not null;default:''" json:"result"` // 第三方处理结果
|
||||
TradeNo string `gorm:"column:trade_no;type:varchar(128);not null;default:'';index" json:"trade_no"` // 第三方退款流水号
|
||||
CompletedAt *time.Time `gorm:"column:completed_at;type:timestamptz" json:"completed_at"` // 完成时间
|
||||
}
|
||||
|
||||
func init() { database.AppendMigrate(&WalletRefund{}) }
|
||||
func (table *WalletRefund) TableName() string { return "wallet_refund" }
|
||||
@@ -40,6 +40,8 @@ func registerUserClient(serviceKey string, engine *gin.Engine) {
|
||||
protected.GET("/gas/contracts", userlogic.ListGasContracts)
|
||||
protected.GET("/gas/orders", userlogic.ListGasOrders)
|
||||
protected.POST("/gas/orders/:identity/cancel", userlogic.CancelGasOrder)
|
||||
protected.POST("/gas/orders/:identity/pay", userlogic.PayGasOrder)
|
||||
protected.POST("/gas/orders/:identity/refunds", userlogic.CreateRefund("gasorder"))
|
||||
protected.GET("/tickets", userlogic.ListTickets)
|
||||
protected.POST("/tickets", userlogic.CreateTicket)
|
||||
protected.POST("/tickets/:identity/confirm", userlogic.ConfirmTicket)
|
||||
@@ -48,7 +50,9 @@ func registerUserClient(serviceKey string, engine *gin.Engine) {
|
||||
protected.POST("/shop/orders", userlogic.CreateShopOrder)
|
||||
protected.POST("/shop/orders/:identity/cancel", userlogic.CancelShopOrder)
|
||||
protected.POST("/shop/orders/:identity/pay", userlogic.PayShopOrder)
|
||||
protected.POST("/shop/orders/:identity/refunds", userlogic.CreateRefund("ec_order"))
|
||||
protected.POST("/shop/orders/:identity/confirm-receipt", userlogic.ConfirmShopReceipt)
|
||||
protected.GET("/refunds", userlogic.ListRefunds)
|
||||
registerClientWalletRoutes(protected, "user_app")
|
||||
}
|
||||
|
||||
|
||||
@@ -93,12 +93,12 @@ func RegisterDelivery(serviceKey string, engine *gin.Engine) {
|
||||
protected.POST("/wallet_recharge", deliverylogic.Recharge)
|
||||
protected.GET("/wallet_bank", deliverylogic.ListBank)
|
||||
protected.GET("/wallet_bank/:identity", deliverylogic.GetBank)
|
||||
protected.GET("/wallet_payment", deliverylogic.ListPayment)
|
||||
protected.GET("/wallet_payment/:identity", deliverylogic.GetPayment)
|
||||
protected.GET("/payment_order", deliverylogic.ListPayment)
|
||||
protected.GET("/payment_order/:identity", deliverylogic.GetPayment)
|
||||
protected.GET("/wallet_record", deliverylogic.ListRecord)
|
||||
protected.GET("/wallet_record/:identity", deliverylogic.GetRecord)
|
||||
protected.GET("/wallet_refund", deliverylogic.ListRefund)
|
||||
protected.GET("/wallet_refund/:identity", deliverylogic.GetRefund)
|
||||
protected.GET("/payment_refund", deliverylogic.ListRefund)
|
||||
protected.GET("/payment_refund/:identity", deliverylogic.GetRefund)
|
||||
protected.GET("/wallet_apply_cash", deliverylogic.ListApplyCash)
|
||||
protected.POST("/wallet_apply_cash", deliverylogic.CreateApplyCash)
|
||||
protected.GET("/wallet_apply_cash/:identity", deliverylogic.GetApplyCash)
|
||||
|
||||
@@ -89,9 +89,9 @@ func registerGasBusinessRoutes(group *gin.RouterGroup) {
|
||||
|
||||
group.GET("/wallet_basic", gaslogic.ListWalletBasic)
|
||||
group.GET("/wallet_bank", gaslogic.ListWalletBank)
|
||||
group.GET("/wallet_payment", gaslogic.ListWalletPayment)
|
||||
group.GET("/payment_order", gaslogic.ListPaymentOrder)
|
||||
group.GET("/wallet_record", gaslogic.ListWalletRecord)
|
||||
group.GET("/wallet_refund", gaslogic.ListWalletRefund)
|
||||
group.GET("/payment_refund", gaslogic.ListPaymentRefund)
|
||||
group.GET("/wallet_apply_cash", gaslogic.ListWalletApplyCash)
|
||||
group.POST("/wallet_apply_cash", gaslogic.CreateWalletApplyCash)
|
||||
group.GET("/fin_settlement", gaslogic.ListFinSettlement)
|
||||
|
||||
15
backend/api/internal/routers/payment.go
Normal file
15
backend/api/internal/routers/payment.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/payment"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// RegisterPaymentReturn 注册无 JWT 的渠道回调;安全性由渠道签名、商户、金额和支付单校验保证。
|
||||
func RegisterPaymentReturn(serviceKey string, engine *gin.Engine) {
|
||||
group := engine.Group(fmt.Sprintf("/%s/payment-return/v1", serviceKey))
|
||||
group.POST("/alipay/notify", payment.AlipayNotify)
|
||||
group.POST("/wechat/notify", payment.WechatNotify)
|
||||
engine.POST(fmt.Sprintf("/%s/internal/v1/payment/close-expired", serviceKey), payment.CloseExpiredHandler)
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/platform/fin"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/platform/gas"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/platform/gasorder"
|
||||
paymentlogic "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/platform/payment"
|
||||
platformlogic "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/platform/platform"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/platform/product"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/platform/staff"
|
||||
@@ -148,18 +149,14 @@ func registerWalletRoute(group *gin.RouterGroup) {
|
||||
bank.GET("", wallet.ListWalletBank)
|
||||
bank.GET("/:identity", wallet.GetWalletBank)
|
||||
|
||||
payment := group.Group("/wallet_payment")
|
||||
payment.GET("", wallet.ListWalletPayment)
|
||||
payment.GET("/:identity", wallet.GetWalletPayment)
|
||||
payment := group.Group("/payment_order")
|
||||
payment.GET("", wallet.ListPaymentOrder)
|
||||
payment.GET("/:identity", wallet.GetPaymentOrder)
|
||||
|
||||
record := group.Group("/wallet_record")
|
||||
record.GET("", wallet.ListWalletRecord)
|
||||
record.GET("/:identity", wallet.GetWalletRecord)
|
||||
|
||||
refund := group.Group("/wallet_refund")
|
||||
refund.GET("", wallet.ListWalletRefund)
|
||||
refund.GET("/:identity", wallet.GetWalletRefund)
|
||||
|
||||
applyCash := group.Group("/wallet_apply_cash")
|
||||
applyCash.GET("", wallet.ListWalletApplyCash)
|
||||
applyCash.GET("/:identity", wallet.GetWalletApplyCash)
|
||||
@@ -213,6 +210,11 @@ func registerPlatformRoute(group *gin.RouterGroup) {
|
||||
}
|
||||
|
||||
func registerFinanceRoute(group *gin.RouterGroup) {
|
||||
refund := group.Group("/payment_refund")
|
||||
refund.GET("", paymentlogic.ListRefund)
|
||||
refund.GET("/:identity", paymentlogic.GetRefund)
|
||||
refund.POST("/:identity/approve", paymentlogic.ApproveRefund)
|
||||
refund.POST("/:identity/reject", paymentlogic.RejectRefund)
|
||||
registerReadOnlyResource(group, "/fin_payment", &models.FinPayment{})
|
||||
settlementList, settlementCreate, settlementGet, settlementUpdate := fin.FinSettlementHandlers()
|
||||
registerWritableResource(group, "/fin_settlement", settlementList, settlementCreate, settlementGet, settlementUpdate, &models.FinSettlement{})
|
||||
|
||||
@@ -211,7 +211,7 @@ func TestPlatformFinanceContentRoutesFollowTheirContracts(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, resource := range []string{
|
||||
"/wallet_basic", "/wallet_bank", "/wallet_payment", "/wallet_record", "/wallet_refund", "/wallet_apply_cash",
|
||||
"/wallet_basic", "/wallet_bank", "/payment_order", "/wallet_record", "/payment_refund", "/wallet_apply_cash",
|
||||
} {
|
||||
path := "/heqi/platform/v1" + resource
|
||||
assertRouteMethods(t, routes, path, http.MethodGet)
|
||||
|
||||
@@ -9,5 +9,6 @@ func Register(serviceKey string, engine *gin.Engine) {
|
||||
RegisterGas(serviceKey, engine)
|
||||
RegisterDelivery(serviceKey, engine)
|
||||
RegisterClient(serviceKey, engine)
|
||||
RegisterPaymentReturn(serviceKey, engine)
|
||||
registerUploadRoute(serviceKey, engine)
|
||||
}
|
||||
|
||||
@@ -341,12 +341,11 @@ func MockData(database *gorm.DB) error {
|
||||
return err
|
||||
}
|
||||
|
||||
walletPayment := models.WalletPayment{
|
||||
Entity: entity(35, common.StatusEnable), PaymentStatus: common.StatusSuccess, WalletBasicID: wallet.ID,
|
||||
PaymentNo: "MOCK-PAYMENT-001", OrderNo: gasOrder.OrderNo,
|
||||
TradeNo: "MOCK-TRADE-001", PaymentType: "gasorder",
|
||||
PayChannel: "balance", PayType: "wallet", Amount: gasOrder.PayableAmount,
|
||||
Args: `{}`, CallbackMsg: `{"status":"success"}`,
|
||||
walletPayment := models.PaymentOrder{
|
||||
Entity: entity(35, common.StatusEnable), PaymentStatus: 23,
|
||||
PaymentNo: "MOCK-PAYMENT-001", RequestNo: "MOCK-REQ-PAYMENT-001", BusinessType: "gasorder", BusinessIdentity: gasOrder.Identity,
|
||||
UserIdentity: user.Identity, MerchantIdentity: "platform", Channel: "wallet", PayType: "wallet", ChannelTradeNo: "MOCK-TRADE-001",
|
||||
Amount: gasOrder.PayableAmount, Subject: "模拟供气订单", ClientArgs: `{}`, ExpiresAt: now.Add(30 * time.Minute), PaidAt: &now,
|
||||
}
|
||||
if err := put(tx, &walletPayment); err != nil {
|
||||
return err
|
||||
@@ -366,12 +365,11 @@ func MockData(database *gorm.DB) error {
|
||||
}
|
||||
|
||||
refundCompletedAt := now
|
||||
refund := models.WalletRefund{
|
||||
Entity: entity(37, common.StatusEnable), RefundStatus: common.StatusCompleted, WalletBasicID: wallet.ID,
|
||||
WalletPaymentID: walletPayment.ID, RefundNo: "MOCK-REFUND-001",
|
||||
OrderIdentity: gasOrder.Identity, Amount: 1000, Reason: "模拟部分退款",
|
||||
OrderInfo: `{"order_no":"MOCK-GASORDER-001"}`, Result: `{"status":"success"}`,
|
||||
TradeNo: "MOCK-REFUND-TRADE-001", CompletedAt: &refundCompletedAt,
|
||||
refund := models.PaymentRefund{
|
||||
Entity: entity(37, common.StatusEnable), RefundStatus: 20, WalletBasicID: wallet.ID,
|
||||
PaymentOrderID: walletPayment.ID, RefundNo: "MOCK-REFUND-001", RequestNo: "MOCK-REQ-REFUND-001",
|
||||
BusinessType: "gasorder", BusinessIdentity: gasOrder.Identity, UserIdentity: user.Identity,
|
||||
Amount: 1000, Reason: "模拟部分退款", ReviewerIdentity: gasAccount.Identity, ReviewedAt: &refundCompletedAt, CompletedAt: &refundCompletedAt,
|
||||
}
|
||||
if err := put(tx, &refund); err != nil {
|
||||
return err
|
||||
@@ -390,7 +388,7 @@ func MockData(database *gorm.DB) error {
|
||||
|
||||
gasOrderPayment := models.GasorderPayment{
|
||||
Entity: entity(39, common.StatusEnable), GasorderBasicID: gasOrder.ID,
|
||||
WalletPaymentID: walletPayment.ID, AttemptNo: 1, Amount: gasOrder.PayableAmount,
|
||||
PaymentOrderID: walletPayment.ID, AttemptNo: 1, Amount: gasOrder.PayableAmount,
|
||||
}
|
||||
if err := put(tx, &gasOrderPayment); err != nil {
|
||||
return err
|
||||
|
||||
@@ -4,6 +4,7 @@ cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdB
|
||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
|
||||
github.com/agiledragon/gomonkey v2.0.2+incompatible h1:eXKi9/piiC3cjJD1658mEE2o3NjkJ5vDLgYjCQu0Xlw=
|
||||
github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
@@ -40,6 +41,7 @@ github.com/jordanlewis/gcassert v0.0.0-20250430164644-389ef753e22e h1:a+PGEeXb+e
|
||||
github.com/jordanlewis/gcassert v0.0.0-20250430164644-389ef753e22e/go.mod h1:ZybsQk6DWyN5t7An1MuPm1gtSZ1xDaTXS9ZjIOxvQrk=
|
||||
github.com/kisielk/errcheck v1.5.0 h1:e8esj/e4R+SAOwFwN+n3zr0nYeCyeweozKfO23MvHzY=
|
||||
github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg=
|
||||
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46 h1:veS9QfglfvqAw2e+eeNT/SbGySq8ajECXJ9e4fPoLhY=
|
||||
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
|
||||
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
|
||||
@@ -2,9 +2,13 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"git.apinb.com/bsm-sdk/core/printer"
|
||||
"git.apinb.com/heqiapp/platforms/backend/worker/internal/config"
|
||||
@@ -14,8 +18,21 @@ import (
|
||||
func main() {
|
||||
config.New("PlatformWorker")
|
||||
impl.NewImpl()
|
||||
printer.Info("[BSM - PlatformWorker] Mock worker started; Redis Streams consumer is not enabled")
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
go closeExpiredPayments(ctx)
|
||||
printer.Info("[BSM - PlatformWorker] payment timeout scheduler started")
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
cancel()
|
||||
}
|
||||
|
||||
func closeExpiredPayments(ctx context.Context) {
|
||||
ticker := time.NewTicker(time.Duration(config.Spec.PaymentAPI.IntervalSeconds) * time.Second); defer ticker.Stop()
|
||||
for { select { case <-ctx.Done(): return; case <-ticker.C:
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodPost, config.Spec.PaymentAPI.BaseURL+"/heqi/internal/v1/payment/close-expired", bytes.NewReader(nil)); if err != nil { continue }
|
||||
request.Header.Set("X-Heqi-Worker-Token", config.Spec.PaymentAPI.Token)
|
||||
response, err := http.DefaultClient.Do(request); if err == nil { _ = response.Body.Close() }
|
||||
} }
|
||||
}
|
||||
|
||||
@@ -6,4 +6,9 @@ Databases:
|
||||
- host=127.0.0.1 user=postgres password=change-me dbname=agent_dev port=5432 sslmode=disable TimeZone=Asia/Shanghai
|
||||
Cache: redis://default:change-me@127.0.0.1:6379/0
|
||||
OnMicroService: false
|
||||
|
||||
PaymentAPI:
|
||||
BaseURL: http://localhost:12426
|
||||
Token: change-me-payment-worker-token
|
||||
IntervalSeconds: 60
|
||||
SecretKey: change-me-to-a-random-string
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Package config 沿用 sample/server 的 BSM 运行配置与地址校验。
|
||||
// Package config 使用仓库统一的 BSM 运行配置与地址校验。
|
||||
package config
|
||||
|
||||
import (
|
||||
@@ -12,10 +12,18 @@ var Spec SrvConfig
|
||||
|
||||
// SrvConfig 保持与 API 进程相同的 BSM 配置结构。
|
||||
type SrvConfig struct {
|
||||
conf.Base `yaml:",inline"`
|
||||
Databases *conf.DBConf `yaml:"Databases"`
|
||||
Rpc map[string]conf.RpcConf `yaml:"Rpc"`
|
||||
Apm *conf.ApmConf `yaml:"APM"`
|
||||
conf.Base `yaml:",inline"`
|
||||
Databases *conf.DBConf `yaml:"Databases"`
|
||||
Rpc map[string]conf.RpcConf `yaml:"Rpc"`
|
||||
Apm *conf.ApmConf `yaml:"APM"`
|
||||
PaymentAPI PaymentAPIConfig `yaml:"PaymentAPI"`
|
||||
}
|
||||
|
||||
// PaymentAPIConfig 保存 Worker 调用支付内部动作所需的最小配置。
|
||||
type PaymentAPIConfig struct {
|
||||
BaseURL string `yaml:"BaseURL"`
|
||||
Token string `yaml:"Token"`
|
||||
IntervalSeconds int `yaml:"IntervalSeconds"`
|
||||
}
|
||||
|
||||
// New 初始化 Worker 配置。
|
||||
@@ -25,5 +33,8 @@ func New(srvKey string) {
|
||||
Spec.BindIP = conf.CheckIP(Spec.BindIP)
|
||||
Spec.Addr = net.JoinHostPort(Spec.BindIP, Spec.Port)
|
||||
conf.NotNil(Spec.Service, Spec.Cache)
|
||||
if Spec.PaymentAPI.BaseURL == "" || Spec.PaymentAPI.Token == "" || Spec.PaymentAPI.IntervalSeconds <= 0 {
|
||||
panic("PaymentAPI configuration is required")
|
||||
}
|
||||
conf.PrintInfo(Spec.Addr)
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ SLA 从事件创建还是用户确认开始计时须由业务方确认;系统
|
||||
## 3. 商城下单至完成
|
||||
|
||||
1. 用户浏览分类和商品,加入购物车或立即购买,选择地址、优惠券、预约时段和支付方式。
|
||||
2. 系统冻结库存、核算优惠和运费/服务费,创建待支付订单;支付回调成功后转为待履约。
|
||||
2. 系统冻结库存、核算优惠和运费/服务费,创建待支付订单;支付回调成功后转为待履约。支付统一创建 `payment_order`,客户端返回不作为成功依据,必须以验签回调或主动查单结果为准。支付单默认 30 分钟过期,由 Worker 触发 API 关单,过期后成功回调进入支付异常待人工确认。
|
||||
3. 若有合同,创建第三方签署任务。需合同的商品应在签约成功后才允许派单;签约失败/超时可取消并按规则退款。
|
||||
4. 按商品履约属性生成配送、安装或维修任务。系统按区域、技能、上班状态、距离、负载和分账规则分配人员;无法派单时进入人工调度队列。
|
||||
5. 配送任务从接单开始记录配送轨迹:接单、出发、位置上报、到达、签收/异常等节点按时间顺序关联订单。用户查看简化进度与预计到达,运营人员查看完整轨迹和定位质量。
|
||||
@@ -97,7 +97,7 @@ SLA 从事件创建还是用户确认开始计时须由业务方确认;系统
|
||||
|
||||
## 7. 资金与提现
|
||||
|
||||
- 支付、充值、退款、佣金、提现均以不可变资金流水记录,余额是流水聚合结果而非唯一事实来源。
|
||||
- 支付、充值、退款、佣金、提现均以不可变资金流水记录,余额是流水聚合结果而非唯一事实来源。首期退款只能由用户端在支付成功后配置天数内发起,已履约订单禁止退款;支持订单项和多次部分退款,累计不超过实付金额。财务审核通过后在同一事务内退入用户钱包并写不可变流水,不调用渠道原路退款。
|
||||
- 提现流程:服务人员/气站提交申请 -> 系统校验可提现余额、实名与风控 -> 财务审核 -> 线下打款/三方打款 -> 回填凭证和结果。
|
||||
- 附件规定“审核通过,线下打款”;本期应支持审核通过、审核拒绝(必填原因)、打款中、已打款、打款失败、撤回。
|
||||
- 固定时间对账应对比平台订单、支付渠道、退款和佣金流水;差异项不得自动提现。
|
||||
|
||||
@@ -95,7 +95,8 @@
|
||||
|
||||
- 用户端 API 固定为 `/heqi/client/v1/user`,令牌客户端为 `user_app`,不得与任何后台或工作人员令牌互用。
|
||||
- 首期实现手机号密码/验证码登录、普通注册、邀请注册、资料、地址、服务归属、已发布内容与阅读确认、商城下单和余额支付、物流查询/确认收货、供气合同及订单查询、工单、钱包充值/提现/银行卡。邀请注册可携带气站 `identity` 和可选配送点 `identity`,服务端在事务内校验组织关系并建立唯一服务归属。
|
||||
- 充值先创建待支付订单;仅开发配置允许 Mock 支付确认,确认后才写余额及不可变流水。微信和支付宝未配置渠道时必须明确返回不可用,不得模拟成功。
|
||||
- 充值先创建待支付订单;仅开发配置允许 Mock 支付确认,确认后才写余额及不可变流水。微信和支付宝未配置渠道时必须明确返回不可用,不得模拟成功。Android/iOS 通过渠道 SDK 调起支付宝 App 支付和微信 App 支付;Web 使用支付宝手机网站支付或微信 JSAPI。客户端回传仅用于提示并轮询服务端支付状态,不能直接推进订单。
|
||||
- 用户可在未履约订单的退款窗口内选择订单项和数量提交退款原因;金额由服务端按实付分摊核定。退款申请、审核结果和钱包入账状态均可在订单页查看。
|
||||
- 商城订单交易状态与物流状态分离;物流单号、公司、发货和收货时间由服务端保存,用户只能查看本人订单并确认收货。
|
||||
- 首期不伪造设备控制、安全事件、押金、消息、发票、收藏、紧急联系人、账户注销和完整售后能力;文档中这些能力保留为后续迭代,不得以静态成功响应冒充已实现。
|
||||
|
||||
|
||||
@@ -90,7 +90,8 @@
|
||||
- 登录令牌短期有效,刷新令牌可撤销;后台高权限账号启用 MFA、IP/设备策略。平台后台管理的平台、气站、配送、员工和业主账号密码按当前实施口径仅要求不少于 6 个字符,不附加复杂度校验。
|
||||
- 权限校验在服务端执行,前端菜单隐藏不构成权限控制。按角色、站点、区域、对象归属联合鉴权。
|
||||
- 手机号、地址、身份证明、收款账户、定位、视频为敏感数据:传输 TLS、存储加密/字段加密、显示脱敏、访问留痕、最小化留存。
|
||||
- 所有支付回调验证签名与金额、订单、商户号一致性;合同文件使用可信第三方原文与哈希存证。
|
||||
- 所有支付回调验证签名与金额、订单、商户号一致性;合同文件使用可信第三方原文与哈希存证。渠道回调入口为 `/heqi/payment-return/v1/{alipay|wechat}/notify`,不使用用户 JWT;必须完成渠道证书验签、商户/appid、平台支付单号、金额、币种和状态校验后才可在数据库事务中推进业务。重复通知必须幂等,原始敏感报文只保存摘要。
|
||||
- `payment_order` 是统一支付尝试事实;`payment_refund` 与 `payment_refund_item` 保存用户退款申请及明细。审批通过与钱包入账必须同事务完成。
|
||||
- 图片/视频上传做文件类型、大小、病毒/恶意内容检测;访问采用短期授权,不使用公开桶。
|
||||
- 设备控制、告警等级调整、资金审核、数据导出、账号注销等高风险操作要求二次确认和审计。
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
@@ -14,8 +14,8 @@ const frontendDefinitions = [
|
||||
].map((match) => ({ name: match[1], mode: match[2] }));
|
||||
const frontendNames = frontendDefinitions.map((item) => item.name);
|
||||
const embeddedResources = new Set([
|
||||
'wallet_basic', 'wallet_bank', 'wallet_payment', 'wallet_record',
|
||||
'wallet_refund', 'wallet_apply_cash',
|
||||
'wallet_basic', 'wallet_bank', 'payment_order', 'wallet_record',
|
||||
'payment_refund', 'wallet_apply_cash',
|
||||
]);
|
||||
|
||||
for (const name of frontendNames) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type ResourceMode =
|
||||
export type ResourceMode =
|
||||
| 'writable'
|
||||
| 'readonly'
|
||||
| 'append_only'
|
||||
@@ -140,6 +140,10 @@ const fieldLabels: Record<string, string> = {
|
||||
reviewed_at: '审核时间',
|
||||
reviewer_name: '审核人',
|
||||
reviewer_identity: '审核人标识',
|
||||
review_remark: '审核说明',
|
||||
business_type: '业务类型',
|
||||
business_identity: '业务标识',
|
||||
user_identity: '用户标识',
|
||||
ymd: '日期',
|
||||
ym: '月份',
|
||||
contract_no: '合同编号',
|
||||
@@ -345,9 +349,16 @@ export const resources: ResourceUiDefinition[] = [
|
||||
{ name: '修改钱包状态', resource: '/wallet_basic/:identity/status', method: 'PATCH', fields: [f('status', { required: true, type: 'select', options: [{ label: '启用', value: 1 }, { label: '停用', value: 2 }, { label: '冻结', value: 4 }] })] },
|
||||
]),
|
||||
define('wallet_bank', '银行卡', 'readonly', []),
|
||||
define('wallet_payment', '钱包支付记录', 'readonly', []),
|
||||
define('payment_order', '钱包支付记录', 'readonly', []),
|
||||
define('wallet_record', '钱包流水', 'readonly', []),
|
||||
define('wallet_refund', '退款记录', 'readonly', []),
|
||||
define('payment_refund', '退款审核', 'readonly', [
|
||||
f('refund_no'), f('business_type'), f('business_identity'), f('user_identity'),
|
||||
f('amount'), f('reason'), f('description'), f('refund_status'),
|
||||
f('reviewer_identity'), f('review_remark'), f('reviewed_at'), f('completed_at'),
|
||||
], 'list', [
|
||||
{ name: '审核通过并退入钱包', resource: '/payment_refund/:identity/approve', fields: [f('remark')], visibleFor: { field: 'refund_status', values: [10] } },
|
||||
{ name: '驳回退款', resource: '/payment_refund/:identity/reject', danger: true, fields: [f('remark', { required: true })], visibleFor: { field: 'refund_status', values: [10] } },
|
||||
], { canCreate: false, canEdit: false, canChangeStatus: false, canArchive: false }),
|
||||
define('wallet_apply_cash', '提现记录', 'readonly', [
|
||||
f('cash_no'),
|
||||
f('wallet_basic_identity', { type: 'identity' }),
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -111,6 +111,7 @@ const routes: AppRouteRecordRaw[] = [
|
||||
group('finance', 'finance', '财务管理', 'icon-bar-chart', 90, [
|
||||
child('finance', 'withdrawals', 'withdrawals', '提现记录', '/wallet_apply_cash', 'wallet_apply_cash'),
|
||||
child('finance', 'payments', 'payments', '支付记录', '/fin_payment', 'fin_payment'),
|
||||
child('finance', 'refunds', 'refunds', '退款审核', '/payment_refund', 'payment_refund'),
|
||||
child('finance', 'settlements', 'settlements', '财务结算', '/fin_settlement', 'fin_settlement'),
|
||||
child('finance', 'reconciliations', 'reconciliations', '财务对账', '/fin_reconciliation', 'fin_reconciliation'),
|
||||
]),
|
||||
|
||||
Reference in New Issue
Block a user