generate-commission-index.ts 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. /**
  2. * 批量生成分润索引表记录脚本
  3. *
  4. * 功能:
  5. * 1. 如果 commissionDetails 有数据,直接使用
  6. * 2. 如果 commissionDetails 没有数据,根据字段生成2层分润:
  7. * - 第1层:agentId 和 incomeAmount
  8. * - 第2层:personalAgentId 和 personalIncomeAmount
  9. */
  10. import { DataSource } from 'typeorm'
  11. import { IncomeRecords } from '../src/entities/income-records.entity'
  12. import { AgentCommissionIndex } from '../src/entities/agent-commission-index.entity'
  13. import { CommissionDetail } from '../src/services/multi-level-commission.service'
  14. import { createApp } from '../src/app'
  15. interface MigrationStats {
  16. total: number
  17. processed: number
  18. success: number
  19. failed: number
  20. skipped: number
  21. withDetails: number
  22. withoutDetails: number
  23. }
  24. class CommissionIndexGenerator {
  25. private dataSource: DataSource
  26. private stats: MigrationStats = {
  27. total: 0,
  28. processed: 0,
  29. success: 0,
  30. failed: 0,
  31. skipped: 0,
  32. withDetails: 0,
  33. withoutDetails: 0
  34. }
  35. constructor(dataSource: DataSource) {
  36. this.dataSource = dataSource
  37. }
  38. /**
  39. * 从 commissionDetails JSON 字符串解析分润明细
  40. */
  41. private parseCommissionDetails(commissionDetails: string | null): CommissionDetail[] | null {
  42. if (!commissionDetails) {
  43. return null
  44. }
  45. try {
  46. const details = JSON.parse(commissionDetails)
  47. if (Array.isArray(details) && details.length > 0) {
  48. return details as CommissionDetail[]
  49. }
  50. return null
  51. } catch (error) {
  52. console.error('解析 commissionDetails 失败:', error)
  53. return null
  54. }
  55. }
  56. /**
  57. * 根据字段生成2层分润明细
  58. */
  59. private generateCommissionDetailsFromFields(
  60. record: IncomeRecords
  61. ): CommissionDetail[] {
  62. const details: CommissionDetail[] = []
  63. // 第1层:agentId 和 incomeAmount
  64. if (record.agentId && record.agentId > 0 && record.incomeAmount && Number(record.incomeAmount) > 0) {
  65. const orderPrice = Number(record.orderPrice) || 0
  66. const incomeAmount = Number(record.incomeAmount)
  67. // 计算分润比例
  68. const rate = orderPrice > 0 ? (incomeAmount / orderPrice) * 100 : 0
  69. details.push({
  70. level: 1,
  71. agentId: record.agentId,
  72. rate: Number(rate.toFixed(2)),
  73. amount: incomeAmount
  74. })
  75. }
  76. // 第2层:personalAgentId 和 personalIncomeAmount
  77. if (record.personalAgentId && record.personalAgentId > 0 && record.personalIncomeAmount && Number(record.personalIncomeAmount) > 0) {
  78. const orderPrice = Number(record.orderPrice) || 0
  79. const personalIncomeAmount = Number(record.personalIncomeAmount)
  80. // 计算分润比例
  81. const rate = orderPrice > 0 ? (personalIncomeAmount / orderPrice) * 100 : 0
  82. details.push({
  83. level: 2,
  84. agentId: record.personalAgentId,
  85. rate: Number(rate.toFixed(2)),
  86. amount: personalIncomeAmount
  87. })
  88. }
  89. return details
  90. }
  91. /**
  92. * 检查是否已存在索引记录
  93. */
  94. private async hasIndexRecords(incomeRecordId: number): Promise<boolean> {
  95. const indexRepository = this.dataSource.getRepository(AgentCommissionIndex)
  96. const count = await indexRepository.count({
  97. where: { incomeRecordId }
  98. })
  99. return count > 0
  100. }
  101. /**
  102. * 处理单条记录
  103. */
  104. private async processSingleRecord(record: IncomeRecords, skipExisting: boolean = true): Promise<void> {
  105. try {
  106. // 检查是否已存在索引记录
  107. if (skipExisting) {
  108. const exists = await this.hasIndexRecords(record.id)
  109. if (exists) {
  110. this.stats.skipped++
  111. return
  112. }
  113. }
  114. // 解析 commissionDetails
  115. let commissionDetails: CommissionDetail[] | null = null
  116. if (record.commissionDetails) {
  117. commissionDetails = this.parseCommissionDetails(record.commissionDetails)
  118. if (commissionDetails) {
  119. this.stats.withDetails++
  120. }
  121. }
  122. // 如果没有 commissionDetails,根据字段生成
  123. if (!commissionDetails || commissionDetails.length === 0) {
  124. commissionDetails = this.generateCommissionDetailsFromFields(record)
  125. this.stats.withoutDetails++
  126. }
  127. // 如果没有分润明细,跳过
  128. if (!commissionDetails || commissionDetails.length === 0) {
  129. this.stats.skipped++
  130. return
  131. }
  132. // 创建索引记录
  133. // 注意:createdAt 使用订单的创建时间(record.createdAt),而不是生成脚本时的时间
  134. const indexRepository = this.dataSource.getRepository(AgentCommissionIndex)
  135. const indexRecords = commissionDetails.map(detail =>
  136. indexRepository.create({
  137. incomeRecordId: record.id,
  138. agentId: detail.agentId,
  139. level: detail.level,
  140. commissionRate: detail.rate,
  141. commissionAmount: detail.amount,
  142. orderNo: record.orderNo,
  143. orderPrice: Number(record.orderPrice),
  144. userId: record.userId,
  145. status: record.status,
  146. createdAt: record.createdAt // 使用订单的创建时间
  147. })
  148. )
  149. await indexRepository.save(indexRecords)
  150. this.stats.success++
  151. if (this.stats.processed % 100 === 0) {
  152. console.log(`已处理: ${this.stats.processed} | 成功: ${this.stats.success} | 失败: ${this.stats.failed} | 跳过: ${this.stats.skipped} | 有明细: ${this.stats.withDetails} | 无明细: ${this.stats.withoutDetails}`)
  153. }
  154. } catch (error) {
  155. this.stats.failed++
  156. console.error(`处理记录失败 (ID: ${record.id}, OrderNo: ${record.orderNo}):`, error)
  157. }
  158. }
  159. /**
  160. * 批量处理订单记录
  161. */
  162. async generateIndex(batchSize: number = 100, startId?: number, skipExisting: boolean = true): Promise<void> {
  163. const incomeRecordsRepository = this.dataSource.getRepository(IncomeRecords)
  164. const indexRepository = this.dataSource.getRepository(AgentCommissionIndex)
  165. console.log('开始生成分润索引表记录...')
  166. console.log(`批次大小: ${batchSize}`)
  167. console.log(`起始ID: ${startId || '无'}`)
  168. console.log(`跳过已存在: ${skipExisting}`)
  169. // 先统计总数
  170. let countQuery = incomeRecordsRepository
  171. .createQueryBuilder('record')
  172. .where('record.delFlag = :delFlag', { delFlag: false })
  173. if (startId) {
  174. countQuery = countQuery.andWhere('record.id >= :startId', { startId })
  175. }
  176. const total = await countQuery.getCount()
  177. this.stats.total = total
  178. console.log(`总记录数: ${total}`)
  179. let offset = 0
  180. let hasMore = true
  181. while (hasMore) {
  182. const queryBuilder = incomeRecordsRepository
  183. .createQueryBuilder('record')
  184. .where('record.delFlag = :delFlag', { delFlag: false })
  185. .orderBy('record.id', 'ASC')
  186. .limit(batchSize)
  187. .offset(offset)
  188. if (startId) {
  189. queryBuilder.andWhere('record.id >= :startId', { startId })
  190. }
  191. const records = await queryBuilder.getMany()
  192. if (records.length === 0) {
  193. hasMore = false
  194. break
  195. }
  196. for (const record of records) {
  197. await this.processSingleRecord(record, skipExisting)
  198. this.stats.processed++
  199. }
  200. offset += batchSize
  201. // 如果这批数据少于批次大小,说明已经处理完所有数据
  202. if (records.length < batchSize) {
  203. hasMore = false
  204. }
  205. }
  206. console.log('\n生成完成!')
  207. console.log(`总计: ${this.stats.total}`)
  208. console.log(`已处理: ${this.stats.processed}`)
  209. console.log(`成功: ${this.stats.success}`)
  210. console.log(`失败: ${this.stats.failed}`)
  211. console.log(`跳过: ${this.stats.skipped}`)
  212. console.log(`有明细: ${this.stats.withDetails}`)
  213. console.log(`无明细: ${this.stats.withoutDetails}`)
  214. }
  215. }
  216. // 主函数
  217. async function main() {
  218. const app = await createApp()
  219. const dataSource = (app as any).dataSource
  220. if (!dataSource) {
  221. console.error('无法获取数据库连接')
  222. process.exit(1)
  223. }
  224. try {
  225. const generator = new CommissionIndexGenerator(dataSource)
  226. // 从命令行参数获取配置
  227. const args = process.argv.slice(2)
  228. const batchSize = parseInt(args[0]) || 100
  229. const startId = args[1] ? parseInt(args[1]) : undefined
  230. const skipExisting = args[2] !== 'false'
  231. console.log('配置参数:')
  232. console.log(` 批次大小: ${batchSize}`)
  233. console.log(` 起始ID: ${startId || '无'}`)
  234. console.log(` 跳过已存在: ${skipExisting}`)
  235. console.log('')
  236. await generator.generateIndex(batchSize, startId, skipExisting)
  237. } catch (error) {
  238. console.error('执行失败:', error)
  239. process.exit(1)
  240. } finally {
  241. await app.close()
  242. }
  243. }
  244. // 运行脚本
  245. if (require.main === module) {
  246. main().catch(console.error)
  247. }
  248. export { CommissionIndexGenerator }