lessonStudent.js 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. 'use strict';
  2. const { CrudService } = require('naf-framework-mongoose-free/lib/service');
  3. const { BusinessError, ErrorCode } = require('naf-core').Error;
  4. const _ = require('lodash');
  5. const assert = require('assert');
  6. const moment = require('moment');
  7. const Transaction = require('mongoose-transactions');
  8. //
  9. class LessonStudentService extends CrudService {
  10. constructor(ctx) {
  11. super(ctx, 'lessonstudent');
  12. this.model = this.ctx.model.Business.LessonStudent;
  13. this.rscModel = this.ctx.model.Relation.RelationStudentCoach;
  14. this.lessonCoachModel = this.ctx.model.Business.LessonCoach;
  15. this.lessonModel = this.ctx.model.Business.Lesson;
  16. this.rcsModel = this.ctx.model.Relation.RelationCoachSchool;
  17. this.rssModel = this.ctx.model.Relation.RelationStudentSchool;
  18. this.billModel = this.ctx.model.Business.Bill;
  19. this.payOrderModel = this.ctx.model.Business.PayOrder;
  20. this.studentModel = this.ctx.model.User.Student;
  21. this.payService = this.ctx.service.wxpay;
  22. this.tran = new Transaction();
  23. }
  24. async checkCanUse({ school_id, student_id, lesson_id }) {
  25. const num = await this.model.count({ school_id, student_id, lesson_id, $or: [{ is_try: '0', is_pay: { $and: [{ $ne: '-3' }, { $ne: '-1' }] } }, { is_try: '1' }] });
  26. if (num > 0) throw new BusinessError(ErrorCode.DATA_EXISTED, '已存在有效的报名信息');
  27. return true;
  28. }
  29. async beforeCreate(body) {
  30. const { lesson_id, school_id, student_id } = body;
  31. // 检查是否已经有数据: 支付失败和已退款不算
  32. const data = await this.model.findOne({ lesson_id, school_id, student_id, $or: [{ is_try: '0', is_pay: { $nin: [ '-1', '-3' ] } }, { is_try: '1' }] });
  33. // 数据已存在
  34. if (data) throw new BusinessError(ErrorCode.DATA_EXISTED, '您已报名');
  35. // 数据不存在
  36. const { is_try } = body;
  37. // 检查是否使用试听名额
  38. if (is_try === '1') {
  39. const num = await this.model.count({ student_id, is_try: '1' });
  40. if (num > 0) throw new BusinessError(ErrorCode.SERVICE_FAULT, '您已使用过免费试听的机会');
  41. }
  42. return body;
  43. }
  44. async create(data) {
  45. // 检查是否能报名上课
  46. data = await this.beforeCreate(data);
  47. // 能报名上课,就直接创建数据
  48. try {
  49. // 创建数据
  50. const id = this.tran.insert('LessonStudent', data);
  51. const { school_id, student_id: payer_id, money, openid } = data;
  52. if (!openid) throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到用户的微信信息');
  53. const payData = {
  54. openid,
  55. school_id,
  56. payer_id,
  57. payer_role: 'Student',
  58. pay_for: 'LessonStudent',
  59. from_id: id,
  60. time: moment().format('YYYY-MM-DD HH:mm:ss'),
  61. order_no: this.ctx.service.business.payOrder.getOrderNo(),
  62. desc: '课程费',
  63. money,
  64. };
  65. // 计算付款单支付金额
  66. // 有余额且余额够,就直接扣余额;余额不足,就直接去支付
  67. const relation = await this.rssModel.findOne({ student_id: payer_id, school_id });
  68. if (!relation) throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到在校记录');
  69. const { money: surplus } = relation;
  70. let billId;
  71. if (this.ctx.minus(surplus, money) >= 0) {
  72. // 余额够,生成账单
  73. const billData = _.pick(payData, [ 'school_id', 'payer_role', 'payer_id', 'pay_for', 'from_id' ]);
  74. billData.type = '-2';
  75. billData.is_pay = '1';
  76. billData.time = moment().format('YYYY-MM-DD HH:mm:ss');
  77. billId = this.tran.insert('Bill', billData);
  78. // 减少余额
  79. const rs = this.ctx.minus(surplus, data.money);
  80. this.tran.update('RelationStudentSchool', relation._id, { money: rs });
  81. // 生成付款单数据,但是不需要支付
  82. payData.status = '1';
  83. payData.config = {
  84. useSurplus: true,
  85. bill: billId,
  86. money,
  87. };
  88. }
  89. const pay_id = this.tran.insert('PayOrder', payData);
  90. const result = { pay_id };
  91. // 如果生成账单id,则说明是全余额付的.修改账单的数据,将pay_id改了
  92. if (billId) this.tran.update('Bill', billId, { pay_id });
  93. else {
  94. // 需要微信支付签名(填充回调函数,回调函数处理)
  95. const wxSign = await this.payService.create({ ...payData, notice_url: this.app.config.payReturn });
  96. result.wxSign = wxSign;
  97. }
  98. await this.tran.run();
  99. return result;
  100. } catch (error) {
  101. await this.tran.rollback();
  102. throw Error(error);
  103. } finally {
  104. this.tran.clean();
  105. }
  106. }
  107. // 计算价格
  108. async toComputed({ lesson_id, student_id }) {
  109. // 通过课程id找到该课的所有教练
  110. const lesson = await this.lessonModel.findById(lesson_id);
  111. if (!lesson) throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到数据');
  112. const data = JSON.parse(JSON.stringify(lesson));
  113. const lessonMoney = _.get(data, 'money');
  114. const lessonType = _.get(data, 'type');
  115. if (!lessonMoney) throw new BusinessError(ErrorCode.DATA_INVALID, '课程设置错误,缺少教练费');
  116. // lesson中的money: 是学生应缴费用;
  117. // lessonStudent中的money:是学生应缴费用(公开课不计算,私教课需要查下有没有教师的优惠);
  118. // lessonCoach中的money: 是教师公开课每人应收的费用
  119. // 学生应缴计算方式: 公开课:lesson中的money; 私教课 lesson中的money - 和该教练的优惠 (可以没有)
  120. // 教练应收计算方式: 公开课: lessonCoach中设置的单价*人数; 私教课: (lesson的money - 对该学生的优惠) 之和
  121. if (lessonType === '0') {
  122. // 公开课
  123. data.real_money = lessonMoney;
  124. } else {
  125. // 找教练和学生的设置
  126. // 虽然用find,但实际上只有一个教练,但凡有2个,就应该不是私教课了
  127. const lessonCoachs = await this.lessonCoachModel.find({ lesson_id });
  128. if (lessonCoachs.length <= 0) return data;
  129. const coach_ids = lessonCoachs.map(i => i.coach_id);
  130. const rsc = await this.rscModel.findOne({ coach_id: coach_ids, student_id });
  131. if (rsc) {
  132. const discountType = _.get(rsc, 'config.discount_type'); // fixed;subtract;discount
  133. const number = _.get(rsc, 'config.number');
  134. if (discountType && number) {
  135. // 优惠目前设置3种: 固定金额; ${num}(num>1):减num元; x折; 打x折(money * (x/100))
  136. if (discountType === 'fixed') {
  137. // 固定金额
  138. data.real_money = number >= 0 ? number : 0;
  139. } else if (discountType === 'subtract') {
  140. data.real_money = lessonMoney - number >= 0 ? lessonMoney - number : 0;
  141. } else if (discountType === 'discount') {
  142. data.real_money = _.round(_.multiply(lessonMoney, _.divide(number, 10)), 2);
  143. }
  144. data.config = _.get(rsc, 'config');
  145. }
  146. }
  147. }
  148. return data;
  149. }
  150. // 退课,将钱退至余额
  151. async toRefund({ lesson_id, student_id }) {
  152. const payOrderService = this.ctx.service.business.payOrder;
  153. const payOrderModel = this.ctx.model.Business.PayOrder;
  154. const billModel = this.ctx.model.Business.Bill;
  155. const data = await this.model.findOne({ lesson_id, student_id });
  156. if (!data) throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到数据');
  157. const lesson = await this.lessonModel.findById(lesson_id);
  158. if (!lesson) throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到课程数据');
  159. if (!moment().isBefore(lesson.refund_hour)) throw new BusinessError(ErrorCode.DATA_INVALID, '已经超过退课时间,无法退课');
  160. if (data.is_pay === '-3') throw new BusinessError(ErrorCode.DATA_INVALID, '已经完成退课,无法再次退课');
  161. if (data.is_try === '1') {
  162. // 试课 修改退款状态, 而不需要退钱,同时也浪费了试课机会
  163. data.is_pay = '-3';
  164. await data.save();
  165. return;
  166. }
  167. // 交钱退课的
  168. if (data.pay_id) {
  169. // 正常交钱的课,将 pay_id 的status 修改为 -3, 同时触发修改这条数据,但是没有退款;
  170. await payOrderService.update({ _id: data.pay_id }, { status: '-3' });
  171. const payOrder = await payOrderModel.findById(data.pay_id);
  172. // 再生成账单记录
  173. const obj = _.pick(payOrder, [ 'school_id', 'payer_role', 'payer_id', 'pay_for', 'from_id' ]);
  174. obj.money = data.money;
  175. obj.type = '2';
  176. obj.is_pay = '1';
  177. obj.time = moment().format('YYYY-MM-DD HH:mm:ss');
  178. // 检查是否有从余额扣款部分
  179. if (_.get(payOrder, 'config,useSurplus')) {
  180. // 有余额扣款部分,查账单把钱加上
  181. const billId = _.get(payOrder, 'config,bill');
  182. const bill = await this.billModel.findById(billId);
  183. bill.is_pay = '-3';
  184. await bill.save();
  185. }
  186. await billModel.create(obj);
  187. // 再将钱返回给余额
  188. let relation;
  189. const { payer_role, payer_id, school_id } = payOrder;
  190. if (payer_role === 'Student') relation = await this.rssModel.findOne({ student_id: payer_id, school_id });
  191. else if (payer_role === 'Coach') relation = await this.rcsModel.findOne({ coach_id: payer_id, school_id });
  192. relation.money = _.get(relation, 'money', 0) + _.get(obj, 'money', 0);
  193. await relation.save();
  194. return;
  195. }
  196. // 从余额付款,退回余额
  197. const obj = {
  198. school_id: data.school_id,
  199. payer_id: student_id,
  200. payer_role: 'Student',
  201. pay_for: 'lessonStudent',
  202. from_id: data._id,
  203. type: '2',
  204. is_pay: '1',
  205. time: moment().format('YYYY-MM-DD HH:mm:ss'),
  206. money: data.money,
  207. };
  208. // 创建退回余额的账单
  209. await billModel.create(obj);
  210. const relation = await this.rssModel.findOne({ student_id, school_id: data.school_id });
  211. relation.money = _.get(relation, 'money', 0) + _.get(obj, 'money', 0);
  212. // 退回至账户
  213. await relation.save();
  214. data.is_pay = '-3';
  215. // 学员退课
  216. await data.save();
  217. }
  218. }
  219. module.exports = LessonStudentService;