class FundWeekly(Article): kind = FUNDWEEKLY.KIND kind_cn = u'基金组合周报' type = FUNDWEEKLY.TYPE title = PropsItem('title', '') description = PropsItem('description', '') content = PropsItem('content', '') author = PropsItem('author', '') _const = FUNDWEEKLY def __repr__(self): return '<FundWeekly id=%s, type=%s, status=%s>' % (self.id, self.type, self.status) @permalink('article.article_detail') def url(self): return {'article_type': 'fund/weekly', 'id': self.id} def mark_as_read(self, user_id): mc.set(FUNDWEEKLY_READ_CACHE_KEY % (self.id, user_id), True) def has_read(self, user_id): return mc.get(FUNDWEEKLY_READ_CACHE_KEY % (self.id, user_id))
class ViewPoint(Article): kind = VIEWPOINT.KIND kind_cn = u'理财师观点' type = VIEWPOINT.TYPE title = PropsItem('title', '') content = PropsItem('content', '') author = PropsItem('author', '') _const = VIEWPOINT def __repr__(self): return '<ViewPoint id=%s, type=%s, status=%s>' % (self.id, self.type, self.status) @permalink('article.article_detail') def url(self): return {'article_type': 'viewpoints', 'id': self.id} def make_feed_entry(self): return FeedEntry(self.title, render(self.content), content_type='html', author=self.author or u'好规划网理财师', url=self.url, updated=self.update_time, published=self.publish_time)
class Insure(ProductBase): ''' 保险 ''' duration = PropsItem('duration', '') # 保险期间 pay_duration = PropsItem('pay_duration', '') # 缴费期限 insure_duty = PropsItem('insure_duty', '') # 保险责任 throng = PropsItem('throng', '') # 适合人群 prospect = PropsItem('prospect', '') # 保费预估 kind = 'insure' _table = 'product_insure' def __repr__(self): return '<Product Insure id=%s, type=%s, status=%s>' % ( self.id, self.type, self.status ) @classmethod def gets_by_type(cls, type): funds = super(Insure, cls).get_all(limit=1000) if not funds: return [] if type and str(type) not in INSURE_TYPE.values(): return [] return filter(lambda x: x.type == str(type), funds) @property def type_name(self): for name, value in INSURE_TYPE.items(): if self.type == value: return INSURE_NAME.get(name) return '未命名'
class Profile(PropsMixin): table_name = 'insurance_profile' user_will = PropsItem('user_will', default=0, secret=True) baby_birthday = PropsItem('baby_birthday', default=datetime.now(), secret=True) baby_gender = PropsItem('baby_gender', default='', secret=True) child_medicare = PropsItem('child_medicare', default='') childins_supplement = PropsItem('childins_supplement', default='') child_genetic = PropsItem('child_genetic', default='') child_edu = PropsItem('child_edu', default='') project = PropsItem('project', default='') is_six = PropsItem('is_six', default=False) result_data = PropsItem('result_data', default={}, secret=True) def __init__(self, account_id, create_time): self.user_id = account_id self.create_time = create_time def get_db(self): return 'insurance_profile' def get_uuid(self): return 'insurance_profile:insurance:%s' % self.user_id @classmethod def add(cls, account_id): if not Account.get(account_id): raise InsuranceNotFoundError(account_id, Account) existent = cls.get(account_id) if existent: return existent sql = ('insert into {.table_name} (account_id, create_time) ' 'values ( %s, %s)').format(cls) params = (account_id, datetime.now()) db.execute(sql, params) db.commit() cls.clear_cache(account_id) return cls.get(account_id) @classmethod @cache(INSURE_PROFILE_CACHE_KEY) def get(cls, account_id): sql = ('select account_id, create_time ' 'from {.table_name} where account_id= %s').format(cls) param = account_id rs = db.execute(sql, param) if rs: return cls(*rs[0]) @classmethod def clear_cache(cls, account_id): mc.delete(INSURE_PROFILE_CACHE_KEY.format(account_id=account_id))
class Feedback(PropsMixin): def __init__(self, id, contact, create_time, update_time): self.id = str(id) self.contact = contact self.create_time = create_time self.update_time = update_time def get_db(self): return 'feedback' def get_uuid(self): return 'feedback:%s' % self.id content = PropsItem('content', '') answer = PropsItem('answer', '') admin = PropsItem('admin', '') @classmethod def add(cls, contact, content): try: id = db.execute('insert into feedback ' '(contact, create_time) ' 'values(%s, %s)', (contact, datetime.now())) if id: db.commit() c = cls.get(id) c.content = content return c else: db.rollback() except IntegrityError: db.rollback() warn('insert feedback failed') @classmethod @cache(FEEDBACK_CACHE_KEY % '{id}') def get(cls, id): rs = db.execute('select id, contact, create_time, ' 'update_time from feedback ' 'where id=%s', (id,)) return cls(*rs[0]) if rs else None @classmethod @pcache(ALL_FEEDBACK_CACHE_KEY) def _get_all_ids(cls, start=0, limit=20): sql = 'select id from feedback order by update_time desc limit %s,%s' rs = db.execute(sql, (start, limit)) ids = [str(id) for (id,) in rs] return ids @classmethod def get_all(cls, start=0, limit=20): ids = cls._get_all_ids(start=start, limit=limit) return [cls.get(id) for id in ids]
class PersonelGroup(PropsMixin): storage = WeakValueDictionary() # the users dict stored users = PropsItem('users', default={}, secret=True) update_time = PropsItem('update_time', None, date_type) def __init__(self, name): self.name = name if name in self.storage: raise ValueError('name %r has been used' % name) self.storage[name] = self @classmethod def get(cls, name): return cls.storage.get(name) def get_uuid(self): return 'group:{name}'.format(name=self.name) def get_db(self): return 'personel' def add_personel(self, user_id, user_name): group = self.users account = Account.get(user_id) if not account: raise ValueError('%s is not existed' % user_id) if user_id in group: raise KeyError('key %s conflicted' % user_id) group.update({user_id: user_name}) self.update_props_items({ 'users': group, 'update_time': str(datetime.now()) }) def del_personel(self, user_id): group = self.users group.pop(user_id, None) self.update_props_items({ 'users': group, 'update_time': str(datetime.now()) }) def update_personel(self, user_id, user_name): group = self.users if user_id in group: group[user_id] = user_name self.update_props_items({ 'users': group, 'update_time': str(datetime.now()) })
class Question(Article): kind = QUESTION.KIND type = QUESTION.TYPE ask = PropsItem('ask', '') answer = PropsItem('answer', '') _const = QUESTION def __repr__(self): return '<Question id=%s, type=%s, status=%s>' % (self.id, self.type, self.status) @permalink('article.article_detail') def url(self): return {'article_type': 'consultations', 'id': self.id}
class Activity61(PropsMixin): table_name = 'insurance_61' phone_number = PropsItem('phone_number', default='') recommend = PropsItem('recommend', default=[]) def __init__(self, account_id): self.user_id = account_id def get_db(self): return 'insurance_61' def get_uuid(self): return 'insurance_61:insurance:%s' % self.user_id @classmethod def add(cls, account_id): if not Account.get(account_id): raise InsuranceNotFoundError(account_id, Account) existent = cls.get(account_id) if existent: return existent sql = ('insert into {.table_name} (account_id, create_time) ' 'values (%s, %s)').format(cls) params = (account_id, datetime.now()) db.execute(sql, params) db.commit() cls.clear_cache(account_id) return cls.get(account_id) @classmethod @cache(INSURE_61_CACHE_KEY) def get(cls, account_id): sql = ('select account_id from {.table_name} where account_id= %s' ).format(cls) param = account_id rs = db.execute(sql, param) if rs: return cls(*rs[0]) @classmethod def clear_cache(cls, account_id): mc.delete(INSURE_61_CACHE_KEY.format(account_id=account_id))
class ZhiwangLadderDuedayProduct(ZhiwangProduct): annual_rate_layers = PropsItem('annual_rate_layers', [], list) last_due_date = PropsItem('last_due_date', None, date_type) @property def first_due_date(self): return self.due_date @property def final_due_date(self): return self.last_due_date or self.due_date @property def local_name(self): return u'%s~%s不定期产品' % (self.profit_period['min'].display_text, self.profit_period['max'].display_text) @cached_property def profit_period(self): min_value = (self.first_due_date - self.start_date).days max_value = (self.final_due_date - self.start_date).days return { 'min': ProfitPeriod(min_value, 'day'), 'max': ProfitPeriod(max_value, 'day') } @cached_property def profit_annual_rate(self): rates = [layer['annual_rate'] for layer in self.annual_rate_layers] return {'min': Decimal(min(rates)), 'max': Decimal(max(rates))} def get_annual_rate_by_date(self, day): assert isinstance(day, datetime.date) return Decimal( self.get_annual_rate_by_days((day - self.start_date).days)) def get_annual_rate_by_days(self, days): matched_layers = [ layer['annual_rate'] for layer in self.annual_rate_layers if days >= layer['min_days'] ] return matched_layers[-1] if matched_layers else 0
class NotSecretTest(PropsMixin): def __init__(self, id=None): self.id = id or 10000 def get_db(self): return 'test_db' def get_uuid(self): return self.id data = PropsItem('data', 'nothing')
class BackwardProfile(PropsMixin): """Do not use this outside.""" person_name = PropsItem('person_name', default='', secret=True) person_ricn = PropsItem('person_ricn', default='', secret=True) def get_uuid(self): return 'user:profile:{account_id}'.format(account_id=self.account_id) def get_db(self): return 'hoard' def __init__(self, account_id): self.account_id = account_id def remove(self): self.update_props_items({ 'person_name': '', 'person_ricn': '', })
class Program(PropsMixin): quota = PropsItem('quota', default={}) def __init__(self): pass def get_db(self): return 'insurance_program' def get_uuid(self): return 'insurance_program:quota'
class P2P(ProductBase): ''' P2P ''' kind = 'p2p' _table = 'product_p2p' year_rate = PropsItem('year_rate', 0) # 预期年化收益率 pay_return_type = PropsItem('pay_return_type', '') # 返还方式 deadline = PropsItem('deadline', '') # 投资期限 min_money = PropsItem('min_money', '') # 购买起点 protect = PropsItem('protect', '') # 保障 def __repr__(self): return '<Product P2P id=%s, type=%s, status=%s>' % (self.id, self.type, self.status) @property def type_name(self): return 'P2P'
class InsProperty(PropsMixin): feerate = PropsItem('feerate', {}) rec_reason = PropsItem('rec_reason', '') buy_url = PropsItem('buy_url') ins_title = PropsItem('ins_title') ins_sub_title = PropsItem('ins_sub_title', 'ins_sub_title') def __init__(self, insurance_id, kind): self.id = insurance_id self.kind = kind self._age = None def get_db(self): return 'insure_fee_rate' def get_uuid(self): return 'insure_fee_rate:insure:%s' % self.id def get_ins_sub_title(self, package_id, insurance_id): return self.ins_sub_title def add_props(self, feerate, rec_reason, buy_url, ins_title, ins_sub_title): self.feerate = feerate self.rec_reason = rec_reason self.buy_url = buy_url self.ins_title = ins_title self.ins_sub_title = ins_sub_title @property def age(self): return self._age @age.setter def age(self, ageobj): self._age = ageobj
class Fund(ProductBase): ''' 基金 ''' kind = 'fund' _table = 'product_fund' code = PropsItem('code', '') # 基金代码 found_date = PropsItem('found_date', '') # 成立日期 index = PropsItem('index', '') # 跟踪指数 risk = PropsItem('risk', '') # 风险 manager = PropsItem('manager', '') # 基金经理 year_rate = PropsItem('year_rate', '') # 近一年涨幅 nickname = PropsItem('nickname', '') # 别名 def __repr__(self): return '<Product fund id=%s, type=%s, status=%s>' % ( self.id, self.type, self.status) @classmethod def gets_by_type(cls, type): funds = super(Fund, cls).get_all(limit=1000) if not funds: return [] if type and str(type) not in FUND_TYPE.values(): return [] return filter(lambda x: x.type == str(type), funds) @property def type_name(self): for name, value in FUND_TYPE.items(): if self.type == value: return FUND_NAME.get(name) return '未命名' @classmethod def gets_by_risk(cls, risk): if risk not in ('高', '中', '低'): return [] funds = super(Fund, cls).get_all(limit=1000) if not funds: return [] return filter(lambda x: x.risk == risk, funds)
class BankCardManager(PropsMixin): """The bank card manager which should be bound with user profiles.""" last_used_bankcard_id = PropsItem('last_used_bankcard_id', default=0) def get_uuid(self): return 'user:{.user_id}:bankcards'.format(self) def get_db(self): return 'hoard' def __init__(self, user_id): self.user_id = user_id def get_all(self, partner=None): """Gets all bank cards of current user. :returns: the list of :class: """ cards = [c for c in BankCard.get_by_user(self.user_id) if c.is_active] cards = sorted(cards, key=attrgetter('is_default'), reverse=True) if cards and not cards[0].is_default: cards[0].is_default = True if partner: cards = [c for c in cards if partner in c.bank.available_in] return cards def get_latest(self): bankcards = sorted( self.get_all(), key=attrgetter('creation_time'), reverse=True) return first(bankcards, default=None) def get_last_used(self): bankcard = BankCard.get(self.last_used_bankcard_id) if bankcard and bankcard.is_active: return bankcard return self.get_latest() def get_default(self): bankcards = self.get_all() card = first(bankcards, default=None) if not card.is_default: card.is_default = True return card def set_default(self, bankcard): if not isinstance(bankcard, BankCard): raise TypeError if bankcard.is_default: return True bankcards = self.get_all() if bankcard not in bankcards: raise BankCardNotActive(bankcard.id_) default_card = self.get_default() default_card.is_default = False bankcard.is_default = True return True def add(self, **kwargs): """Adds a new bank card for current user. :params kwargs: the same as :meth:`BankCard.add` except ``user_id``. :returns: the created bank card. """ # 静默删除已存在但未使用过的同一银行的卡 for bankcard in self.get_multi_by_bank(kwargs['bank_id']): self.remove(bankcard.card_number, silent=True) # 静默删除同一个卡号但未使用过的卡 bankcard = self.get_by_card_number(kwargs['card_number']) if bankcard: self.remove(bankcard.card_number, silent=True) return BankCard.add(user_id=self.user_id, **kwargs) def create_or_update(self, card_number, **kwargs): # TODO (tonyseek) 不应该让所有字段都可以更新 cards = self.get_all() is_default = False if cards else True kwargs.update({'is_default': is_default}) card = self.get_by_card_number(card_number) if card: card.update(**kwargs) self.last_used_bankcard_id = card.id_ return card else: return self.add(card_number=card_number, **kwargs) def get_by_card_number(self, card_number): digest = calculate_checksum(card_number) return first( (c for c in self.get_all() if c.card_number_sha1 == digest), None) def get(self, id_): return first((c for c in self.get_all() if c.id_ == str(id_)), None) def get_multi_by_bank(self, bank_id): return [c for c in self.get_all() if c.bank_id == str(bank_id)] def remove(self, card_number, silent=False): try: BankCard.delete_by_card_number(card_number, user_id=self.user_id) except CardDeletingError as deleting_error: if silent: rsyslog.send('%s\t%s' % (card_number, deleting_error), 'bankcard_removing_denied') else: raise else: rsyslog.send(card_number, 'bankcare_removed') def restore(self, id_): return BankCard.restore(id_, self.user_id)
class Product(PropsMixin): """产品""" table_name = 'hoarder_product' cache_key = 'hoarder:product:v1:{product_id}' cache_by_vendor = 'hoarder:products:v1:{vendor_id}' class Status(Enum): """产品状态""" # 在售 on_sell = 'S' # 废弃 deprecated = 'D' class Type(Enum): """产品期限分类""" # 固定期限类 classic = 'C' # 不定期限类 dynamic = 'D' # 不限期限类 unlimited = 'U' class RedeemType(Enum): """赎回类型""" # 到期自动赎回 auto = '2' # 用户申请赎回 user = '******' class Kind(Enum): """父子产品分类""" # 父产品 father = 'F' # 子产品 child = 'C' # 产品名称 name = PropsItem('name', u'', unicode_type) # 产品可用配额 quota = PropsItem('quota', 0, Decimal) # 产品总配额 total_quota = PropsItem('total_quota', 0, Decimal) # 产品当日配额 today_quota = PropsItem('today_quota', 0, Decimal) # 产品累计交易额 total_amount = PropsItem('total_amount', 0, Decimal) # 客户最大可购买 金额(客户对产品 购买的最大累计 金额) total_buy_amount = PropsItem('total_buy_amount', 0, Decimal) # 最小赎回金额(针对允许用户主动 发起赎回的产品, 例如活期类日日盈产品等) min_redeem_amount = PropsItem('min_redeem_amount', 0, Decimal) # 最大赎回金额 max_redeem_amount = PropsItem('max_redeem_amount', 0, Decimal) # 每日赎回限额 day_redeem_amount = PropsItem('day_redeem_amount', 0, Decimal) # 加息年化利率 interest_rate_hike = PropsItem('interest_rate_hike', 0, Decimal) # 备注信息 description = PropsItem('description', u'', unicode_type) # 上架下架控制 is_taken_down = PropsItem('is_taken_down', True, bool) # 预售控制 is_pre_sale = PropsItem('is_pre_sale', False, bool) # 预售时间 pre_hour = PropsItem('pre_hour') expire_period_unit = PropsItem('expire_period_unit', PeriodUnit.day.value, int) expire_period = PropsItem('expire_period', 0, int) is_accepting_bonus = False def __init__(self, product_id, remote_id, status, product_type, kind, min_amount, max_amount, rate_type, rate, effect_day_condition, effect_day, effect_day_unit, redeem_type, start_sell_date, end_sell_date, update_time, creation_time, vendor_id): self.id_ = product_id self.remote_id = remote_id self._status = status self._type = product_type self._kind = kind self.min_amount = min_amount self.max_amount = max_amount self.rate_type = rate_type self.rate = rate self.effect_day_condition = effect_day_condition self.effect_day = effect_day self.effect_day_unit = effect_day_unit # 赎回类型 self.redeem_type = redeem_type self.start_sell_date = start_sell_date self.end_sell_date = end_sell_date self.update_time = update_time self.creation_time = creation_time self.vendor_id = vendor_id def get_db(self): return 'hoarder' def get_uuid(self): return 'product:{product_id}'.format(product_id=self.id_) @property def status(self): return self.Status(self._status) @property def ptype(self): return self.Type(self._type) @property def kind(self): return self.Kind(self._kind) @property def product_type(self): return self.ptype @property def can_redeem(self): """是否可手动赎回""" t = self.RedeemType(self.redeem_type) if t is self.RedeemType.auto: return False # TODO:增加其他不可赎回限制条件 return True @property def value_date(self): """预期起息日""" start_time = datetime.now() + timedelta(days=1) return get_effect_date(start_time) @property def check_benefit_date(self): if self.ptype is self.Type.unlimited: start_time = datetime(self.value_date.year, self.value_date.month, self.value_date.day) return get_effect_date(start_time + timedelta(days=1)) return self.value_date @property def expect_due_date(self): """预期到期日""" if self.ptype is not self.Type.unlimited: start_time = datetime(self.value_date.year, self.value_date.month, self.value_date.day) return get_effect_date(start_time + timedelta(days=self.frozen_days)) @property def is_sold_out(self): from .order import HoarderOrder if self.kind is self.Kind.father: local_daily_sold_amount = HoarderOrder.get_product_daily_sold_amount_by_now( self.id_) if local_daily_sold_amount >= self.quota - get_savings_new_comer_product_threshold( ): return True if self.quota < self.min_amount: return True return False @property def is_in_sale_period(self): return self.start_sell_date <= date.today() < self.end_sell_date @property def is_on_sale(self): if self.is_taken_down or self.is_sold_out: return False if self.status is self.Status.deprecated: return False return True @classmethod @cache(cache_key) def get(cls, product_id): sql = ( 'select id, remote_id, status, type, kind, min_amount, max_amount, rate_type, rate,' ' effect_day_condition, effect_day, effect_day_unit, redeem_type,' ' start_sell_date, end_sell_date, update_time, creation_time, vendor_id' ' from {.table_name} where id=%s').format(cls) params = (product_id, ) rs = db.execute(sql, params) if rs: return cls(*rs[0]) @classmethod def add_or_update(cls, vendor_id, remote_id, name, quota, total_quota, today_quota, total_amount, total_buy_amount, min_redeem_amount, max_redeem_amount, day_redeem_amount, interest_rate_hike, description, product_type, min_amount, max_amount, rate_type, rate, effect_day_condition, effect_day, effect_day_unit, redeem_type, start_sell_date, end_sell_date, expire_period=1, expire_period_unit=PeriodUnit.day.value, is_child_product=False): assert isinstance(product_type, cls.Type) sql = ( 'insert into {.table_name} (remote_id, status, type, kind, min_amount, max_amount,' 'rate_type, rate, effect_day_condition, effect_day, effect_day_unit,' 'redeem_type, start_sell_date, end_sell_date, update_time,' 'creation_time, vendor_id) ' 'values (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) ' 'on duplicate key update rate=values(rate), update_time=values(update_time)' ).format(cls) kind = cls.Kind.child.value if is_child_product else cls.Kind.father.value params = (remote_id, cls.Status.on_sell.value, product_type.value, kind, min_amount, max_amount, rate_type, rate, effect_day_condition, effect_day, effect_day_unit, redeem_type, start_sell_date, end_sell_date, datetime.now(), datetime.now(), vendor_id) rs = db.execute(sql, params) db.commit() if rs: cls.clear_cache(rs) p = cls.get_by_remote_id(vendor_id, remote_id) original_quota = p.quota if p: p.name = name p.quota = quota p.total_quota = total_quota p.today_quota = today_quota p.total_amount = total_amount p.total_buy_amount = total_buy_amount p.min_redeem_amount = min_redeem_amount p.max_redeem_amount = max_redeem_amount p.day_redeem_amount = day_redeem_amount p.interest_rate_hike = interest_rate_hike p.description = description p.expire_period = expire_period p.expire_period_unit = expire_period_unit # 当销售周期内产品额度调整时,发出BC通知 if bearychat.configured: quota_txt = format_number(quota, locale='en_US') original_quota_txt = format_number(original_quota, locale='en_US') if quota < p.min_amount: txt = u'最近一笔:**¥{}**,当前额度:**¥{}**'.format( original_quota_txt, quota_txt) attachment = bearychat.attachment(title=None, text=txt, color='#ffa500', images=[]) bearychat.say( u'产品 **{}** **售罄** 啦,正在尝试释放未支付订单,请周知。'.format(name), attachments=[attachment]) if quota > int(original_quota) + int(p.min_amount): txt = u'更新前额度:**¥{}**, 当前额度:**¥{}**'.format( original_quota_txt, quota_txt) attachment = bearychat.attachment(title=None, text=txt, color='#a5ff00', images=[]) bearychat.say( u'产品 **{}** **额度** 增加啦 :clap:,请周知。'.format(name), attachments=[attachment]) return p @classmethod @cache(cache_by_vendor) def get_product_ids_by_vendor_id(cls, vendor_id): sql = 'select id from {.table_name} where vendor_id=%s'.format(cls) params = (vendor_id, ) rs = db.execute(sql, params) return [p[0] for p in rs if rs] @classmethod def get_products_by_vendor_id(cls, vendor_id): return [ cls.get(id_) for id_ in cls.get_product_ids_by_vendor_id(vendor_id) ] @classmethod def get_by_remote_id(cls, vendor_id, remote_id): sql = 'select id from {.table_name} where vendor_id=%s and remote_id=%s'.format( cls) params = ( vendor_id, remote_id, ) rs = db.execute(sql, params) if rs: return cls.get(rs[0][0]) if rs else None def go_on_sale(self): """上架""" if not self.is_taken_down: raise ProductDuplicateSaleModeError() if self.status is self.Status.deprecated: raise ProductDeprecatedError() if self.quota < self.min_amount: raise ProductLowQuotaError() self.is_taken_down = False def go_off_sale(self): """下架""" self.is_taken_down = True @classmethod def get_products_on_sale(cls): vendors = Vendor.get_all() for vendor in vendors: for p in cls.get_products_by_vendor_id(vendor.id_): if p.is_on_sale: yield p @property def vendor(self): return Vendor.get(self.vendor_id) @classmethod def clear_cache(cls, product_id): mc.delete(cls.cache_key.format(product_id=product_id)) p = cls.get(product_id) if p: mc.delete(cls.cache_by_vendor.format(vendor_id=p.vendor_id)) @property def frozen_days(self): if PeriodUnit(self.expire_period_unit) is PeriodUnit.day: return self.expire_period if PeriodUnit(self.expire_period_unit) is PeriodUnit.month: return self.expire_period * 30 @property def profit_period(self): from core.models.hoard.common import ProfitPeriod value = ProfitPeriod(self.frozen_days, 'day') return {'min': value, 'max': value}
class RedeemCode(PropsMixin): """兑换码为8位随机码或者定制码, 用来换取对应奖品 兑换码有两种,一种是8位随机码,由数字和字母随机生成,另外一种为定制码(可能包含汉字), 用户可根据兑换码在规定时间内,根据活动规则,领取兑换码内包含的奖品(奖品因活动不同而不同), 使用兑换码时不区分字母的大小写 :param code: 兑换码 :param activity_id: 兑换码所属活动的id :param max_usage_limit_per_code: 每个兑换码允许使用的最大次数 """ table_name = 'redeem_code' cache_key = 'redeem_code:{id_}' cache_ids_by_activity_id_key = 'redeem_code:activity_id:{activity_id}' #: 兑换码用途的详细描述 description = PropsItem('description', '') class Status(Enum): """兑换码的使用状态 兑换码的状态分为有效和无效,有效的兑换码才能兑换礼包 """ #: 有效 validated = 'V' #: 无效 invalidated = 'I' class Source(Enum): """兑换码生成方式 兑换码可由后台管理员根据需求生成或者由系统调用相应方法生成 """ #: 系统 robot = 'R' #: 管理员 manager = 'M' class Kind(Enum): """兑换码兑换策略 兑换码进行分发礼包的策略,根据不同的策略完成分发 """ # 正常 normal_package = 1 # 特殊 special_package = 2 def __init__(self, id_, code, source, kind, status, activity_id, max_usage_limit_per_code, creation_time, effective_time, expire_time): self.id_ = str(id_) self.code = code self._source = source self._kind = kind self._status = status self.activity_id = str(activity_id) self.max_usage_limit_per_code = max_usage_limit_per_code self.creation_time = creation_time self.effective_time = effective_time self.expire_time = expire_time def get_db(self): return 'redeem_code' def get_uuid(self): return 'redeem_code:{id_}'.format(id_=self.id_) @cached_property def kind(self): return self.Kind(self._kind) @cached_property def activity(self): return RedeemCodeActivity.get(self.activity_id) @property def is_effective(self): return self.effective_time <= datetime.now() @property def is_expired(self): return self.expire_time < datetime.now() @property def status(self): return self.Status(self._status) @status.setter def status(self, new_status): assert isinstance(new_status, self.Status) self._status = new_status.value @cached_property def source(self): return self.Source(self._source) @classmethod def create(cls, activity_id, kind, description, max_usage_limit_per_code, customized_code, effective_time, expire_time, _commit=True): assert isinstance(effective_time, date) assert isinstance(expire_time, date) assert max_usage_limit_per_code >= 1 if customized_code: code = (customized_code.encode('utf-8') if is_include_chinese(customized_code) else customized_code) else: code = ''.join(random.sample(REDEEM_CODE_ALPHABET, 8)) sql = ('insert into {.table_name} (code, activity_id, source,' ' max_usage_limit_per_code, kind, status, creation_time,' ' effective_time, expire_time)' ' values (%s, %s, %s, %s, %s, %s, %s, %s, %s)').format(cls) params = (code, activity_id, cls.Source.manager.value, max_usage_limit_per_code, kind, cls.Status.validated.value, datetime.now(), effective_time, expire_time) try: id_ = db.execute(sql, params) except MySQLdb.IntegrityError: raise RedeemCodeExistedError() if _commit: db.commit() #: 清除缓存 cls.clear_cache(id_) cls.clear_cache_ids_by_activity_id(activity_id) instance = cls.get(id_) instance.description = unicode(description) return instance @classmethod @cache(cache_key) def get(cls, id_): sql = ( 'select id, code, source, kind, status, activity_id, max_usage_limit_per_code,' ' creation_time, effective_time, expire_time' ' from {.table_name} where id=%s').format(cls) params = (id_, ) rs = db.execute(sql, params) if rs: return cls(*rs[0]) @classmethod def get_id_by_code(cls, code): sql = 'select id from {.table_name} where code=%s'.format(cls) params = (code, ) rs = db.execute(sql, params) if rs: return rs[0][0] @classmethod def get_by_code(cls, code): id_ = cls.get_id_by_code(code) return cls.get(id_) @classmethod @cache(cache_ids_by_activity_id_key) def get_ids_by_activity_id(cls, activity_id): sql = 'select id from {.table_name} where activity_id=%s'.format(cls) params = (activity_id, ) rs = db.execute(sql, params) if rs: return [r[0] for r in rs] @classmethod def get_by_activity_id(cls, activity_id): ids = cls.get_ids_by_activity_id(activity_id) return cls.get_multi_by_ids(ids) @classmethod def get_multi_by_ids(cls, ids): return [cls.get(str(id_)) for id_ in ids] def invalidate(self): sql = 'update {.table_name} set status = %s where id=%s'.format(self) params = (self.Status.invalidated.value, self.id_) db.execute(sql, params) db.commit() #: 清除缓存并更新兑换码可用状态 self.clear_cache(self.id_) self.clear_cache_ids_by_activity_id(self.activity_id) self._clear_cached_properties() self.status = self.Status.invalidated @classmethod def create_multi_codes(cls, activity_id, kind, description, max_usage_limit_per_code, redeem_code_count, effective_time, expire_time): customized_code = None try: for _ in xrange(int(redeem_code_count)): cls.create(activity_id, kind, description, max_usage_limit_per_code, customized_code, effective_time, expire_time, _commit=False) except: db.rollback() raise else: db.commit() def _redeem_normal_package(self, user, redeem_code_usage): distribute_welfare_gift(user, self.activity.reward_welfare_package_kind, redeem_code_usage) kind_to_strategy = {Kind.normal_package: _redeem_normal_package} def _apply_strategy(self, user, redeem_code_usage): strategy = self.kind_to_strategy[self.kind] return strategy(self, user, redeem_code_usage) def redeem(self, user): self.check_for_available(user) redeem_code_usage = RedeemCodeUsage.add(self, user) try: self._apply_strategy(user, redeem_code_usage) except (RedeemCodeIneffectiveError, RedeemCodeExpiredError, RedemptionBeyondLimitPerUserError): redeem_code_usage.delete_by_id(redeem_code_usage.id_) raise def check_for_available(self, user): if not self.is_effective: raise RedeemCodeIneffectiveError() if self.is_expired: raise RedeemCodeExpiredError() @classmethod def clear_cache(cls, id_): mc.delete(cls.cache_key.format(id_=id_)) @classmethod def clear_cache_ids_by_activity_id(cls, activity_id): mc.delete( cls.cache_ids_by_activity_id_key.format(activity_id=activity_id)) def _clear_cached_properties(self): self.__dict__.pop('source', None) self.__dict__.pop('kind', None) self.__dict__.pop('activity', None)
class Mail(PropsMixin): table_name = 'email' def __init__(self, id_, sender, receiver, kind_id, creation_time): self.id_ = str(id_) self.sender = sender self.receiver = receiver self.kind_id = kind_id self.creation_time = creation_time mail_args = PropsItem('mail_args', '') sender_name = PropsItem('sender_name', '') def __repr__(self): return '<Mail id=%s>' % (self.id_) def get_uuid(self): return 'email:%s' % (self.id_) def get_db(self): return 'email' @cached_property def kind(self): return MailKind.get(self.kind_id) @cached_property def mail_body(self): return render_template(self.kind.template, **self.mail_args) @classmethod def _create(cls, sender, receiver, mail_kind, sender_name, commit_=True, **mail_args): if validate_email(receiver) != errors.err_ok: raise ValueError('email format is not valid!') sql = ('insert into {.table_name}' ' (sender, receiver, kind_id, creation_time)' ' values (%s, %s, %s, %s)').format(cls) params = (sender, receiver, mail_kind.id_, datetime.now()) id_ = db.execute(sql, params) if commit_: db.commit() instance = cls.get(id_) instance.update_props_items({ 'mail_args': mail_args, 'sender_name': sender_name, }) return instance @classmethod def create(cls, receivers, mail_kind, sender='', sender_name='', callback=None, **mail_args): assert isinstance(mail_kind, MailKind) if isinstance(receivers, basestring): return cls._create(sender, receivers, mail_kind, sender_name, **mail_args) elif isinstance(receivers, list): multi_mails = [] try: for receiver in receivers: multi_mails.append(cls._create( sender, receiver, mail_kind, sender_name, commit_=False, **mail_args)) except: db.rollback() raise else: db.commit() return multi_mails else: raise TypeError('receivers need to be string or list!') def send(self): if not (current_app and current_app.debug): email_sender.produce(self.id_) @classmethod def send_multi(cls, mails): for mail in mails: mail.send() @classmethod def get(cls, id_): sql = ('select id, sender, receiver, kind_id, creation_time' ' from {.table_name} where id=%s').format(cls) params = (id_,) rs = db.execute(sql, params) if rs: return cls(*rs[0]) @classmethod def gets(cls, ids): return [cls.get(id) for id in ids] def delete(self): sql = 'delete from {.table_name} where id=%s'.format(self) params = (self.id_,) db.execute(sql, params) db.commit() self.clean_props_item()
class HoardOrder(PropsMixin): """The order entity created by users.""" provider = yirendai table_name = 'hoard_order' cache_key = 'hoard:order:{id_}:v1' fin_order_cache_key = 'hoard:fin:order:{fin_order_id}' orders_by_user_cache_key = 'hoard:orders:{user_id}:2' cache_key_for_total_orders = 'hoard:orders:total:{user_id}:2' stashed_order_id = PropsItem('stashed_order_id') def get_uuid(self): return 'order:{.id_}'.format(self) def get_db(self): return 'hoard' def __init__(self, id_, service_id, user_id, creation_time, fin_order_id, order_amount, order_id, bankcard_id, status): self.id_ = str(id_) self.service_id = str(service_id) self.user_id = str(user_id) self.creation_time = creation_time self.fin_order_id = str(fin_order_id) if fin_order_id else None self.order_amount = order_amount self.order_id = str(order_id) if order_id else None self.bankcard_id = str(bankcard_id) if bankcard_id else None self.status = OrderStatus(status) def __str__(self): return '<HoardOrder %s>' % self.id_ def is_owner(self, user): """Checks the specific user is order owner or not.""" return user and user.id == self.user_id @cached_property def user(self): return Account.get(self.user_id) @cached_property def profit_period(self): return self.service.profit_period['min'] # XXX: For API @cached_property def annual_rate(self): return self.service.profit_annual_rate['min'] def bind_bankcard(self, bankcard): """Binds a bank card.""" if not bankcard or bankcard.user_id != self.user_id: raise ValueError('invalid bankcard %r' % bankcard) sql = ('update {.table_name} set bankcard_id = %s ' 'where id = %s').format(self) params = (bankcard.id_, self.id_) self._commit_and_refresh(sql, params) def restore_bankcard(self, force=False): if not self.bankcard_id: raise ValueError('missing bankcard_id') try: return BankCard.restore(self.bankcard_id, self.user_id) except BankCardChanged as e: if force: new_bankcard_id = e.args[0] self.migrate_bankcard(self.bankcard_id, new_bankcard_id) self.bankcard_id = new_bankcard_id try: del self.bankcard except AttributeError: pass return self.bankcard else: raise @classmethod def migrate_bankcard(cls, old_bankcard_id, new_bankcard_id): order_ids = cls.get_id_list_by_bankcard_id(old_bankcard_id) sql = ('update {.table_name} set bankcard_id = %s ' 'where id = %s').format(cls) for order_id in order_ids: db.execute(sql, (new_bankcard_id, order_id)) db.commit() for order_id in order_ids: order = cls.get(order_id) order.clear_cache() rsyslog.send('%s to %s\t%r' % (old_bankcard_id, new_bankcard_id, order_ids), tag='hoard_migrate_bankcard') def track_for_payment(self): if self.status is OrderStatus.unpaid: mq_payment_tracking.produce(self.id_) def mark_as_paid(self, order_id): """Marks this order as paid. :param order_id: the ``orderNo`` from Yixin API. """ if self.is_success: raise DuplicatePaymentError('order has been paid', self.id_) sql = ('update {.table_name} set order_id = %s, status = %s' 'where id = %s').format(self) params = (order_id, OrderStatus.paid.value, self.id_) self._commit_and_refresh(sql, params) # trigger event yrd_order_paid.send(self) # request to confirm mq_confirming.produce(self.id_) def mark_as_confirmed(self): if self.status is OrderStatus.unpaid: raise ValueError('order has not been paid', self.id_) if self.status is OrderStatus.confirmed: return sql = ('update {.table_name} set status = %s' 'where id = %s').format(self) params = (OrderStatus.confirmed.value, self.id_) self._commit_and_refresh(sql, params) # trigger event yrd_order_confirmed.send(self) # request to register withdrawing mq_withdrawing.produce(self.id_) def mark_as_exited(self): if self.status is OrderStatus.exited: return sql = ('update hoard_order set status = %s ' 'where id = %s').format(self) params = (OrderStatus.exited.value, self.id_) self._commit_and_refresh(sql, params) # remove event push code for batch updating # trigger event # yrd_order_exited.send(self) # request exit notification sms when order exit is caught in time # expect_exit_date = ( # self.creation_time.date() + relativedelta(days=self.profit_period.value)) # if (datetime.date.today() - expect_exit_date).days < 6: # mq_sms_sender.produce(self.id_) def track_for_exited(self): if self.status is OrderStatus.exited: return # request to check status mq_exiting_checker.produce(self.id_) def mark_as_failure(self): if self.status is OrderStatus.confirmed or OrderStatus.exited: raise DuplicatePaymentError('order has been confirmed', self.id_) sql = ('update {.table_name} set status = %s' 'where id = %s').format(self) params = (OrderStatus.failure.value, self.id_) self._commit_and_refresh(sql, params) # trigger event yrd_order_failure.send(self) @classmethod def get_orders_by_period(cls, date_from, date_to, closure): sql = ('select id from hoard_order where status=%s ' 'and date(creation_time) between %s and %s').format(cls) params = (OrderStatus.confirmed.value, date_from, date_to) rs = db.execute(sql, params) orders = (cls.get(r[0]) for r in rs) return [ order for order in orders if int(order.service.frozen_time) == closure ] def _commit_and_refresh(self, sql, params): # 执行SQL并提交 db.execute(sql, params) db.commit() # 清除数据库缓存 self.clear_cache() # 刷新实例内属性值 new_state = vars(self.get(self.id_)) vars(self).update(new_state) # 清除实例内缓存 self.__dict__.pop('bankcard', None) @property def is_success(self): """``True`` if this order has been paid.""" return self.status in [ OrderStatus.confirmed, OrderStatus.paid, OrderStatus.exited ] @property def is_failure(self): return self.status == OrderStatus.failure @cached_property def service(self): return YixinService.get(self.service_id) @cached_property def due_date(self): order_info, _ = self._fetch_order_info() return order_info['frozenDatetime'] @classmethod def check_before_adding(cls, service_id, user_id, order_amount): yixin_service = YixinService.get(service_id) yixin_account = YixinAccount.get_by_local(user_id) user = Account.get(user_id) # checks the related entities if not yixin_service: raise NotFoundError(service_id, YixinService) if not user: raise NotFoundError(user_id, Account) if not yixin_account: raise UnboundAccountError(user_id) # checks the identity if not has_real_identity(user): raise InvalidIdentityError # checks available if yixin_service.sell_out: raise SellOutError(yixin_service.uuid) if yixin_service.take_down: raise TakeDownError(yixin_service.uuid) # checks the amount type if not isinstance(order_amount, decimal.Decimal): raise TypeError('order_amount must be decimal') # checks the amount range amount_range = (yixin_service.invest_min_amount, yixin_service.invest_max_amount) if (order_amount.is_nan() or order_amount < 0 or order_amount < yixin_service.invest_min_amount or order_amount > yixin_service.invest_max_amount): raise OutOfRangeError(order_amount, amount_range) @classmethod def add(cls, service_id, user_id, order_amount, fin_order_id, creation_time=None): """Creates a unpaid order. :param service_id: the UUID of chosen P2P service. :param user_id: the user id of order creator. :param order_amount: the payment amount for this order. :param fin_order_id: the UUID of remote order. :returns: the created order. """ cls.check_before_adding(service_id, user_id, order_amount) creation_time = creation_time or datetime.datetime.now() sql = ('insert into {.table_name} (service_id, user_id, order_amount,' ' fin_order_id, creation_time, status) ' 'values (%s, %s, %s, %s, %s, %s)').format(cls) params = (service_id, user_id, order_amount, fin_order_id, creation_time, OrderStatus.unpaid.value) id_ = db.execute(sql, params) db.commit() order = cls.get(id_) order.clear_cache() return order @classmethod @cache(cache_key) def get(cls, id_): sql = ('select id, service_id, user_id, creation_time, fin_order_id,' ' order_amount, order_id, bankcard_id, status ' 'from {.table_name} where id = %s').format(cls) params = (id_, ) rs = db.execute(sql, params) if rs: return cls(*rs[0]) @classmethod def get_by_order_no(cls, order_id): sql = ('select id from {.table_name} where order_id = %s').format(cls) params = (order_id, ) rs = db.execute(sql, params) if rs: return cls.get(rs[0][0]) @classmethod @cache(fin_order_cache_key) def get_id_by_fin_order_id(cls, fin_order_id): sql = ('select id from {.table_name} ' 'where fin_order_id = %s').format(cls) params = (fin_order_id, ) rs = db.execute(sql, params) if rs: return rs[0][0] @classmethod def get_by_fin_order_id(cls, fin_order_id): id = cls.get_id_by_fin_order_id(fin_order_id) if id: return cls.get(id) @classmethod @cache(orders_by_user_cache_key) def get_id_list_by_user_id(cls, user_id): sql = ('select id from {.table_name} where user_id = %s ' 'order by creation_time desc').format(cls) params = (user_id, ) rs = db.execute(sql, params) if rs: return [r[0] for r in rs] @classmethod def gets_by_user_id(cls, user_id): """get all paid orders by user""" id_list = cls.get_id_list_by_user_id(user_id) orders = [cls.get(id_) for id_ in id_list or []] return [o for o in orders if o.is_success] @classmethod def gets_by_user_in_period(cls, user_id, start, end): """get user successed orders in period""" id_list = cls.get_id_list_by_user_id(user_id) if id_list: orders = [cls.get(id_) for id_ in id_list] successful_orders = [ order for order in orders if order.status is OrderStatus.confirmed or OrderStatus.exited ] return [ o for o in successful_orders if start <= o.creation_time < end ] @classmethod def gets_by_date(cls, date): sql = ('select id from {.table_name} ' 'where DATE(creation_time) = %s ').format(cls) params = (date, ) rs = db.execute(sql, params) if rs: return [cls.get(r[0]) for r in rs] @classmethod def get_id_list_by_bankcard_id(cls, bankcard_id): sql = 'select id from {.table_name} where bankcard_id = %s'.format(cls) params = (bankcard_id, ) rs = db.execute(sql, params) return [r[0] for r in rs] @classmethod def get_multi_by_bankcard(cls, bankcard_id): id_list = cls.get_id_list_by_bankcard_id(bankcard_id) orders = [cls.get(id_) for id_ in id_list or []] return [o for o in orders if o.is_success] def clear_cache(self): mc.delete(self.cache_key.format(id_=self.id_)) mc.delete(self.cache_key_for_total_orders.format(user_id=self.user_id)) mc.delete(self.orders_by_user_cache_key.format(user_id=self.user_id)) mc.delete( self.fin_order_cache_key.format(fin_order_id=self.fin_order_id)) @classmethod @cache(cache_key_for_total_orders) def get_total_orders(cls, user_id): sql = ('select count(id) from {.table_name} where user_id=%s ' 'and (status=%s or status=%s or status=%s)').format(cls) params = (user_id, OrderStatus.paid.value, OrderStatus.confirmed.value, OrderStatus.exited.value) rs = db.execute(sql, params) return rs[0][0] @cached_property def bankcard(self): if not self.bankcard_id: return return BankCard.get(self.bankcard_id) @classmethod def gets_by_month(cls, date): # start, end should be 'yyyy-mm' year, month = date.split('-') sql = ('select id from {.table_name} where year(creation_time) = %s ' 'and month(creation_time) = %s').format(cls) params = (year, month) rs = db.execute(sql, params) if rs: return [cls.get(r[0]) for r in rs] def register_for_withdrawing(self, client, token): return client.query.set_finance_exit_bank_info( token, self.fin_order_id, self.bankcard.bank.yxlib_id, self.bankcard.city_id, self.bankcard.province_id, self.bankcard.card_number, self.bankcard.local_bank_name) @cached_property def expected_profit(self): """预期的到期总收益.""" order_info, order_status = self._fetch_order_info() if order_status == u'已转出': return decimal.Decimal(order_info['incomeAmount']) monthly_ratio = self.service.expected_income / 100 / 12 result = monthly_ratio * self.order_amount * self.service.frozen_time return round_half_up(result, 2) def fetch_status(self, orders=None): """该订单在宜人贷的状态.""" order_info, order_status = self._fetch_order_info(orders) if order_info and order_status: return order_status return getattr(self, '_order_status', None) # for testing def fetch_daily_profit(self, orders=None): """该订单平均到每日的收益.""" amount = decimal.Decimal(self.order_amount) order_info, order_status = self._fetch_order_info(orders) if not order_info or not order_status: return decimal.Decimal(0) if order_status not in [u'攒钱中', u'转出中']: return decimal.Decimal(0) return (amount * decimal.Decimal(order_info['expectedIncome']) / 100 / 365) def fetch_profit_until(self, date, orders=None): """该订单到某日为止的累计收益.""" order_info, order_status = self._fetch_order_info(orders) # 已转出则返回已结算的收益 if order_status == u'已转出': return decimal.Decimal(order_info['incomeAmount']) # 未转出则使用预期每日收益按日累加 invest_date = arrow.get(order_info['investDate']).date() days = (date - invest_date).days if days <= 0: return decimal.Decimal(0) return days * self.fetch_daily_profit(orders) def _fetch_orders(self): from .profile import HoardProfile profile = HoardProfile.add(self.user_id) return profile.orders() def _fetch_order_info(self, orders=None): orders = orders or self._fetch_orders() for order, order_info, order_status in orders: if order.id_ == self.id_: return order_info, order_status return None, None
class XMLoansDigest(PropsMixin): table_name = 'hoard_xm_loans_digest' cache_key = 'hoard:xm:loans_digest:id:{id_}:v1' cache_key_by_asset_id = 'hoard:xm:loans_digest:asset_id:{asset_id}:v1' # 出借咨询与服务协议 contract_no = PropsItem('contract_no', '', unicode_type) # 资金出借/回收方式 reinvest = PropsItem('reinvest', '', unicode_type) # 实际出借金额 principle_amount = PropsItem('principle_amount', 0, Decimal) # 协议与债权占比(持有比例) receipt_hold_scale = PropsItem('receipt_hold_scale', 0, Decimal) # 初始出借日期 invest_start_date = PropsItem('invest_start_date', '', date_type) def __init__(self, id_, asset_id, creation_time): self.id_ = id_ self.asset_id = asset_id self.creation_time = creation_time def get_db(self): return 'hoard' def get_uuid(self): return 'xm:loans_digest:{0}'.format(self.id_) @property def loans(self): return XMLoan.get_multi_by_loans_digest_id(self.id_) @classmethod def create(cls, asset, loans_digest_info): assert isinstance(asset, XMAsset) assert isinstance(loans_digest_info, CreditorRights) sql = 'insert into {.table_name} (asset_id, creation_time) values (%s, %s)'.format( cls) params = (asset.id_, datetime.datetime.now()) id_ = db.execute(sql, params) db.commit() instance = cls.get(id_) instance.update_props_items({ 'contract_no': loans_digest_info['loan_receipt_no'], 'reinvest': loans_digest_info['invest_lending_type'], 'principle_amount': str(loans_digest_info['loan_receipt_amt']), 'receipt_hold_scale': loans_digest_info['receipt_hold_scale'], 'invest_start_date': loans_digest_info.start_date.isoformat() }) # 创建借贷人记录 loans = loans_digest_info try: XMLoan.create(instance, loans, _commit=False) except: db.rollback() raise else: db.commit() cls.clear_cache(id_) cls.clear_cache_by_asset_id(asset.id_) return instance @classmethod def update(cls, loans_digest, loans_digest_info): assert isinstance(loans_digest_info, CreditorRights) assert isinstance(loans_digest, XMLoansDigest) loans = loans_digest_info.loans if len(loans) > len(loans_digest.loans): loan_receipt_no_list = [ loan.loan_receipt_no for loan in loans_digest.loans ] try: for loan in loans: if loan.loan_receipt_no not in loan_receipt_no_list: XMLoan.create(loans_digest, loan, _commit=False) except: db.rollback() raise else: db.commit() return loans_digest @classmethod def create_or_update(cls, asset, loans_digest_info): loans_digest = cls.get_by_asset_id(asset.id_) if loans_digest: return cls.update(loans_digest, loans_digest_info) else: return cls.create(asset, loans_digest_info) @classmethod @cache(cache_key) def get(cls, id_): sql = 'select id, asset_id, creation_time from {.table_name} where id=%s'.format( cls) params = (id_, ) rs = db.execute(sql, params) if rs: return cls(*rs[0]) @classmethod @cache(cache_key_by_asset_id) def get_id_by_asset_id(cls, asset_id): sql = 'select id from {.table_name} where asset_id=%s'.format(cls) params = (asset_id, ) rs = db.execute(sql, params) if rs: return str(rs[0][0]) @classmethod def get_by_asset_id(cls, asset_id): return cls.get(cls.get_id_by_asset_id(asset_id)) @classmethod def clear_cache(cls, id_): mc.delete(cls.cache_key.format(id_=id_)) @classmethod def clear_cache_by_asset_id(cls, asset_id): mc.delete(cls.cache_key_by_asset_id.format(asset_id=asset_id))
class Package(PropsMixin): table_name = 'insurance_package' insurance_ability = PropsItem('insurance_ability', '') addition_ability = PropsItem('addition_ability', '') name = PropsItem('name', u'基础套餐') title = PropsItem('title') sub_title = PropsItem('sub_title') quota = PropsItem('quota') quota_b = PropsItem('quota_b', 'quota_b commedy') radar = PropsItem('radar') def __init__(self, package_id, insurance_id, rec_rank_in_package, package_rec_rank): self.id = package_id self.insurance_id = insurance_id self.rec_rank_in_package = rec_rank_in_package self.package_rec_rank = package_rec_rank def get_db(self): return 'package_insurance' def get_uuid(self): return 'package_insurance:ability:%s' % self.id @classmethod def add(cls, package_id, pkg_name, insurance_id, insurance_name, status, rec_rank_in_package, package_rec_rank): sql = ('insert into {.table_name} ' '(package_id, pkg_name, insurance_id, insurance_name, status, ' 'rec_rank_in_package, package_rec_rank, create_time, update_time) ' 'values ( %s, %s, %s, %s, %s, %s, %s ,%s, %s)').format(cls) params = (package_id, pkg_name, insurance_id, insurance_name, status, rec_rank_in_package, package_rec_rank, datetime.now(), datetime.now()) db.execute(sql, params) db.commit() cls.clear_cache(package_id) cls.get(package_id) @classmethod @cache(PACKAGE_CACHE_KEY) def get(cls, package_id): sql = ('select package_id, insurance_id,rec_rank_in_package, ' 'package_rec_rank from {.table_name} where package_id = %s').format(cls) param = package_id rs = db.execute(sql, param) return [cls(*item) for item in rs if item is not None] @classmethod def clear_cache(cls, package_id): mc.delete(PACKAGE_CACHE_KEY.format(package_id=package_id)) @classmethod def get_by_insurance_id(cls, insurance_id): sql = ('select package_id, insurance_id from {.table_name} ' 'where insurance_id = %s').format(cls) param = insurance_id rs = db.execute(sql, param) pkg_ids = [str(pkg_id) for pkg_id in rs] return [cls.get(pkg_id) for pkg_id in pkg_ids] # get package by package_id and insurance_id @classmethod def get_by_pkg_id_insurance_id(cls, pkg_id, insurance_id): return cls(pkg_id, insurance_id) def cat_ability(self, ageobj): if (ageobj.birth[0] == 0 and ageobj.birth[1] < 60 and self.id in [PACKAGE_UPGRADE_1, PACKAGE_UPGRADE_2]): return ''.join([self.insurance_ability, self.addition_ability]) return self.insurance_ability
class Article(PropsMixin): # need to add # kind is uuid # type is article type _const = {} kind = 'article' type = 'article' # admin record _add_admin = PropsItem('add_admin', '') _publish_admin = PropsItem('publish_admin', '') _delete_admin = PropsItem('delete_admin', '') def __init__(self, id, category, create_time, update_time, publish_time, status): self.id = str(id) self.category = str(category) self.create_time = create_time self.update_time = update_time self.publish_time = publish_time self.status = int(status) def __repr__(self): return '<Article id=%s, type=%s, status=%s>' % (self.id, self.type, self.status) def get_uuid(self): return '%s:content:%s' % (self.kind, self.id) def get_db(self): # All the articles save in one CouchDB return 'article' @classmethod @cache(ARTICLE_CACHE_KEY % '{doc_id}') def get(cls, doc_id): from .viewpoint import ViewPoint from .question import Question from .fundweekly import FundWeekly doc_id = str(doc_id) rs = db.execute( 'select id, type, category, create_time, update_time, ' 'publish_time, ' 'status from article where id=%s', (doc_id, )) if rs: (id, type, category, create_time, update_time, publish_time, status) = rs[0] if type == VIEWPOINT.TYPE: return ViewPoint(id, category, create_time, update_time, publish_time, status) elif type == QUESTION.TYPE: return Question(id, category, create_time, update_time, publish_time, status) elif type == FUNDWEEKLY.TYPE: return FundWeekly(id, category, create_time, update_time, publish_time, status) @classmethod def gets(cls, doc_ids): return [cls.get(id) for id in doc_ids] @classmethod @pcache(ARTICLE_CATE_CACHE_KEY % ('{type}', '{category}', '{status}'), count=20) def _get_article_ids_by_type_and_category(cls, type, category, status=STATUS.PUBLISHED, start=0, limit=20): rs = db.execute( 'select id ' 'from article ' 'where type=%s and category=%s and status=%s ' 'order by publish_time desc limit %s, %s', (type, category, status, start, limit)) return [str(id) for (id, ) in rs] @classmethod def get_articles_by_type_and_category(cls, type, category, status=STATUS.PUBLISHED, start=0, limit=20): ids = cls._get_article_ids_by_type_and_category( type, category, status, start, limit) return cls.gets(ids) @classmethod def get_articles_by_category(cls, category, status=STATUS.PUBLISHED, start=0, limit=20): return cls.get_articles_by_type_and_category(cls.type, category, status, start, limit) @classmethod @pcache(ARTICLE_ALL_CACHE_KEY % ('{type}', '{status}')) def _get_all_ids(cls, type, status=STATUS.PUBLISHED, start=0, limit=20): rs = db.execute( 'select id from article ' 'where status=%s and type=%s ' 'order by publish_time desc limit %s,%s ', (status, type, start, limit)) ids = [str(id) for (id, ) in rs] return ids @classmethod def get_all(cls, type=None, status=STATUS.PUBLISHED, start=0, limit=20): if type is None: type = cls.type ids = cls._get_all_ids(type=type, status=status, start=start, limit=limit) return cls.gets(ids) @classmethod @cache(ARTICLE_COUNT_CACHE_KEY % ('{type}', '{status}')) def get_count(cls, type=None, status=STATUS.PUBLISHED): if type is None: type = cls.type return db.execute( 'select count(id) from article ' 'where status=%s and type=%s', (status, type))[0][0] @classmethod def get_count_by_category(cls, category): ids = cls._get_article_ids_by_type_and_category(cls.kind, category) return len(ids) @classmethod def add(cls, type=None, category=0, create_time=None, admin_id=None): type = type or cls.type create_time = create_time or datetime.now() try: id = db.execute( 'insert into article ' '(type, category, create_time) ' 'values (%s, %s, %s)', (type, category, create_time)) if id: db.commit() article = cls.get(id) article.clear_cache() article._add_admin = admin_id return article else: db.rollback() except IntegrityError: db.rollback() warn('insert article failed') def is_deleted(self): return self.status == STATUS.DELETED def is_published(self): return self.status == STATUS.PUBLISHED @property def category_name(self): _cate_type = self._const.CATEGORY if _cate_type: return _cate_type.get(self.category) def _upadte_status(self, status): db.execute('update article set status=%s where id=%s', (status, self.id)) db.commit() self.clear_cache() def delete(self): self._upadte_status(STATUS.DELETED) def publish(self, publish_time=None): publish_time = publish_time or datetime.now() db.execute('update article set publish_time=%s where id=%s', (publish_time, self.id)) self._upadte_status(STATUS.PUBLISHED) self.clear_cache() def hide(self): self._upadte_status(STATUS.NONE) def clear_cache(self): mc.delete(ARTICLE_CACHE_KEY % (self.id)) for name, status in STATUS.items(): mc.delete(ARTICLE_ALL_CACHE_KEY % (self.type, status)) mc.delete(ARTICLE_ALL_TYPE_CACHE_KEY % (status)) mc.delete(ARTICLE_COUNT_CACHE_KEY % (self.type, status)) mc.delete(ARTICLE_CATE_CACHE_KEY % (self.type, self.category, status))
class ShortMessage(PropsMixin): """短消息""" # default tag tag = u'好规划' # storage of sms sending info receiver_mobile = PropsItem('receiver_mobile', '') sms_kind_id = PropsItem('sms_kind_id', '') sms_args = PropsItem('sms_args', {}) is_sent = PropsItem('is_sent', False) def __init__(self, uuid): self.uuid = UUID(uuid) def get_db(self): return 'sms' def get_uuid(self): return 'item:{uuid}'.format(uuid=self.uuid.hex) @property def kind(self): return ShortMessageKind.get(self.sms_kind_id) @classmethod def create(cls, mobile, sms_kind, user_id=None, **sms_args): """为已注册用户发送短信""" assert isinstance(sms_kind, ShortMessageKind) if validate_phone(mobile) != errors.err_ok: raise ValueError(u'invalid mobile %s' % mobile) if sms_kind.need_verify: if not (user_id and Account.get(user_id)): raise ValueError(u'unable to verify user %s' % user_id) v = Verify.add(user_id, sms_kind.verify_type, sms_kind.verify_delta) sms_args.update(verify_code=v.code) sms = cls(uuid4().hex) # simply check formatting sms_kind.content.format(**sms_args) sms.update_props_items({ u'receiver_mobile': mobile, u'sms_kind_id': sms_kind.id_, u'sms_args': sms_args }) return sms @classmethod def get(cls, uuid): return cls(uuid) if uuid else None def send_async(self): """将短信发送加入队列进行异步发送""" if not self.is_sent: mq_sms_sender.produce(self.uuid.hex) def send(self, provider=None): """即时发送短信""" if self.is_sent: return True for k, v in self.sms_args.items(): self.sms_args[k] = v.decode('utf-8') text = self.kind.content.format(**self.sms_args) # 如果不强制使用其他服务商,则经由短信类型偏好服务商发送 channel = provider or self.kind.prefer_provider result = SMSClient.send(self.receiver_mobile, text, self.tag, channel) if result: rsyslog.send(u'\t'.join([ self.uuid.hex, self.receiver_mobile, str(self.sms_kind_id), text ]), tag=u'sms_history') self.is_sent = True return result
class Notification(EntityModel, PropsMixin, PushSupport): """消息通知""" table_name = 'notification' cache_key = 'notification:{id_}:v1' cache_key_by_user_id = 'notification:user:{user_id}:v1' cache_key_by_user_unread = 'notification:user:{user_id}:unread:v1' cache_key_by_user_and_kind = 'notification:user:{user_id}:kind:{kind_id}:v1' #: the properties to render(id of linked entity is recommended) properties = PropsItem('properties', {}) def __init__(self, id_, user_id, kind_id, is_read, creation_time, read_time): self.id_ = str(id_) self.user_id = str(user_id) self.kind_id = str(kind_id) self.is_read = bool(is_read) self.creation_time = creation_time self.read_time = read_time def get_uuid(self): return 'item:{id_}'.format(id_=self.id_) def get_db(self): return 'notification' @property def status(self): return self.Status(self._status) @cached_property def user(self): return Account.get(self.user_id) @cached_property def kind(self): return NotificationKind.get(self.kind_id) @cached_property def template(self): return render_template( self.kind.common_template_location, palette=self, link=self.kind.web_target_link) @cached_property def title(self): # 暂要求单播通知不使用消息类型中的默认标题 return render_template_def( self.kind.common_template_location, 'notification_title', palette=self).strip() @cached_property def timestamp(self): return render_template_def( self.kind.common_template_location, 'notification_timestamp', palette=self).strip() @cached_property def content(self): # 暂要求单播通知不使用消息类型中的默认内容 return render_template_def( self.kind.common_template_location, 'notification_content', palette=self).strip() def mark_as_read(self): """标记消息为已读""" if self.is_read: return read_time = datetime.now() sql = 'update {.table_name} set is_read=%s, read_time=%s where id=%s'.format(self) params = (True, read_time, self.id_) db.execute(sql, params) db.commit() # 刷新实例属性并清除缓存 self.is_read = True self.read_time = read_time self.clear_cache(self.id_) self.clear_cache_by_user(self.user_id) self.clear_cache_by_user_and_kind(self.user_id, self.kind_id) @property def allow_push(self): return self.kind.allow_push @property def is_unicast_push_only(self): return self.kind.is_unicast_push_only @property def push_platforms(self): """实际推送平台将由具体类型优先定夺,以类型定义为fallback默认值""" from core.models.welfare import Package if self.allow_push: if self.kind is welfare_gift_notification: # 礼包类型单播推送 package = Package.get(self.properties.get('welfare_package_id')) return package.kind.push_platforms return self.kind.push_platforms def make_push_pack(self, audience, platform): """创建单播通知(面向设备)的推送""" from core.models.welfare import Package from core.models.pusher.element import Pack, Notice, SingleDeviceAudience assert isinstance(audience, SingleDeviceAudience) assert isinstance(platform, Platform) if self.allow_push: if self.kind is welfare_gift_notification: # 礼包类型单播推送 package = Package.get(self.properties.get('welfare_package_id')) notice = Notice( package.kind.description or self.content, title=self.title) else: # 其他类型单播推送 notice = Notice(self.content, title=self.title) return Pack( audience, notice, [platform], target_link=self.kind.app_target_link, needs_following_up=True) @classmethod def create(cls, user_id, kind, properties=None): assert isinstance(kind, NotificationKind) assert properties is None or isinstance(properties, dict) # 校验参数 user = Account.get(user_id) if not user: raise ValueError('invalid user id') if kind.is_once_only: id_list = cls.get_id_list_by_user_and_kind(user.id_, kind.id_) if id_list: return cls.get(id_list[0]) sql = ('insert into {.table_name} (user_id, kind_id, is_read, ' 'creation_time) values (%s, %s, %s, %s)').format(cls) params = (user_id, kind.id_, False, datetime.now()) id_ = db.execute(sql, params) db.commit() instance = cls.get(id_) instance.properties = properties or {} # 单播消息则提交并加入推送队列 cls.clear_cache_by_user(user.id_) cls.clear_cache_by_user_and_kind(user.id_, kind.id_) # 由推送控制中心来记录和完成推送 if kind.allow_push: mq_notification_push.produce(str(id_)) return instance @classmethod @cache(cache_key) def get(cls, id_): sql = ('select id, user_id, kind_id, is_read, creation_time, ' 'read_time from {.table_name} where id=%s').format(cls) params = (id_,) rs = db.execute(sql, params) if rs: return cls(*rs[0]) @classmethod @cache(cache_key_by_user_id) def get_id_list_by_user_id(cls, user_id): sql = ('select id from {.table_name} where user_id=%s ' 'order by creation_time desc').format(cls) params = (user_id,) rs = db.execute(sql, params) return [r[0] for r in rs] @classmethod @cache(cache_key_by_user_unread) def get_unread_id_list_by_user_id(cls, user_id): sql = ('select id from {.table_name} where user_id=%s ' 'and is_read=%s order by creation_time desc').format(cls) params = (user_id, False) rs = db.execute(sql, params) return [r[0] for r in rs] @classmethod @cache(cache_key_by_user_and_kind) def get_id_list_by_user_and_kind(cls, user_id, kind_id): sql = ('select id from {.table_name} where user_id=%s ' 'and kind_id=%s order by creation_time desc').format(cls) params = (user_id, kind_id) rs = db.execute(sql, params) return [r[0] for r in rs] @classmethod def get_multi_by_user(cls, user_id): id_list = cls.get_id_list_by_user_id(user_id) return cls.get_multi(id_list) @classmethod def get_multi_unreads_by_user(cls, user_id): id_list = cls.get_unread_id_list_by_user_id(user_id) return cls.get_multi(id_list) @classmethod def get_multi_by_user_and_kind(cls, user_id, kind_id): id_list = cls.get_id_list_by_user_and_kind(user_id, kind_id) return cls.get_multi(id_list) @classmethod def get_multi(cls, id_list): return [cls.get(id_) for id_ in id_list] @classmethod def get_merged_popout_template(cls, notifications): kinds = list(set([n.kind for n in notifications])) if len(kinds) > 1: raise ValueError('template merge of different notifications is not supported') return render_template(kinds[0].popout_template_location, palettes=notifications) @classmethod def clear_cache(cls, id_): mc.delete(cls.cache_key.format(**locals())) @classmethod def clear_cache_by_user(cls, user_id): mc.delete(cls.cache_key_by_user_id.format(**locals())) mc.delete(cls.cache_key_by_user_unread.format(**locals())) @classmethod def clear_cache_by_user_and_kind(cls, user_id, kind_id): mc.delete(cls.cache_key_by_user_and_kind.format(**locals()))
class Asset(PropsMixin): """资产""" table_name = 'hoarder_asset' class Status(Enum): """本地资产状态""" unpaid = 'U' earning = 'E' withdrawing = 'W' redeemed = 'R' cancel = 'C' cache_key = 'hoarder:asset:{asset_id}:v1' user_cache_key = 'hoarder:assets:user:{user_id}:{product_id}:v1' product_cache_key = 'hoarder:assets:all_ids:{product_id}:v1' order_code_cache_key = 'hoarder:asset:order:{order_code}:{product_id}:v1' user_id_cache_key = 'hoarder:assets:user_id:{user_id}:v1' # 实时年化收益率(日日盈类产品每日更新) actual_annual_rate = PropsItem('actual_annual_rate', 0.00, Decimal) # 累计收益 hold_profit = PropsItem('hold_profit', 0.00, Decimal) # 持有资产 hold_amount = PropsItem('hold_amount', 0.00, Decimal) # 未到账资产 uncollected_amount = PropsItem('uncollected_amount', 0.00, Decimal) # 昨日收益 yesterday_profit = PropsItem('yesterday_profit', 0, Decimal) # 剩余免费赎回次数 residual_redemption_times = PropsItem('residual_redemption_times', 0, int) def __init__(self, id_, asset_no, order_code, bankcard_id, bank_account, product_id, user_id, status, remote_status, annual_rate, create_amount, current_amount, fixed_service_fee, service_fee_rate, base_interest, expect_interest, current_interest, interest_start_date, interest_end_date, expect_payback_date, buy_time, creation_time, update_time): self.id_ = id_ self.asset_no = asset_no self.order_code = order_code self.bankcard_id = bankcard_id self.bank_account = bank_account self.product_id = product_id self.user_id = user_id self._status = status self.remote_status = remote_status self.annual_rate = annual_rate self.create_amount = create_amount self.current_amount = current_amount self.fixed_service_fee = fixed_service_fee self.service_fee_rate = service_fee_rate self.base_interest = base_interest self.expect_interest = expect_interest self.current_interest = current_interest self.interest_start_date = interest_start_date self.interest_end_date = interest_end_date self.expect_payback_date = expect_payback_date self.buy_time = buy_time self.creation_time = creation_time self.update_time = update_time def __str__(self): return '<Asset %s>' % self.id_ def get_db(self): return 'hoarder' def get_uuid(self): return 'hoarder:asset:{.id_}'.format(self) def is_owner(self, user): return user and user.id_ == self.user_id @cached_property def product(self): return Product.get(self.product_id) @cached_property def user(self): return Account.get(self.user_id) @property def bankcard(self): if not self.bankcard_id: return return BankCard.get(self.bankcard_id) @property def status(self): return self.Status(self._status) @status.setter def status(self, item): sql = 'update {.table_name} set status=%s where id=%s;'.format(self) params = ( item.value, self.id_, ) db.execute(sql, params) db.commit() self.clear_cache() self._status = item.value @property def display_status(self): return { 'U': u'处理中', 'E': u'攒钱中', 'W': u'转出中', 'R': u'已转出', 'C': u'已取消', }.get(self.status.value, u'未知') @property def frozen_days(self): return (self.interest_end_date - self.interest_start_date).days @property def daily_profit(self): """资产平均到每日的收益.""" # 当资产到期时,每日收益变为0 if self.status is not self.Status.earning: return Decimal(0) # 当超出攒钱封闭期时,每日收益变为0 if (self.interest_start_date.date() <= datetime.date.today() < self.interest_end_date.date()): return self.expect_interest / self.frozen_days return Decimal(0) def update_service_fee(self, fixed_service_fee, service_fee_rate): need_update = False if self.fixed_service_fee != fixed_service_fee: self.fixed_service_fee = fixed_service_fee need_update = True if self.service_fee_rate != service_fee_rate: self.service_fee_rate = service_fee_rate need_update = True if not need_update: return sql = ( 'update {.table_name} set fixed_service_fee=%s, service_fee_rate=%s ' 'where id=%s').format(self) params = ( self.fixed_service_fee, self.service_fee_rate, self.id_, ) db.execute(sql, params) db.commit() self.clear_cache() def fetch_profit_until(self, date): """该资产到某日为止的累计收益.""" # 已转出则返回已结算的收益 if self.status is self.Status.redeemed: return self.current_interest # 未转出则使用预期每日收益按日累加 if date >= self.interest_end_date.date(): return self.expect_interest elif date < self.interest_start_date.date(): return Decimal(0) else: return (date - self.interest_start_date.date()).days * self.daily_profit @classmethod def add(cls, asset_no, order_code, bankcard_id, bank_account, product_id, user_id, status, remote_status, fixed_service_fee, service_fee_rate, annual_rate, create_amount, current_amount, base_interest, expect_interest, current_interest, interest_start_date, interest_end_date, expect_payback_date, buy_time, creation_time=None): sql = ( 'insert into {.table_name}(asset_no, order_code, bankcard_id, bank_account, ' 'product_id, user_id, status, remote_status, fixed_service_fee, service_fee_rate, ' 'annual_rate, create_amount, ' 'current_amount, base_interest, expect_interest, current_interest, ' 'interest_start_date, interest_end_date, expect_payback_date, buy_time, ' 'creation_time) values (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, ' '%s, %s, %s, %s, %s, %s, %s, %s, %s)').format(cls) params = (asset_no, order_code, bankcard_id, bank_account, product_id, user_id, status.value, remote_status, fixed_service_fee, service_fee_rate, annual_rate, create_amount, current_amount, base_interest, expect_interest, current_interest, interest_start_date, interest_end_date, expect_payback_date, buy_time, creation_time or datetime.datetime.now()) id_ = db.execute(sql, params) db.commit() instance = cls.get(id_) instance.clear_cache() return instance def update_bankcard(self, new_card): """更新资产回款卡(当且仅当用户挂失银行卡情况被确认并修改后调用)""" assert isinstance(new_card, BankCard) if new_card.user_id != self.user_id or new_card.status is not BankCard.Status.active: raise InvalidRedeemBankCardError() sql = 'update {.table_name} set bankcard_id=%s where id=%s'.format( self) params = (new_card.id_, self.id_) self._commit_and_refresh(sql, params) @classmethod @cache(cache_key) def get(cls, asset_id): sql = ( 'select id, asset_no, order_code, bankcard_id, bank_account, product_id, ' 'user_id, status, remote_status, annual_rate, create_amount, current_amount, ' 'fixed_service_fee, service_fee_rate, base_interest, expect_interest, ' 'current_interest, interest_start_date, interest_end_date, ' 'expect_payback_date, buy_time, creation_time, update_time from {.table_name} ' 'where id = %s').format(cls) params = (asset_id, ) rs = db.execute(sql, params) return cls(*rs[0]) if rs else None @classmethod def get_by_asset_no_with_product_id(cls, asset_no, product_id): sql = ('select id from {.table_name} where ' 'asset_no=%s, product_id=%s').format(cls) params = ( asset_no, product_id, ) rs = db.execute(sql, params) return cls.get(rs[0][0]) if rs else None @classmethod @cache(order_code_cache_key) def get_by_order_code_with_product_id(cls, order_code, product_id): sql = ('select id from {.table_name} where ' 'order_code=%s, product_id=%s').format(cls) params = ( order_code, product_id, ) rs = db.execute(sql, params) return cls.get(rs[0][0]) if rs else None @classmethod @cache(user_cache_key) def get_id_list_by_user_id_with_product_id(cls, user_id, product_id): sql = ( 'select id from {.table_name} where user_id = %s and product_id=%s ' 'order by creation_time desc').format(cls) params = ( user_id, product_id, ) rs = db.execute(sql, params) return [r[0] for r in rs] @classmethod def gets_by_user_id_with_product_id(cls, user_id, product_id): ids = cls.get_id_list_by_user_id_with_product_id(user_id, product_id) return [cls.get(id_) for id_ in ids] def _commit_and_refresh(self, sql, params): db.execute(sql, params) db.commit() self.clear_cache() new_state = vars(self.get(self.id_)) vars(self).update(new_state) def clear_cache(self): mc.delete(self.cache_key.format(asset_id=self.id_)) mc.delete(self.user_id_cache_key.format(user_id=self.user_id)) mc.delete( self.user_cache_key.format(user_id=self.user_id, product_id=self.product_id)) mc.delete( self.order_code_cache_key.format(order_code=self.order_code, product_id=self.product_id)) mc.delete(self.product_cache_key.format(product_id=self.product_id)) @classmethod @cache(user_id_cache_key) def get_id_list_by_user_id(cls, user_id): sql = ('select id from {.table_name} where user_id = %s ' 'order by creation_time desc').format(cls) params = (user_id, ) rs = db.execute(sql, params) return [r[0] for r in rs] @classmethod @cache(product_cache_key) def get_ids_by_product_id(cls, product_id): sql = ('select id from {.table_name} where product_id = %s ' 'order by creation_time desc').format(cls) params = (product_id, ) rs = db.execute(sql, params) return [r[0] for r in rs] @classmethod def gets_by_user_id(cls, user_id): ids = cls.get_id_list_by_user_id(user_id) return [cls.get(id_) for id_ in ids] @property def total_amount(self): return self.hold_amount + self.uncollected_amount @property def redeemed_amount_today(self): return HoarderOrder.get_redeemed_amount_by_user_today(self.user_id) @property def remaining_amount_today(self): if not self.product.can_redeem: return 0 return min(self.hold_amount, self.product.day_redeem_amount - self.redeemed_amount_today, self.product.max_redeem_amount - self.redeemed_amount_today)
class HoarderOrder(PropsMixin): table_name = 'hoarder_order' cache_key = 'hoarder:order:{id_}' cache_ids_by_user_key = 'hoarder:order:user:{user_id}:ids' raw_product_sold_amount_cache_key = 'hoarder:order:sold_amount:raw_product:{product_id}:' product_daily_sold_amount_cache_key = 'hoarder:order:daily_sold_amount:product:{product_id}:' order_amount_cache_key_by_user = '******' #: 资产交割后支付金额 repay_amount = PropsItem('repay_amount', default='') #: 加急手续费 exp_sell_fee = PropsItem('exp_sell_fee', default='') #: 固定手续费 fixed_service_fee = PropsItem('fixed_service_fee', default='') #: 赎回手续费 service_fee = PropsItem('service_fee', default='') class Direction(Enum): #: 存入 save = 'S' #: 赎回 redeem = 'R' class Status(Enum): #: 本地购买初始状态 unpaid = 'U' #: 本地支付前状态 committed = 'C' #: 远端暂时搁置状态 shelved = 'V' #: 远端正在支付状态 paying = 'P' #: 远端已成功状态 success = 'S' #: 远端已失败状态 failure = 'F' #: 远端申请赎回状态 applyed = 'A' #: 远端正在赎回状态 redeeming = 'R' #: 远端待回款状态 waiting_back = 'W' #: 远端回款中状态 backing = 'B' #: 远端回款结束 backed = 'D' #: 状态显示文案 Status.unpaid.display_text = u'未支付' Status.committed.display_text = u'未支付' Status.shelved.display_text = u'处理中' Status.paying.display_text = u'处理中' Status.applyed.display_text = u'转出中' Status.redeeming.display_text = u'转出中' Status.waiting_back.display_text = u'待回款' Status.backing.display_text = u'回款中' Status.backed.display_text = u'已转出' Status.success.display_text = u'已存入' Status.failure.display_text = u'订单失败' # 状态迁移顺序 Status.unpaid.sequence = 0 Status.committed.sequence = 1 Status.shelved.sequence = 2 Status.paying.sequence = 3 Status.applyed.sequence = 3 Status.redeeming.sequence = 4 Status.waiting_back.sequence = 5 Status.backing.sequence = 6 Status.backed.sequence = 7 Status.success.sequence = 7 Status.failure.sequence = 7 #: 远端与本地状态映射 MUTUAL_STATUS_MAP = { OrderStatus.waiting: Status.paying, OrderStatus.paying: Status.shelved, OrderStatus.payed: Status.success, OrderStatus.applying: Status.success, OrderStatus.applyed: Status.success, OrderStatus.finished: Status.success, OrderStatus.auto_cancel: Status.failure, OrderStatus.user_cancel: Status.failure, } MUTUAL_REDEEM_MAP = { RedeemStatus.applyed: Status.applyed, RedeemStatus.redeeming: Status.redeeming, RedeemStatus.waiting_back: Status.waiting_back, RedeemStatus.backing: Status.backing, RedeemStatus.backed: Status.backed, } #: 状态和颜色映射 ORDER_STATUS_COLOR_MAP = { u'处理中': '#9B9B9B', u'已存入': '#6192B3', u'转出中': '#F5A623', u'回款中': '#F5A623', u'错误': '#D42C41', u'已转出': '#6C9F31', u'未知状态': '#9B9B9B', } def __init__(self, id_, user_id, product_id, bankcard_id, amount, pay_amount, expect_interest, order_code, pay_code, direction, status, remote_status, start_time, due_time, update_time, creation_time): self.id_ = str(id_) self.user_id = str(user_id) self.product_id = str(product_id) self.bankcard_id = str(bankcard_id) self.amount = amount self.pay_amount = pay_amount self.expect_interest = expect_interest self.order_code = order_code if order_code else None self.pay_code = pay_code if pay_code else None self._direction = direction self._status = status self.remote_status = remote_status self.start_time = start_time self.due_time = due_time self.update_time = update_time self.creation_time = creation_time def __str__(self): return '<HoarderOrder {.id_}>'.format(self) def get_db(self): return 'hoarder' def get_uuid(self): return 'hoarder:order:{.id_}'.format(self) @cached_property def owner(self): return Account.get(self.user_id) @cached_property def direction(self): return self.Direction(self._direction) @cached_property def product(self): return HoarderProduct.get(self.product_id) @cached_property def asset(self): from .asset import Asset return Asset.get_by_order_code(self.order_code) @cached_property def profit_period(self): return ProfitPeriod((self.due_date - self.start_date).days, 'day') @property def profit_hikes(self): return [] @property def coupon(self): if self.coupon_record: return self.coupon_record.coupon @property def coupon_record(self): from core.models.welfare import CouponUsageRecord return CouponUsageRecord.get_by_partner_order(self.product.vendor_id, self.id_) @property def woods_burning(self): from core.models.welfare import FirewoodBurning return FirewoodBurning.get_by_provider_order(self.product.vendor_id, self.id_) @property def display_status(self): return self.status.display_text @property def computed_expect_interest(self): return (self.actual_annual_rate * self.amount * self.profit_period.value / 100 / 365) @property def original_annual_rate(self): return self.product.annual_rate @property def actual_annual_rate(self): return self.original_annual_rate @property def bankcard(self): return BankCard.get(self.bankcard_id) @property def status(self): return self.Status(self._status) @property def status_color(self): return self.ORDER_STATUS_COLOR_MAP.get(self.display_status) @status.setter def status(self, new_status): self.check_before_setting_status(new_status) sql = 'update {.table_name} set status=%s, update_time=%s where id=%s;'.format( self) params = (new_status.value, datetime.now(), self.id_) db.execute(sql, params) db.commit() self.clear_cache(self.id_) self.clear_cache_by_user(self.user_id) self.act_after_setting_status(new_status) def update_by_remote_status(self, remote_status): """ remote_status 为OrderStatus 或 RedeemStatus""" local_status = self.get_local_status_by_remote_status(remote_status) if not local_status: raise UnsupportedStatusError(type(remote_status)) self.check_before_setting_status(local_status) sql = ( 'update {.table_name} set status=%s, remote_status=%s, update_time=%s' ' where id=%s;').format(self) params = (local_status.value, remote_status.value, datetime.now(), self.id_) db.execute(sql, params) db.commit() self.clear_cache(self.id_) self.clear_cache_by_user(self.user_id) self.act_after_setting_status(local_status) def get_local_status_by_remote_status(self, remote_status): """ remote_status 为OrderStatus 或 RedeemStatus""" if isinstance(remote_status, OrderStatus): return self.MUTUAL_STATUS_MAP.get(remote_status) elif isinstance(remote_status, RedeemStatus): return self.MUTUAL_REDEEM_MAP.get(remote_status) def check_before_setting_status(self, new_status): if not isinstance(new_status, self.Status): raise ValueError(u'错误的状态类型:%r' % new_status) # 不可反向跳转状态 if new_status.sequence < self.status.sequence: raise SequenceError() def act_after_setting_status(self, new_status): old_status = self._status # 本身已是成功状态则直接舍弃。 if self.Status(old_status) is self.Status.success: return self._status = new_status.value rsyslog.send('order %s status changes from %s to %s' % (self.id_, old_status, self._status), tag='hoarder_order_status_change') if new_status in [self.Status.shelved, self.Status.paying]: # 当支付未完成时将订单放入状态跟踪MQ self.track_for_payment() return if new_status is self.Status.success: # 当支付成功时将订单放入获取资产MQ并发送成功广播信号 rsyslog.send('fetch asset for order %s' % self.id_, tag='hoarder_fetch_asset') hoarder_asset_fetching.produce(self.id_) hoarder_order_succeeded.send(self) return if new_status is self.Status.failure: # 当支付失败时发送失败广播信号 hoarder_order_failed.send(self) @classmethod def check_before_adding(cls, vendor, user_id, bankcard_id, product_id, amount): if not vendor: raise NotFoundEntityError(vendor.id_, Account) product = HoarderProduct.get(product_id) local_account = Account.get(user_id) bankcard = BankCard.get(bankcard_id) hoarder_account = HoarderAccount.get(vendor.id_, user_id) if not local_account: raise NotFoundEntityError(user_id, Account) if not bankcard: raise NotFoundEntityError(bankcard_id, BankCard) if not hoarder_account: raise UnboundAccountError(user_id) if not has_real_identity(local_account): raise InvalidIdentityError() # 产品是否处于可售状态 if product.is_sold_out: raise SoldOutError(product_id) # 产品是否处于正常销售状态 if product.is_taken_down: raise SuspendedError(product_id) # 产品是否处于在售状态 if not product.is_on_sale: raise OffShelfError(product_id) # checks the product amount limit if not isinstance(amount, Decimal): raise TypeError('order amount must be decimal') amount_range = (product.min_amount, product.max_amount) amount_check = [ amount.is_nan(), amount < 0, amount < amount_range[0], amount > amount_range[1] ] if any(amount_check): raise OutOfRangeError(amount, amount_range) @classmethod def add(cls, user_id, product_id, bankcard_id, amount, order_code, direction, status, remote_status, expect_interest=None, pay_amount=None, pay_code=None, repay_amount=None, redeem_pay_amount=None, exp_sell_fee=None, fixed_service_fee=None, service_fee=None, start_time=None, due_time=None): assert isinstance(status, cls.Status) sql = ( 'insert into {.table_name} (user_id, product_id, bankcard_id, amount, pay_amount,' 'expect_interest, order_code, pay_code, direction, status, remote_status, start_time,' ' due_time, update_time, creation_time) ' 'values(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)' ).format(cls) params = (user_id, product_id, bankcard_id, amount, pay_amount, expect_interest, order_code, pay_code, direction.value, status.value, remote_status.value, start_time, due_time, datetime.now(), datetime.now()) id_ = db.execute(sql, params) db.commit() cls.clear_cache(id_) cls.clear_cache_by_user(user_id) instance = cls.get(id_) instance.update_props_items({ 'repay_amount': repay_amount, 'exp_sell_fee': exp_sell_fee, 'service_fee': service_fee, 'fixed_service_fee': fixed_service_fee }) return instance @classmethod @cache(cache_key) def get(cls, id_): sql = ( 'select id, user_id, product_id, bankcard_id, amount, pay_amount, expect_interest, ' 'order_code, pay_code, direction, status, remote_status, start_time, due_time, ' 'update_time, creation_time ' 'from {.table_name} where id=%s').format(cls) params = (id_, ) rs = db.execute(sql, params) return cls(*rs[0]) if rs else None @classmethod @cache(cache_ids_by_user_key) def get_ids_by_user(cls, user_id): sql = ('select id from {.table_name} where user_id=%s' ' order by creation_time desc').format(cls) params = (user_id, ) rs = db.execute(sql, params) return [str(r[0]) for r in rs] @classmethod def get_multi_by_user(cls, user_id): ids = cls.get_ids_by_user(user_id) return cls.get_multi(ids) @classmethod def gets_by_user_with_page(cls, user_id, offset, count): """分页获取订单信息 过滤掉失败和未支付订单""" sql = ( 'select id from {.table_name} where user_id=%s and not status=%s and not status=%s' 'order by creation_time desc limit %s, %s').format(cls) params = ( user_id, cls.Status.unpaid.value, cls.Status.failure.value, offset, count, ) rs = db.execute(sql, params) return cls.get_multi([str(r[0]) for r in rs]) if rs else [] @classmethod def get_redeemed_amount_by_user_today(cls, user_id): sql = ( 'select sum(amount) from {.table_name} where user_id=%s and direction=%s' ' and creation_time > %s').format(cls) params = (user_id, cls.Direction.redeem.value, datetime.today().date()) rs = db.execute(sql, params) return round_half_up( rs[0][0], 2) if rs and rs[0] and rs[0][0] else Decimal('0.00') @classmethod def get_ids_by_bankcard(cls, bankcard_id): sql = 'select id from {.table_name} where bankcard_id=%s'.format(cls) params = (bankcard_id, ) rs = db.execute(sql, params) return [str(r[0]) for r in rs] @classmethod def get_multi_by_bankcard(cls, bankcard_id): ids = cls.get_ids_by_bankcard(bankcard_id) return cls.get_multi(ids) @classmethod def is_bankcard_swiped(cls, bankcard): assert isinstance(bankcard, BankCard) return bool( len([ o for o in cls.get_multi_by_bankcard(bankcard.id_) if o.status is cls.Status.success ])) @classmethod @cache(raw_product_sold_amount_cache_key) def get_raw_product_sold_amount(cls, raw_product_id): sql = ('select sum(amount) from {.table_name} ' 'where product_id=%s and (status=%s or status=%s)').format(cls) params = (raw_product_id, cls.Status.paying.value, cls.Status.success.value) rs = db.execute(sql, params) return rs[0][0] or 0 @classmethod @cache(product_daily_sold_amount_cache_key) def get_product_daily_sold_amount_by_now(cls, product_id): sql = ('select sum(amount) from {.table_name}' ' where product_id=%s and (status=%s or status=%s)' ' and creation_time between %s and %s').format(cls) end_time = datetime.now() start_time = datetime.combine(end_time, time.min) params = (product_id, cls.Status.paying.value, cls.Status.success.value, start_time, end_time) rs = db.execute(sql, params) return rs[0][0] or 0 @classmethod def get_by_order_code(cls, order_code): sql = 'select id from {.table_name} where order_code=%s'.format(cls) params = (order_code, ) rs = db.execute(sql, params) return cls.get(rs[0][0]) if rs else None @classmethod @cache(order_amount_cache_key_by_user) def get_order_amount_by_user(cls, user_id): sql = ('select count(id) from {.table_name} ' 'where user_id=%s and (status=%s or status=%s)').format(cls) params = (user_id, cls.Status.paying.value, cls.Status.success.value) rs = db.execute(sql, params) return rs[0][0] @classmethod def get_multi(cls, ids): return [cls.get(id_) for id_ in ids] @classmethod def clear_cache(cls, id_): mc.delete(cls.cache_key.format(**locals())) @classmethod def clear_cache_by_user(cls, user_id): mc.delete(cls.cache_ids_by_user_key.format(**locals())) mc.delete(cls.order_amount_cache_key_by_user.format(**locals())) @classmethod def clear_local_sold_amount_cache(cls, product_id): mc.delete(cls.raw_product_sold_amount_cache_key.format(**locals())) def track_for_payment(self): hoarder_payment_tracking.produce(self.id_) def lock_bonus(self): """对订单礼券、抵扣金进行加锁冻结(发生在订单提交支付前)""" from core.models.welfare import FirewoodWorkflow, FirewoodBurning if self.woods_burning: flow = FirewoodWorkflow(self.user_id) flow.pick(self.woods_burning, tags=[FirewoodBurning.Kind.deduction.name]) if self.coupon: self.coupon.shell_out(self.product, self.amount) def confirm_bonus(self): """确认礼券、抵扣金被使用(发生在订单已经被告知成功)""" from core.models.welfare import FirewoodWorkflow if self.status is not self.Status.success: raise ValueError('order %s payment has not succeeded' % self.id_) if self.woods_burning: FirewoodWorkflow(self.user_id).burn(self.woods_burning) if self.coupon: self.coupon.confirm_consumption() self.coupon_record.commit() # TODO: 增加折扣判断 # for hike in self.profit_hikes: # hike.achieve() def unlock_bonus(self): """释放礼券、抵扣金(发生在订单已经被告知失败)""" from core.models.welfare import FirewoodWorkflow if self.status not in [ self.Status.paying, self.Status.committed, self.Status.failure ]: raise ValueError('order %s payment has not terminated' % self.id_) if self.woods_burning: FirewoodWorkflow(self.user_id).release(self.woods_burning) if self.coupon: self.coupon.put_back_wallet() for hike in self.profit_hikes: hike.renew()
class ZhiwangWrappedProduct(PropsMixin, ProfitHikeMixin): """ Wrapped product is partly reformed from the raw product provided by zhiwang to fit our use. """ class Type(Enum): newcomer = 'GH_NEWCOMER' Type.newcomer.label = u'新手专享' # store table_name = 'hoard_zhiwang_wrapped_product' cache_key = 'hoard:zhiwang:wrapped_product:{id_}' all_ids_cache_key = 'hoard:zhiwang:wrapped_product:all_ids' product_ids_by_raw_id_cache_key = 'hoard:zhiwang:wrapped_product:raw_id:{raw_id}' product_by_kind_and_raw_id_cache_key = ( 'hoard:zhiwang:wrapped_product:kind_id:{kind_id}:raw_id:{raw_product_id}' ) # local attrs name = PropsItem('name', '', unicode_type) allocated_amount = PropsItem('allocated_amount', 0, Decimal) min_amount = PropsItem('min_amount', 0, Decimal) max_amount = PropsItem('max_amount', 0, Decimal) annual_rate = PropsItem('annual_rate', 0, Decimal) start_date = PropsItem('start_date', None, date_type) due_date = PropsItem('due_date', None, date_type) # local config is_taken_down = PropsItem('is_taken_down', True) # delegation sale_mode = DelegatedProperty('sale_mode', to='raw_product') product_type = DelegatedProperty('product_type', to='raw_product') # 合作方 provider = zhiwang # 接受用户使用礼券抵扣优惠 is_accepting_bonus = False def __init__(self, id_, kind_id, raw_product_id, creation_time): self.id_ = str(id_) self.kind_id = str(kind_id) self.raw_product_id = str(raw_product_id) self.creation_time = creation_time def get_db(self): return 'hoard' def get_uuid(self): return 'zhiwang:wrapped_product:{id_}'.format(id_=self.id_) @cached_property def kind(self): from .wrapper_kind import WrapperKind return WrapperKind.get(self.kind_id) @cached_property def raw_product(self): from .product import ZhiwangProduct return ZhiwangProduct.get(self.raw_product_id) @property def display_privilege(self): raise NotImplementedError @property def wrapped_product_type(self): return self.kind.wrapped_product_type @property def is_either_sold_out(self): """是否已售罄""" from .product import SaleMode from .order import ZhiwangOrder # 首先判断基础产品总量是否售罄 if self.raw_product.is_sold_out: return True # 当产品销售模式为共享时,取决于父产品的销售情况 if self.sale_mode is SaleMode.share: return self.raw_product.is_either_sold_out elif self.sale_mode is SaleMode.mutex: local_sold_amount = ZhiwangOrder.get_wrapped_product_sold_amount( self.raw_product.product_id, self.id_) return local_sold_amount > (self.allocated_amount - ZW_SAFE_RESERVATION_AMOUNT) else: raise ValueError('invalid sale mode %s' % self.sale_mode) @property def in_sale(self): """是否在销售期内""" return self.raw_product.in_sale @property def in_stock(self): # 是否可售状态决定于以下几种情况 # 1. 是否在销售期内 # 2. 是否已售罄 # 3. 是否因其他原因而被暂时设置为暂停销售 if self.is_either_sold_out or self.is_taken_down: return False return self.in_sale @classmethod def create(cls, raw_product, wrapper_kind): # check the limit if (min(wrapper_kind.limit) < raw_product.min_amount or max(wrapper_kind.limit) > raw_product.max_amount): raise InvalidWrapRule(wrapper_kind.limit) # check the frozen time raw_days_period = [ raw_product.profit_period['min'].value, raw_product.profit_period['max'].value ] if not min(raw_days_period) <= wrapper_kind.frozen_days.value <= max( raw_days_period): raise InvalidWrapRule(wrapper_kind.id_) instance = cls.get_by_kind_and_raw_product_id(wrapper_kind.id_, raw_product.product_id) if instance is None: sql = ('insert into {.table_name} (kind_id, raw_product_id, ' 'creation_time) values (%s, %s, %s)').format(cls) params = (wrapper_kind.id_, raw_product.product_id, datetime.now()) id_ = db.execute(sql, params) db.commit() instance = cls.get(id_) instance.deploy(wrapper_kind) cls.clear_cache(id_) cls.clear_all_ids_cache() cls.clear_product_ids_by_raw_id_cache(raw_product.product_id) cls.clear_product_by_kind_and_raw_cache(wrapper_kind.id_, raw_product.product_id) return instance def deploy(self): raise NotImplementedError def is_qualified(self, user_id): raise NotImplementedError @classmethod @cache(cache_key) def get(cls, id_): from .wrapper_kind import WrapperKind sql = ( 'select id, kind_id, raw_product_id, creation_time from {.table_name} ' 'where id=%s').format(cls) params = (id_, ) rs = db.execute(sql, params) if rs: kind = WrapperKind.get(rs[0][1]).wrapped_product_type # FIXME: 避免使用subclasses反射 pcls = next(scls for scls in cls.__subclasses__() if scls.wrapped_product_kind is kind) return pcls(*rs[0]) @classmethod def get_all(cls): """获取所有可展示子产品""" ids = cls.get_all_ids() products = (cls.get(id_) for id_ in ids) return [p for p in products if p.in_sale] @classmethod @cache(all_ids_cache_key) def get_all_ids(cls): sql = ('select id from {.table_name} order by creation_time desc' ).format(cls) rs = db.execute(sql) return [r[0] for r in rs] @classmethod def get_multi_by_raw(cls, raw_id): product_ids = cls.get_ids_by_raw(raw_id) return [cls.get(product_id) for product_id in product_ids] @classmethod @cache(product_ids_by_raw_id_cache_key) def get_ids_by_raw(cls, raw_id): sql = ( 'select id from {.table_name} where raw_product_id=%s').format(cls) params = (raw_id, ) rs = db.execute(sql, params) return [r[0] for r in rs] @classmethod @cache(product_by_kind_and_raw_id_cache_key) def get_by_kind_and_raw_product_id(cls, kind_id, raw_product_id): sql = ( 'select id from {.table_name} where kind_id=%s and raw_product_id=%s' ).format(cls) params = (kind_id, raw_product_id) rs = db.execute(sql, params) if rs: return cls.get(rs[0][0]) @classmethod def clear_cache(cls, id_): mc.delete(cls.cache_key.format(**locals())) @classmethod def clear_all_ids_cache(cls): mc.delete(cls.all_ids_cache_key) @classmethod def clear_product_ids_by_raw_id_cache(cls, raw_id): mc.delete(cls.product_ids_by_raw_id_cache_key.format(**locals())) @classmethod def clear_product_by_kind_and_raw_cache(cls, kind_id, raw_product_id): mc.delete(cls.product_by_kind_and_raw_id_cache_key.format(**locals()))
class Announcement(EntityModel, PropsMixin): """The announcement information of whole site.""" class SubjectType(Enum): notice = 'N' class Status(Enum): present = 'P' absent = 'A' class ContentType(Enum): markdown = 'M' ContentType.markdown.to_html = render_markdown table_name = 'site_announcement' cache_key = 'site:announcement:{id_}' cache_by_date_key = 'site:announcement:date:{date}:ids' #: The subject of announcement subject = PropsItem('subject', default=u'') #: The content of announcement content = PropsItem('content', default=u'') def __init__(self, id_, subject_type_code, content_type_code, status_code, start_time, stop_time, endpoint, creation_time): self.id_ = bytes(id_) self.subject_type_code = subject_type_code self.content_type_code = content_type_code self.status_code = status_code #: The announcement be visible since this time self.start_time = start_time #: The announcement be invisible since this time self.stop_time = stop_time #: The announcement only be visible in special Flask endpoint self.endpoint = endpoint #: The creation time of announcement self.creation_time = creation_time @cached_property def subject_type(self): """The subject type of announcement. :rtype: :class:`.Announcement.SubjectType` """ return self.SubjectType(self.subject_type_code) @cached_property def content_type(self): """The content type of announcement. :rtype: :class:`.Announcement.ContentType` """ return self.ContentType(self.content_type_code) @property def content_as_html(self): """The announcement content as HTML format.""" return self.content_type.to_html(self.content).strip() @property def status(self): """The status of announcement. :rtype: :class:`.Announcement.Status` """ return self.Status(self.status_code) def get_db(self): return 'site_announcement' def get_uuid(self): return self.id_ @classmethod @cache(cache_key) def get(cls, id_): sql = ('select id, subject_type, content_type, status, start_time,' ' stop_time, endpoint, creation_time ' 'from {0} where id = %s').format(cls.table_name) params = (id_, ) rs = db.execute(sql, params) if rs: return cls(*rs[0]) @classmethod def add(cls, subject, subject_type, content, content_type, start_time, stop_time, endpoint): assert isinstance(subject_type, cls.SubjectType) assert isinstance(content_type, cls.ContentType) assert start_time < stop_time assert datetime.datetime.now() < stop_time initial_status = cls.Status.present sql = ('insert into {0} (subject_type, content_type, status,' ' start_time, stop_time, endpoint, creation_time) ' 'values (%s, %s, %s, %s, %s, %s, %s)').format(cls.table_name) params = (subject_type.value, content_type.value, initial_status.value, start_time, stop_time, endpoint, datetime.datetime.now()) id_ = db.execute(sql, params) db.commit() cls.clear_cache(id_) for date in datetime_range(start_time, stop_time): cls.clear_cache_by_date(date) instance = cls.get(id_) instance.subject = subject instance.content = content return instance @classmethod @cache(cache_by_date_key) def get_ids_by_date(cls, date): assert isinstance(date, datetime.date) sql = ('select id from {0} where date(start_time) <= %s and' ' date(stop_time) > %s').format(cls.table_name) params = (date, date) rs = db.execute(sql, params) return [r[0] for r in rs] @classmethod def get_multi_by_date(cls, date, status=Status.present): ids = cls.get_ids_by_date(date) announcements = (cls.get(id_) for id_ in ids) return [a for a in announcements if a.status is status] @classmethod def clear_cache(cls, id_): mc.delete(cls.cache_key.format(**locals())) @classmethod def clear_cache_by_date(cls, date): mc.delete(cls.cache_by_date_key.format(**locals())) def is_suitable(self, request): return glob.fnmatch.fnmatch(request.endpoint, self.endpoint)
class XMLoan(PropsMixin): table_name = 'hoard_xm_loan' cache_key = 'hoard:xm:loan:id:{id_}:v1' cache_key_by_loans_digest_id = 'hoard:xm:loan:loans_digest_id:{loans_digest_id}' # 借款协议编号 loan_receipt_no = PropsItem('loan_receipt_no', '', unicode_type) # 投资编号 invest_id = PropsItem('invest_id', '', int) # 借款人姓名 debtor_name = PropsItem('debtor_name', '', unicode_type) # 借款人身份证号 debtor_ricn = PropsItem('debtor_ricn', '', unicode_type) # 借款人身份 debtor_type = PropsItem('debtor_type', '', unicode_type) # 借款用途 debt_purpose = PropsItem('debt_purpose', '', unicode_type) # 借款金额 lending_amount = PropsItem('lending_amount', 0, Decimal) # 借款人开始还款的日期 start_date = PropsItem('start_date', '', date_type) def __init__(self, id_, loans_digest_id, creation_time): self.id_ = id_ self.loans_digest_id = loans_digest_id self.creation_time = creation_time def get_db(self): return 'hoard' def get_uuid(self): return 'xm:loan:{0}'.format(self.id_) @classmethod @cache(cache_key) def get(cls, id_): sql = ('select id, loans_digest_id, creation_time from {.table_name}' ' where id=%s').format(cls) params = (id_, ) rs = db.execute(sql, params) if rs: return cls(*rs[0]) @classmethod @cache(cache_key_by_loans_digest_id) def get_ids_by_loans_digest_id(cls, loans_digest_id): sql = 'select id from {.table_name} where loans_digest_id=%s'.format( cls) params = (loans_digest_id, ) rs = db.execute(sql, params) return [str(r[0]) for r in rs] @classmethod def get_multi(cls, ids): return [cls.get(id_) for id_ in ids] @classmethod def get_multi_by_loans_digest_id(cls, loans_digest_id): return cls.get_multi(cls.get_ids_by_loans_digest_id(loans_digest_id)) @classmethod def create(cls, loans_digest, loans, _commit=True): assert isinstance(loans_digest, XMLoansDigest) sql = 'insert into {.table_name} (loans_digest_id, creation_time) values(%s, %s)'.format( cls) params = (loans_digest.id_, datetime.datetime.now()) id_ = db.execute(sql, params) if _commit: db.commit() instance = cls.get(id_) cls.clear_cache(id_) cls.clear_cache_by_loans_digest_id(loans_digest.id_) instance.update_props_items({ 'loan_receipt_no': loans.loan_receipt_no, 'invest_id': loans.order_id, 'debtor_name': loans.bc_name, 'debtor_ricn': loans.debtor_identity_no, 'debtor_type': loans.debtor_type, 'debt_purpose': loans.debt_desc, 'lending_amount': str(loans.loan_receipt_amt), 'start_date': loans.start_date.isoformat() }) return instance @classmethod def clear_cache(cls, id_): mc.delete(cls.cache_key.format(id_=id_)) @classmethod def clear_cache_by_loans_digest_id(cls, loans_digest_id): mc.delete( cls.cache_key_by_loans_digest_id.format( loans_digest_id=loans_digest_id))