diff --git a/backend/mci-server/src/main/java/com/sino/mci/channel/service/ChannelAccountService.java b/backend/mci-server/src/main/java/com/sino/mci/channel/service/ChannelAccountService.java index a66c5b1..66276ed 100644 --- a/backend/mci-server/src/main/java/com/sino/mci/channel/service/ChannelAccountService.java +++ b/backend/mci-server/src/main/java/com/sino/mci/channel/service/ChannelAccountService.java @@ -234,6 +234,12 @@ public class ChannelAccountService { person = personMapper.selectById(identity.getPersonId()); person.setPersonName(contact.getName()); person.setPhone(contact.getMobile()); + Person existing = personMapper.selectById(person.getId()); + if (existing != null) { + person.setInstitutionId(existing.getInstitutionId()); + person.setSupplierId(existing.getSupplierId()); + person.setPersonType(existing.getPersonType()); + } personMapper.updateById(person); identity.setChannelUserName(contact.getName()); @@ -273,6 +279,13 @@ public class ChannelAccountService { conversation.setConversationName(conv.getChatName()); conversation.setOwnerAccountId(account.getId()); conversation.setSubjectId(subjectId); + Conversation existing = conversationMapper.selectById(conversation.getId()); + if (existing != null) { + conversation.setInstitutionId(existing.getInstitutionId()); + conversation.setSupplierId(existing.getSupplierId()); + conversation.setContractId(existing.getContractId()); + conversation.setChannelCode(existing.getChannelCode()); + } conversationMapper.updateById(conversation); return conversation; } diff --git a/backend/mci-server/src/test/java/com/sino/mci/channel/controller/WeComSyncTest.java b/backend/mci-server/src/test/java/com/sino/mci/channel/controller/WeComSyncTest.java index 0a29874..3846943 100644 --- a/backend/mci-server/src/test/java/com/sino/mci/channel/controller/WeComSyncTest.java +++ b/backend/mci-server/src/test/java/com/sino/mci/channel/controller/WeComSyncTest.java @@ -179,6 +179,84 @@ class WeComSyncTest { assertThat(receiveMaster).isNotNull(); } + @Test + void shouldPreservePersonBusinessFieldsDuringContactSync() throws Exception { + String authToken = createTenantAndLogin(mockMvc, TENANT_CODE, "企微同步租户"); + ChannelAccount account = createWeComAccount(); + + Person person = new Person(); + person.setTenantCode(TENANT_CODE); + person.setPersonCode("user001"); + person.setPersonName("原始姓名"); + person.setPhone("13800138001"); + person.setPersonType(1); + person.setInstitutionId(100L); + person.setSupplierId(200L); + person.setStatus(1); + personMapper.insert(person); + + ChannelIdentity identity = new ChannelIdentity(); + identity.setTenantCode(TENANT_CODE); + identity.setPersonId(person.getId()); + identity.setChannelType(CHANNEL_TYPE); + identity.setChannelUserId("user001"); + identity.setChannelUserName("原始姓名"); + identity.setSubjectId(account.getSubjectId()); + identity.setStatus(1); + channelIdentityMapper.insert(identity); + + mockMvc.perform(post("/msg-platform/api/v1/channel-account/{account_id}/sync-contacts", account.getId()) + .contextPath("/msg-platform") + .header("Authorization", authToken) + .header("X-Tenant-Code", TENANT_CODE) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(0)); + + Person updated = personMapper.selectById(person.getId()); + assertThat(updated.getPersonName()).isEqualTo("张三"); + assertThat(updated.getInstitutionId()).isEqualTo(100L); + assertThat(updated.getSupplierId()).isEqualTo(200L); + assertThat(updated.getPersonType()).isEqualTo(1); + } + + @Test + void shouldPreserveConversationBusinessFieldsDuringConversationSync() throws Exception { + String authToken = createTenantAndLogin(mockMvc, TENANT_CODE, "企微同步租户"); + ChannelAccount account = createWeComAccount(); + + Conversation conversation = new Conversation(); + conversation.setTenantCode(TENANT_CODE); + conversation.setConversationType(2); + conversation.setChannelType(CHANNEL_TYPE); + conversation.setChannelConversationId("chat001"); + conversation.setOwnerAccountId(account.getId()); + conversation.setSubjectId(account.getSubjectId()); + conversation.setConversationName("原始群名"); + conversation.setInstitutionId(100L); + conversation.setSupplierId(200L); + conversation.setContractId(300L); + conversation.setChannelCode("ORIGINAL"); + conversation.setCreateSource(1); + conversation.setStatus(1); + conversationMapper.insert(conversation); + + mockMvc.perform(post("/msg-platform/api/v1/channel-account/{account_id}/sync-conversations", account.getId()) + .contextPath("/msg-platform") + .header("Authorization", authToken) + .header("X-Tenant-Code", TENANT_CODE) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(0)); + + Conversation updated = conversationMapper.selectById(conversation.getId()); + assertThat(updated.getConversationName()).isEqualTo("测试客户群"); + assertThat(updated.getInstitutionId()).isEqualTo(100L); + assertThat(updated.getSupplierId()).isEqualTo(200L); + assertThat(updated.getContractId()).isEqualTo(300L); + assertThat(updated.getChannelCode()).isEqualTo("ORIGINAL"); + } + private ChannelAccount createWeComAccount() { ChannelSubject subject = new ChannelSubject(); subject.setTenantCode(TENANT_CODE); diff --git a/scripts/regression/wecom-sync-protect-business-fields.sh b/scripts/regression/wecom-sync-protect-business-fields.sh new file mode 100644 index 0000000..308748b --- /dev/null +++ b/scripts/regression/wecom-sync-protect-business-fields.sh @@ -0,0 +1,140 @@ +#!/bin/bash +set -e + +# Regression script for Task 1: business fields must survive WeCom sync. +# +# Pre-requisites: +# - mci-server is running (default http://localhost/msg-platform) +# - WeCom sync client is mocked or the real client returns a conversation +# whose channelConversationId matches the one created below. +# - The conversation API currently does NOT expose contractId/institutionId/ +# supplierId/channelCode, so after creating the conversation via API you +# must set contract_id = 100 directly in the DB (or extend the DTO). +# - jq and curl are installed. + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR/../.." + +BASE_URL="${BASE_URL:-http://localhost/msg-platform}" + +command -v curl >/dev/null 2>&1 || { echo "curl is required but not installed."; exit 1; } +command -v jq >/dev/null 2>&1 || { echo "jq is required but not installed."; exit 1; } + +TENANT_CODE="wecom-boundary-$(date +%s)" + +make_request() { + local method=$1 + local url=$2 + local payload=$3 + shift 3 + local headers=("$@") + + if [ "$method" = "GET" ]; then + curl -s -S -X GET "${headers[@]}" "$url" + elif [ -n "$payload" ]; then + curl -s -S -X "$method" "${headers[@]}" -H "Content-Type: application/json" -d "$payload" "$url" + else + curl -s -S -X "$method" "${headers[@]}" -H "Content-Type: application/json" "$url" + fi +} + +assert_code_zero() { + local name=$1 + local response=$2 + local code + code=$(echo "$response" | jq -r '.code // .errcode // empty') + + if [ "$code" = "0" ]; then + echo "[PASS] $name" + return 0 + else + echo "[FAIL] $name (code=$code)" + echo " response: $response" + exit 1 + fi +} + +# 1. Create tenant +echo "==> 1. Create tenant" +TENANT_RESPONSE=$(make_request POST "$BASE_URL/api/v1/account/tenant" \ + "{\"tenantCode\":\"$TENANT_CODE\",\"tenantName\":\"WeCom Boundary Test\"}") +assert_code_zero "Create tenant" "$TENANT_RESPONSE" + +# 2. Create admin user and login +echo "==> 2. Create admin user and login" +make_request POST "$BASE_URL/api/v1/account/user" \ + '{"username":"admin","password":"admin123","realName":"Admin","roleCodes":["tenant_admin"]}' \ + -H "X-Tenant-Code: $TENANT_CODE" >/dev/null + +LOGIN_RESPONSE=$(make_request POST "$BASE_URL/api/v1/account/login" \ + '{"username":"admin","password":"admin123"}' \ + -H "X-Tenant-Code: $TENANT_CODE") +assert_code_zero "Login" "$LOGIN_RESPONSE" +TOKEN=$(echo "$LOGIN_RESPONSE" | jq -r '.data.token') +AUTH_HEADER="Authorization: Bearer $TOKEN" +COMMON_HEADERS=(-H "$AUTH_HEADER" -H "X-Tenant-Code: $TENANT_CODE") + +# 3. Create channel subject (corp credentials are required for sync) +echo "==> 3. Create channel subject" +SUBJECT_RESPONSE=$(make_request POST "$BASE_URL/api/v1/channel-subject" \ + '{"channelType":"wecom","subjectCode":"sino","subjectName":"Sino Boundary","corpId":"ww-boundary","corpSecret":"secret","agentId":"1000002"}' \ + "${COMMON_HEADERS[@]}") +assert_code_zero "Create channel subject" "$SUBJECT_RESPONSE" +SUBJECT_ID=$(echo "$SUBJECT_RESPONSE" | jq -r '.data.id') + +# 4. Create channel account +echo "==> 4. Create channel account" +ACCOUNT_RESPONSE=$(make_request POST "$BASE_URL/api/v1/channel-account" \ + "{\"accountType\":1,\"channelType\":\"wecom\",\"accountId\":\"boundary-account\",\"accountName\":\"Boundary Account\",\"subjectId\":$SUBJECT_ID}" \ + "${COMMON_HEADERS[@]}") +assert_code_zero "Create channel account" "$ACCOUNT_RESPONSE" +ACCOUNT_ID=$(echo "$ACCOUNT_RESPONSE" | jq -r '.data.id') + +# 5. Create conversation via API +echo "==> 5. Create conversation" +CONVERSATION_RESPONSE=$(make_request POST "$BASE_URL/api/v1/conversation" \ + '{"conversationType":2,"channelType":"wecom","channelConversationId":"chat-boundary-001","conversationName":"Boundary Group"}' \ + "${COMMON_HEADERS[@]}") +assert_code_zero "Create conversation" "$CONVERSATION_RESPONSE" +CONVERSATION_ID=$(echo "$CONVERSATION_RESPONSE" | jq -r '.data.id') + +# 6. Set business fields in DB (contract_id=100). +# The public API does not expose these fields yet, so this step is performed +# directly in the database. Example: +# +# UPDATE mci_conversation +# SET contract_id = 100, +# institution_id = 1000, +# supplier_id = 2000, +# channel_code = 'ORIGINAL' +# WHERE id = ; +# +echo "==> 6. Set contractId=100 in DB for conversation $CONVERSATION_ID" +echo " (execute the SQL shown in the script comments, then press any key)" +read -r -n 1 -s || true + +# 7. Call sync-conversations +echo "==> 7. Call sync-conversations" +SYNC_RESPONSE=$(make_request POST "$BASE_URL/api/v1/channel-account/$ACCOUNT_ID/sync-conversations" \ + '' "${COMMON_HEADERS[@]}") +assert_code_zero "Sync conversations" "$SYNC_RESPONSE" + +# 8. Query conversation and assert contractId is still 100 +echo "==> 8. Query conversation and assert business fields" +QUERY_RESPONSE=$(make_request GET "$BASE_URL/api/v1/conversation/$CONVERSATION_ID" \ + '' "${COMMON_HEADERS[@]}") +assert_code_zero "Query conversation" "$QUERY_RESPONSE" + +CONTRACT_ID=$(echo "$QUERY_RESPONSE" | jq -r '.data.contractId // empty') +if [ "$CONTRACT_ID" = "100" ]; then + echo "[PASS] contractId preserved (100) after sync" +else + echo "[FAIL] contractId was overwritten or not set (got '$CONTRACT_ID')" + echo " response: $QUERY_RESPONSE" + exit 1 +fi + +echo "" +echo "=========================" +echo "All regression checks passed." +echo "========================="