activity-fee.js 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. /**
  2. * 活动费用展示口径(A3「活动费用展示层口径统一」共用工具)
  3. *
  4. * 数据契约(A2,字段名与取值对齐 A2 的 ActivityDTO,A2 未合入期间取默认值降级):
  5. * audienceScope: 'all'(默认)| 'member_only' —— 是否仅会员可参加
  6. * audienceType : 'adult'(默认)| 'include_child' | 'include_elder' | 'include_child_elder'
  7. *
  8. * 费用档位(A2 §2.1;长者复用成人价 —— D1 决策:不新增长者价格列):
  9. * 成人·非会员 price 成人·会员 memberPrice
  10. * 儿童·非会员 childFee 儿童·会员 childMemberFee
  11. *
  12. * 兼容存量(A2 §2.4):parentFee / parentMemberFee 降级为「成人非会员价」的历史别名
  13. * 成人非会员价 = price > 0 ? price : parentFee
  14. * 成人会员价 = memberPrice > 0 ? memberPrice : parentMemberFee
  15. * 老活动(只填 parent/parentMember 费用、price 为 0)在展示层仍要正确显示成人价。
  16. *
  17. * 注意:ActivityDTO.price 已被后端按会员等级解析(会员拿到 memberPrice),
  18. * memberPrice 仍是原始会员价。故本工具取"非会员列"用 price 优先 + parentFee 回落,
  19. * 取"会员列"用 memberPrice 优先 + parentMemberFee 回落。
  20. */
  21. export const AUDIENCE_SCOPE_ALL = 'all'
  22. export const AUDIENCE_SCOPE_MEMBER_ONLY = 'member_only'
  23. export const AUDIENCE_TYPE_ADULT = 'adult'
  24. export const AUDIENCE_TYPE_INCLUDE_CHILD = 'include_child'
  25. export const AUDIENCE_TYPE_INCLUDE_ELDER = 'include_elder'
  26. export const AUDIENCE_TYPE_INCLUDE_CHILD_ELDER = 'include_child_elder'
  27. const VALID_AUDIENCE_TYPES = [
  28. AUDIENCE_TYPE_ADULT,
  29. AUDIENCE_TYPE_INCLUDE_CHILD,
  30. AUDIENCE_TYPE_INCLUDE_ELDER,
  31. AUDIENCE_TYPE_INCLUDE_CHILD_ELDER
  32. ]
  33. export const IDENTITY_ADULT = 'adult'
  34. export const IDENTITY_CHILD = 'child'
  35. export const IDENTITY_ELDER = 'elder'
  36. const IDENTITY_LABELS = { adult: '成人', child: '儿童', elder: '长者' }
  37. /* eslint-disable no-restricted-globals */
  38. export function num(value) {
  39. if (value === null || value === undefined || value === '') return 0
  40. var n = Number(value)
  41. return isNaN(n) ? 0 : n
  42. }
  43. /* eslint-enable no-restricted-globals */
  44. /**
  45. * 归一化 audienceType:空值/非法值一律回落 adult(兼容存量活动与 A2 未合入期)
  46. * @param {string} type
  47. * @returns {string} 'adult' | 'include_child' | 'include_elder' | 'include_child_elder'
  48. */
  49. export function normalizeAudienceType(type) {
  50. var t = String(type == null ? '' : type).trim()
  51. if (VALID_AUDIENCE_TYPES.indexOf(t) >= 0) return t
  52. return AUDIENCE_TYPE_ADULT
  53. }
  54. /**
  55. * 归一化 audienceScope:空值/非法值一律回落 all
  56. * @param {string} scope
  57. * @returns {string} 'all' | 'member_only'
  58. */
  59. export function normalizeAudienceScope(scope) {
  60. return scope === AUDIENCE_SCOPE_MEMBER_ONLY ? AUDIENCE_SCOPE_MEMBER_ONLY : AUDIENCE_SCOPE_ALL
  61. }
  62. /** audienceType 是否含儿童人群 */
  63. export function audienceHasChild(type) {
  64. var t = normalizeAudienceType(type)
  65. return t === AUDIENCE_TYPE_INCLUDE_CHILD || t === AUDIENCE_TYPE_INCLUDE_CHILD_ELDER
  66. }
  67. /** audienceType 是否含长者人群 */
  68. export function audienceHasElder(type) {
  69. var t = normalizeAudienceType(type)
  70. return t === AUDIENCE_TYPE_INCLUDE_ELDER || t === AUDIENCE_TYPE_INCLUDE_CHILD_ELDER
  71. }
  72. /**
  73. * 成人档两列费用(分):{ regular, member }
  74. * 非会员列 price 优先、parentFee 回落;会员列 memberPrice 优先、parentMemberFee 回落
  75. */
  76. export function adultFees(activity) {
  77. var a = activity || {}
  78. var price = num(a.price)
  79. var memberPrice = num(a.memberPrice)
  80. return {
  81. regular: price > 0 ? price : num(a.parentFee),
  82. member: memberPrice > 0 ? memberPrice : num(a.parentMemberFee)
  83. }
  84. }
  85. /** 儿童档两列费用(分):{ regular, member } */
  86. export function childFees(activity) {
  87. var a = activity || {}
  88. return {
  89. regular: num(a.childFee),
  90. member: num(a.childMemberFee)
  91. }
  92. }
  93. /** 长者档费用:复用成人价(D1:不新增长者价格列) */
  94. export function elderFees(activity) {
  95. return adultFees(activity)
  96. }
  97. /** 身份标签文案:成人 / 儿童 / 长者 */
  98. export function identityLabel(identity) {
  99. return IDENTITY_LABELS[identity] || '成人'
  100. }
  101. /**
  102. * 身份判定(A2 §2.4 / D4):age < 18 → 儿童;age >= 60 → 长者;
  103. * 其余(含 age 为空/缺失)一律成人 —— 绝不可缺省判为儿童,否则会多收费。
  104. * @param {object} member FamilyMemberVO(需含 age)
  105. * @returns {string} 'adult' | 'child' | 'elder'
  106. */
  107. export function classifyIdentity(member) {
  108. var m = member || {}
  109. var age = num(m.age)
  110. if (age > 0 && age < 18) return IDENTITY_CHILD
  111. if (age >= 60) return IDENTITY_ELDER
  112. return IDENTITY_ADULT
  113. }
  114. /**
  115. * 当前用户在某身份下的生效单价(分)
  116. * 会员:取会员价列,会员价留空(0)→ 回落非会员价
  117. * 非会员:取非会员价列;长者复用成人价
  118. * @param {object} activity 活动对象(price/memberPrice/childFee/childMemberFee/parentFee/parentMemberFee)
  119. * @param {string} identity 'adult' | 'child' | 'elder'
  120. * @param {boolean} isMember 当前用户是否会员
  121. * @returns {number} 分值;0 表示免费
  122. */
  123. export function priceForIdentity(activity, identity, isMember) {
  124. var fees = identity === IDENTITY_CHILD ? childFees(activity) : adultFees(activity)
  125. if (isMember) {
  126. return fees.member > 0 ? fees.member : fees.regular
  127. }
  128. return fees.regular
  129. }
  130. /** 活动是否收费:任一人群任一档 > 0(纳入 price/memberPrice,修复"只填成人价误显免费") */
  131. export function hasAnyFee(activity) {
  132. var adult = adultFees(activity)
  133. var child = childFees(activity)
  134. return adult.regular > 0 || adult.member > 0 || child.regular > 0 || child.member > 0
  135. }
  136. /** 是否存在会员专享价(用于非会员引导开通会员) */
  137. export function hasAnyMemberFee(activity) {
  138. var adult = adultFees(activity)
  139. var child = childFees(activity)
  140. return adult.member > 0 || child.member > 0
  141. }
  142. /**
  143. * 当前用户可用档位的最小生效单价(分)
  144. * 可用档位 = 成人档(恒含)+ 儿童档(audienceType 含 child 时);长者复用成人价,不额外计档
  145. * @returns {number} 0 表示无可用付费档位(免费活动)
  146. */
  147. export function minAvailablePrice(activity, isMember) {
  148. var a = activity || {}
  149. var best = -1
  150. var adult = priceForIdentity(a, IDENTITY_ADULT, isMember)
  151. if (adult > 0) best = adult
  152. if (audienceHasChild(a.audienceType)) {
  153. var child = priceForIdentity(a, IDENTITY_CHILD, isMember)
  154. if (child > 0 && (best < 0 || child < best)) best = child
  155. }
  156. return best < 0 ? 0 : best
  157. }
  158. /**
  159. * 双价展示文案(与活动详情页原 feeText 口径一致)
  160. * 会员价更低时:「80.00(会员) / 100.00」;否则只显示主价;regular=0 且 member>0 →「会员免费」
  161. * @param {number} regularFee 非会员价(分)
  162. * @param {number} memberFee 会员价(分)
  163. * @returns {string} 如 "100.00" / "80.00(会员) / 100.00" / "0.00" / "会员免费"
  164. */
  165. export function feeText(regularFee, memberFee) {
  166. var regular = num(regularFee) / 100
  167. var member = num(memberFee) / 100
  168. if (member > 0 && member < regular) return member.toFixed(2) + '(会员) / ' + regular.toFixed(2)
  169. if (regular === 0 && member === 0) return '0.00'
  170. if (regular === 0) return '会员免费'
  171. return regular.toFixed(2)
  172. }
  173. /**
  174. * 海报价格文案信息:按 audienceType 选档位标签与价格
  175. * 1) 含儿童人群且儿童档有价 → 儿童价(儿童)
  176. * 2) 含长者人群 → 成人价(长者) ← 长者复用成人价
  177. * 3) 其余 → 成人价(成人)
  178. * @returns {{label:string, text:string}} label 为 '成人'/'儿童'/'长者'/'免费'
  179. */
  180. export function posterPriceInfo(activity) {
  181. var a = activity || {}
  182. var t = normalizeAudienceType(a.audienceType)
  183. var adult = adultFees(a)
  184. var child = childFees(a)
  185. if (!hasAnyFee(a)) return { label: '免费', text: '免费' }
  186. var label = '成人'
  187. var regular = adult.regular
  188. var member = adult.member
  189. if (audienceHasChild(t) && (child.regular > 0 || child.member > 0)) {
  190. label = '儿童'
  191. regular = child.regular
  192. member = child.member
  193. } else if (audienceHasElder(t)) {
  194. label = '长者'
  195. }
  196. return { label: label, text: feeText(regular, member) + '(' + label + ')' }
  197. }
  198. /**
  199. * audienceType → 后台多选勾选数组('adult' 恒选)
  200. * @param {string} type
  201. * @returns {string[]} 如 ['adult', 'child']
  202. */
  203. export function audienceTypeList(type) {
  204. var t = normalizeAudienceType(type)
  205. var list = ['adult']
  206. if (audienceHasChild(t)) list.push('child')
  207. if (audienceHasElder(t)) list.push('elder')
  208. return list
  209. }
  210. /**
  211. * 后台多选勾选数组 → audienceType 编码
  212. * @param {string[]} list 如 ['adult', 'child']
  213. * @returns {string} 'adult' | 'include_child' | 'include_elder' | 'include_child_elder'
  214. */
  215. export function encodeAudienceType(list) {
  216. var arr = list || []
  217. var hasChild = false
  218. var hasElder = false
  219. for (var i = 0; i < arr.length; i++) {
  220. if (arr[i] === 'child') hasChild = true
  221. else if (arr[i] === 'elder') hasElder = true
  222. }
  223. if (hasChild && hasElder) return AUDIENCE_TYPE_INCLUDE_CHILD_ELDER
  224. if (hasChild) return AUDIENCE_TYPE_INCLUDE_CHILD
  225. if (hasElder) return AUDIENCE_TYPE_INCLUDE_ELDER
  226. return AUDIENCE_TYPE_ADULT
  227. }
  228. /** audienceScope 展示文案 */
  229. export function audienceScopeLabel(scope) {
  230. return scope === AUDIENCE_SCOPE_MEMBER_ONLY ? '仅会员参加' : '全部用户'
  231. }