'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'); const { find } = require('lodash'); // 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 toRePay({ id }) { const data = await this.model.findById(id); console.log(data); if (!data) throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到报名信息'); if (data.is_pay === '1') throw new BusinessError(ErrorCode.SERVICE_FAULT, '该课程已支付成功'); const payOrder = await this.payOrderModel.findById(data.pay_id); if (!payOrder) throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到课程缴费信息'); const { openid, money, school_id, payer_id: student_id, order_no } = payOrder; try { // 关闭订单 await this.payService.close(order_no); // 删了重做 this.tran.remove('LessonStudent', id); this.tran.remove('PayOrder', data.pay_id); await this.tran.run(); this.tran.clean(); const result = await this.create({ openid, money, school_id, student_id, lesson_id: data.lesson_id }); return result; } catch (error) { await this.tran.rollback(); } finally { this.tran.clean(); } } async create(data) { console.log(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(); this.tran.clean(); // 然后将pay_id赋给课程 this.tran.update('LessonStudent', id, { pay_id }); await this.tran.run(); return result; } catch (error) { await this.tran.rollback(); throw new 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 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; } try { // 正常交钱的课,将 pay_id 的status 修改为 -3, 同时触发修改这条数据,但是没有退款; console.log(data); const payOrder = await this.payOrderModel.findById(data.pay_id); if (!payOrder) throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到付款单信息'); // 再生成账单记录 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'); // 再将钱返回给余额 let relation, model; const { payer_role, payer_id, school_id } = payOrder; if (payer_role === 'Student') { relation = await this.rssModel.findOne({ student_id: payer_id, school_id }); model = 'RelationStudentSchool'; } else if (payer_role === 'Coach') { relation = await this.rcsModel.findOne({ coach_id: payer_id, school_id }); model = 'RelationCoachSchool'; } const newMoney = this.ctx.plus(relation.money, obj.money); // 退至余额 this.tran.update(model, relation._id, { money: newMoney }); // 生成退款账单 this.tran.insert('Bill', obj); // 付款单状态修改 this.tran.update('PayOrder', data.pay_id, { status: '-3' }); // 课程-学生状态修改 this.tran.update('LessonStudent', data._id, { is_pay: '-3' }); await this.tran.run(); return; } catch (error) { await this.tran.rollback(); console.log(error); throw Error(error); } finally { this.tran.clean(); } } } module.exports = LessonStudentService;