feat(payment): 支持赛里木湖品牌收款页与独立缓存

This commit is contained in:
2026-09-23 15:08:48 +08:00
parent 3823d10423
commit d12dbc6bb0
9 changed files with 224 additions and 41 deletions
@@ -0,0 +1,21 @@
{
"images": [
{
"filename": "payment_sailimu_background.png",
"idiom": "universal",
"scale": "1x"
},
{
"idiom": "universal",
"scale": "2x"
},
{
"idiom": "universal",
"scale": "3x"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

@@ -0,0 +1,21 @@
{
"images": [
{
"filename": "payment_sailimu_logo.png",
"idiom": "universal",
"scale": "1x"
},
{
"idiom": "universal",
"scale": "2x"
},
{
"idiom": "universal",
"scale": "3x"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

@@ -35,6 +35,14 @@ struct PayPageConfig: Codable, Sendable, Equatable {
subtitle: "那拉提景区" subtitle: "那拉提景区"
) )
/// 赛里木湖景区随包默认文案,图片由展示层选择本地资源。
static let sailimuDefault = PayPageConfig(
background: "",
logo: "",
title: "赛里木湖旅拍管理\n收费平台",
subtitle: "赛里木湖景区"
)
/// 将服务端非空有效字段合并到当前可用配置。 /// 将服务端非空有效字段合并到当前可用配置。
func merging(_ response: PayPageConfigResponse) -> PayPageConfig { func merging(_ response: PayPageConfigResponse) -> PayPageConfig {
PayPageConfig( PayPageConfig(
@@ -61,10 +69,22 @@ enum PayPageBrandingPolicy {
/// 那拉提景区正式 ID。 /// 那拉提景区正式 ID。
static let nalatiScenicId = 128 static let nalatiScenicId = 128
/// 判断当前景区是否启用品牌收款页。 /// 赛里木湖景区正式 ID。
static let sailimuScenicId = 125
/// 判断当前景区是否为那拉提。
static func isNalati(scenicId: Int) -> Bool { static func isNalati(scenicId: Int) -> Bool {
scenicId == nalatiScenicId scenicId == nalatiScenicId
} }
/// 返回支持品牌收款页的景区默认配置;其他景区继续使用普通页面。
static func defaultConfig(scenicId: Int) -> PayPageConfig? {
switch scenicId {
case nalatiScenicId: return .nalatiDefault
case sailimuScenicId: return .sailimuDefault
default: return nil
}
}
} }
/// 收款页当前账号信息的展示文本格式化规则。 /// 收款页当前账号信息的展示文本格式化规则。
@@ -42,9 +42,7 @@ final class PaymentCollectionDetailsViewModel {
scenicName = appStore.session.currentScenicName scenicName = appStore.session.currentScenicName
staffId = appStore.session.userId staffId = appStore.session.userId
isVoiceBroadcastOpen = appStore.payment.isOpenReceiveVoice isVoiceBroadcastOpen = appStore.payment.isOpenReceiveVoice
if PayPageBrandingPolicy.isNalati(scenicId: scenicId) { restorePayPageConfig()
payPageConfig = payPageConfigCache.config(scenicId: scenicId) ?? .nalatiDefault
}
} }
var hasPayCode: Bool { var hasPayCode: Bool {
@@ -60,8 +58,26 @@ final class PaymentCollectionDetailsViewModel {
scenicName.isEmpty ? "暂无景区" : scenicName scenicName.isEmpty ? "暂无景区" : scenicName
} }
var usesNalatiBranding: Bool { /// 是否启用品牌收款页,包含那拉提与赛里木湖。
PayPageBrandingPolicy.isNalati(scenicId: scenicId) var usesBranding: Bool {
PayPageBrandingPolicy.defaultConfig(scenicId: scenicId) != nil
}
/// 当前景区的随包背景资源名称。
var brandBackgroundAssetName: String {
scenicId == PayPageBrandingPolicy.sailimuScenicId
? "payment_sailimu_background" : "payment_nalati_background"
}
/// 当前景区的随包 Logo 资源名称。
var brandLogoAssetName: String {
scenicId == PayPageBrandingPolicy.sailimuScenicId
? "payment_sailimu_logo" : "payment_nalati_logo"
}
/// 品牌主标题的最大行数,赛里木湖允许两行展示。
var brandTitleNumberOfLines: Int {
scenicId == PayPageBrandingPolicy.sailimuScenicId ? 2 : 1
} }
var displayPhotographerName: String { var displayPhotographerName: String {
@@ -80,14 +96,18 @@ final class PaymentCollectionDetailsViewModel {
scenicName = appStore.session.currentScenicName scenicName = appStore.session.currentScenicName
staffId = appStore.session.userId staffId = appStore.session.userId
isVoiceBroadcastOpen = appStore.payment.isOpenReceiveVoice isVoiceBroadcastOpen = appStore.payment.isOpenReceiveVoice
if usesNalatiBranding { restorePayPageConfig()
payPageConfig = payPageConfigCache.config(scenicId: scenicId) ?? .nalatiDefault
} else {
payPageConfig = .nalatiDefault
}
notifyStateChange() notifyStateChange()
} }
private func restorePayPageConfig() {
guard let defaults = PayPageBrandingPolicy.defaultConfig(scenicId: scenicId) else {
payPageConfig = .nalatiDefault
return
}
payPageConfig = payPageConfigCache.config(scenicId: scenicId) ?? defaults
}
func loadPayCode(api: PaymentAPI) async { func loadPayCode(api: PaymentAPI) async {
guard !loading else { return } guard !loading else { return }
loading = true loading = true
@@ -116,12 +136,14 @@ final class PaymentCollectionDetailsViewModel {
func loadPayPageConfig(api: PaymentAPI) async { func loadPayPageConfig(api: PaymentAPI) async {
let requestedScenicId = appStore.session.currentScenicId let requestedScenicId = appStore.session.currentScenicId
guard PayPageBrandingPolicy.isNalati(scenicId: requestedScenicId) else { return } guard let defaults = PayPageBrandingPolicy.defaultConfig(scenicId: requestedScenicId) else { return }
do { do {
let response = try await api.payPageConfig(scenicId: requestedScenicId) let response = try await api.payPageConfig(scenicId: requestedScenicId)
guard appStore.session.currentScenicId == requestedScenicId else { return } guard appStore.session.currentScenicId == requestedScenicId,
let resolved = payPageConfig.merging(response) scenicId == requestedScenicId else { return }
let cached = payPageConfigCache.config(scenicId: requestedScenicId) ?? defaults
let resolved = cached.merging(response)
payPageConfigCache.save(resolved, scenicId: requestedScenicId) payPageConfigCache.save(resolved, scenicId: requestedScenicId)
payPageConfig = resolved payPageConfig = resolved
brandingRefreshVersion += 1 brandingRefreshVersion += 1
@@ -101,15 +101,16 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
} }
override func setupNavigationBar() { override func setupNavigationBar() {
title = viewModel.usesNalatiBranding ? nil : "收款详情" title = viewModel.usesBranding ? nil : "收款详情"
} }
override func setupUI() { override func setupUI() {
let usesBranding = viewModel.usesNalatiBranding let usesBranding = viewModel.usesBranding
view.backgroundColor = usesBranding ? .clear : AppColor.pageBackground view.backgroundColor = usesBranding ? .clear : AppColor.pageBackground
scrollView.showsVerticalScrollIndicator = !usesBranding scrollView.showsVerticalScrollIndicator = !usesBranding
scrollView.isScrollEnabled = true scrollView.isScrollEnabled = true
scrollView.alwaysBounceVertical = true scrollView.alwaysBounceVertical = true
scrollView.contentInsetAdjustmentBehavior = .always
contentStack.axis = .vertical contentStack.axis = .vertical
contentStack.spacing = usesBranding ? brandSectionSpacing : AppSpacing.md contentStack.spacing = usesBranding ? brandSectionSpacing : AppSpacing.md
@@ -187,7 +188,7 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
configureBrandHeader() configureBrandHeader()
backgroundImageView.contentMode = .scaleAspectFill backgroundImageView.contentMode = .scaleAspectFill
backgroundImageView.clipsToBounds = true backgroundImageView.clipsToBounds = true
backgroundImageView.image = UIImage(named: "payment_nalati_background") backgroundImageView.image = UIImage(named: viewModel.brandBackgroundAssetName)
view.addSubview(backgroundImageView) view.addSubview(backgroundImageView)
} }
@@ -320,30 +321,24 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
} }
override func setupConstraints() { override func setupConstraints() {
if viewModel.usesNalatiBranding { if viewModel.usesBranding {
backgroundImageView.snp.makeConstraints { make in backgroundImageView.snp.makeConstraints { make in
make.edges.equalToSuperview() make.edges.equalToSuperview()
} }
} }
scrollView.snp.makeConstraints { make in scrollView.snp.makeConstraints { make in
if viewModel.usesNalatiBranding { make.edges.equalToSuperview()
make.leading.trailing.equalToSuperview()
make.top.equalTo(view.safeAreaLayoutGuide.snp.top).offset(-44)
make.bottom.equalTo(view.safeAreaLayoutGuide)
} else {
make.edges.equalTo(view.safeAreaLayoutGuide)
}
} }
contentContainerView.snp.makeConstraints { make in contentContainerView.snp.makeConstraints { make in
make.edges.equalToSuperview() make.edges.equalTo(scrollView.contentLayoutGuide)
make.width.equalTo(scrollView.snp.width) make.width.equalTo(scrollView.frameLayoutGuide)
if viewModel.usesNalatiBranding { if viewModel.usesBranding {
make.height.greaterThanOrEqualTo(scrollView.snp.height) make.height.greaterThanOrEqualTo(scrollView.snp.height)
} }
} }
contentStack.snp.makeConstraints { make in contentStack.snp.makeConstraints { make in
make.width.equalTo(scrollView.snp.width).offset(-AppSpacing.screenHorizontalInset * 2) make.width.equalTo(scrollView.snp.width).offset(-AppSpacing.screenHorizontalInset * 2)
if viewModel.usesNalatiBranding { if viewModel.usesBranding {
make.centerX.equalToSuperview() make.centerX.equalToSuperview()
make.top.bottom.equalToSuperview().inset(AppSpacing.screenHorizontalInset) make.top.bottom.equalToSuperview().inset(AppSpacing.screenHorizontalInset)
} else { } else {
@@ -403,7 +398,7 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
} }
override var preferredStatusBarStyle: UIStatusBarStyle { override var preferredStatusBarStyle: UIStatusBarStyle {
viewModel.usesNalatiBranding ? .darkContent : .default viewModel.usesBranding ? .darkContent : .default
} }
private func loadData() async { private func loadData() async {
@@ -426,7 +421,7 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
if viewModel.isPayCodeLoading { if viewModel.isPayCodeLoading {
qrImageView.isHidden = true qrImageView.isHidden = true
qrContainerView.isHidden = true qrContainerView.isHidden = true
qrPlaceholderView.backgroundColor = viewModel.usesNalatiBranding qrPlaceholderView.backgroundColor = viewModel.usesBranding
? .white ? .white
: AppColor.inputBackground : AppColor.inputBackground
qrPlaceholderView.isHidden = false qrPlaceholderView.isHidden = false
@@ -437,7 +432,7 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
} else if let image = viewModel.qrImage { } else if let image = viewModel.qrImage {
qrImageView.image = image qrImageView.image = image
qrImageView.isHidden = false qrImageView.isHidden = false
qrContainerView.isHidden = !viewModel.usesNalatiBranding qrContainerView.isHidden = !viewModel.usesBranding
qrPlaceholderView.isHidden = true qrPlaceholderView.isHidden = true
qrLoadingIndicator.stopAnimating() qrLoadingIndicator.stopAnimating()
setActionRowAvailable(true) setActionRowAvailable(true)
@@ -527,7 +522,7 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
brandHeaderStack.alignment = .center brandHeaderStack.alignment = .center
brandHeaderStack.spacing = brandScreenHeight <= 700 ? 6 : 8 brandHeaderStack.spacing = brandScreenHeight <= 700 ? 6 : 8
brandLogoImageView.image = UIImage(named: "payment_nalati_logo") brandLogoImageView.image = UIImage(named: viewModel.brandLogoAssetName)
brandLogoImageView.contentMode = .scaleAspectFit brandLogoImageView.contentMode = .scaleAspectFit
brandLogoImageView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) brandLogoImageView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
@@ -537,7 +532,7 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
) )
brandTitleLabel.textColor = UIColor(hex: 0x102D54) brandTitleLabel.textColor = UIColor(hex: 0x102D54)
brandTitleLabel.textAlignment = .center brandTitleLabel.textAlignment = .center
brandTitleLabel.numberOfLines = 1 brandTitleLabel.numberOfLines = viewModel.brandTitleNumberOfLines
brandTitleLabel.adjustsFontSizeToFitWidth = true brandTitleLabel.adjustsFontSizeToFitWidth = true
brandTitleLabel.minimumScaleFactor = 0.8 brandTitleLabel.minimumScaleFactor = 0.8
@@ -604,7 +599,7 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
@MainActor @MainActor
private func applyBrandConfigIfNeeded() { private func applyBrandConfigIfNeeded() {
guard viewModel.usesNalatiBranding else { return } guard viewModel.usesBranding else { return }
let config = viewModel.payPageConfig let config = viewModel.payPageConfig
brandTitleLabel.text = config.title brandTitleLabel.text = config.title
brandSubtitleLabel.text = config.subtitle brandSubtitleLabel.text = config.subtitle
@@ -615,13 +610,13 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
setBrandImage( setBrandImage(
backgroundImageView, backgroundImageView,
urlString: config.background, urlString: config.background,
fallbackName: "payment_nalati_background", fallbackName: viewModel.brandBackgroundAssetName,
forceRefresh: forceRefresh forceRefresh: forceRefresh
) )
setBrandImage( setBrandImage(
brandLogoImageView, brandLogoImageView,
urlString: config.logo, urlString: config.logo,
fallbackName: "payment_nalati_logo", fallbackName: viewModel.brandLogoAssetName,
forceRefresh: forceRefresh forceRefresh: forceRefresh
) )
appliedBrandConfig = config appliedBrandConfig = config
@@ -639,6 +634,7 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
guard let url = URL(string: trimmed), guard let url = URL(string: trimmed),
url.scheme?.lowercased() == "https", url.scheme?.lowercased() == "https",
url.host != nil else { url.host != nil else {
imageView.kf.cancelDownloadTask()
imageView.image = fallback imageView.image = fallback
return return
} }
@@ -655,7 +651,7 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
} }
private func applyBrandNavigationAppearanceIfNeeded() { private func applyBrandNavigationAppearanceIfNeeded() {
guard viewModel.usesNalatiBranding, let navigationBar = navigationController?.navigationBar else { return } guard viewModel.usesBranding, let navigationBar = navigationController?.navigationBar else { return }
previousStandardAppearance = navigationBar.standardAppearance previousStandardAppearance = navigationBar.standardAppearance
previousScrollEdgeAppearance = navigationBar.scrollEdgeAppearance previousScrollEdgeAppearance = navigationBar.scrollEdgeAppearance
previousCompactAppearance = navigationBar.compactAppearance previousCompactAppearance = navigationBar.compactAppearance
@@ -677,7 +673,7 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
} }
private func restoreNavigationAppearanceIfNeeded() { private func restoreNavigationAppearanceIfNeeded() {
guard viewModel.usesNalatiBranding, let navigationBar = navigationController?.navigationBar else { return } guard viewModel.usesBranding, let navigationBar = navigationController?.navigationBar else { return }
if let previousStandardAppearance { if let previousStandardAppearance {
navigationBar.standardAppearance = previousStandardAppearance navigationBar.standardAppearance = previousStandardAppearance
} }
@@ -4,6 +4,7 @@
// //
import XCTest import XCTest
import UIKit
@testable import suixinkan @testable import suixinkan
/// 收款详情 ViewModel 测试。 /// 收款详情 ViewModel 测试。
@@ -187,14 +188,14 @@ final class PaymentCollectionDetailsViewModelTests: XCTestCase {
) )
} }
func testNonNalatiDoesNotRequestPayPageConfig() async { func testUnbrandedScenicDoesNotRequestPayPageConfig() async {
let session = MockURLSession(responses: []) let session = MockURLSession(responses: [])
let api = PaymentAPI(client: APIClient(environment: .testing, session: session)) let api = PaymentAPI(client: APIClient(environment: .testing, session: session))
let viewModel = PaymentCollectionDetailsViewModel(appStore: appStore) let viewModel = PaymentCollectionDetailsViewModel(appStore: appStore)
await viewModel.loadPayPageConfig(api: api) await viewModel.loadPayPageConfig(api: api)
XCTAssertFalse(viewModel.usesNalatiBranding) XCTAssertFalse(viewModel.usesBranding)
XCTAssertEqual(session.requests.count, 0) XCTAssertEqual(session.requests.count, 0)
} }
@@ -209,4 +210,96 @@ final class PaymentCollectionDetailsViewModelTests: XCTestCase {
XCTAssertEqual(production.config(scenicId: 128), .nalatiDefault) XCTAssertEqual(production.config(scenicId: 128), .nalatiDefault)
XCTAssertNil(testing.config(scenicId: 128)) XCTAssertNil(testing.config(scenicId: 128))
} }
func testSailimuRefreshPersistsOnlyItsOwnConfigAndReusesBrandUI() async throws {
let suite = "PaymentCollectionDetailsViewModelTests.Sailimu.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suite)!
defer { defaults.removePersistentDomain(forName: suite) }
let cache = PayPageConfigCache(defaults: defaults, environment: .testing)
cache.save(.nalatiDefault, scenicId: 128)
let cached = PayPageConfig(
background: "https://cache.example.com/sailimu.png",
logo: "https://cache.example.com/sailimu-logo.png",
title: "缓存赛里木湖标题",
subtitle: "赛里木湖景区"
)
cache.save(cached, scenicId: 125)
appStore.session.currentScenicId = 125
let viewModel = PaymentCollectionDetailsViewModel(appStore: appStore, payPageConfigCache: cache)
XCTAssertTrue(viewModel.usesBranding)
XCTAssertEqual(viewModel.payPageConfig, cached)
let response = """
{"code":100000,"msg":"success","data":{
"background":"https://new.example.com/sailimu.png",
"logo":"", "title":"最新赛里木湖标题", "subtitle":""
}}
""".data(using: .utf8)!
let session = MockURLSession(responses: [response])
let api = PaymentAPI(client: APIClient(environment: .testing, session: session))
await viewModel.loadPayPageConfig(api: api)
let url = try XCTUnwrap(session.requests.first?.url)
XCTAssertEqual(url.path, "/api/yf-handset-app/photog/pay-page-config")
XCTAssertEqual(URLComponents(url: url, resolvingAgainstBaseURL: false)?
.queryItems?.first(where: { $0.name == "scenic_id" })?.value, "125")
XCTAssertEqual(viewModel.payPageConfig.title, "最新赛里木湖标题")
XCTAssertEqual(viewModel.payPageConfig.background, "https://new.example.com/sailimu.png")
XCTAssertEqual(viewModel.payPageConfig.logo, cached.logo)
XCTAssertEqual(viewModel.payPageConfig.subtitle, cached.subtitle)
XCTAssertEqual(viewModel.brandingRefreshVersion, 1)
XCTAssertEqual(cache.config(scenicId: 128), .nalatiDefault)
XCTAssertEqual(cache.config(scenicId: 125), viewModel.payPageConfig)
let reopened = PaymentCollectionDetailsViewModel(appStore: appStore, payPageConfigCache: cache)
XCTAssertEqual(reopened.payPageConfig, viewModel.payPageConfig)
appStore.session.currentScenicId = 128
viewModel.refreshLocalState()
XCTAssertEqual(viewModel.payPageConfig, .nalatiDefault)
appStore.session.currentScenicId = 125
viewModel.refreshLocalState()
XCTAssertEqual(viewModel.payPageConfig, reopened.payPageConfig)
}
func testSailimuRequestFailurePreservesDefaultsOrItsOwnCache() async {
let suite = "PaymentCollectionDetailsViewModelTests.SailimuFailure.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suite)!
defer { defaults.removePersistentDomain(forName: suite) }
let cache = PayPageConfigCache(defaults: defaults, environment: .testing)
cache.save(.nalatiDefault, scenicId: 128)
appStore.session.currentScenicId = 125
let viewModel = PaymentCollectionDetailsViewModel(appStore: appStore, payPageConfigCache: cache)
let session = MockURLSession(responses: [])
let api = PaymentAPI(client: APIClient(environment: .testing, session: session))
XCTAssertEqual(viewModel.payPageConfig, .sailimuDefault)
await viewModel.loadPayPageConfig(api: api)
XCTAssertEqual(viewModel.payPageConfig, .sailimuDefault)
XCTAssertNil(cache.config(scenicId: 125))
let cached = PayPageConfig(background: "https://cache.example.com/lake.png",
logo: "", title: "缓存标题", subtitle: "赛里木湖")
cache.save(cached, scenicId: 125)
viewModel.refreshLocalState()
await viewModel.loadPayPageConfig(api: api)
XCTAssertEqual(viewModel.payPageConfig, cached)
XCTAssertEqual(cache.config(scenicId: 125), cached)
XCTAssertEqual(cache.config(scenicId: 128), .nalatiDefault)
XCTAssertEqual(viewModel.brandingRefreshVersion, 0)
}
func testBrandPresentationUsesMatchingBundledAssetsAndTitleLines() {
let viewModel = PaymentCollectionDetailsViewModel(appStore: appStore)
for (scenicId, prefix, lines) in [(125, "payment_sailimu", 2), (128, "payment_nalati", 1)] {
appStore.session.currentScenicId = scenicId
viewModel.refreshLocalState()
XCTAssertTrue(viewModel.usesBranding)
XCTAssertEqual(viewModel.brandBackgroundAssetName, "\(prefix)_background")
XCTAssertEqual(viewModel.brandLogoAssetName, "\(prefix)_logo")
XCTAssertEqual(viewModel.brandTitleNumberOfLines, lines)
XCTAssertNotNil(UIImage(named: viewModel.brandBackgroundAssetName))
XCTAssertNotNil(UIImage(named: viewModel.brandLogoAssetName))
}
}
} }
+10
View File
@@ -55,6 +55,16 @@ final class PaymentModelsTests: XCTestCase {
XCTAssertFalse(PayPageBrandingPolicy.isNalati(scenicId: 0)) XCTAssertFalse(PayPageBrandingPolicy.isNalati(scenicId: 0))
} }
func testBrandDefaultsAreScopedToSupportedScenicIds() {
XCTAssertEqual(PayPageBrandingPolicy.defaultConfig(scenicId: 128), .nalatiDefault)
XCTAssertEqual(PayPageBrandingPolicy.defaultConfig(scenicId: 125), .sailimuDefault)
XCTAssertEqual(PayPageConfig.sailimuDefault.title, "赛里木湖旅拍管理\n收费平台")
XCTAssertEqual(PayPageConfig.sailimuDefault.subtitle, "赛里木湖景区")
XCTAssertFalse(PayPageBrandingPolicy.isNalati(scenicId: 125))
XCTAssertNil(PayPageBrandingPolicy.defaultConfig(scenicId: 127))
XCTAssertNil(PayPageBrandingPolicy.defaultConfig(scenicId: 0))
}
func testPaymentAccountDisplayFormatterUsesRealNameAndAllUniqueStores() { func testPaymentAccountDisplayFormatterUsesRealNameAndAllUniqueStores() {
XCTAssertEqual( XCTAssertEqual(
PaymentAccountDisplayFormatter.photographerName(realName: " 摄影师真实姓名 "), PaymentAccountDisplayFormatter.photographerName(realName: " 摄影师真实姓名 "),