You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

419 lines
13 KiB

  1. /**
  2. * 价格计算工具类
  3. * 根据当前地址日期宠物类型等因素计算价格保留2位小数
  4. */
  5. class PriceCalculator {
  6. constructor(priceConfig) {
  7. this.priceConfig = priceConfig || this.getDefaultPriceConfig();
  8. }
  9. // 获取默认价格配置
  10. getDefaultPriceConfig() {
  11. return {
  12. basePrice: {
  13. normal: 75,
  14. holiday: 85,
  15. weekend: 80
  16. },
  17. memberDiscount: {
  18. 'new': 0.95,
  19. 'regular': 0.9,
  20. 'silver': 0.88,
  21. 'gold': 0.85
  22. },
  23. preFamiliarize: {
  24. price: 40,
  25. holidayRate: 1.2
  26. },
  27. multiService: {
  28. two: {
  29. price: 45,
  30. holidayRate: 1.1
  31. },
  32. three: {
  33. price: 130,
  34. holidayRate: 1.1
  35. }
  36. },
  37. petExtra: {
  38. largeDog: {
  39. price: 40,
  40. holidayRate: 1.1
  41. },
  42. mediumDog: {
  43. price: 30,
  44. holidayRate: 1.1
  45. },
  46. smallDog: {
  47. price: 15,
  48. holidayRate: 1.1
  49. },
  50. cat: {
  51. price: 10,
  52. holidayRate: 1.1
  53. }
  54. },
  55. freeQuota: {
  56. threshold: 30,
  57. rules: [
  58. {
  59. type: 'cat',
  60. count: 3,
  61. freeAmount: 30,
  62. description: '3只及以上猫免费30元'
  63. },
  64. {
  65. type: 'smallDog',
  66. count: 2,
  67. freeAmount: 30,
  68. description: '2只及以上小型犬免费30元'
  69. },
  70. {
  71. type: 'mediumDog',
  72. count: 1,
  73. freeAmount: 30,
  74. description: '1只及以上中型犬免费30元'
  75. },
  76. {
  77. type: 'mixed',
  78. count: 0,
  79. freeAmount: 25,
  80. description: '混合类型免费25元(1猫1小型犬)'
  81. }
  82. ]
  83. },
  84. holidays: [
  85. '2024-07-15', '2024-07-16', '2024-07-17',
  86. '2024-10-01', '2024-10-02', '2024-10-03'
  87. ],
  88. weekends: [0, 6],
  89. customServices: {
  90. priceConfig: {},
  91. holidayRate: 1.1
  92. },
  93. cityConfig: {
  94. priceRates: {
  95. 'beijing': 1.2,
  96. 'shanghai': 1.15,
  97. 'guangzhou': 1.1,
  98. 'shenzhen': 1.15,
  99. 'default': 1.0
  100. }
  101. }
  102. };
  103. }
  104. // 保留2位小数
  105. roundToTwoDecimals(value) {
  106. return Math.round(value * 100) / 100;
  107. }
  108. // 根据地址获取城市代码
  109. getCityCode(address) {
  110. if (!address) return 'default';
  111. const cityMapping = {
  112. '北京': 'beijing',
  113. '上海': 'shanghai',
  114. '广州': 'guangzhou',
  115. '深圳': 'shenzhen'
  116. };
  117. for (const [cityName, cityCode] of Object.entries(cityMapping)) {
  118. if (address.includes(cityName)) {
  119. return cityCode;
  120. }
  121. }
  122. return 'default';
  123. }
  124. // 获取城市价格倍率
  125. getCityPriceRate(address) {
  126. const cityCode = this.getCityCode(address);
  127. return this.priceConfig.cityConfig.priceRates[cityCode] || 1.0;
  128. }
  129. // 判断日期类型
  130. getDatePriceType(dateString) {
  131. const date = new Date(dateString);
  132. const dayOfWeek = date.getDay();
  133. if (this.priceConfig.holidays.includes(dateString)) {
  134. return 'holiday';
  135. }
  136. if (this.priceConfig.weekends.includes(dayOfWeek)) {
  137. return 'weekend';
  138. }
  139. return 'normal';
  140. }
  141. // 获取基础服务价格
  142. getBasePriceByDate(dateString) {
  143. const priceType = this.getDatePriceType(dateString);
  144. return this.priceConfig.basePrice[priceType] || this.priceConfig.basePrice.normal;
  145. }
  146. // 获取宠物额外费用
  147. getPetExtraCost(pet, dateString) {
  148. const priceType = this.getDatePriceType(dateString);
  149. let baseCost = 0;
  150. let holidayRate = 1;
  151. if (pet.petType === 'cat') {
  152. baseCost = this.priceConfig.petExtra.cat.price;
  153. holidayRate = this.priceConfig.petExtra.cat.holidayRate;
  154. } else if (pet.petType === 'dog' && pet.bodyType.includes('小型')) {
  155. baseCost = this.priceConfig.petExtra.smallDog.price;
  156. holidayRate = this.priceConfig.petExtra.smallDog.holidayRate;
  157. } else if (pet.petType === 'dog' && pet.bodyType.includes('中型')) {
  158. baseCost = this.priceConfig.petExtra.mediumDog.price;
  159. holidayRate = this.priceConfig.petExtra.mediumDog.holidayRate;
  160. } else if (pet.petType === 'dog' && pet.bodyType.includes('大型')) {
  161. baseCost = this.priceConfig.petExtra.largeDog.price;
  162. holidayRate = this.priceConfig.petExtra.largeDog.holidayRate;
  163. }
  164. if (priceType === 'holiday') {
  165. baseCost = baseCost * holidayRate;
  166. }
  167. return this.roundToTwoDecimals(baseCost);
  168. }
  169. // 获取多次服务费用
  170. getMultiServiceCost(feedCount, dateString) {
  171. const priceType = this.getDatePriceType(dateString);
  172. let baseCost = 0;
  173. let holidayRate = 1;
  174. if (feedCount === 2) {
  175. baseCost = this.priceConfig.multiService.two.price;
  176. holidayRate = this.priceConfig.multiService.two.holidayRate;
  177. } else if (feedCount === 3) {
  178. baseCost = this.priceConfig.multiService.three.price;
  179. holidayRate = this.priceConfig.multiService.three.holidayRate;
  180. }
  181. if (priceType === 'holiday') {
  182. baseCost = baseCost * holidayRate;
  183. }
  184. return this.roundToTwoDecimals(baseCost);
  185. }
  186. // 获取提前熟悉费用
  187. getPreFamiliarizeCost(dateString) {
  188. const priceType = this.getDatePriceType(dateString);
  189. let baseCost = this.priceConfig.preFamiliarize.price;
  190. if (priceType === 'holiday') {
  191. baseCost = baseCost * this.priceConfig.preFamiliarize.holidayRate;
  192. }
  193. return this.roundToTwoDecimals(baseCost);
  194. }
  195. // 计算套餐免费额度
  196. calculateFreeQuota(pets) {
  197. let totalPetCost = pets.reduce((acc, pet) => {
  198. return acc + this.getPetExtraCost(pet, pet.serviceDate);
  199. }, 0);
  200. if (totalPetCost <= this.priceConfig.freeQuota.threshold) {
  201. return 0;
  202. }
  203. let freeAmount = 0;
  204. const petCounts = {
  205. cat: pets.filter(pet => pet.petType === 'cat').length,
  206. smallDog: pets.filter(pet => pet.petType === 'dog' && pet.bodyType.includes('小型')).length,
  207. mediumDog: pets.filter(pet => pet.petType === 'dog' && pet.bodyType.includes('中型')).length,
  208. largeDog: pets.filter(pet => pet.petType === 'dog' && pet.bodyType.includes('大型')).length
  209. };
  210. for (const rule of this.priceConfig.freeQuota.rules) {
  211. if (rule.type === 'cat' && petCounts.cat >= rule.count) {
  212. freeAmount += rule.freeAmount;
  213. petCounts.cat -= rule.count;
  214. } else if (rule.type === 'smallDog' && petCounts.smallDog >= rule.count) {
  215. freeAmount += rule.freeAmount;
  216. petCounts.smallDog -= rule.count;
  217. } else if (rule.type === 'mediumDog' && petCounts.mediumDog >= rule.count) {
  218. freeAmount += rule.freeAmount;
  219. petCounts.mediumDog -= rule.count;
  220. } else if (rule.type === 'mixed' && petCounts.cat >= 1 && petCounts.smallDog >= 1) {
  221. freeAmount += rule.freeAmount;
  222. petCounts.cat -= 1;
  223. petCounts.smallDog -= 1;
  224. }
  225. }
  226. return this.roundToTwoDecimals(freeAmount);
  227. }
  228. // 计算会员折扣
  229. calculateMemberDiscount(originalPrice, memberLevel) {
  230. const discount = this.priceConfig.memberDiscount[memberLevel] || 1;
  231. const discountedPrice = originalPrice * discount;
  232. return this.roundToTwoDecimals(discountedPrice);
  233. }
  234. // 计算定制服务费用
  235. calculateCustomServiceCost(customServices, dateString) {
  236. const priceType = this.getDatePriceType(dateString);
  237. let totalCost = 0;
  238. customServices.forEach(service => {
  239. let servicePrice = service.price * service.quantity;
  240. if (priceType === 'holiday') {
  241. servicePrice = servicePrice * this.priceConfig.customServices.holidayRate;
  242. }
  243. totalCost += servicePrice;
  244. });
  245. return this.roundToTwoDecimals(totalCost);
  246. }
  247. // 计算单日总价
  248. calculateDailyPrice(pets, dateString, feedCount = 1, customServices = []) {
  249. // 明细数组
  250. let priceDetails = [];
  251. // 基础服务费用
  252. let baseServiceCost = this.getBasePriceByDate(dateString);
  253. priceDetails.push({
  254. name: '专业喂养',
  255. formula: `¥${baseServiceCost.toFixed(2)} x ${pets.length}`,
  256. amount: this.roundToTwoDecimals(baseServiceCost * pets.length)
  257. });
  258. // 宠物额外费用
  259. let petExtraCost = pets.reduce((acc, pet) => {
  260. return acc + this.getPetExtraCost(pet, dateString);
  261. }, 0);
  262. if (petExtraCost > 0) {
  263. // 细分每只宠物
  264. pets.forEach(pet => {
  265. const extra = this.getPetExtraCost(pet, dateString);
  266. if (extra > 0) {
  267. priceDetails.push({
  268. name: '额外宠物费用',
  269. formula: `${pet.petType === 'cat' ? '猫' : pet.bodyType}${pet.petType === 'dog' ? '犬' : ''} 1只 x ¥${extra.toFixed(2)}`,
  270. amount: extra
  271. });
  272. }
  273. });
  274. }
  275. // 多次服务费用
  276. let multiServiceCost = this.getMultiServiceCost(feedCount, dateString);
  277. if (multiServiceCost > 0) {
  278. priceDetails.push({
  279. name: '上门次数',
  280. formula: `${feedCount === 2 ? '1天2次' : feedCount === 3 ? '1天3次' : '1天1次'} x ¥${multiServiceCost.toFixed(2)}`,
  281. amount: multiServiceCost
  282. });
  283. }
  284. // 定制服务费用
  285. let customServiceCost = this.calculateCustomServiceCost(customServices, dateString);
  286. if (customServiceCost > 0) {
  287. customServices.forEach(service => {
  288. if (service.quantity > 0) {
  289. priceDetails.push({
  290. name: '定制服务',
  291. formula: `${service.name} | ¥${service.price} x ${service.quantity}`,
  292. amount: this.roundToTwoDecimals(service.price * service.quantity)
  293. });
  294. }
  295. });
  296. }
  297. // 计算免费额度
  298. let freeQuota = this.calculateFreeQuota(pets);
  299. if (freeQuota > 0) {
  300. priceDetails.push({
  301. name: '套餐免费额度',
  302. formula: `${freeQuota.toFixed(2)}`,
  303. amount: -freeQuota
  304. });
  305. }
  306. // 总价(未折扣)
  307. let totalOriginalPrice = baseServiceCost + petExtraCost + multiServiceCost + customServiceCost - freeQuota;
  308. return {
  309. baseServiceCost: this.roundToTwoDecimals(baseServiceCost),
  310. petExtraCost: this.roundToTwoDecimals(petExtraCost),
  311. multiServiceCost: this.roundToTwoDecimals(multiServiceCost),
  312. customServiceCost: this.roundToTwoDecimals(customServiceCost),
  313. freeQuota: this.roundToTwoDecimals(freeQuota),
  314. totalOriginalPrice: this.roundToTwoDecimals(totalOriginalPrice),
  315. priceType: this.getDatePriceType(dateString),
  316. priceDetails // 新增明细
  317. };
  318. }
  319. // 计算订单总价(含城市倍率和会员折扣)
  320. calculateOrderTotal(orderData, userAddress, memberLevel = 'new') {
  321. const cityRate = this.getCityPriceRate(userAddress);
  322. // 计算每日费用
  323. const dailyPrices = orderData.pets.map(pet => {
  324. const dailyPrice = this.calculateDailyPrice(
  325. [pet],
  326. pet.serviceDate,
  327. pet.feedCount,
  328. pet.customServices || []
  329. );
  330. // 应用城市倍率
  331. dailyPrice.totalWithCityRate = this.roundToTwoDecimals(dailyPrice.totalOriginalPrice * cityRate);
  332. // 明细也加上城市倍率
  333. dailyPrice.priceDetails = dailyPrice.priceDetails.map(detail => ({
  334. ...detail,
  335. amount: this.roundToTwoDecimals(detail.amount * cityRate),
  336. formula: cityRate !== 1.0 ? `${detail.formula} x 城市倍率${cityRate}` : detail.formula
  337. }));
  338. return dailyPrice;
  339. });
  340. // 汇总明细
  341. let priceDetails = [];
  342. dailyPrices.forEach((d, idx) => {
  343. d.priceDetails.forEach(item => {
  344. priceDetails.push({
  345. ...item,
  346. date: orderData.pets[idx]?.serviceDate || ''
  347. });
  348. });
  349. });
  350. // 提前熟悉费用
  351. let preFamiliarizeCost = 0;
  352. if (orderData.needPreFamiliarize && orderData.needPreFamiliarize.length > 0) {
  353. const firstServiceDate = orderData.pets[0]?.serviceDate;
  354. if (firstServiceDate) {
  355. preFamiliarizeCost = this.getPreFamiliarizeCost(firstServiceDate);
  356. preFamiliarizeCost = this.roundToTwoDecimals(preFamiliarizeCost * cityRate);
  357. priceDetails.push({
  358. name: '提前熟悉',
  359. formula: `¥${this.getPreFamiliarizeCost(firstServiceDate)} x 城市倍率${cityRate}`,
  360. amount: preFamiliarizeCost
  361. });
  362. }
  363. }
  364. // 计算总价
  365. let totalOriginalPrice = dailyPrices.reduce((acc, daily) => {
  366. return acc + daily.totalWithCityRate;
  367. }, 0) + preFamiliarizeCost;
  368. // 应用会员折扣
  369. const totalWithDiscount = this.calculateMemberDiscount(totalOriginalPrice, memberLevel);
  370. return {
  371. dailyPrices,
  372. preFamiliarizeCost,
  373. totalOriginalPrice: this.roundToTwoDecimals(totalOriginalPrice),
  374. totalWithDiscount: this.roundToTwoDecimals(totalWithDiscount),
  375. cityRate: this.roundToTwoDecimals(cityRate),
  376. memberDiscount: this.roundToTwoDecimals(totalOriginalPrice - totalWithDiscount),
  377. memberLevel,
  378. priceDetails // 新增明细
  379. };
  380. }
  381. }
  382. export default PriceCalculator;