完善合作获客员备注名:保存时校验长度,弹窗展示 saler_real_name。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -34,9 +34,11 @@
|
|||||||
### 获客员备注名
|
### 获客员备注名
|
||||||
|
|
||||||
1. 合作获客员列表每行右上角「修改备注」→ 弹窗编辑备注名
|
1. 合作获客员列表每行右上角「修改备注」→ 弹窗编辑备注名
|
||||||
2. 保存 → `POST .../sale-user/update-remark`,Body:`{ "sale_user_id": Int, "photog_remark_name": String }`
|
2. 备注名最多 20 个字符;点击保存时校验,超出则提示且不提交
|
||||||
3. 展示规则:优先 `photog_remark_name`,为空时回退获客员原名(`name` / `saler_name` / `sale_user_name`)
|
3. 修改备注弹窗展示 `saler_real_name`:「为 {获客员真实姓名} 设置备注名」
|
||||||
4. 绑定确认页(`BindAcquirerView`)仍展示真实获客员名,不使用备注名
|
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`)仍展示真实获客员名,不使用备注名
|
||||||
|
|
||||||
## 与订单模块关系
|
## 与订单模块关系
|
||||||
|
|
||||||
|
|||||||
@ -65,6 +65,8 @@ struct CooperativeSaler: Decodable, Identifiable {
|
|||||||
let bindingId: Int
|
let bindingId: Int
|
||||||
let name: String
|
let name: String
|
||||||
let salerName: String
|
let salerName: String
|
||||||
|
let salerRealName: String
|
||||||
|
let saleUserName: String
|
||||||
let photogRemarkName: String
|
let photogRemarkName: String
|
||||||
let phone: String
|
let phone: String
|
||||||
let phoneMasked: String
|
let phoneMasked: String
|
||||||
@ -95,6 +97,8 @@ struct CooperativeSaler: Decodable, Identifiable {
|
|||||||
case bindingId = "binding_id"
|
case bindingId = "binding_id"
|
||||||
case name
|
case name
|
||||||
case salerName = "saler_name"
|
case salerName = "saler_name"
|
||||||
|
case salerRealName = "saler_real_name"
|
||||||
|
case saleUserName = "sale_user_name"
|
||||||
case photogRemarkName = "photog_remark_name"
|
case photogRemarkName = "photog_remark_name"
|
||||||
case phone
|
case phone
|
||||||
case phoneMasked = "phone_masked"
|
case phoneMasked = "phone_masked"
|
||||||
@ -111,6 +115,8 @@ struct CooperativeSaler: Decodable, Identifiable {
|
|||||||
bindingId = try container.decodeLossyInt(forKey: .bindingId) ?? 0
|
bindingId = try container.decodeLossyInt(forKey: .bindingId) ?? 0
|
||||||
name = try container.decodeLossyString(forKey: .name)
|
name = try container.decodeLossyString(forKey: .name)
|
||||||
salerName = try container.decodeLossyString(forKey: .salerName)
|
salerName = try container.decodeLossyString(forKey: .salerName)
|
||||||
|
salerRealName = try container.decodeLossyString(forKey: .salerRealName)
|
||||||
|
saleUserName = try container.decodeLossyString(forKey: .saleUserName)
|
||||||
photogRemarkName = try container.decodeLossyString(forKey: .photogRemarkName)
|
photogRemarkName = try container.decodeLossyString(forKey: .photogRemarkName)
|
||||||
phone = try container.decodeLossyString(forKey: .phone)
|
phone = try container.decodeLossyString(forKey: .phone)
|
||||||
phoneMasked = try container.decodeLossyString(forKey: .phoneMasked)
|
phoneMasked = try container.decodeLossyString(forKey: .phoneMasked)
|
||||||
@ -128,6 +134,8 @@ struct CooperativeSaler: Decodable, Identifiable {
|
|||||||
bindingId: bindingId,
|
bindingId: bindingId,
|
||||||
name: name,
|
name: name,
|
||||||
salerName: salerName,
|
salerName: salerName,
|
||||||
|
salerRealName: salerRealName,
|
||||||
|
saleUserName: saleUserName,
|
||||||
photogRemarkName: remark,
|
photogRemarkName: remark,
|
||||||
phone: phone,
|
phone: phone,
|
||||||
phoneMasked: phoneMasked,
|
phoneMasked: phoneMasked,
|
||||||
@ -144,6 +152,8 @@ struct CooperativeSaler: Decodable, Identifiable {
|
|||||||
bindingId: Int,
|
bindingId: Int,
|
||||||
name: String,
|
name: String,
|
||||||
salerName: String,
|
salerName: String,
|
||||||
|
salerRealName: String,
|
||||||
|
saleUserName: String,
|
||||||
photogRemarkName: String,
|
photogRemarkName: String,
|
||||||
phone: String,
|
phone: String,
|
||||||
phoneMasked: String,
|
phoneMasked: String,
|
||||||
@ -157,6 +167,8 @@ struct CooperativeSaler: Decodable, Identifiable {
|
|||||||
self.bindingId = bindingId
|
self.bindingId = bindingId
|
||||||
self.name = name
|
self.name = name
|
||||||
self.salerName = salerName
|
self.salerName = salerName
|
||||||
|
self.salerRealName = salerRealName
|
||||||
|
self.saleUserName = saleUserName
|
||||||
self.photogRemarkName = photogRemarkName
|
self.photogRemarkName = photogRemarkName
|
||||||
self.phone = phone
|
self.phone = phone
|
||||||
self.phoneMasked = phoneMasked
|
self.phoneMasked = phoneMasked
|
||||||
|
|||||||
@ -16,3 +16,22 @@ enum CooperationOrderFeature {
|
|||||||
permissionContext.canAccess(uri)
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -35,7 +35,10 @@ final class CooperationAcquirerViewModel: ObservableObject {
|
|||||||
api: CooperationOrderServing
|
api: CooperationOrderServing
|
||||||
) async throws {
|
) async throws {
|
||||||
guard saleUserId > 0 else { return }
|
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)
|
try await api.saleUserUpdateRemark(saleUserId: saleUserId, photogRemarkName: trimmedRemark)
|
||||||
acquirers = acquirers.map { acquirer in
|
acquirers = acquirers.map { acquirer in
|
||||||
guard acquirer.saleUserId == saleUserId else { return acquirer }
|
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 }
|
||||||
|
}
|
||||||
|
|||||||
@ -88,6 +88,10 @@ struct CooperationAcquirerView: View {
|
|||||||
Button("保存") {
|
Button("保存") {
|
||||||
// 必须在弹窗关闭前同步捕获,避免 alert dismiss 清空 state 导致接口未调用。
|
// 必须在弹窗关闭前同步捕获,避免 alert dismiss 清空 state 导致接口未调用。
|
||||||
guard let acquirer = remarkEditingAcquirer else { return }
|
guard let acquirer = remarkEditingAcquirer else { return }
|
||||||
|
if let message = CooperationOrderRemarkName.validationMessage(for: remarkDraft) {
|
||||||
|
toastCenter.show(message)
|
||||||
|
return
|
||||||
|
}
|
||||||
let saleUserId = acquirer.saleUserId
|
let saleUserId = acquirer.saleUserId
|
||||||
let draft = remarkDraft
|
let draft = remarkDraft
|
||||||
showRemarkAlert = false
|
showRemarkAlert = false
|
||||||
@ -98,7 +102,7 @@ struct CooperationAcquirerView: View {
|
|||||||
}
|
}
|
||||||
} message: {
|
} message: {
|
||||||
if let acquirer = remarkEditingAcquirer {
|
if let acquirer = remarkEditingAcquirer {
|
||||||
Text("为 \(acquirer.salerName.cooperationNonEmptyOrDefault(acquirer.name)) 设置备注名")
|
Text("为 \(acquirer.salerRealName.cooperationNonEmptyOrDefault("—")) 设置备注名")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,6 +24,7 @@ final class CooperationOrderAPITests: XCTestCase {
|
|||||||
XCTAssertEqual(response.list.first?.displayId, 101)
|
XCTAssertEqual(response.list.first?.displayId, 101)
|
||||||
XCTAssertEqual(response.list.first?.displayName, "张哥")
|
XCTAssertEqual(response.list.first?.displayName, "张哥")
|
||||||
XCTAssertEqual(response.list.first?.photogRemarkName, "张哥")
|
XCTAssertEqual(response.list.first?.photogRemarkName, "张哥")
|
||||||
|
XCTAssertEqual(response.list.first?.salerRealName, "管铜彬")
|
||||||
XCTAssertEqual(response.list.first?.displayPhone, "19281871413")
|
XCTAssertEqual(response.list.first?.displayPhone, "19281871413")
|
||||||
XCTAssertEqual(response.list.first?.bindingId, 5)
|
XCTAssertEqual(response.list.first?.bindingId, 5)
|
||||||
XCTAssertEqual(response.list.first?.shareCode, "INV-000101")
|
XCTAssertEqual(response.list.first?.shareCode, "INV-000101")
|
||||||
|
|||||||
43
suixinkanTests/CooperationOrderRemarkNameTests.swift
Normal file
43
suixinkanTests/CooperationOrderRemarkNameTests.swift
Normal file
@ -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(" 张哥 "), "张哥")
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -11,6 +11,7 @@
|
|||||||
"binding_id": 5,
|
"binding_id": 5,
|
||||||
"bound_at": "2026-06-25 17:11:01",
|
"bound_at": "2026-06-25 17:11:01",
|
||||||
"name": "管铜彬",
|
"name": "管铜彬",
|
||||||
|
"saler_real_name": "管铜彬",
|
||||||
"photog_remark_name": "张哥",
|
"photog_remark_name": "张哥",
|
||||||
"phone": "19281871413",
|
"phone": "19281871413",
|
||||||
"phone_masked": "192****1413",
|
"phone_masked": "192****1413",
|
||||||
|
|||||||
Reference in New Issue
Block a user