diff --git a/suixinkan/Features/CooperationOrder/CooperationOrder.md b/suixinkan/Features/CooperationOrder/CooperationOrder.md index d79f581..f98e44d 100644 --- a/suixinkan/Features/CooperationOrder/CooperationOrder.md +++ b/suixinkan/Features/CooperationOrder/CooperationOrder.md @@ -34,9 +34,11 @@ ### 获客员备注名 1. 合作获客员列表每行右上角「修改备注」→ 弹窗编辑备注名 -2. 保存 → `POST .../sale-user/update-remark`,Body:`{ "sale_user_id": Int, "photog_remark_name": String }` -3. 展示规则:优先 `photog_remark_name`,为空时回退获客员原名(`name` / `saler_name` / `sale_user_name`) -4. 绑定确认页(`BindAcquirerView`)仍展示真实获客员名,不使用备注名 +2. 备注名最多 20 个字符;点击保存时校验,超出则提示且不提交 +3. 修改备注弹窗展示 `saler_real_name`:「为 {获客员真实姓名} 设置备注名」 +4. 保存 → `POST .../sale-user/update-remark`,Body:`{ "sale_user_id": Int, "photog_remark_name": String }` +5. 展示规则:优先 `photog_remark_name`,为空时回退获客员原名(`name` / `saler_name` / `sale_user_name`) +6. 绑定确认页(`BindAcquirerView`)仍展示真实获客员名,不使用备注名 ## 与订单模块关系 diff --git a/suixinkan/Features/CooperationOrder/Models/CooperationOrderModels.swift b/suixinkan/Features/CooperationOrder/Models/CooperationOrderModels.swift index 02d2770..c6f0516 100644 --- a/suixinkan/Features/CooperationOrder/Models/CooperationOrderModels.swift +++ b/suixinkan/Features/CooperationOrder/Models/CooperationOrderModels.swift @@ -65,6 +65,8 @@ struct CooperativeSaler: Decodable, Identifiable { let bindingId: Int let name: String let salerName: String + let salerRealName: String + let saleUserName: String let photogRemarkName: String let phone: String let phoneMasked: String @@ -95,6 +97,8 @@ struct CooperativeSaler: Decodable, Identifiable { case bindingId = "binding_id" case name case salerName = "saler_name" + case salerRealName = "saler_real_name" + case saleUserName = "sale_user_name" case photogRemarkName = "photog_remark_name" case phone case phoneMasked = "phone_masked" @@ -111,6 +115,8 @@ struct CooperativeSaler: Decodable, Identifiable { bindingId = try container.decodeLossyInt(forKey: .bindingId) ?? 0 name = try container.decodeLossyString(forKey: .name) salerName = try container.decodeLossyString(forKey: .salerName) + salerRealName = try container.decodeLossyString(forKey: .salerRealName) + saleUserName = try container.decodeLossyString(forKey: .saleUserName) photogRemarkName = try container.decodeLossyString(forKey: .photogRemarkName) phone = try container.decodeLossyString(forKey: .phone) phoneMasked = try container.decodeLossyString(forKey: .phoneMasked) @@ -128,6 +134,8 @@ struct CooperativeSaler: Decodable, Identifiable { bindingId: bindingId, name: name, salerName: salerName, + salerRealName: salerRealName, + saleUserName: saleUserName, photogRemarkName: remark, phone: phone, phoneMasked: phoneMasked, @@ -144,6 +152,8 @@ struct CooperativeSaler: Decodable, Identifiable { bindingId: Int, name: String, salerName: String, + salerRealName: String, + saleUserName: String, photogRemarkName: String, phone: String, phoneMasked: String, @@ -157,6 +167,8 @@ struct CooperativeSaler: Decodable, Identifiable { self.bindingId = bindingId self.name = name self.salerName = salerName + self.salerRealName = salerRealName + self.saleUserName = saleUserName self.photogRemarkName = photogRemarkName self.phone = phone self.phoneMasked = phoneMasked diff --git a/suixinkan/Features/CooperationOrder/Services/CooperationOrderFeature.swift b/suixinkan/Features/CooperationOrder/Services/CooperationOrderFeature.swift index d2b56fe..1dd3939 100644 --- a/suixinkan/Features/CooperationOrder/Services/CooperationOrderFeature.swift +++ b/suixinkan/Features/CooperationOrder/Services/CooperationOrderFeature.swift @@ -16,3 +16,22 @@ enum CooperationOrderFeature { permissionContext.canAccess(uri) } } + +/// 合作获客员备注名校验规则,与 Android 端保持一致。 +enum CooperationOrderRemarkName { + /// 备注名最大字符数。 + static let maxLength = 20 + + /// 去除首尾空白后的备注名。 + static func trimmed(_ remark: String) -> String { + remark.trimmingCharacters(in: .whitespacesAndNewlines) + } + + /// 保存前校验备注名,超限时返回错误提示。 + static func validationMessage(for remark: String) -> String? { + guard trimmed(remark).count <= maxLength else { + return "备注名不能超过\(maxLength)个字符" + } + return nil + } +} diff --git a/suixinkan/Features/CooperationOrder/ViewModels/CooperationAcquirerViewModel.swift b/suixinkan/Features/CooperationOrder/ViewModels/CooperationAcquirerViewModel.swift index 0c92f5a..431a50e 100644 --- a/suixinkan/Features/CooperationOrder/ViewModels/CooperationAcquirerViewModel.swift +++ b/suixinkan/Features/CooperationOrder/ViewModels/CooperationAcquirerViewModel.swift @@ -35,7 +35,10 @@ final class CooperationAcquirerViewModel: ObservableObject { api: CooperationOrderServing ) async throws { guard saleUserId > 0 else { return } - let trimmedRemark = remark.trimmingCharacters(in: .whitespacesAndNewlines) + if let message = CooperationOrderRemarkName.validationMessage(for: remark) { + throw CooperationOrderRemarkValidationError(message: message) + } + let trimmedRemark = CooperationOrderRemarkName.trimmed(remark) try await api.saleUserUpdateRemark(saleUserId: saleUserId, photogRemarkName: trimmedRemark) acquirers = acquirers.map { acquirer in guard acquirer.saleUserId == saleUserId else { return acquirer } @@ -43,3 +46,10 @@ final class CooperationAcquirerViewModel: ObservableObject { } } } + +/// 合作获客员备注名校验失败错误。 +struct CooperationOrderRemarkValidationError: LocalizedError { + let message: String + + var errorDescription: String? { message } +} diff --git a/suixinkan/Features/CooperationOrder/Views/CooperationAcquirerView.swift b/suixinkan/Features/CooperationOrder/Views/CooperationAcquirerView.swift index 78b5851..e7b5439 100644 --- a/suixinkan/Features/CooperationOrder/Views/CooperationAcquirerView.swift +++ b/suixinkan/Features/CooperationOrder/Views/CooperationAcquirerView.swift @@ -88,6 +88,10 @@ struct CooperationAcquirerView: View { Button("保存") { // 必须在弹窗关闭前同步捕获,避免 alert dismiss 清空 state 导致接口未调用。 guard let acquirer = remarkEditingAcquirer else { return } + if let message = CooperationOrderRemarkName.validationMessage(for: remarkDraft) { + toastCenter.show(message) + return + } let saleUserId = acquirer.saleUserId let draft = remarkDraft showRemarkAlert = false @@ -98,7 +102,7 @@ struct CooperationAcquirerView: View { } } message: { if let acquirer = remarkEditingAcquirer { - Text("为 \(acquirer.salerName.cooperationNonEmptyOrDefault(acquirer.name)) 设置备注名") + Text("为 \(acquirer.salerRealName.cooperationNonEmptyOrDefault("—")) 设置备注名") } } } diff --git a/suixinkanTests/CooperationOrderAPITests.swift b/suixinkanTests/CooperationOrderAPITests.swift index 5fa95b5..226c141 100644 --- a/suixinkanTests/CooperationOrderAPITests.swift +++ b/suixinkanTests/CooperationOrderAPITests.swift @@ -24,6 +24,7 @@ final class CooperationOrderAPITests: XCTestCase { XCTAssertEqual(response.list.first?.displayId, 101) XCTAssertEqual(response.list.first?.displayName, "张哥") XCTAssertEqual(response.list.first?.photogRemarkName, "张哥") + XCTAssertEqual(response.list.first?.salerRealName, "管铜彬") XCTAssertEqual(response.list.first?.displayPhone, "19281871413") XCTAssertEqual(response.list.first?.bindingId, 5) XCTAssertEqual(response.list.first?.shareCode, "INV-000101") diff --git a/suixinkanTests/CooperationOrderRemarkNameTests.swift b/suixinkanTests/CooperationOrderRemarkNameTests.swift new file mode 100644 index 0000000..293240e --- /dev/null +++ b/suixinkanTests/CooperationOrderRemarkNameTests.swift @@ -0,0 +1,43 @@ +// +// CooperationOrderRemarkNameTests.swift +// suixinkanTests +// +// Created by Codex on 2026/7/2. +// + +import XCTest +@testable import suixinkan + +/// 合作获客员备注名校验规则测试。 +final class CooperationOrderRemarkNameTests: XCTestCase { + /// 测试超出上限时返回错误提示。 + func testValidationMessageWhenExceedsMaxLength() { + let input = String(repeating: "名", count: CooperationOrderRemarkName.maxLength + 1) + XCTAssertEqual( + CooperationOrderRemarkName.validationMessage(for: input), + "备注名不能超过\(CooperationOrderRemarkName.maxLength)个字符" + ) + } + + /// 测试合法长度时不返回错误。 + func testValidationMessageAcceptsValidRemark() { + XCTAssertNil(CooperationOrderRemarkName.validationMessage(for: "张哥")) + XCTAssertNil( + CooperationOrderRemarkName.validationMessage( + for: String(repeating: "名", count: CooperationOrderRemarkName.maxLength) + ) + ) + } + + /// 测试保存前会去除首尾空白后再校验。 + func testValidationUsesTrimmedLength() { + let inner = String(repeating: "张", count: CooperationOrderRemarkName.maxLength) + XCTAssertNil(CooperationOrderRemarkName.validationMessage(for: " \(inner) ")) + XCTAssertNotNil(CooperationOrderRemarkName.validationMessage(for: " \(inner)多 ")) + } + + /// 测试去除首尾空白。 + func testTrimmedRemovesSurroundingWhitespace() { + XCTAssertEqual(CooperationOrderRemarkName.trimmed(" 张哥 "), "张哥") + } +} diff --git a/suixinkanTests/Fixtures/cooperative_salers_success.json b/suixinkanTests/Fixtures/cooperative_salers_success.json index b95a59f..301f585 100644 --- a/suixinkanTests/Fixtures/cooperative_salers_success.json +++ b/suixinkanTests/Fixtures/cooperative_salers_success.json @@ -11,6 +11,7 @@ "binding_id": 5, "bound_at": "2026-06-25 17:11:01", "name": "管铜彬", + "saler_real_name": "管铜彬", "photog_remark_name": "张哥", "phone": "19281871413", "phone_masked": "192****1413",