community.service.ts 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. /**
  2. * 小区管理模块 — 业务服务层
  3. *
  4. * 对应规范文档:
  5. * §5.5「录入小区基础档案」
  6. * §5.6「维护小区—群—门店对应关系」
  7. *
  8. * 业务规则:
  9. * 1. 小区户数无法从 QiWe 自动获取,需人工录入
  10. * 2. 群与小区/门店的绑定关系禁止自动猜测——必须人工确认
  11. * 3. 未绑定群在看板中标记为「待维护」
  12. * 4. 报表按小区/门店聚合时,通过绑定表 INNER JOIN
  13. */
  14. import { AppError } from '../../../../shared/errors/app-error.js';
  15. import type {
  16. Community,
  17. CreateCommunityRequest,
  18. CommunityRoomBinding,
  19. CreateBindingRequest,
  20. } from '../models/community.model.js';
  21. // ---------- 内存存储(TODO: 替换为数据库)----------
  22. const communityStore = new Map<number, Community>();
  23. const bindingStore = new Map<number, CommunityRoomBinding>();
  24. let nextCommunityId = 1;
  25. let nextBindingId = 1;
  26. // ============================================================
  27. // 小区档案 CRUD
  28. // ============================================================
  29. /**
  30. * 录入小区基础档案
  31. *
  32. * 户数是触达率计算的分母,必须准确。
  33. * 房价、交房时间等字段供看板和经营分析使用。
  34. */
  35. export function createCommunity(data: CreateCommunityRequest): Community {
  36. const now = new Date().toISOString();
  37. const community: Community = {
  38. id: nextCommunityId++,
  39. name: data.name,
  40. totalHouseholds: data.totalHouseholds,
  41. avgPrice: data.avgPrice || 0,
  42. deliveryYear: data.deliveryYear || 0,
  43. storeId: data.storeId,
  44. address: data.address || '',
  45. remark: data.remark || '',
  46. createdAt: now,
  47. updatedAt: now,
  48. };
  49. communityStore.set(community.id, community);
  50. return community;
  51. }
  52. /**
  53. * 查询小区列表(支持按门店筛选)
  54. */
  55. export function listCommunities(storeId?: number): Community[] {
  56. let result = Array.from(communityStore.values());
  57. if (storeId !== undefined) {
  58. result = result.filter((c) => c.storeId === storeId);
  59. }
  60. return result;
  61. }
  62. /**
  63. * 查询单个小区详情
  64. */
  65. export function getCommunityById(id: number): Community {
  66. const community = communityStore.get(id);
  67. if (!community) {
  68. throw new AppError(404, 'COMMUNITY_NOT_FOUND', `小区 ID=${id} 不存在`);
  69. }
  70. return community;
  71. }
  72. /**
  73. * 更新小区信息
  74. */
  75. export function updateCommunity(
  76. id: number,
  77. data: Partial<CreateCommunityRequest> = {},
  78. ): Community {
  79. const community = getCommunityById(id);
  80. if (data.name !== undefined) community.name = data.name;
  81. if (data.totalHouseholds !== undefined) community.totalHouseholds = data.totalHouseholds;
  82. if (data.avgPrice !== undefined) community.avgPrice = data.avgPrice;
  83. if (data.deliveryYear !== undefined) community.deliveryYear = data.deliveryYear;
  84. if (data.storeId !== undefined) community.storeId = data.storeId;
  85. if (data.address !== undefined) community.address = data.address;
  86. if (data.remark !== undefined) community.remark = data.remark;
  87. community.updatedAt = new Date().toISOString();
  88. communityStore.set(id, community);
  89. return community;
  90. }
  91. /**
  92. * 删除小区
  93. */
  94. export function deleteCommunity(id: number): void {
  95. getCommunityById(id);
  96. communityStore.delete(id);
  97. // 同时清理关联的绑定关系
  98. for (const [key, binding] of bindingStore) {
  99. if (binding.communityId === id) {
  100. bindingStore.delete(key);
  101. }
  102. }
  103. }
  104. // ============================================================
  105. // 小区-群-门店绑定
  106. // ============================================================
  107. /**
  108. * 创建小区-群-门店绑定关系
  109. *
  110. * 业务约束:
  111. * - 同一个 roomId 不可重复绑定到不同的小区
  112. * - 绑定禁止自动猜测,必须人工确认
  113. *
  114. * @throws {AppError} 当 roomId 已被绑定时
  115. */
  116. export function createBinding(data: CreateBindingRequest): CommunityRoomBinding {
  117. // 检查 roomId 是否已绑定
  118. for (const binding of bindingStore.values()) {
  119. if (binding.roomId === data.roomId) {
  120. throw new AppError(
  121. 409,
  122. 'BINDING_DUPLICATE',
  123. `群 ${data.roomId} 已绑定到小区 ID=${binding.communityId},不可重复绑定`,
  124. );
  125. }
  126. }
  127. const binding: CommunityRoomBinding = {
  128. id: nextBindingId++,
  129. communityId: data.communityId,
  130. roomId: data.roomId,
  131. storeId: data.storeId,
  132. createdAt: new Date().toISOString(),
  133. };
  134. bindingStore.set(binding.id, binding);
  135. return binding;
  136. }
  137. /**
  138. * 查询绑定关系列表(按小区或门店筛选)
  139. */
  140. export function listBindings(communityId?: number, storeId?: number): CommunityRoomBinding[] {
  141. let result = Array.from(bindingStore.values());
  142. if (communityId !== undefined) {
  143. result = result.filter((b) => b.communityId === communityId);
  144. }
  145. if (storeId !== undefined) {
  146. result = result.filter((b) => b.storeId === storeId);
  147. }
  148. return result;
  149. }
  150. /**
  151. * 删除绑定关系(解绑)
  152. */
  153. export function deleteBinding(id: number): void {
  154. const binding = bindingStore.get(id);
  155. if (!binding) {
  156. throw new AppError(404, 'BINDING_NOT_FOUND', `绑定 ID=${id} 不存在`);
  157. }
  158. bindingStore.delete(id);
  159. }
  160. /**
  161. * 按 roomId 查询绑定(用于合规等模块关联查询)
  162. */
  163. export function getBindingByRoomId(roomId: string): CommunityRoomBinding | undefined {
  164. for (const binding of bindingStore.values()) {
  165. if (binding.roomId === roomId) {
  166. return binding;
  167. }
  168. }
  169. return undefined;
  170. }