'use strict'; const { CrudService } = require('naf-framework-mongoose-free/lib/service'); const { BusinessError, ErrorCode } = require('naf-core').Error; const _ = require('lodash'); const assert = require('assert'); const moment = require('moment'); const Transaction = require('mongoose-transactions'); // class LessonStudentService extends CrudService { constructor(ctx) { super(ctx, 'lessonstudent'); this.model = this.ctx.model.Business.LessonStudent; this.rscModel = this.ctx.model.Relation.RelationStudentCoach; this.lessonCoachModel = this.ctx.model.Business.LessonCoach; this.lessonModel = this.ctx.model.Business.Lesson; this.rcsModel = this.ctx.model.Relation.RelationCoachSchool; this.rssModel = this.ctx.model.Relation.RelationStudentSchool; this.billModel = this.ctx.model.Business.Bill; this.payOrderModel = this.ctx.model.Business.PayOrder; this.studentModel = this.ctx.model.User.Student; this.payService = this.ctx.service.wxpay; this.tran = new Transaction(); } async checkCanUse({ school_id, student_id, lesson_id }) { 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' }] }); if (num > 0) throw new BusinessError(ErrorCode.DATA_EXISTED, '已存在有效的报名信息'); return true; } async beforeCreate(body) { const { lesson_id, school_id, student_id } = body; // 检查是否已经有数据: 支付失败和已退款不算 const data = await this.model.findOne({ lesson_id, school_id, student_id, $or: [{ is_try: '0', is_pay: { $nin: [ '-1', '-3' ] } }, { is_try: '1' }] }); // 数据已存在 if (data) throw new BusinessError(ErrorCode.DATA_EXISTED, '您已报名'); // 数据不存在 const { is_try } = body; // 检查是否使用试听名额 if (is_try === '1') { const num = await this.model.count({ student_id, is_try: '1' }); if (num > 0) throw new BusinessError(ErrorCode.SERVICE_FAULT, '您已使用过免费试听的机会'); } return body; } async create(data) { // 检查是否能报名上课 data = await this.beforeCreate(data); // 能报名上课,就直接创建数据 try { // 创建数据 const id = this.tran.insert('LessonStudent', data); const { school_id, student_id: payer_id, money, openid } = data; if (!openid) throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到用户的微信信息'); const payData = { openid, school_id, payer_id, payer_role: 'Student', pay_for: 'LessonStudent', from_id: id, time: moment().format('YYYY-MM-DD HH:mm:ss'), order_no: this.ctx.service.business.payOrder.getOrderNo(), desc: '课程费', money, }; // 计算付款单支付金额 // 有余额且余额够,就直接扣余额;余额不足,就直接去支付 const relation = await this.rssModel.findOne({ student_id: payer_id, school_id }); if (!relation) throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到在校记录'); const { money: surplus } = relation; let billId; if (this.ctx.minus(surplus, money) >= 0) { // 余额够,生成账单 const billData = _.pick(payData, [ 'school_id', 'payer_role', 'payer_id', 'pay_for', 'from_id' ]); billData.type = '-2'; billData.is_pay = '1'; billData.time = moment().format('YYYY-MM-DD HH:mm:ss'); billId = this.tran.insert('Bill', billData); // 减少余额 const rs = this.ctx.minus(surplus, data.money); this.tran.update('RelationStudentSchool', relation._id, { money: rs }); // 生成付款单数据,但是不需要支付 payData.status = '1'; payData.config = { useSurplus: true, bill: billId, money, }; } const pay_id = this.tran.insert('PayOrder', payData); const result = { pay_id }; // 如果生成账单id,则说明是全余额付的.修改账单的数据,将pay_id改了 if (billId) this.tran.update('Bill', billId, { pay_id }); else { // 需要微信支付签名(填充回调函数,回调函数处理) const wxSign = await this.payService.create({ ...payData, notice_url: this.app.config.payReturn }); result.wxSign = wxSign; } await this.tran.run(); return result; } catch (error) { await this.tran.rollback(); throw Error(error); } finally { this.tran.clean(); } } // 计算价格 async toComputed({ lesson_id, student_id }) { // 通过课程id找到该课的所有教练 const lesson = await this.lessonModel.findById(lesson_id); if (!lesson) throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到数据'); const data = JSON.parse(JSON.stringify(lesson)); const lessonMoney = _.get(data, 'money'); const lessonType = _.get(data, 'type'); if (!lessonMoney) throw new BusinessError(ErrorCode.DATA_INVALID, '课程设置错误,缺少教练费'); // lesson中的money: 是学生应缴费用; // lessonStudent中的money:是学生应缴费用(公开课不计算,私教课需要查下有没有教师的优惠); // lessonCoach中的money: 是教师公开课每人应收的费用 // 学生应缴计算方式: 公开课:lesson中的money; 私教课 lesson中的money - 和该教练的优惠 (可以没有) // 教练应收计算方式: 公开课: lessonCoach中设置的单价*人数; 私教课: (lesson的money - 对该学生的优惠) 之和 if (lessonType === '0') { // 公开课 data.real_money = lessonMoney; } else { // 找教练和学生的设置 // 虽然用find,但实际上只有一个教练,但凡有2个,就应该不是私教课了 const lessonCoachs = await this.lessonCoachModel.find({ lesson_id }); if (lessonCoachs.length <= 0) return data; const coach_ids = lessonCoachs.map(i => i.coach_id); const rsc = await this.rscModel.findOne({ coach_id: coach_ids, student_id }); if (rsc) { const discountType = _.get(rsc, 'config.discount_type'); // fixed;subtract;discount const number = _.get(rsc, 'config.number'); if (discountType && number) { // 优惠目前设置3种: 固定金额; ${num}(num>1):减num元; x折; 打x折(money * (x/100)) if (discountType === 'fixed') { // 固定金额 data.real_money = number >= 0 ? number : 0; } else if (discountType === 'subtract') { data.real_money = lessonMoney - number >= 0 ? lessonMoney - number : 0; } else if (discountType === 'discount') { data.real_money = _.round(_.multiply(lessonMoney, _.divide(number, 10)), 2); } data.config = _.get(rsc, 'config'); } } } return data; } // 退课,将钱退至余额 async toRefund({ lesson_id, student_id }) { const payOrderService = this.ctx.service.business.payOrder; const payOrderModel = this.ctx.model.Business.PayOrder; const billModel = this.ctx.model.Business.Bill; const data = await this.model.findOne({ lesson_id, student_id }); if (!data) throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到数据'); const lesson = await this.lessonModel.findById(lesson_id); if (!lesson) throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到课程数据'); if (!moment().isBefore(lesson.refund_hour)) throw new BusinessError(ErrorCode.DATA_INVALID, '已经超过退课时间,无法退课'); if (data.is_pay === '-3') throw new BusinessError(ErrorCode.DATA_INVALID, '已经完成退课,无法再次退课'); if (data.is_try === '1') { // 试课 修改退款状态, 而不需要退钱,同时也浪费了试课机会 data.is_pay = '-3'; await data.save(); return; } // 交钱退课的 if (data.pay_id) { // 正常交钱的课,将 pay_id 的status 修改为 -3, 同时触发修改这条数据,但是没有退款; await payOrderService.update({ _id: data.pay_id }, { status: '-3' }); const payOrder = await payOrderModel.findById(data.pay_id); // 再生成账单记录 const obj = _.pick(payOrder, [ 'school_id', 'payer_role', 'payer_id', 'pay_for', 'from_id' ]); obj.money = data.money; obj.type = '2'; obj.is_pay = '1'; obj.time = moment().format('YYYY-MM-DD HH:mm:ss'); // 检查是否有从余额扣款部分 if (_.get(payOrder, 'config,useSurplus')) { // 有余额扣款部分,查账单把钱加上 const billId = _.get(payOrder, 'config,bill'); const bill = await this.billModel.findById(billId); bill.is_pay = '-3'; await bill.save(); } await billModel.create(obj); // 再将钱返回给余额 let relation; const { payer_role, payer_id, school_id } = payOrder; if (payer_role === 'Student') relation = await this.rssModel.findOne({ student_id: payer_id, school_id }); else if (payer_role === 'Coach') relation = await this.rcsModel.findOne({ coach_id: payer_id, school_id }); relation.money = _.get(relation, 'money', 0) + _.get(obj, 'money', 0); await relation.save(); return; } // 从余额付款,退回余额 const obj = { school_id: data.school_id, payer_id: student_id, payer_role: 'Student', pay_for: 'lessonStudent', from_id: data._id, type: '2', is_pay: '1', time: moment().format('YYYY-MM-DD HH:mm:ss'), money: data.money, }; // 创建退回余额的账单 await billModel.create(obj); const relation = await this.rssModel.findOne({ student_id, school_id: data.school_id }); relation.money = _.get(relation, 'money', 0) + _.get(obj, 'money', 0); // 退回至账户 await relation.save(); data.is_pay = '-3'; // 学员退课 await data.save(); } } module.exports = LessonStudentService;