order.service.js 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. /**
  2. * 订单服务:商品、订单创建、状态机与幂等标记已支付
  3. *
  4. * 订单状态:
  5. * PENDING —— 已创建待支付
  6. * PAID —— 支付成功(已发货)
  7. * CLOSED —— 超时/关闭
  8. */
  9. const { paymentId, outTradeNo } = require('../utils/id');
  10. /** 商品目录(价格单位:分;retail_price_usd 为海外渠道展示用) */
  11. const PRODUCTS = [
  12. {
  13. id: 'p001',
  14. name: '橘子',
  15. quota: 1,
  16. retail_price: 1100, // ¥11.00
  17. retail_price_usd: 0,
  18. expire_policy: 'once',
  19. expire_policy_text: '一次性',
  20. description: '演示商品',
  21. methods: ['wechat', 'alipay', 'stripe', 'apple', 'google'],
  22. },
  23. ];
  24. /** 订单存储(内存 Map;演示足够,重启丢失) */
  25. const orders = new Map();
  26. /**
  27. * 按 id 查找商品
  28. * @returns {object|undefined}
  29. */
  30. function findProduct(productId) {
  31. return PRODUCTS.find((p) => p.id === productId);
  32. }
  33. /**
  34. * 创建订单
  35. * @param {object} param
  36. * @param {string} param.productId 商品 ID
  37. * @param {string} param.source 来源:app|web
  38. * @param {string} param.currency 币种(默认 cny,按商品零售价计)
  39. * @returns {{ id, productId, name, amount, currency, status, outTradeNo, createdAt }}
  40. */
  41. function createOrder({ productId, source = 'app', currency = 'cny' }) {
  42. const product = findProduct(productId);
  43. if (!product) {
  44. const err = new Error(`product not found: ${productId}`);
  45. err.code = 'PRODUCT_NOT_FOUND';
  46. throw err;
  47. }
  48. const order = {
  49. id: paymentId(),
  50. outTradeNo: outTradeNo(),
  51. productId,
  52. name: product.name,
  53. amount: product.retail_price,
  54. currency,
  55. status: 'PENDING',
  56. channel: null,
  57. channelTradeNo: null,
  58. source,
  59. createdAt: new Date().toISOString(),
  60. paidAt: null,
  61. };
  62. orders.set(order.id, order);
  63. return order;
  64. }
  65. /** 查询订单 */
  66. function getOrder(orderId) {
  67. return orders.get(orderId);
  68. }
  69. /** 按商户订单号(out_trade_no)查询订单(回调通知用) */
  70. function getOrderByOutTradeNo(outTradeNo) {
  71. for (const order of orders.values()) {
  72. if (order.outTradeNo === outTradeNo) return order;
  73. }
  74. return null;
  75. }
  76. /** 按商户订单号标记已支付(幂等) */
  77. function markPaidByOutTradeNo(outTradeNo, meta) {
  78. const order = getOrderByOutTradeNo(outTradeNo);
  79. return order ? markPaid(order.id, meta) : false;
  80. }
  81. /**
  82. * 标记订单已支付(幂等):仅 PENDING → PAID 成功执行一次
  83. * @returns {boolean} 本次是否真正完成变更
  84. */
  85. function markPaid(orderId, { channel, channelTradeNo }) {
  86. const order = orders.get(orderId);
  87. if (!order) return false;
  88. if (order.status !== 'PENDING') return false; // 已处理过,幂等
  89. order.status = 'PAID';
  90. order.channel = channel;
  91. order.channelTradeNo = channelTradeNo;
  92. order.paidAt = new Date().toISOString();
  93. return true;
  94. }
  95. /**
  96. * 查询支付状态(前端轮询/最终确认)
  97. * @returns {{ success: boolean, note: string }}
  98. */
  99. function getStatus(orderId) {
  100. const order = orders.get(orderId);
  101. if (!order) {
  102. return { success: false, note: '订单不存在' };
  103. }
  104. if (order.status === 'PAID') {
  105. return { success: true, note: '支付成功' };
  106. }
  107. if (order.status === 'CLOSED') {
  108. return { success: false, note: '订单已关闭' };
  109. }
  110. return { success: false, note: '等待支付' };
  111. }
  112. /** 全部商品列表(前端定价展示) */
  113. function listProducts() {
  114. return PRODUCTS;
  115. }
  116. module.exports = {
  117. findProduct,
  118. createOrder,
  119. getOrder,
  120. markPaid,
  121. getStatus,
  122. listProducts,
  123. };