products.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. import re
  2. import sys
  3. import json
  4. import time
  5. import pymysql
  6. from tw.config import conn, table
  7. from datetime import datetime
  8. from random import randint
  9. from collections import namedtuple
  10. from openpyxl import Workbook
  11. from openpyxl.styles import PatternFill, colors
  12. '''
  13. twong 辅助工具,扫描商品表,及其关联表
  14. 自动更新不合理数据
  15. 对于价格方面的数据,则导出为 excel 表
  16. 运行需自行配置参数,并打开 commit 语句
  17. '''
  18. SHOW_MAX_DAYS = 800 # 超过天数自动下架
  19. NOW = int(time.time()) # 当前时间戳
  20. INVALID_CHAR=r'[\s,。 ,.{}()\[\]<>]+'
  21. PTN_INVALID = r'^{}|{}$'.format(INVALID_CHAR, INVALID_CHAR)
  22. # config excel
  23. excel = Workbook()
  24. sheet = excel.active
  25. sheet.title = '问题商品'
  26. sheet.freeze_panes = 'A2' # freeze above or left
  27. # init sheet header
  28. sheet['A1'] = 'ID'
  29. sheet['B1'] = '商品'
  30. sheet['C1'] = 'SKU'
  31. sheet['D1'] = '成本'
  32. sheet['E1'] = '价格'
  33. sheet['F1'] = '原价'
  34. sheet['G1'] = '利润率'
  35. sheet['H1'] = '其他问题'
  36. for cols in sheet.iter_cols(min_col=1,max_col=8,min_row=1,max_row=1):
  37. for cell in cols:
  38. cell.fill = PatternFill('solid', fgColor='C5C1AA')
  39. sheet['A1'].fill = PatternFill('solid', fgColor='C5C1AA')
  40. def updateResult(productId, result, skuName, stock=0, price=0, otPrice=0, brokerage=0, brokerage2=0):
  41. '''
  42. 更新 result 表中 result 字段中的值
  43. '''
  44. if not result:
  45. print('product={} result={}'.format(productId, result))
  46. return
  47. # print('updateResult:', productId, skuName)
  48. values = result.get('value')
  49. if not values:
  50. print('ERROR: no value field')
  51. return
  52. if isinstance(values, list):
  53. pass
  54. elif isinstance(values, dict):
  55. values = values.values()
  56. else:
  57. print('ERROR: bad value type')
  58. for value in values:
  59. detail = value.get('detail')
  60. if not detail:
  61. print('ERROR: no detail')
  62. return
  63. values = detail.values()
  64. values = [str(v) for v in values]
  65. sku = ','.join(sorted(values))
  66. if sku == skuName:
  67. value['stock'] = stock if stock else value.get('stock')
  68. value['ot_price'] = otPrice if otPrice else value['ot_price']
  69. value['price'] = price if price else value['price']
  70. value['brokerage'] = 0 if brokerage else 0
  71. value['brokerage_two'] = 0 if brokerage2 else 0
  72. return result
  73. def fixResultInvalidChar(productId, result):
  74. '''
  75. 检查 result 中规格中的各种非法字符
  76. '''
  77. if not result:
  78. return
  79. attrs = result.get('attr', [])
  80. if attrs:
  81. for attr in attrs:
  82. value = attr.get('value', '')
  83. attr['value'] = re.sub(PTN_INVALID, '', value)
  84. detail = attr.get('detail', [])
  85. for i, d in enumerate(detail):
  86. detail[i] = re.sub(PTN_INVALID, '', str(d))
  87. values = result.get('value', [])
  88. if isinstance(values, list):
  89. pass
  90. elif isinstance(values, dict):
  91. values = values.values()
  92. else:
  93. print('unknown value type')
  94. return
  95. for value in values:
  96. for i in range(len(attrs)):
  97. key = 'value{}'.format(i+1)
  98. valuex = value.get(key)
  99. value[key] = re.sub(PTN_INVALID, '', valuex)
  100. detail = value.get('detail', {})
  101. removing_keys = []
  102. adding_keys = {}
  103. for k, v in detail.items():
  104. if re.search(PTN_INVALID, k):
  105. removing_keys.append(k)
  106. newk = re.sub(PTN_INVALID, '', k)
  107. adding_keys[newk] = re.sub(PTN_INVALID, '', v)
  108. detail[k] = re.sub(PTN_INVALID, '', v)
  109. for k in removing_keys:
  110. del detail[k]
  111. for k, v in adding_keys.items():
  112. detail[k] = v
  113. with conn:
  114. with conn.cursor() as cursor:
  115. sql = '''SELECT id, store_name, image, slider_image, spec_type, price, cost, stock, ficti, add_time
  116. FROM {}
  117. WHERE is_show=1 AND is_del=0 '''.format(table('store_product'))
  118. cursor.execute(sql)
  119. products = cursor.fetchall()
  120. errors = {}
  121. # 遍历所有商品
  122. for pd in products:
  123. productId = pd[0]
  124. productName = pd[1]
  125. specType = pd[4]
  126. image =pd[2]
  127. images = json.loads(pd[3])
  128. ficti = pd[8]
  129. addTime = pd[9]
  130. chFicti = False # 是否需要更新虚拟销量
  131. chProductStock = False # 是否需要更新库存
  132. errors[productId] = {
  133. 'name': productName,
  134. }
  135. sq2 = '''SELECT result
  136. FROM {}
  137. WHERE product_id={}'''.format(table('store_product_attr_result'), productId)
  138. cursor.execute(sq2)
  139. result = cursor.fetchone()
  140. if not result or len(result) <= 0:
  141. # print('{}-{} 在 result 表中无数据.'.format(productId, productName))
  142. errors[productId]['others'] = errors[productId].get('others', '') + 'attr_result 表中无数据;'
  143. result = {}
  144. else:
  145. result = json.loads(result[0])
  146. fixResultInvalidChar(productId, result)
  147. sq3 = '''SELECT suk, stock, sales, price, cost, ot_price, brokerage, brokerage_two, `unique`
  148. FROM {}
  149. WHERE product_id={}'''.format(table('store_product_attr_value'), productId)
  150. cursor.execute(sq3)
  151. values = cursor.fetchall()
  152. # 检查商品图片是否完整
  153. if not image or not images:
  154. errors[productId]['others'] = errors[productId].get('others', '') + ' 没有设置产品图;'
  155. # 虚拟销量必须为 0-20
  156. if ficti < 0 or ficti > 20:
  157. chFicti = True
  158. # 上架超过一定时间自动下架,并将 add_time 改为当前日期
  159. days = int( (NOW - addTime) / 86400 )
  160. if days > SHOW_MAX_DAYS:
  161. sql = '''UPDATE {} SET is_show=0, add_time={} WHERE id={}'''.format(table('store_product'), NOW, productId)
  162. with conn.cursor() as cs:
  163. cs.execute(sql)
  164. print('{}-{} 过期{}天 已启动下架'.format(productId, productName, days))
  165. # 检查 price, cost, stock 必须为正值
  166. chResult = False # 是否需要更新 attr_result 表
  167. errors[productId]['sku'] = {} # 商品 sku 错误记录
  168. totalStock = 0
  169. eachStock = 0 # 每个 sku 的库存,为了让sku 一致
  170. for sku in values:
  171. chStock, chOtPrice, chBrokerage, chName = False, False, False, False
  172. skuName = sku[0]
  173. skuNameList = skuName.split(',')
  174. for i, p in enumerate(skuNameList):
  175. if re.search(PTN_INVALID, str(p)):
  176. chName = True
  177. skuNameList[i] = re.sub(PTN_INVALID, '', p)
  178. # print(skuNameList)
  179. newNameList = list(set(skuNameList))
  180. if chName or len(newNameList) != len(skuNameList):
  181. skuName = ','.join(sorted(newNameList))
  182. stock = sku[1]
  183. sales = sku[2]
  184. price = sku[3]
  185. cost = sku[4]
  186. otPrice = sku[5]
  187. brokerage = sku[6]
  188. brokerage2 = sku[7]
  189. unique = sku[8]
  190. errors[productId]['sku'][skuName] = {}
  191. # 库存为 5-200
  192. if stock < 5 or stock > 200: # stock
  193. chStock = True
  194. chProductStock = True
  195. else:
  196. totalStock += stock
  197. # 利润率 >= 30%
  198. if price <= 0: # price
  199. errors[productId]['sku'][skuName]['price'] = float(price)
  200. if cost <= 0: # cost
  201. errors[productId]['sku'][skuName]['cost'] = float(cost)
  202. if price < cost:
  203. errors[productId]['sku'][skuName]['price'] = float(price)
  204. errors[productId]['sku'][skuName]['cost'] = float(cost)
  205. errors[productId]['sku'][skuName]['others'] = errors[productId]['sku'][skuName].get('others', '') + '价格低于成本;'
  206. if price > 0:
  207. profitRate = round(float(1 - cost/price), 2)
  208. if profitRate < 0.3:
  209. errors[productId]['sku'][skuName]['price'] = float(price)
  210. errors[productId]['sku'][skuName]['cost'] = float(cost)
  211. errors[productId]['sku'][skuName]['profit'] = profitRate
  212. errors[productId]['sku'][skuName]['others'] = errors[productId]['sku'][skuName].get('others', '') + ' 利润率不足 30%;'
  213. if otPrice < price:
  214. chOtPrice = True
  215. # 商品自定义返利必须为0
  216. if brokerage !=0 or brokerage2 != 0:
  217. chBrokerage = True
  218. # 更新 result 结构
  219. if not eachStock or eachStock < sales:
  220. eachStock = randint(50, 150)+sales if chStock else 0
  221. newOtPrice = int(float(price) * 1.4) if chOtPrice else 0
  222. newBrokerage = 1 if chBrokerage else 0
  223. newBrokerage2 = 1 if chBrokerage else 0
  224. updateResult(productId, result, skuName, stock=eachStock, price=0, otPrice=newOtPrice, brokerage=newBrokerage, brokerage2=newBrokerage2)
  225. # print(result)
  226. # 更新 SQL (attr_value 表)
  227. parts = []
  228. if chName:
  229. parts.append("suk='{}'".format(skuName))
  230. if chStock:
  231. totalStock += eachStock
  232. parts.append('stock={}'.format(eachStock - sales))
  233. if chOtPrice:
  234. parts.append('ot_price={}'.format(newOtPrice))
  235. if parts:
  236. sparts = ','.join(parts)
  237. sql = ''' UPDATE {}
  238. SET {}, brokerage=0, brokerage_two=0
  239. WHERE `unique`='{}' '''.format(table('store_product_attr_value'), sparts, unique)
  240. with conn.cursor() as cs:
  241. cs.execute(sql)
  242. print('{}-{}-{} 自动修改库存名称等: sql={}'.format(productId, productName, skuName, sql))
  243. chResult = (chResult or chStock or chOtPrice or chBrokerage)
  244. # 更新 attr_result 表
  245. if chResult:
  246. sql = '''UPDATE {}
  247. SET result='{}'
  248. WHERE product_id={}'''.format(table('store_product_attr_result'), json.dumps(result, ensure_ascii=False), productId)
  249. with conn.cursor() as cs:
  250. cs.execute(sql)
  251. print('更新 result. reason: {}, : sql={}'.format(sparts, sql))
  252. # update products
  253. parts = []
  254. if chFicti:
  255. newVal = randint(0, 20)
  256. parts.append('ficti={}'.format(newVal))
  257. if chProductStock:
  258. parts.append('stock={}'.format(totalStock))
  259. if parts:
  260. sparts = ','.join(parts)
  261. sql = '''UPDATE {}
  262. SET {}
  263. WHERE id={}'''.format(table('store_product'), sparts, productId)
  264. with conn.cursor() as cs:
  265. cs.execute(sql)
  266. print('{}-{} 更改虚拟销量和库存 {}'.format(productId, productName, sparts))
  267. conn.commit()
  268. counter = 1 # skip header line
  269. cell = lambda ch,c: '{}{}'.format(ch, c)
  270. for pId, v in errors.items():
  271. writen = False
  272. if len(v) > 2:
  273. counter += 1
  274. sheet[cell('A', counter)] = pId
  275. sheet[cell('B', counter)] = v['name']
  276. sheet[cell('H', counter)] = v.get('others', '')
  277. writen = True
  278. for k, s in v['sku'].items():
  279. if len(s) > 0:
  280. counter += 1
  281. if not writen:
  282. sheet[cell('A', counter)] = pId
  283. sheet[cell('B', counter)] = v['name']
  284. sheet[cell('H', counter)] = v.get('other', '')
  285. writen = True
  286. sheet[cell('C', counter)] = k
  287. sheet[cell('D', counter)] = s.get('cost', '')
  288. sheet[cell('E', counter)] = s.get('price', '')
  289. sheet[cell('F', counter)] = s.get('ot_price', '')
  290. sheet[cell('G', counter)] = s.get('profit', '')
  291. sheet[cell('H', counter)] = s.get('others', '')
  292. # sheet['A1'] = 'ID'
  293. # sheet['B1'] = '商品'
  294. # sheet['C1'] = 'SKU'
  295. # sheet['D1'] = '成本'
  296. # sheet['E1'] = '价格'
  297. # sheet['F1'] = '原价'
  298. # sheet['G1'] = '利润率'
  299. # sheet['H1'] = '其他问题'
  300. excel.save(filename=datetime.now().strftime('%Y-%m-%d.xls'))
  301. sys.exit()