products.py 13 KB

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