// // SettingsViewController.swift // VenusKitto // // Created by Neoa on 2025/8/27. // import Foundation import UIKit import StoreKit import AnyThinkBanner import AnyThinkSDK import AnyThinkInterstitial import AnyThinkNative // MARK: - Settings final class SettingsViewController: UIViewController, ATAdLoadingDelegate, ATBannerDelegate, ATInterstitialDelegate, ATNativeADDelegate { // 背景图 private let bgImageView: UIImageView = { let iv = UIImageView(image: UIImage(named: "wode465")) iv.contentMode = .scaleAspectFill iv.clipsToBounds = true return iv }() private let titleLabel: UILabel = { let l = UILabel() l.text = "" l.font = .systemFont(ofSize: 28, weight: .semibold) l.textColor = UIColor(hex: "#2B2B2B") return l }() // 顶部用户卡片(手绘边框) private let profileCard = UIImageView(image: UIImage(named: "wode402")) private let avatarView = UIImageView() private let phoneLabel = UILabel() private let idLabel = UILabel() private let profileChevron = UIImageView(image: UIImage(systemName: "chevron.right")) // 三个功能行 private lazy var rateRow = makeRow(title: "给个好评", action: #selector(tapRate)) private lazy var peteRow = makeRow(title: "我的宠物", action: #selector(tapPet)) private lazy var feedbackRow = makeRow(title: "意见反馈", action: #selector(tapFeedback)) private lazy var aboutRow = makeRow(title: "关于我们", action: #selector(tapAbout)) // MARK: - 广告相关属性 // 广告位ID private let interstitialPlacementID = "b68d88cc36bca8" private let bannerPlacementID = "b68d88cc2ab33d" private let nativeRenderPlacementID = "b68d88cc1605f1" // 横幅广告容器 private let bannerContainer: UIView = { let v = UIView() v.backgroundColor = .clear v.translatesAutoresizingMaskIntoConstraints = false return v }() private var bannerHeightConstraint: NSLayoutConstraint? private var bannerView: ATBannerView? private var bannerSize: CGSize { let screenWidth = UIScreen.main.bounds.width let aspectRatio: CGFloat = 320 / 50 let bannerWidth = screenWidth let bannerHeight = bannerWidth / aspectRatio return CGSize(width: bannerWidth, height: bannerHeight) } // 原生信息流广告容器 private let nativeAdContainer: UIView = { let view = UIView() view.backgroundColor = .clear view.translatesAutoresizingMaskIntoConstraints = false view.isUserInteractionEnabled = true return view }() private var nativeAdView: ATNativeADView? private var selfRenderView: RenderUnitView? private var nativeAdOffer: ATNativeAdOffer? private var nativeAdHeightConstraint: NSLayoutConstraint? // 广告展示开始时间(毫秒),key 为 placementID private var adStartTimes: [String: Int64] = [:] // 当前时间戳(毫秒) private func nowMillis() -> Int64 { Int64(Date().timeIntervalSince1970 * 1000) } // 将毫秒时间戳格式化为 "yyyy-MM-dd HH:mm:ss" private func formatMillisToString(_ ms: Int64) -> String { let date = Date(timeIntervalSince1970: TimeInterval(ms) / 1000.0) let df = DateFormatter() df.locale = Locale(identifier: "en_US_POSIX") df.timeZone = TimeZone.current df.dateFormat = "yyyy-MM-dd HH:mm:ss" return df.string(from: date) } // ===== 当天广告观看次数(持久化到 UserDefaults)===== private func adCountKeyForToday() -> String { let df = DateFormatter() df.locale = Locale(identifier: "en_US_POSIX") df.dateFormat = "yyyy-MM-dd" return "adCount_" + df.string(from: Date()) } private func incrementTodayAdCount() { let key = adCountKeyForToday() let current = UserDefaults.standard.integer(forKey: key) UserDefaults.standard.set(current + 1, forKey: key) } private func todayAdCount() -> Int { let key = adCountKeyForToday() return UserDefaults.standard.integer(forKey: key) } override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = UIColor(hex: "#FFFEFC") setupBackground() if let backImage = UIImage(named: "AddPet385") { let backButton = UIBarButtonItem(image: backImage.withRenderingMode(.alwaysOriginal), style: .plain, target: self, action: #selector(tapCancel)) navigationItem.leftBarButtonItem = backButton } NotificationCenter.default.addObserver(self, selector: #selector(handleAvatarUpdated(_:)), name: Notification.Name("UserAvatarUpdated"), object: nil) buildUI() fillUserInfo() // 加载广告 loadBannerAd() loadNativeAd() // // 延迟加载插屏广告 // DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { // self.loadInterstitialAd() // } } deinit { NotificationCenter.default.removeObserver(self, name: Notification.Name("UserAvatarUpdated"), object: nil) } @objc private func handleAvatarUpdated(_ note: Notification) { if let urlStr = note.object as? String, let url = URL(string: urlStr) { // 直接用通知携带的新 URL 刷新头像 loadImage(from: url) { [weak self] img in self?.avatarView.image = img ?? UIImage(named: "Home372") } // 同步到本地,保证下次进入可以读到 UserDefaults.standard.set(urlStr, forKey: "memberIcon") } else if let urlStr = (UserDefaults.standard.string(forKey: "memberIcon")), let url = URL(string: urlStr) { // 兜底:没有携带 object 时,从本地读取刷新 loadImage(from: url) { [weak self] img in self?.avatarView.image = img ?? UIImage(named: "Home372") } } } private func buildUI() { // 顶部标题 view.addSubview(titleLabel) titleLabel.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ titleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 12), titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 24) ]) // 用户卡片 profileCard.contentMode = .scaleToFill profileCard.isUserInteractionEnabled = true let tapCard = UITapGestureRecognizer(target: self, action: #selector(tapProfile)) profileCard.addGestureRecognizer(tapCard) view.addSubview(profileCard) profileCard.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ profileCard.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 12), profileCard.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), profileCard.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), profileCard.heightAnchor.constraint(equalToConstant: 92) ]) avatarView.backgroundColor = UIColor(hex: "#FFF4CC") avatarView.layer.cornerRadius = 12 avatarView.layer.masksToBounds = true avatarView.contentMode = .scaleAspectFill phoneLabel.font = .systemFont(ofSize: 16, weight: .semibold) phoneLabel.textColor = UIColor(hex: "#2B2B2B") idLabel.font = .systemFont(ofSize: 12) idLabel.textColor = UIColor(hex: "#9B9B9B") profileChevron.tintColor = UIColor(hex: "#5B4227") profileChevron.setContentCompressionResistancePriority(.required, for: .horizontal) [avatarView, phoneLabel, idLabel, profileChevron].forEach { $0.translatesAutoresizingMaskIntoConstraints = false profileCard.addSubview($0) } NSLayoutConstraint.activate([ avatarView.leadingAnchor.constraint(equalTo: profileCard.leadingAnchor, constant: 16), avatarView.centerYAnchor.constraint(equalTo: profileCard.centerYAnchor), avatarView.widthAnchor.constraint(equalToConstant: 56), avatarView.heightAnchor.constraint(equalToConstant: 56), profileChevron.trailingAnchor.constraint(equalTo: profileCard.trailingAnchor, constant: -12), profileChevron.centerYAnchor.constraint(equalTo: profileCard.centerYAnchor), profileChevron.widthAnchor.constraint(equalToConstant: 16), profileChevron.heightAnchor.constraint(equalToConstant: 16), phoneLabel.leadingAnchor.constraint(equalTo: avatarView.trailingAnchor, constant: 12), phoneLabel.trailingAnchor.constraint(lessThanOrEqualTo: profileChevron.leadingAnchor, constant: -8), phoneLabel.bottomAnchor.constraint(equalTo: profileCard.centerYAnchor, constant: -2), idLabel.leadingAnchor.constraint(equalTo: phoneLabel.leadingAnchor), idLabel.topAnchor.constraint(equalTo: profileCard.centerYAnchor, constant: 2), idLabel.trailingAnchor.constraint(lessThanOrEqualTo: profileChevron.leadingAnchor, constant: -8) ]) // 添加广告容器 view.addSubview(bannerContainer) view.addSubview(nativeAdContainer) // 三个功能 row let stack = UIStackView(arrangedSubviews: [rateRow, peteRow, feedbackRow, aboutRow]) stack.axis = .vertical stack.spacing = 12 stack.alignment = .fill view.addSubview(stack) stack.translatesAutoresizingMaskIntoConstraints = false // 设置广告容器约束 bannerHeightConstraint = bannerContainer.heightAnchor.constraint(equalToConstant: 0) bannerHeightConstraint?.isActive = true nativeAdHeightConstraint = nativeAdContainer.heightAnchor.constraint(equalToConstant: 0) nativeAdHeightConstraint?.isActive = true NSLayoutConstraint.activate([ // 横幅广告容器(位于用户卡片下方) bannerContainer.topAnchor.constraint(equalTo: profileCard.bottomAnchor, constant: 8), bannerContainer.leadingAnchor.constraint(equalTo: view.leadingAnchor), bannerContainer.trailingAnchor.constraint(equalTo: view.trailingAnchor), // 功能按钮栈(位于横幅广告下方) stack.topAnchor.constraint(equalTo: bannerContainer.bottomAnchor, constant: 16), stack.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), stack.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), stack.bottomAnchor.constraint(lessThanOrEqualTo: nativeAdContainer.topAnchor, constant: -16), // 原生信息流广告容器(位于页面底部) nativeAdContainer.leadingAnchor.constraint(equalTo: view.leadingAnchor), nativeAdContainer.trailingAnchor.constraint(equalTo: view.trailingAnchor), nativeAdContainer.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor) ]) } private func setupBackground() { view.insertSubview(bgImageView, at: 0) bgImageView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ bgImageView.topAnchor.constraint(equalTo: view.topAnchor), bgImageView.leadingAnchor.constraint(equalTo: view.leadingAnchor), bgImageView.trailingAnchor.constraint(equalTo: view.trailingAnchor), bgImageView.bottomAnchor.constraint(equalTo: view.bottomAnchor) ]) } @objc private func tapCancel() { navigationController?.popViewController(animated: true) } private func makeRow(title: String, action: Selector) -> UIControl { let container = UIControl() container.backgroundColor = .white container.layer.cornerRadius = 14 container.layer.shadowColor = UIColor.black.cgColor container.layer.shadowOpacity = 0.08 container.layer.shadowRadius = 10 container.layer.shadowOffset = CGSize(width: 0, height: 4) container.heightAnchor.constraint(equalToConstant: 64).isActive = true let label = UILabel() label.text = title label.font = .systemFont(ofSize: 16) label.textColor = UIColor(hex: "#2B2B2B") let chevron = UIImageView(image: UIImage(systemName: "chevron.right")) chevron.tintColor = UIColor(hex: "#5B4227") chevron.setContentCompressionResistancePriority(.required, for: .horizontal) container.addSubview(label) container.addSubview(chevron) label.translatesAutoresizingMaskIntoConstraints = false chevron.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ label.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 16), label.centerYAnchor.constraint(equalTo: container.centerYAnchor), chevron.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -16), chevron.centerYAnchor.constraint(equalTo: container.centerYAnchor), chevron.widthAnchor.constraint(equalToConstant: 16), chevron.heightAnchor.constraint(equalToConstant: 16) ]) container.addTarget(self, action: action, for: .touchUpInside) return container } private func fillUserInfo() { // 显示手机号(打码)与 ID let phone = UserDefaults.standard.string(forKey: "memberPhone") ?? "登录" let uid = UserDefaults.standard.string(forKey: "userId") ?? "--" phoneLabel.text = maskedPhone(phone) idLabel.text = "ID:\(uid)" if let urlStr = UserDefaults.standard.string(forKey: "memberIcon"), let url = URL(string: urlStr) { loadImage(from: url) { [weak self] img in self?.avatarView.image = img ?? UIImage(named: "Home372") } } else { avatarView.image = UIImage(named: "Home372") } } private func maskedPhone(_ s: String) -> String { let digits = s.filter { $0.isNumber } guard digits.count >= 7 else { return s } let start = digits.prefix(3) let end = digits.suffix(4) return "\(start)****\(end)" } private func loadImage(from url: URL, completion: @escaping (UIImage?) -> Void) { URLSession.shared.dataTask(with: url) { data, _, _ in guard let data = data, let img = UIImage(data: data) else { DispatchQueue.main.async { completion(nil) } return } DispatchQueue.main.async { completion(img) } }.resume() } // MARK: - Actions @objc private func tapProfile() { // 检查登录状态 let isLoggedIn = UserDefaults.standard.bool(forKey: "isLogggedIn") let userToken = UserDefaults.standard.string(forKey: "userToken") // 如果没有登录状态或者没有有效的token,弹出登录界面 if !isLoggedIn || userToken?.isEmpty != false { showLoginViewController() return } // 已登录,跳转到个人信息页 let userSettingsVC = UserSettingsViewController() userSettingsVC.hidesBottomBarWhenPushed = true navigationController?.pushViewController(userSettingsVC, animated: true) } private func showLoginViewController() { let loginVC = LoginViewController() let navController = UINavigationController(rootViewController: loginVC) navController.modalPresentationStyle = .fullScreen present(navController, animated: true) } @objc private func tapRate() { // 跳 App Store 评分(占位) SKStoreReviewController.requestReview() } @objc private func tapPet() { // 检查登录状态 let isLoggedIn = UserDefaults.standard.bool(forKey: "isLogggedIn") let userToken = UserDefaults.standard.string(forKey: "userToken") // 如果没有登录状态或者没有有效的token,弹出登录界面 if !isLoggedIn || userToken?.isEmpty != false { showLoginViewController() return } //进入我的宠物界面 let vc = MyPetsViewController() vc.hidesBottomBarWhenPushed = true navigationController?.pushViewController(vc, animated: true) } @objc private func tapFeedback() { let vc = FeedbackViewController() vc.hidesBottomBarWhenPushed = true navigationController?.pushViewController(vc, animated: true) } @objc private func tapAbout() { let vc = AboutViewController() vc.hidesBottomBarWhenPushed = true navigationController?.pushViewController(vc, animated: true) } // MARK: - 广告方法实现 // MARK: - 横幅广告方法 private func loadBannerAd() { var extra: [AnyHashable: Any] = [:] extra[kATAdLoadingExtraBannerAdSizeKey] = NSValue(cgSize: bannerSize) extra[kATAdLoadingExtraMediaExtraKey] = "SettingsVC" ATAdManager.shared().loadAD(withPlacementID: bannerPlacementID, extra: extra, delegate: self) } private func showBannerIfReady() { let config = ATShowConfig(scene: nil, showCustomExt: nil) guard let bView = ATAdManager.shared().retrieveBannerView(forPlacementID: bannerPlacementID, config: config) else { return } bView.delegate = self bView.presentingViewController = self bView.translatesAutoresizingMaskIntoConstraints = false bannerContainer.addSubview(bView) NSLayoutConstraint.activate([ bView.centerXAnchor.constraint(equalTo: bannerContainer.centerXAnchor), bView.topAnchor.constraint(equalTo: bannerContainer.topAnchor), bView.widthAnchor.constraint(equalToConstant: bannerSize.width), bView.heightAnchor.constraint(equalToConstant: bannerSize.height) ]) bannerView = bView bannerHeightConstraint?.constant = bannerSize.height UIView.animate(withDuration: 0.25) { self.view.layoutIfNeeded() } } private func removeBanner() { bannerView?.destroyBanner() bannerView?.removeFromSuperview() bannerView = nil bannerHeightConstraint?.constant = 0 UIView.animate(withDuration: 0.25) { self.view.layoutIfNeeded() } } // MARK: - 插屏广告方法 private func loadInterstitialAd() { let screenWidth = UIScreen.main.bounds.width let size = CGSize(width: screenWidth - 30.0, height: 300.0) let extra: [AnyHashable: Any] = [ kATAdLoadingExtraMediaExtraKey: "SettingsView", kATInterstitialExtraAdSizeKey: NSValue(cgSize: size) ] ATAdManager.shared().loadAD(withPlacementID: interstitialPlacementID, extra: extra, delegate: self) } private func showInterstitialAd() { guard ATAdManager.shared().interstitialReady(forPlacementID: interstitialPlacementID) else { print("插屏广告尚未加载完成") return } ATAdManager.shared().showInterstitial(withPlacementID: interstitialPlacementID, in: self, delegate: self) } // MARK: - 原生信息流广告方法 private func loadNativeAd() { let extra: [AnyHashable: Any] = [ kATAdLoadingExtraMediaExtraKey: "SettingsVC", kATExtraInfoNativeAdSizeKey: NSValue(cgSize: CGSize(width: UIScreen.main.bounds.width - 24, height: 170)) ] ATAdManager.shared().loadAD(withPlacementID: nativeRenderPlacementID, extra: extra, delegate: self) } private func showNativeAdIfReady() { // 检查广告是否准备好 guard ATAdManager.shared().nativeAdReady(forPlacementID: nativeRenderPlacementID) else { print("Native ad not ready") return } // 获取广告offer guard let offer = ATAdManager.shared().getNativeAdOffer(withPlacementID: nativeRenderPlacementID) else { print("Failed to get native ad offer") return } self.nativeAdOffer = offer // 创建自渲染视图 let selfRenderView = RenderUnitView(offer: offer) self.selfRenderView = selfRenderView // 初始化配置 let config = ATNativeADConfiguration() config.adFrame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 170) config.mediaViewFrame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 150) config.delegate = self config.rootViewController = self config.sizeToFit = true config.videoPlayType = .alwaysAutoPlayType // 设置logo位置 config.logoViewFrame = CGRect(x: UIScreen.main.bounds.width - 48, y: 140, width: 36, height: 20) // 创建原生广告视图 let nativeAdView = ATNativeADView(configuration: config, currentOffer: offer, placementID: nativeRenderPlacementID) self.nativeAdView = nativeAdView // 获取mediaView if let mediaView = nativeAdView.getMediaView() { selfRenderView.installMediaView(mediaView) } // 创建可点击组件数组 var clickableViewArray: [UIView] = [] clickableViewArray.append(contentsOf: [ selfRenderView.headlineLabel, selfRenderView.bodyLabel, selfRenderView.heroImageView ]) // 注册点击事件 nativeAdView.registerClickableViewArray(clickableViewArray) // 绑定组件 let info = ATNativePrepareInfo.load { prepareInfo in prepareInfo.textLabel = selfRenderView.bodyLabel prepareInfo.titleLabel = selfRenderView.headlineLabel prepareInfo.mainImageView = selfRenderView.heroImageView prepareInfo.dislikeButton = selfRenderView.closeControl if let mv = selfRenderView.mediaContainer { prepareInfo.mediaView = mv } } nativeAdView.prepare(with: info) // 渲染广告 offer.renderer(with: config, selfRenderView: selfRenderView, nativeADView: nativeAdView) // 添加到容器(必须把 nativeAdView 放到视图层级中,点击与视频追踪才会生效) nativeAdContainer.subviews.forEach { $0.removeFromSuperview() } nativeAdContainer.addSubview(nativeAdView) nativeAdView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ nativeAdView.leadingAnchor.constraint(equalTo: nativeAdContainer.leadingAnchor), nativeAdView.trailingAnchor.constraint(equalTo: nativeAdContainer.trailingAnchor), nativeAdView.topAnchor.constraint(equalTo: nativeAdContainer.topAnchor), nativeAdView.bottomAnchor.constraint(equalTo: nativeAdContainer.bottomAnchor) ]) // 自渲染视图放入 nativeAdView 内 nativeAdView.addSubview(selfRenderView) selfRenderView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ selfRenderView.leadingAnchor.constraint(equalTo: nativeAdView.leadingAnchor), selfRenderView.trailingAnchor.constraint(equalTo: nativeAdView.trailingAnchor), selfRenderView.topAnchor.constraint(equalTo: nativeAdView.topAnchor), selfRenderView.bottomAnchor.constraint(equalTo: nativeAdView.bottomAnchor) ]) // 设置容器高度约束 nativeAdHeightConstraint?.constant = 170 UIView.animate(withDuration: 0.25) { self.view.layoutIfNeeded() } print("Native self-render ad displayed successfully") } private func removeNativeAd() { // 清理自渲染视图 selfRenderView?.teardown() selfRenderView?.removeFromSuperview() selfRenderView = nil // 清理原生广告视图 nativeAdView?.removeFromSuperview() nativeAdView?.destroyNative() nativeAdView = nil // 清理offer nativeAdOffer = nil nativeAdHeightConstraint?.constant = 0 UIView.animate(withDuration: 0.25) { self.view.layoutIfNeeded() } } // MARK: - ATAdLoadingDelegate func didFinishLoadingAD(withPlacementID placementID: String) { if placementID == bannerPlacementID { if ATAdManager.shared().bannerAdReady(forPlacementID: placementID) { showBannerIfReady() } print("Banner Ad Loaded: \(placementID)") } else if placementID == interstitialPlacementID { if ATAdManager.shared().interstitialReady(forPlacementID: placementID) { showInterstitialAd() } print("Interstitial Ad Loaded: \(placementID)") } else if placementID == nativeRenderPlacementID { if ATAdManager.shared().nativeAdReady(forPlacementID: placementID) { showNativeAdIfReady() } print("Native Ad Loaded: \(placementID)") } } func didFailToLoadAD(withPlacementID placementID: String, error: Error) { print("Ad load failed(\(placementID)): \(error)") } func didRevenue(forPlacementID placementID: String, extra: [AnyHashable : Any]!) { print("Ad revenue: placement=\(placementID), extra=\(extra ?? [:])") uploadAdRevenue(placementID: placementID, extra: extra ?? [:]) } // MARK: - ATBannerDelegate func bannerView(_ bannerView: ATBannerView, didTapCloseButtonWithPlacementID placementID: String, extra: [AnyHashable : Any]!) { removeBanner() } func bannerView(_ bannerView: ATBannerView, didShowAdWithPlacementID placementID: String, extra: [AnyHashable : Any]!) { print("Banner did show: \(placementID)") adStartTimes[placementID] = nowMillis() incrementTodayAdCount() } func bannerView(_ bannerView: ATBannerView, didClickWithPlacementID placementID: String, extra: [AnyHashable : Any]!) { print("Banner did click: \(placementID)") } func bannerView(_ bannerView: ATBannerView, didAutoRefreshWithPlacement placementID: String, extra: [AnyHashable : Any]!) { print("Banner auto refresh: \(placementID)") } func bannerView(_ bannerView: ATBannerView, failedToAutoRefreshWithPlacementID placementID: String, error: Error) { print("Banner auto refresh failed: \(placementID) error=\(error)") } func bannerView(_ bannerView: ATBannerView, didDeepLinkOrJumpForPlacementID placementID: String, extra: [AnyHashable : Any]!, result success: Bool) { print("Banner deeplink/jump: \(placementID) success=\(success)") } // MARK: - ATInterstitialDelegate func interstitialDidShow(forPlacementID placementID: String, extra: [AnyHashable : Any]) { print("插屏广告展示完成:\(placementID) \(extra)") adStartTimes[placementID] = nowMillis() incrementTodayAdCount() } func interstitialDidClick(forPlacementID placementID: String, extra: [AnyHashable : Any]) { print("插屏广告点击:\(placementID)") } func interstitialDidClose(forPlacementID placementID: String, extra: [AnyHashable : Any]) { print("插屏广告关闭:\(placementID) \(extra)") } // MARK: - ATNativeADDelegate func didShowNativeAd(in adView: ATNativeADView, placementID: String, extra: [AnyHashable: Any]?) { print("Native ad shown for \(placementID)") adStartTimes[placementID] = nowMillis() incrementTodayAdCount() } func didTapCloseButton(in adView: ATNativeADView, placementID: String, extra: [AnyHashable: Any]?) { print("Native ad closed for \(placementID)") removeNativeAd() } func didClickNativeAd(in adView: ATNativeADView, placementID: String, extra: [AnyHashable: Any]?) { print("Native ad clicked for \(placementID)") } // MARK: - 广告收益追踪 private func uploadAdRevenue(placementID: String, extra: [AnyHashable: Any]) { // 计算 begin / finish(毫秒)并格式化为 "yyyy-MM-dd HH:mm:ss" let beginMs = adStartTimes[placementID] ?? nowMillis() let finishMs = nowMillis() print("testaa \(beginMs) \(String(describing: adStartTimes[placementID])) \(finishMs) \(placementID)") adStartTimes.removeValue(forKey: placementID) // 避免复用旧开始时间 let beginTime = formatMillisToString(beginMs) let finishTime = formatMillisToString(finishMs) // 将 AnyHashable-key 的字典转为 String-key,且递归转为 JSON 可序列化类型 func jsonSafe(_ value: Any) -> Any { if value is String || value is NSNumber || value is NSNull { return value } if let d = value as? [String: Any] { return d.mapValues { jsonSafe($0) } } if let d = value as? [AnyHashable: Any] { var nd: [String: Any] = [:] d.forEach { nd[String(describing: $0.key)] = jsonSafe($0.value) } return nd } if let arr = value as? [Any] { return arr.map { jsonSafe($0) } } if let v = value as? NSValue { // 处理常见 CoreGraphics 结构体;其余转描述串 let type = String(cString: v.objCType) #if canImport(CoreGraphics) if type == "{CGSize=dd}" { let s = v.cgSizeValue return ["width": s.width, "height": s.height] } else if type == "{CGPoint=dd}" { let p = v.cgPointValue return ["x": p.x, "y": p.y] } else if type == "{CGRect={CGPoint=dd}{CGSize=dd}}" { let r = v.cgRectValue return ["x": r.origin.x, "y": r.origin.y, "width": r.size.width, "height": r.size.height] } #endif return v.description } if let date = value as? Date { return ISO8601DateFormatter().string(from: date) } if let url = value as? URL { return url.absoluteString } return String(describing: value) } // 注意:这里用 jsonSafe 处理每个值 var extraDict: [String: Any] = [:] extra.forEach { (k, v) in extraDict[String(describing: k)] = jsonSafe(v) } // 便捷取值工具 func str(_ key: String) -> String { if let v = extraDict[key] { return String(describing: v) } return "" } func intVal(_ key: String) -> Int { if let v = extraDict[key] as? Int { return v } if let s = extraDict[key] as? String, let i = Int(s) { return i } if let d = extraDict[key] as? Double { return Int(d) } return 0 } func dbl(_ key: String) -> Double { if let v = extraDict[key] as? Double { return v } if let s = extraDict[key] as? String, let d = Double(s) { return d } if let i = extraDict[key] as? Int { return Double(i) } return 0 } // —— 字段映射(对齐安卓) —— // adSourceId let adSourceId = intVal("adsource_id") // networkFormId / networkName / networkPlacementId let networkFormId = intVal("network_firm_id") let networkName = str("network_name") let networkPlacementId = str("network_placement_id") // placementId(iOS 回传里一般是 adunit_id;若无,用回调给的 placementID) let placementIdValue = str("adunit_id").isEmpty ? placementID : str("adunit_id") // recordId(有的 key 叫 requestId,也可能是 req_id,兜底) let recordId = str("requestId").isEmpty ? str("req_id") : str("requestId") // revenue(publisher_revenue) let revenue = dbl("publisher_revenue") // userId / nickName // let userId = UserDefaults.standard.string(forKey: "roleID") ?? "" let nickName = UserDefaults.standard.string(forKey: "memberName") ?? "" // ecpm:优先取 adsource_price;若没有,用 revenue*1000 估算一个(单位同 revenue) var ecpmStr = str("adsource_price") if ecpmStr.isEmpty { let ecpm = revenue * 1000.0 ecpmStr = String(format: "%.6f", ecpm) } // adSourceIndex / adSourceType(优先 adsource_bid_type: 0=非竞价, 1=竞价) let adSourceIndex = intVal("adsource_index") // adSourceType 依据 adunit_format 映射:Native->0, RewardedVideo->1, Banner->2, Interstitial->3 let adUnitFormatRaw = str("adunit_format") let adUnitFormat = adUnitFormatRaw.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() let adSourceType: Int switch adUnitFormat { case "native": adSourceType = 0 case "rewardedvideo", "rewarded_video", "rewarded": adSourceType = 1 case "banner": adSourceType = 2 case "interstitial": adSourceType = 3 default: // 兜底逻辑:保持原有取值(若 SDK 提供 adsource_bid_type 则沿用) adSourceType = intVal("adsource_bid_type") } // resultJson:把 SDK 回传完整塞进去,便于排查 let resultJsonData = try? JSONSerialization.data(withJSONObject: extraDict, options: []) let resultJson = resultJsonData.flatMap { String(data: $0, encoding: .utf8) } ?? "{}" // appId:iOS 若无可用 App ID,可用 Bundle Identifier 兜底 let appId = kTakuAppID let adCount = todayAdCount() // 组装请求体(与安卓 map 字段完全一致) let body: [String: Any] = [ "adSourceId": adSourceId, "beginTime": beginTime, "finishTime": finishTime, "networkFormId": networkFormId, "networkName": networkName, "networkPlacementId": networkPlacementId, "nickName": nickName, "placementId": placementIdValue, "recordId": recordId, "revenue": revenue, "userId": "", "ecpm": ecpmStr, "adSourceIndex": adSourceIndex, "adSourceType": adSourceType, "resultJson": resultJson, "appId": appId, "adCount": adCount, "iosId": UIDevice.current.identifierForVendor?.uuidString ?? "", //iOS 设备id "begintimestamp": beginMs, "finishtimestamp": finishMs ] // 发起上报 let urlString = "\(apiBaseURL)/ad/saveRecord" guard let url = URL(string: urlString) else { print("[AD-Upload] URL 无效: \(urlString)") return } var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.httpBody = try? JSONSerialization.data(withJSONObject: body, options: []) if let body = request.httpBody, let bodyString = String(data: body, encoding: .utf8) { print("[AD-Upload] Request URL: \(url)") print("[AD-Upload] Request Headers: \(request.allHTTPHeaderFields ?? [:])") print("[AD-Upload] Request Body: \(bodyString)") } URLSession.shared.dataTask(with: request) { data, response, error in if let error = error { print("[AD-Upload] 网络错误: \(error.localizedDescription)") return } if let http = response as? HTTPURLResponse { print("[AD-Upload] 响应状态码: \(http.statusCode)") } if let data = data, let s = String(data: data, encoding: .utf8) { print("[AD-Upload] 响应: \(s)") } }.resume() } }